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.
### 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`)
now honour the configured `instance.theme`. `base_login.html` sets
`<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.
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."""

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.
Step 2 verifies the user is already cd'd into the workspace folder
that the /home onboarding page's visible "Step 2 — create your
workspace folder" told them to create manually (`mkdir -p
~/{workspace_dir} && cd ~/{workspace_dir}`). The pasted script
DOES NOT auto-create the folder that would silently override an
that the /home onboarding page's visible "Step 2 — pick a folder"
told them to create manually (`mkdir -p ~/Desktop/{workspace_dir}
&& cd ~/Desktop/{workspace_dir}`). The pasted script DOES NOT
auto-create the folder that would silently override an
intentional choice to install at a different path (e.g. the user
cd'd to `~/work/agnes-prod` on purpose). Instead we `pwd`, compare
to `$HOME/{workspace_dir}`, and on mismatch warn loudly and ask the
user to either re-paste from the right folder or explicitly confirm
"install here" in the current cwd.
to `$HOME/Desktop/{workspace_dir}`, and on mismatch warn loudly and
ask the user to either re-paste from the right folder or explicitly
confirm "install here" in the current cwd.
`{workspace_dir}` and `{instance_brand}` are placeholders pre-substituted
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
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 [
"",
"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",
" 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",
" re-create the folder here — the user may have intentionally cd'd",
" to a different location to install {instance_brand} there instead.",
"",
" Run:",
" 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` does NOT match, STOP and tell the user verbatim:",
"",
" \"You are in <current-pwd>, but {instance_brand} is normally",
" installed in ~/{workspace_dir} (see /home Step 2). Either run",
" mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}",
" installed in ~/Desktop/{workspace_dir} (see /home Step 2). Either run",
" mkdir -p ~/Desktop/{workspace_dir} && cd ~/Desktop/{workspace_dir}",
" in your terminal now and re-paste this setup script, OR reply",
" 'install here' to install {instance_brand} in <current-pwd>",
" instead. Reply 'abort' to stop.\"",
@ -549,7 +549,7 @@ def _restart_claude_lines(step_num: str) -> list[str]:
return [
"",
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.",
]

View file

@ -2485,7 +2485,7 @@ What can I help you with?
{% endwith %}
</div>
<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>
</details>
</div>
@ -2607,7 +2607,7 @@ What can I help you with?
{% if onboarded %}
{# 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
renders again on next reload. Discrete by design — onboarded
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>
<h1>You're all set up</h1>
<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.
</p>
</div>

View file

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

View file

@ -921,7 +921,7 @@ def test_restart_claude_step_emitted_unconditionally():
def test_restart_claude_substitutes_workspace_dir():
"""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
joined = "\n".join(resolve_lines(
@ -930,7 +930,7 @@ def test_restart_claude_substitutes_workspace_dir():
workspace_dir="FoundryAI",
))
assert "9) Restart Claude Code" in joined
assert "~/FoundryAI" in joined
assert "~/Desktop/FoundryAI" 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.
assert "pwd" in joined
assert "$HOME/Agnes" in joined # default workspace_dir
assert "~/Agnes" in joined
assert "$HOME/Desktop/Agnes" in joined # default workspace_dir under Desktop
assert "~/Desktop/Agnes" in joined
# Warning copy that references the /home Step 2 manual mkdir line.
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.
assert "'install here'" in joined
@ -1060,13 +1060,13 @@ def test_step_2_warning_substitutes_custom_brand():
workspace_dir="FoundryAI",
))
assert "2) Verify the user is already in the workspace folder." in joined
assert "$HOME/FoundryAI" in joined
assert "~/FoundryAI" in joined
assert "mkdir -p ~/FoundryAI && cd ~/FoundryAI" in joined
assert "$HOME/Desktop/FoundryAI" in joined
assert "~/Desktop/FoundryAI" in joined
assert "mkdir -p ~/Desktop/FoundryAI && cd ~/Desktop/FoundryAI" in joined
# Brand + workspace_dir thread through the warning copy (the text
# wraps across two lines so we check the substrings separately).
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.
assert "{workspace_dir}" 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.
assert "install dir confirmed in step 2" in joined
# 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
# connection visible.
assert "'install here'" in joined