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:
commit
eb75c8d204
7 changed files with 51 additions and 34 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. #}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue