Merge pull request #374 from keboola/vr/2026-05-21-fixes

fix(web): install-prompt Step 2 + restart cue match Desktop install path
This commit is contained in:
Vojtech 2026-05-21 18:56:15 +04:00 committed by GitHub
commit eb75c8d204
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 51 additions and 34 deletions

View file

@ -87,6 +87,23 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
standard step-lede size instead of the previous 13px chip. standard step-lede size instead of the previous 13px chip.
### Fixed ### Fixed
- Google Workspace connector prompt's Step 8 verify no longer asks
Claude to parse a row count out of `gws drive files list` / `gws
chat spaces list` JSON. Claude would improvise a `python3 -c 'f"…
{len(d.get(\"files\",[]))}…"'` snippet that fails two ways: f-string
expressions reject backslashes in Python <3.12 (`SyntaxError`), and
`gws` can emit a banner before the JSON body (`json.JSONDecodeError`).
Step 8 now treats exit code 0 as success, drops the `<N> drive
file(s), <M> chat space(s) visible` counts, and explicitly warns
against both anti-patterns. The summary-grep prefix (`✅ Google
Workspace ready —`) is preserved.
- Install-script Step 2 + Step 9 restart cue + post-install `/home` hero
now reference `~/Desktop/<workspace_dir>` to match the `/home` "Step 2
— pick a folder" recommendation users actually run (`mkdir -p
~/Desktop/<workspace_dir>`). Previously the pasted setup script
checked `pwd` against `$HOME/<workspace_dir>` and would warn
"Foundry AI is normally installed in ~/FoundryAI" even though the
/home page had just sent the user to `~/Desktop/FoundryAI`.
- Pre-login pages (`/login`, magic-link screens, first-time `/setup`) - Pre-login pages (`/login`, magic-link screens, first-time `/setup`)
now honour the configured `instance.theme`. `base_login.html` sets now honour the configured `instance.theme`. `base_login.html` sets
`<html data-theme="...">` from `instance_theme`, additionally loads `<html data-theme="...">` from `instance_theme`, additionally loads

View file

@ -343,7 +343,7 @@ _GWS_PROMPT_TAIL_TEMPLATE = """
7. Find where gws stored my credentials (`gws auth status` should show the path; typically ~/.config/gws/ on Unix, %APPDATA%\\gws\\ on Windows). chmod 600 on Unix; on native Windows, restrict ACLs to my user with `icacls "$creds_path" /inheritance:r /grant:r "$env:USERNAME:F"` file is already in my user profile so this needs no admin. 7. Find where gws stored my credentials (`gws auth status` should show the path; typically ~/.config/gws/ on Unix, %APPDATA%\\gws\\ on Windows). chmod 600 on Unix; on native Windows, restrict ACLs to my user with `icacls "$creds_path" /inheritance:r /grant:r "$env:USERNAME:F"` file is already in my user profile so this needs no admin.
8. Verify with two low-impact reads, one per scope group: `gws drive files list --params '{{"pageSize": 1}}'` (Drive scope landed) and `gws chat spaces list --params '{{"pageSize": 1}}'` (Chat scope landed). If both return 200 with valid JSON, print ` Google Workspace ready connected as <my email>. <N> drive file(s), <M> chat space(s) visible.` (exact prefix the final summary grep for it). On any failure, print ` Google Workspace setup failed: <which call failed (drive|chat)>, HTTP/status <code>. <one-line hint to fix (rotate creds | rerun gws auth login --full | etc.)>.` and stop. Never echo tokens, file/message metadata, or scope strings to chat. 8. Verify with two low-impact reads, one per scope group: `gws drive files list --params '{{"pageSize": 1}}'` (Drive scope landed) and `gws chat spaces list --params '{{"pageSize": 1}}'` (Chat scope landed). Treat exit code 0 from each invocation as success do NOT pipe gws output into `python3 -c 'f"..."'` (f-string expressions reject backslashes in Python <3.12, so escaping `\\"files\\"` inside a shell-quoted f-string raises SyntaxError) and do NOT call `json.load(sys.stdin)` on the raw stream (gws may emit log lines or a banner before the JSON body, which trips `JSONDecodeError`). If you really need to count rows for diagnostics, write the stdout to a temp file first and parse it with a plain `json.loads(open(path).read())` inside a `try/except`. If both calls exit 0, print ` Google Workspace ready connected as <my email from `gws auth status`>. Drive + Chat scopes verified.` (exact prefix the final summary grep for it). On any failure, print ` Google Workspace setup failed: <which call failed (drive|chat)>, exit <code>. <one-line hint to fix (rotate creds | rerun gws auth login --full | etc.)>.` and stop. Never echo tokens, file/message metadata, or scope strings to chat.
9. Remind me how to revoke later: `gws auth logout` clears local creds; the OAuth grant also appears at https://myaccount.google.com/permissions for Google-side revocation.""" 9. Remind me how to revoke later: `gws auth logout` clears local creds; the OAuth grant also appears at https://myaccount.google.com/permissions for Google-side revocation."""

View file

@ -337,19 +337,19 @@ def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]:
"""Steps 2-4 — workspace folder check, then `agnes init` + smoke verify. """Steps 2-4 — workspace folder check, then `agnes init` + smoke verify.
Step 2 verifies the user is already cd'd into the workspace folder Step 2 verifies the user is already cd'd into the workspace folder
that the /home onboarding page's visible "Step 2 — create your that the /home onboarding page's visible "Step 2 — pick a folder"
workspace folder" told them to create manually (`mkdir -p told them to create manually (`mkdir -p ~/Desktop/{workspace_dir}
~/{workspace_dir} && cd ~/{workspace_dir}`). The pasted script && cd ~/Desktop/{workspace_dir}`). The pasted script DOES NOT
DOES NOT auto-create the folder that would silently override an auto-create the folder that would silently override an
intentional choice to install at a different path (e.g. the user intentional choice to install at a different path (e.g. the user
cd'd to `~/work/agnes-prod` on purpose). Instead we `pwd`, compare cd'd to `~/work/agnes-prod` on purpose). Instead we `pwd`, compare
to `$HOME/{workspace_dir}`, and on mismatch warn loudly and ask the to `$HOME/Desktop/{workspace_dir}`, and on mismatch warn loudly and
user to either re-paste from the right folder or explicitly confirm ask the user to either re-paste from the right folder or explicitly
"install here" in the current cwd. confirm "install here" in the current cwd.
`{workspace_dir}` and `{instance_brand}` are placeholders pre-substituted `{workspace_dir}` and `{instance_brand}` are placeholders pre-substituted
by :func:`resolve_lines` from the operator-configured brand. Defaults by :func:`resolve_lines` from the operator-configured brand. Defaults
keep `~/Agnes` behavior for instances that don't set the brand knob. keep `~/Desktop/Agnes` behavior for instances that don't set the brand knob.
`agnes init --workspace .` continues to use the current cwd, so once `agnes init --workspace .` continues to use the current cwd, so once
step 2 has confirmed (or the user has explicitly accepted) the step 2 has confirmed (or the user has explicitly accepted) the
@ -373,24 +373,24 @@ def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]:
return [ return [
"", "",
"2) Verify the user is already in the workspace folder.", "2) Verify the user is already in the workspace folder.",
" The /home page's visible \"Step 2 — create your workspace folder\"", " The /home page's visible \"Step 2 — pick a folder\"",
" already asked the user to run", " already asked the user to run",
" mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}", " mkdir -p ~/Desktop/{workspace_dir} && cd ~/Desktop/{workspace_dir}",
" in their terminal BEFORE pasting this script. Do NOT silently", " in their terminal BEFORE pasting this script. Do NOT silently",
" re-create the folder here — the user may have intentionally cd'd", " re-create the folder here — the user may have intentionally cd'd",
" to a different location to install {instance_brand} there instead.", " to a different location to install {instance_brand} there instead.",
"", "",
" Run:", " Run:",
" pwd", " pwd",
" Expected: $HOME/{workspace_dir} (i.e. ~/{workspace_dir})", " Expected: $HOME/Desktop/{workspace_dir} (i.e. ~/Desktop/{workspace_dir})",
"", "",
" If `pwd` matches the expected path: continue silently to step 3.", " If `pwd` matches the expected path: continue silently to step 3.",
"", "",
" If `pwd` does NOT match, STOP and tell the user verbatim:", " If `pwd` does NOT match, STOP and tell the user verbatim:",
"", "",
" \"You are in <current-pwd>, but {instance_brand} is normally", " \"You are in <current-pwd>, but {instance_brand} is normally",
" installed in ~/{workspace_dir} (see /home Step 2). Either run", " installed in ~/Desktop/{workspace_dir} (see /home Step 2). Either run",
" mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}", " mkdir -p ~/Desktop/{workspace_dir} && cd ~/Desktop/{workspace_dir}",
" in your terminal now and re-paste this setup script, OR reply", " in your terminal now and re-paste this setup script, OR reply",
" 'install here' to install {instance_brand} in <current-pwd>", " 'install here' to install {instance_brand} in <current-pwd>",
" instead. Reply 'abort' to stop.\"", " instead. Reply 'abort' to stop.\"",
@ -549,7 +549,7 @@ def _restart_claude_lines(step_num: str) -> list[str]:
return [ return [
"", "",
f"{step_num}) Restart Claude Code so every plugin, MCP server, and SessionStart hook installed above actually loads:", f"{step_num}) Restart Claude Code so every plugin, MCP server, and SessionStart hook installed above actually loads:",
" Tell me to type `/exit` (or close the Claude Code session entirely), then run `claude` again from this same directory — the install dir confirmed in step 2 (`~/{workspace_dir}` on the default path, or whatever cwd the user explicitly accepted with 'install here').", " Tell me to type `/exit` (or close the Claude Code session entirely), then run `claude` again from this same directory — the install dir confirmed in step 2 (`~/Desktop/{workspace_dir}` on the default path, or whatever cwd the user explicitly accepted with 'install here').",
" The next session boots with all marketplace plugins, every connector's keychain entries / OAuth grants, and the agnes-welcome + refresh-marketplace SessionStart hooks active. This is the last action before the Confirm summary — once I'm back in Claude Code, setup is complete.", " The next session boots with all marketplace plugins, every connector's keychain entries / OAuth grants, and the agnes-welcome + refresh-marketplace SessionStart hooks active. This is the last action before the Confirm summary — once I'm back in Claude Code, setup is complete.",
] ]

View file

@ -2485,7 +2485,7 @@ What can I help you with?
{% endwith %} {% endwith %}
</div> </div>
<div class="install-note"> <div class="install-note">
The preview above is the exact text the button copies; the placeholder is replaced with a real token at click time. Don't create <code>~/{{ workspace_dir }}/Projects/</code> manually — the bundled plugin offers to set it up after install. The preview above is the exact text the button copies; the placeholder is replaced with a real token at click time. Don't create <code>~/Desktop/{{ workspace_dir }}/Projects/</code> manually — the bundled plugin offers to set it up after install.
</div> </div>
</details> </details>
</div> </div>
@ -2607,7 +2607,7 @@ What can I help you with?
{% if onboarded %} {% if onboarded %}
{# Offboarding escape hatch shown only after the hero has disappeared. {# Offboarding escape hatch shown only after the hero has disappeared.
Lets the analyst (e.g. after wiping ~/{{ workspace_dir }}) flip the Lets the analyst (e.g. after wiping ~/Desktop/{{ workspace_dir }}) flip the
users.onboarded boolean back to false so the full install hero users.onboarded boolean back to false so the full install hero
renders again on next reload. Discrete by design — onboarded renders again on next reload. Discrete by design — onboarded
users land on /home expecting the nav hub, not a setup screen. #} users land on /home expecting the nav hub, not a setup screen. #}

View file

@ -114,7 +114,7 @@
<div class="eyebrow">Welcome back, {{ display_name }}</div> <div class="eyebrow">Welcome back, {{ display_name }}</div>
<h1>You're all set up</h1> <h1>You're all set up</h1>
<p> <p>
Open Claude Code in any project under <code>~/{{ workspace_dir }}/Projects/</code> Open Claude Code in any project under <code>~/Desktop/{{ workspace_dir }}/Projects/</code>
and start a session — your data and plugins are already synced. Use the cards below to jump into the parts of {{ instance_brand }} you need. and start a session — your data and plugins are already synced. Use the cards below to jump into the parts of {{ instance_brand }} you need.
</p> </p>
</div> </div>

View file

@ -137,8 +137,8 @@ class TestInstanceBrand:
assert "Set up the Foundry AI CLI on this machine." in joined assert "Set up the Foundry AI CLI on this machine." in joined
# Step 2 is a pwd-check now (no auto-mkdir); brand + workspace_dir # Step 2 is a pwd-check now (no auto-mkdir); brand + workspace_dir
# thread through the warning copy + expected-path string. # thread through the warning copy + expected-path string.
assert "$HOME/FoundryAI" in joined assert "$HOME/Desktop/FoundryAI" in joined
assert "mkdir -p ~/FoundryAI && cd ~/FoundryAI" in joined assert "mkdir -p ~/Desktop/FoundryAI && cd ~/Desktop/FoundryAI" in joined
assert "Bootstrap your Foundry AI workspace" in joined assert "Bootstrap your Foundry AI workspace" in joined
assert "Foundry AI workspace is ready" in joined assert "Foundry AI workspace is ready" in joined
# No raw placeholders survive substitution. # No raw placeholders survive substitution.
@ -148,15 +148,15 @@ class TestInstanceBrand:
def test_default_brand_keeps_agnes_branding(self, tmp_path, monkeypatch): def test_default_brand_keeps_agnes_branding(self, tmp_path, monkeypatch):
"""Backwards-compat: callers that don't pass brand/workspace_dir """Backwards-compat: callers that don't pass brand/workspace_dir
get the literal 'Agnes' / '~/Agnes' rendering.""" get the literal 'Agnes' / '~/Desktop/Agnes' rendering."""
mod = self._reload(tmp_path, monkeypatch) mod = self._reload(tmp_path, monkeypatch)
from app.web.setup_instructions import resolve_lines from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines("agnes.whl")) joined = "\n".join(resolve_lines("agnes.whl"))
assert "Set up the Agnes CLI on this machine." in joined assert "Set up the Agnes CLI on this machine." in joined
# Step 2 is a pwd-check (no auto-mkdir); default path threads # Step 2 is a pwd-check (no auto-mkdir); default path threads
# through as `$HOME/Agnes` + the warning's manual-mkdir example. # through as `$HOME/Desktop/Agnes` + the warning's manual-mkdir example.
assert "$HOME/Agnes" in joined assert "$HOME/Desktop/Agnes" in joined
assert "mkdir -p ~/Agnes && cd ~/Agnes" in joined assert "mkdir -p ~/Desktop/Agnes && cd ~/Desktop/Agnes" in joined
assert "Bootstrap your Agnes workspace" in joined assert "Bootstrap your Agnes workspace" in joined
assert "Agnes workspace is ready" in joined assert "Agnes workspace is ready" in joined
mod._instance_config = None mod._instance_config = None

View file

@ -921,7 +921,7 @@ def test_restart_claude_step_emitted_unconditionally():
def test_restart_claude_substitutes_workspace_dir(): def test_restart_claude_substitutes_workspace_dir():
"""The restart-claude body interpolates the workspace folder so the """The restart-claude body interpolates the workspace folder so the
user sees their actual `~/<brand>` path, not a literal placeholder.""" user sees their actual `~/Desktop/<brand>` path, not a literal placeholder."""
from app.web.setup_instructions import resolve_lines from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines( joined = "\n".join(resolve_lines(
@ -930,7 +930,7 @@ def test_restart_claude_substitutes_workspace_dir():
workspace_dir="FoundryAI", workspace_dir="FoundryAI",
)) ))
assert "9) Restart Claude Code" in joined assert "9) Restart Claude Code" in joined
assert "~/FoundryAI" in joined assert "~/Desktop/FoundryAI" in joined
assert "{workspace_dir}" not in joined assert "{workspace_dir}" not in joined
@ -1030,12 +1030,12 @@ def test_step_2_checks_pwd_does_not_auto_mkdir():
# `pwd` check + expected-path comparison must be present. # `pwd` check + expected-path comparison must be present.
assert "pwd" in joined assert "pwd" in joined
assert "$HOME/Agnes" in joined # default workspace_dir assert "$HOME/Desktop/Agnes" in joined # default workspace_dir under Desktop
assert "~/Agnes" in joined assert "~/Desktop/Agnes" in joined
# Warning copy that references the /home Step 2 manual mkdir line. # Warning copy that references the /home Step 2 manual mkdir line.
assert "/home Step 2" in joined assert "/home Step 2" in joined
assert "mkdir -p ~/Agnes && cd ~/Agnes" in joined assert "mkdir -p ~/Desktop/Agnes && cd ~/Desktop/Agnes" in joined
# Explicit "install here" / "abort" decision tree. # Explicit "install here" / "abort" decision tree.
assert "'install here'" in joined assert "'install here'" in joined
@ -1060,13 +1060,13 @@ def test_step_2_warning_substitutes_custom_brand():
workspace_dir="FoundryAI", workspace_dir="FoundryAI",
)) ))
assert "2) Verify the user is already in the workspace folder." in joined assert "2) Verify the user is already in the workspace folder." in joined
assert "$HOME/FoundryAI" in joined assert "$HOME/Desktop/FoundryAI" in joined
assert "~/FoundryAI" in joined assert "~/Desktop/FoundryAI" in joined
assert "mkdir -p ~/FoundryAI && cd ~/FoundryAI" in joined assert "mkdir -p ~/Desktop/FoundryAI && cd ~/Desktop/FoundryAI" in joined
# Brand + workspace_dir thread through the warning copy (the text # Brand + workspace_dir thread through the warning copy (the text
# wraps across two lines so we check the substrings separately). # wraps across two lines so we check the substrings separately).
assert "but Foundry AI is normally" in joined assert "but Foundry AI is normally" in joined
assert "installed in ~/FoundryAI" in joined assert "installed in ~/Desktop/FoundryAI" in joined
# No placeholders survive into the rendered text. # No placeholders survive into the rendered text.
assert "{workspace_dir}" not in joined assert "{workspace_dir}" not in joined
assert "{instance_brand}" not in joined assert "{instance_brand}" not in joined
@ -1084,7 +1084,7 @@ def test_step_9_restart_references_install_dir_not_hardcoded():
# Wording references the step-2 confirmation. # Wording references the step-2 confirmation.
assert "install dir confirmed in step 2" in joined assert "install dir confirmed in step 2" in joined
# Default path still mentioned as the expected baseline. # Default path still mentioned as the expected baseline.
assert "~/Agnes" in joined assert "~/Desktop/Agnes" in joined
# The "install here" callout in the restart step keeps the user-flow # The "install here" callout in the restart step keeps the user-flow
# connection visible. # connection visible.
assert "'install here'" in joined assert "'install here'" in joined