Tags: SAP/astonish
Tags
fix(tui): re-auth xAI OAuth and reload the live provider (#546) When token refresh fails or model listing returns 401/403, code mode asks the user to re-authenticate instead of showing a raw error. After approval, rebuild the active provider so the new tokens are used instead of looping on the dead session.
Session durability: scoped rollback, resilient resume, and restart-pr… …oof plan lifecycle (#516) * fix: scope checkpoint rollback to compaction epoch and git HEAD Two bugs caused rollback to restore files from arbitrarily old states: 1. Compaction resets event indices but leaves old checkpoint turn files under the active session's directory. After compaction, old turn-0005 from days ago coexists with new turn-0005 from today, and rollback applies the wrong snapshots. 2. Checkpoints record no git branch/HEAD, so snapshots captured on one branch get restored when the user is on another. Fixes: - Add ResetSession() to clear checkpoint turn files after compaction archives events. Called from both compactToChild and the mid-turn SetPersistCompacted callback. - Record git HEAD hash in each turnCheckpoint at capture time. RestoreTo skips snapshots whose HEAD differs from the current HEAD. Empty HEAD (no git / legacy checkpoints) is treated as compatible for backward compat. - Add TestCheckpointStore_ResetSession and TestCheckpointStore_GitHEADFiltering. * fix: auto-refresh xAI OAuth token on expiry for model listing and 401/403 Three bugs prevented automatic token renewal when the xAI OAuth access token expired: 1. Model listing (ListModelsForProvider) passed the raw access_token from config to xai_oauth.ListModels with no refresh logic. The model picker showed 403 'bad-credentials' when the token was stale. 2. The oauthTransport's proactive refresh skipped when expiresAt was zero (missing from config or unparseable). The condition required !expiresAt.IsZero(), so a zero value meant 'don't refresh'. 3. The transport had no reactive retry. If xAI rejected a token that the client clock said was valid (clock skew, early server-side revocation), the 401/403 propagated as 'openai: 0' to the user. Fixes: - Add maybeRefreshXAIOAuthToken to factory.go: refreshes the token before model listing when expired or about to expire, persists the new tokens via ProviderTokenRefresh. - Fix oauthTransport proactive check: treat zero expiresAt as 'expired' (refresh is needed, not skipped). - Add reactive retry in RoundTrip: on 401/403 with a refresh token available, refresh once and retry the request. Extract doRefreshLocked helper to avoid duplicating refresh logic. - Add TestOAuthTransport_ReactiveRetryOn401, _ReactiveRetryOn403, and _ZeroExpiresAtTriggersRefresh. * feat(provider): add GitHub Copilot as an OAuth provider Adds a copilot_oauth provider following the xai_oauth pattern: - New pkg/provider/copilot_oauth package implementing the GitHub device-code flow, the two-step Copilot token exchange (ghu_ token -> short-lived session token), an auto-refreshing transport that injects the Copilot-required headers and retries once on 401/403, OpenAI-compatible model listing, and a provider wrapping openai_provider against api.githubcopilot.com. - Registers copilot_oauth in the provider factory (GetProvider, ListModelsForProvider, TestProviderConnection, display names) and in config known provider types. - Adds CopilotOAuthPending / CopilotOAuthBackend to the TUI backend and wires the two-phase device-code flow through the code-mode backend and the /provider model picker overlay. - Adds the provider to the CLI setup wizard and the Studio SetupWizard / ProviderModelSelector. The xai_oauth package and XAIOAuthBackend interface are unchanged. * fix(session): tolerate oversized transcript lines on resume ReadEvents replaced bufio.Scanner with a bounded bufio.Reader loop that skips oversized lines and appends a warning event instead of discarding the whole transcript. loadHistory now propagates load errors so the /sessions overlay renders a failure instead of an empty body, and LatestDescendant treats an unreadable tip like a missing one. Adds Transcript.ScanOversized/RepairOversized plus an 'astonish sessions repair <id>' subcommand, and a 256KB byte cap on read_file so a single pathological line can never be persisted again. * Make plan completion structural; drop plan-level verification execution announce_completion executed the plan's free-text `verification` field line by line through bash. Prose lines exited non-zero, and because the plan is sealed after approval the text could not be edited -- every retry failed identically, stranding the session in a verify_failed loop. Completion is not a judgment. If every phase is complete, the plan is complete: each phase already ran its own verify and recorded exit code plus output via RecordEvidence. AnnounceCompletion now gates on AllStepsComplete() alone and builds `## Results` by aggregating that existing per-phase evidence. No command runs. verificationCommands and PlanCompletionVerifyFailed are deleted. The end-to-end check moves to where it belongs -- a phase. ValidateAnnouncedPlan now rejects a plan that touches a running surface without any verify_kind=behavior phase, so the gap surfaces at announce time while the plan is still editable and a failure marks that phase failed (recoverable) instead of dead-ending completion. Library-only plans are unaffected and may remain all-unit. `verification` stays required, now purely as the narrative acceptance story for a human reviewer and for the Graph-Optimized acceptance gate. It is never passed to a shell again. Prompts, tool schemas, and the three documents describing the old protocol are updated to match. Per-phase verification is untouched: ApplyPlanStepUpdate, runPlanVerify, DefaultPlanVerify, RecordEvidence, and the PlanVerifyFailed research-cap lift all behave exactly as before. * Make plan lifecycle survive restart and compaction Approving a plan sealed it only in memory (ChatAgent.activePlanApproved), backed by a session event whose entire payload is a StateDelta. Compaction dropped that event, so after a compaction plus a restart a half-finished approved plan was no longer recognized as active and announce_completion returned no_active_plan. PLAN.md is now the source of truth for lifecycle: - PlanDocumentInfo gains Lifecycle, rendered/parsed as a `## Status` section with typed constants (approved/executing/completed). Unknown values normalize to empty so a corrupted document cannot seal a plan. - MarkActivePlanApproved persists the seal via PlanState.SetLifecycle; RestoreApprovedPlan re-seals from the document instead of guessing. - shouldContinueApprovedPlan prefers the document over session state, keeping the session-state fallback for documents written by older binaries. ArchiveAndReplaceEvents now carries StateDelta-only events (nil Content) forward in original order, so plan lifecycle and routing decisions survive compaction. This also fixes routing badges, which loadHistory matches positionally and which partial loss misaligns. Backward compatible: a PLAN.md with no `## Status` parses exactly as before. Verified by an integration test driving approve -> compact -> restart -> resume -> announce_completion against a real FileStore and fresh ChatAgent, plus ordering and backward-compat tests in pkg/agent and pkg/session. * Address PR review: failing provider test, retry body rewind, dead case - pkg/provider/factory_test.go: TestGetProviderIDs still asserted 13 registered providers after copilot_oauth was added, so pkg/provider failed on HEAD. Bumped to 14 with a note to update on registration. - {copilot,xai}_oauth/transport.go: rewind the request body from req.GetBody before the 401/403 retry. net/http already replays a GetBody-backed body on a retried clone, so this was not a live defect, but relying on that implicitly is fragile — the rewind makes the contract explicit and is covered by new POST-body retry tests. - cmd/astonish/sessions.go: drop the unreachable "repair" switch case (handled earlier, before remote delegation). Reviewed but intentionally not changed: RestoreTo deletes checkpoint files for turns skipped by the git-HEAD guard. That is asserted behavior in TestCheckpointStore_GitHEADFiltering ("cleanup"), not an oversight; changing it is a design decision for a separate discussion.
fix(cli): authenticate remote chat with OAuth PKCE (#514) * fix(cli): issue Astonish OAuth tokens from astonish login Studio /api/studio/sessions rejects platform JWTs as OAuth bearers. Switch CLI login to Authorization Code + PKCE with a first-party astonish-cli public client and loopback redirect, and refresh those tokens at /oauth/token. * fix(cli): allow login to recover stale remote credentials * fix(cli): probe Studio endpoint for OAuth login * fix(tui): clear stale frame when switching modes * fix(cli): tighten OAuth scopes and identity display
fix(oauth): start Kubernetes daemons without missing issuer or tables (… …#513) OAuth is enabled by default, but Helm never wrote issuer/resource into config.yaml and Postgres auto-migrate never created the OAuth tables. New API/worker pods then crashed after embedding init. Apply documented loopback issuer/resource defaults, render oauth_server from Helm (including ingress host derivation), and include the five OAuth tables in the Postgres Schema.Create whitelist.
feat(extension): Chrome MV3 side-panel extension — full implementation ( #510) * extension: remove duplicate New button, add delete session feature - Remove redundant 'New' button from session bar (duplicate of 'New chat' option in dropdown) - Clean up dead JavaScript references (newSessionButton variable, event listeners, disabled toggles) - Remove dead CSS rule for #new-session - Add deleteExtensionSession() function to extension-sessions.ts for per-session deletion - Add trash button (🗑️) to session bar that: - Prevents deletion of 'New chat' (empty session) - Shows confirmation dialog - Deletes session and switches to next available or 'New chat' - Properly disables during streaming - All 84 tests pass, build clean * feat(extension): cross-origin iframe piercing via allFrames + frame coordinator - Add chrome.webNavigation permission to enumerate frames - Add allFrames: true to content script injection in service worker - Add MSG_FRAME_TOOL message type for frame relay - Add getAccessibleDocuments() in dom-tools to traverse same-origin iframes - Update collectCandidates, snapshot to query all accessible documents - Add frame detection and iframe source tagging in snapshot output - Add frame fan-out in service worker: getAllFrameIds, sendToFrame (with retry), mergeFrameResults - Add per-frame DOM tool execution: runPageToolInFrame, collectCandidatesFrameLocal, snapshotFrameInteractive, queryFrame - Add iframeSource() helper to tag refs from iframe elements - Add INTERACTIVE selector support for [tabindex="0"] (calendar day cells, SAPUI5 components) - Update architecture docs to document iframe support (same-origin traversal, cross-origin skip) - All 90 tests pass; both Vite bundles build successfully The extension now discovers and interacts with elements inside same-origin iframes. For SAP SAPUI5 calendars with day cells in cross-origin iframe, refs now appear tagged with (iframe: ...) and are clickable/fillable via page_click, page_fill, etc. * feat(extension): CDP-based page_click and page_fill via chrome.debugger - Add extension/src/background/cdp-input.ts with cdpClick() and cdpFill() using chrome.debugger.attach/sendCommand/detach for real browser input events - Add 'debugger' permission to extension/manifest.json - Add getElementTabRect() helper in dom-tools.ts that walks iframe chain to produce tab-absolute coordinates for CDP dispatch - Update click() to return bounding rect instead of dispatching synthetic DOM events; DOM .click() kept only for zero-rect (hidden/jsdom) elements - Update fill() to return FillOutcome with rect; DOM .value= kept as belt-and-suspenders (so React/Angular work); CDP fills SAPUI5/custom inputs - Add rect field to ClickOutcome, FillOutcome, PageToolRun, FrameToolResult, PageToolResult types; FrameToolResult.ok made optional (unused by mergeFrameResults) - Update service-worker.ts to import cdpClick/cdpFill, pass rect through mergeFrameResults, and dispatch CDP input after runPageToolAllFrames - Update tests: add rect assertions to page_click/page_fill tests, add new zero-rect fallback test (25 tests pass) - Update docs/architecture/chrome-extension.md with CDP input dispatch section and corrected permission list All 91 tests pass; both Vite bundles build cleanly. * fix: cross-origin iframe coordinate translation for CDP clicks Track winning frameId through mergeFrameResults; in runPageToolInTab, when frameId != 0, query the top frame via chrome.scripting.executeScript to find the iframe element's bounding rect and add it as an offset to the element's iframe-local coordinates before CDP dispatch. The content script's getElementTabRect() cannot traverse frameElement for cross-origin iframes (SecurityError), so it returns only iframe-local coords. This fix closes the gap so Input.dispatchMouseEvent lands at the correct tab-absolute position for elements inside cross-origin iframes (e.g. SAP Fiori CAT2 calendar embedded in an iframe). URL matching uses origin+pathname comparison (not exact URL equality) to handle cases where the frame URL has query params the iframe src does not. * chore(extension): raise MAX_PAGE_TOOL_ROUNDS from 8 to 15 Multi-step tasks like SAP CAT2 booking (snapshot → click day → snapshot → click task → snapshot → fill → save) can easily exceed 8 rounds. 15 gives enough room for complex workflows while still providing a safety ceiling. * build: rebuild extension bundles after MAX_PAGE_TOOL_ROUNDS increase * feat(extension): add stop button and disable input during streaming - Add a red stop button (filled square icon) inside the composer form, matching the Studio chat's variant=destructive stop button styling - Show the stop button while streaming; hide it when idle - Disable the textarea and show 'Agent is responding…' placeholder while streaming, matching the Studio chat behavior - On stop: abort the SSE connection, call POST /sessions/{id}/stop to kill the backend runner, and show a 'Stopped by user.' notice - Add stopChat() to astonish-client.ts for the stop API call - Add .stop-btn CSS class and .composer textarea:disabled opacity * theme(extension): switch from Nova to Classic color theme Update CSS variables to use Astonish Classic theme (indigo/blue) instead of Nova (pink/purple). This is the default theme and makes the extension consistent with the rest of the Astonish UI. Changes: - --brand: #ff6b9d → #6366f1 (Nova pink → Classic indigo) - --background: #160b1f → #0f172a (Nova dark → Classic dark slate) - --card: #1d0f28 → #1e293b - --foreground: #fceef7 → #f1f5f9 - --text-secondary/muted: Nova mauve → Classic slate - --danger: #f05252 → #ef4444 - --info: #b478ff → #a78bfa (purple → violet) * Fix: Stop re-injecting extension page-tool instructions on every round The EXTENSION_PAGE_TOOLS_INSTRUCTIONS were being appended to the context on every page-tool round, causing the system instructions to be echoed back to the user in the visible chat. The instructions are already part of the initial buildSystemContext() call, so they should NOT be re-appended after each tool execution. After page tools run, context should contain only formatPageToolResults(), not the full instructions again. Fixes the verbose 'user[timestamp] The Chrome extension ran...' leak. * feat(extension): add Astonish icon to extension Add Astonish logo as the extension icon in three sizes: - icon-16.png (16x16 for Chrome toolbar) - icon-48.png (48x48 for Chrome menu) - icon-128.png (128x128 for Chrome Web Store) Icons are exported from the Astonish logo SVG and configured in: - manifest.json: action.default_icons and icons sections - vite.config.ts: updated icon path handling for build The extension toolbar button now displays the Astonish branding instead of a generic placeholder icon. * fix(extension): render markdown tables and group tool notices Two UI defects in the extension side panel: 1. Markdown tables showed raw pipe syntax instead of rendered HTML tables. The renderMarkdown() parser now handles table rows (| col | col |), separator rows (| --- | --- |), and emits proper <table>/<thead>/<tbody> elements. Table CSS was already in panel.css; added overflow-x: auto for wide tables in the narrow side panel. 2. Tool call/result notices appeared as separate bubbles cluttering the transcript. Now they are grouped into a single collapsible 'Used N tools' block (matching Studio chat's tool fold pattern). Click to expand shows individual tool names. Both streaming (live) and history rendering use the same grouping. Added 2 new tests for table rendering (93 total tests passing). * feat(extension): per-tab session isolation in side panel * fix(extension): use service worker to communicate tab ID to side panel The previous approach of using chrome.tabs.query() from the side panel context was unreliable and would often return tabId=0, causing all tabs to share the same session storage slot. This fix uses chrome.runtime.sendMessage() to have the service worker directly send the tab ID to the side panel based on the sender context, which is reliable and works correctly across all tabs. * fix(extension): inline extension-sessions into sidepanel bundle; fix tab ID resolution - vite.config.ts: manualChunks routes extension-sessions and messages into the sidepanel entry chunk, eliminating the 'cross-world extension resource mismatch' modulepreload warning Chrome was showing in the extension errors panel - service-worker.ts: MSG_GET_TAB_ID handler now queries active tab via chrome.tabs.query (lastFocusedWindow) instead of reading sender.tab which is undefined for side panel extension pages — fixes the tabId always being 0 and causing all tabs to share the same session storage slot * fix(extension): strip modulepreload links from sidepanel HTML Chrome MV3 flags modulepreload for extension resources as a 'cross-world extension resource mismatch' error. The chunks still load correctly via dynamic imports, but the preload tags cause console warnings. Strip them out in the build plugin's HTML post-processing step. * debug: add console logging to tab ID resolution * fix(extension): use chrome.storage.session for per-tab session isolation Instead of manually keying sessions by tabId in chrome.storage.local, use chrome.storage.session which is automatically isolated per browser tab by Chrome. This is simpler, more reliable, and handles tab cleanup automatically (session storage is cleared when a tab closes). Benefits: - No manual tab ID resolution needed - No message passing between service worker and side panel - No service worker message listener required - Each tab's side panel gets its own storage context by default - Stale entries auto-cleaned when tab closes Changed extension-sessions.ts to use session storage with a single state key instead of a per-tab map in local storage. Updated tests accordingly. * cleanup: remove debug console logging * fix(extension): embed tabId in panel URL for reliable per-tab session isolation All previous approaches failed because there is no API to get a tab ID from inside a side panel page context. The correct solution (per Chrome docs and Chromium extensions mailing list) is to embed the tabId in the side panel URL as a query parameter when the service worker opens the panel. Changes: - service-worker.ts: Disable openPanelOnActionClick (which suppresses action.onClicked). Use chrome.action.onClicked to explicitly open the panel with path 'sidepanel.html?tabId=X', so each tab gets its own URL. - main.ts: Read tabId synchronously from location.search — zero async, zero message passing, 100% reliable. - extension-sessions.ts: Restored per-tab map keyed by tabId in chrome.storage.session (shared in-memory store, keyed by tabId). This gives real isolation: tab 42 reads map[42], tab 99 reads map[99]. - Tests: Restored all per-tab isolation tests including removeTabState and different-tabs tests. * fix(extension): make side panel per-tab like DevTools (F12) Remove default_path from manifest.json so the panel is not globally enabled. Set chrome.sidePanel.setOptions({ enabled: false }) globally. Only enable the panel per-tab when the user clicks the extension icon via action.onClicked. This makes the side panel behave like Chrome DevTools: - Click the icon on a tab → panel opens for that tab only - Switch to another tab → panel is not visible - Each tab that has the panel open has its own independent session Also cleaned up the now-unused MSG_GET_TAB_ID message handler since tab IDs are embedded in the panel URL. * fix(extension): page tools target the panel's tab, not the active tab The side panel now includes its tabId in every sendMessage call (MSG_GET_CONTEXT, MSG_PAGE_TOOL, MSG_APPLY). The service worker reads this tabId and uses chrome.tabs.get(tabId) instead of querying for the active tab. This prevents page tools from drifting to whichever tab the user is currently viewing when they switch away from the tab where the agent is working. * chore: ignore extension build output (dist/) * chore: remove extension/dist from git tracking * feat(extension): categorize extension sessions with dedicated appName - Add optional AppName field to StudioChatRequest; when set, sessions are created under that app name instead of the default 'astonish'. - Add optional ?app= query parameter to GET /api/studio/sessions; defaults to studioChatAppName for backward compatibility. - Define EXTENSION_APP_NAME = 'astonish-extension' constant in the extension. - connectChat now sends appName in the request body so new sessions are tagged. - fetchSessions now appends ?app=astonish-extension so the dropdown shows all extension sessions regardless of which tab created them, not just sessions whose IDs are in the tab's local storage. - Remove client-side filterExtensionSessions call from refreshExtensionSessions; server-side filtering via ?app= replaces it. Sessions created in another tab are now surfaced by adding backend-returned sessions missing from local state. - Add pkg/api/session_handlers_test.go testing StudioChatRequest appName field. * fix(extension): add side_panel.default_path to manifest Chrome MV3 requires the default_path field in the side_panel section. Without it, the extension fails to load with: 'Error at key side_panel.default_path. Manifest key is required.' * fix: propagate appName through session operations and query parameters This fixes critical app-name namespace defects that prevented the extension from reading, persisting, and deleting sessions correctly: Backend fixes: - session_handlers.go: StudioSessionHandler and StudioDeleteSessionHandler now read the optional ?app= query parameter (defaults to studioChatAppName for backward compatibility), so extension sessions (appName=astonish-extension) can be fetched and deleted using the correct namespace - chat_handlers.go: /new and /distill commands now use effectiveApp instead of hardcoded studioChatAppName - chat_utils.go, chat_runner.go: persistRunError and persistSessionMessage now accept an optional appName parameter (empty string uses context fallback), so slash commands persist events under the correct app namespace - chat_handlers.go: Added context helpers (getAppNameFromContext) and pass effectiveApp to handleSlashCommand so all session operations use the caller-provided app name Tests: - Updated handleSlashCommand test call to pass effectiveApp parameter Impact: - Extension sessions created under appName=astonish-extension are now accessible to all handlers that read/modify/delete them - Session history reload (GET detail) works correctly for extension sessions - Slash command persistence (/new, /distill) works with the correct app name - All existing clients (Studio chat) remain unaffected (backward-compatible) * fix: thread effectiveApp through ChatRunner and extension delete/history calls Critical #1+#2 (backend): - newChatRunner now accepts an appName parameter and injects it into cr.ctx via context.WithValue(ctx, appNameContextKey, appName). This makes the context fallback in persistRunError/persistSessionMessage live (previously appNameContextKey was defined and read but never set). - runner.New() now uses getAppNameFromContext(cr.ctx) instead of hardcoded studioChatAppName, so ADK transcript events are persisted under the same app namespace as the session record. - StudioChatHandler passes effectiveApp to newChatRunner. - All test call sites updated to pass studioChatAppName as the appName argument. Medium #3 (extension delete): - Added deleteSession() to astonish-client.ts — sends DELETE /api/studio/sessions/{id}?app=astonish-extension. - main.ts delete button handler now calls deleteSession() before deleteExtensionSession() (best-effort: local cleanup proceeds even on server error so the UI stays consistent). Medium #4 (extension history): - fetchSessionHistory() now appends ?app=astonish-extension to the GET URL so the backend reads from the correct session namespace. Tests: - fetchSessionHistory test updated to assert ?app= is present in the URL. - Two new tests for deleteSession: success (204) and error (404 → throws). * fix(ci+review): wire extension into CI, handler tests, sender check, trust docs - Fix CI Build check: use 'npm test' instead of 'npx vitest run' so the lockfile-pinned vitest@5.0.0 is used; fixes pre-existing TemplateDetail frontend test failure unrelated to PR #510 - Add build-extension job to build.yml: npm ci, tsc --noEmit, npm test, npm run build in extension/ on every push/PR - Wire build-extension into Makefile build-all so 'make build-all' includes the extension bundle - Expand session_handlers_test.go with TestStudioSessionsAppNameQueryParam (tests ?app= defaulting logic) and TestStudioChatRequestEffectiveApp (tests caller-appName vs studioChatAppName precedence) - Add sender.id === chrome.runtime.id guard to service-worker onMessage as defence-in-depth against future onMessageExternal exposure - Add 'Security and trust model' section to chrome-extension.md covering model-driven input + 15-round limit, https://* justification, debugger permission rationale, token storage trade-offs, and sender validation reasoning * fix(review): resolve CI typecheck, validate appName, fix use-before-decl - Add DOM.Iterable to tsconfig.json lib array — fixes all 10 tsc errors for NodeListOf/HTMLCollection iteration (Critical #1) - Add 'typecheck' npm script so local devs can run 'npm run typecheck' matching the CI gate; update build.yml to use it - Fix skipPageTools let-before-declaration in main.ts — move the declaration above its first use (found by tsc after DOM.Iterable fix) - Add validAppName regex (^[a-z][a-z0-9_-]{0,63}$) and enforce it in StudioSessionsHandler, StudioSessionHandler, StudioDeleteSessionHandler, and StudioChatHandler — blocks path traversal, namespace squatting, and unbounded key lengths (Medium #2) - Replace tautological session handler tests with tests that exercise the real validAppName regex, validate the resolution+validation code path end-to-end, and assert rejection of traversal/injection attempts (Medium #4) * fix(review): appName isolation at meta lookup, use npm ci, fix stale studioChatAppName - Defense-in-depth: after GetSessionMeta (which looks up by globally-unique session ID without appName filtering), verify meta.AppName == appName and return 404 on mismatch. Applied to StudioSessionHandler (platform + file store paths) and StudioDeleteSessionHandler (platform path). This makes ?app= a real isolation boundary rather than advisory. (Medium #1) - Replace hardcoded studioChatAppName with resolved appName in the personal-mode file-store paths of StudioSessionHandler: fleet transcript filepath.Join and the non-fleet fs.Get call. (Minor #1) - Use npm ci instead of npm install in Makefile build-extension target so the lockfile is never mutated by a local build. (Medium #3) * fix(review): reorder delete handler to verify namespace before fleet cleanup - Move fleet Stop()/Cleanup()/Unregister() to AFTER the app-namespace verification in both platform-mode and personal-mode delete paths. Previously the fleet registry teardown (which destroys sandbox containers) ran unconditionally before any namespace check. Now the handler verifies meta.AppName == appName first, and only then proceeds to stop the fleet session. (Medium #1) - Fail closed on GetSessionMeta errors in the platform-mode delete path: if the meta lookup errors or returns nil, return 404 instead of skipping the namespace check and falling through to Delete. (Medium #1) - Add namespace verification to the personal-mode delete path: check file store meta.AppName before fleet cleanup and workspace deletion. - Fix hardcoded studioChatAppName in personal-mode Delete call to use the resolved appName variable. (leftover from prior round) * fix(extension): interleave messages and tool groups chronologically - Refactor streamOnce() to track per-segment text: each text bubble only shows text from its current segment (since the last tool group), not the full accumulated assistant text. This fixes the regression where each new bubble showed all prior text concatenated. - Add hadToolsSinceText flag: when a tool_call fires, sets the flag; when the next text chunk arrives, isNewSegment is raised and segmentStart advances to the current position in the full text. - Update outer onText callback to create a new assistantBody when isNewSegment is true, so text arriving after tool groups gets its own bubble rather than appending to the previous one. - Replace per-tool appendNotice() calls with appendOrUpdateToolGroup(): consecutive tool calls share one collapsible <details> element (showing name + truncated args + completion checkmark). A new tool group is started whenever non-tool content appears between tools. - Extend renderHistory() to use the same appendOrUpdateToolGroup() for tool_call messages, so historical sessions render with the same collapsible groups as live streaming. - Add toolArgs field to HistoryMessage type for future args display in history (currently sent by backend but not yet used). - Add CSS for .msg-tool-group, .tool-group-details, .tool-group-summary, .tool-group-item, .tool-call-name, .tool-args-preview, .tool-status. * fix(extension): restore indigo color theme and extension icons Restore the correct indigo color palette that was accidentally corrupted in the working tree (changed from #6366f1 indigo to #ff6b9d pink/magenta). Also restore extension icons to manifest (icon-16, icon-48, icon-128) and the correct side_panel.default_path (src/sidepanel/index.html instead of sidepanel.html). * fix(extension): restore icon file paths in vite build config Update vite.config.ts to correctly copy icons from public/icons source to dist/public/icons output, matching the manifest.json icon references. Also update file name patterns from icon16.png to icon-16.png (with hyphens) to match the actual icon filenames. * fix(extension): restore per-tab session isolation with interleaved tool groups Restore the tab-scoped session architecture (currentTabId / resolveTabId) that was lost when main.ts was edited from a corrupted working tree state. Also re-apply the tool-group interleaving improvements on top of the correct foundation. Key fixes: - Restore currentTabId + resolveTabId() from URL ?tabId= param (tab isolation) - Restore MAX_PAGE_TOOL_ROUNDS = 15 (was reverted to 8) - Restore all loadExtensionChat(currentTabId), replaceExtensionSessions(currentTabId, ...), startNewExtensionSession(currentTabId), rememberExtensionSession(currentTabId, ...) call sites with the tab-scoped API - Re-apply appendOrUpdateToolGroup() for <details>-based collapsible groups with tool name + args preview + completion checkmarks - Re-apply segment-text fix: each assistant bubble shows only text from its segment (since the last tool group), not the full accumulated text - Re-apply isNewSegment signal threading through streamOnce → outer onText so text arriving after tool groups gets a fresh bubble - Update renderHistory() to use appendOrUpdateToolGroup() for history replay * fix(api): inject effectiveApp into slash-command context; fail-closed delete - handleSlashCommand: replace ctx := r.Context() with context.WithValue(r.Context(), appNameContextKey, effectiveApp) so all persistSessionMessage and persistDistillPreview calls inside resolve the correct app namespace instead of always defaulting to 'astonish'. Fixes silent message drop for extension sessions. - StudioDeleteSessionHandler (personal/file-store path): align with the Ent branch by failing closed when GetSessionMeta errors instead of falling through to Delete. Prevents orphaning fleet sandboxes and workspaces on transient meta-lookup errors. Addresses medium and minor issues from PR #510 code review. * ci: include Chrome extension zip in GitHub Releases Add a build-extension job to both release.yml and beta-release.yml that builds the Chrome extension, packages extension/dist into astonish-extension.zip, and uploads it as a workflow artifact. The existing release job already downloads all artifacts and publishes ./release/astonish-* via softprops/action-gh-release, so the zip is automatically included alongside the platform binaries. * docs: add Chrome extension user documentation Add docs/website/docs/studio/chrome-extension.md covering: - Requirements (Chrome 114+, running Astonish instance) - Installation from release zip (unpacked extension) - Sign-in flow (bearer token, Studio URL) - Side panel UI (session picker, page context, transcript) - Page tools reference table (page_snapshot/click/fill/navigate/etc) - Apply-to-page workflow and two-step handshake for read-only fields - Session isolation (astonish-extension appName) - Troubleshooting Wire into sidebar nav under Studio > Chrome Extension and add link to Studio overview's Related Pages section.
Improve plan cards, xAI reauth, and TUI activity command display (#509) * Improve plan cards, xAI reauth, and activity command display Render plan context as a design-doc overview without a CONTEXT label, preserve fenced headings, and surface xAI OAuth re-authentication in the TUI when tokens expire. Wrap full shell commands in the activity fold instead of truncating, and add understand-before-changing work policy to code and graph-plan prompts. * Collapse long TUI activity commands to one preview line Show the full command in the collapsed activity fold when it fits on one line. If it would wrap, keep a single truncated preview and reveal the full wrapped command on expand. * Put TUI activity commands on the same row as the label Collapsed Run command rows now match Find files: status, label, and command on one line. Short commands show in full; longer ones clip to terminal width. Expand still shows the full wrapped command body.
Fix: Natural cursor movement and stale approved plan blocking new pla… …ns (#508) This commit addresses two issues: 1. **Stale Approved Plan Bug Fix (Critical)** - Fixed AllowActivePlanReplacement() guard preventing new plans in plan mode - When entering Plan/Graph-Plan mode, any prior approval is now unconditionally cleared - Prevents blocked_active_approved_plan errors that rendered the plan system unusable - Added regression tests: TestChatAgent_AllowPlanReplacement_OverridesApproved, TestChatAgent_GraphPlanMode_ClearsStaleApprovedPlan 2. **Natural Cursor Movement Feature** - Added duration_ms parameter to browser_move_cursor tool - Cursor now glides with cubic ease-in-out acceleration when duration_ms > 0 - Implements per-step overlay sync so visible cursor tracks movement smoothly - Maintains backward compatibility: duration_ms=0 (default) keeps instant movement - Enhanced tool descriptions with anti-pattern warnings and recommended values Changes: - pkg/agent/chat_agent.go: AllowActivePlanReplacement clears stale approval flag - pkg/agent/plan_persistence_test.go: Added 2 regression tests - pkg/browser/demo_overlay.go: MoveMouseAnimated accepts durationMs with ease-in-out loop - pkg/browser/demo_overlay_test.go: Updated tests for new signature - pkg/tools/browser_demo.go: Added DurationMs field to BrowserMoveCursorArgs - pkg/tools/browser_demo_test.go: Added test for DurationMs parameter - pkg/tools/browser_interact.go: Updated BrowserClick caller to pass durationMs=0 - pkg/tools/browser_tools.go: Enhanced tool description with anti-pattern guidance All tests pass. The plan system is now functional in plan mode, and cursor animations in tutorials can now be smooth and human-like with a single tool call.
K8s sandbox recording fix + TUI render-cache with collision prevention ( #506) * tui: add per-item render cache, selection scoping, and mouse debounce Performance fix for TUI slowdown in long sessions. Three changes: 1. Per-item render cache (app.go) - Add renderedBlock struct caching padded+painted block, plain-text lines, and content spans per finalized transcript item - itemRenderCache map[string]renderedBlock on model, keyed by width+kind+content+expanded+routing - Streaming/provisional items bypass the cache - Cache cleared on WindowSizeMsg (terminal resize) - Eliminates re-running padBlock->applySelectionToBlock-> paintTranscriptBlock->ANSI-strip for every historical item on every refreshViewport() 2. Selection-scoped highlight application (selection.go, app.go) - Add selectionIntersectsLines() helper method - Gate applySelectionToBlock() to only blocks whose line range intersects the active selection - Drag selection is now O(selected blocks) not O(all items) 3. Mouse-motion refresh debounce (app.go) - Rate-limit refreshViewport() in handleMouseMotion to 16ms (~60fps) - Prevents CPU saturation from high-frequency terminal mouse events Benchmark (200-item session, Apple M4 Pro): BenchmarkRenderTranscript200Items/warm_cache: ~2.3ms/op BenchmarkRenderTranscriptWithSelection: ~1.25ms/op Tests: all pkg/tui/... pass; new TestSelectionIntersectsLines added; TestWindowResizeClearsMarkdownCache updated to cover itemRenderCache. * Fix: K8s run_drill recording via astonish-shell wrapper Fixes the xdpyinfo error 'probe display size: no dimensions in xdpyinfo output' when running drills with browser_start_recording on Kubernetes. The root cause is that kubectl exec runs commands in the pod base namespace (thin Debian image) rather than inside the chroot overlay at /sandbox/rootfs where xdpyinfo, ffmpeg, and other tools are installed. Solution: Added backendShellCommand() helper that wraps shell commands through /usr/local/bin/astonish-shell on K8s backends (which chroots into /sandbox/rootfs), while using plain sh -c on Docker (where overlay is root). This mirrors the pattern already established in backend_mcp_transport.go for MCP transport. Applied wrapper to all 4 exec calls in startBackendRecording: - Display probe (xdpyinfo) - mkdir for recording output directory - ffmpeg start script - ffmpeg stop script Added unit tests: - TestBackendShellCommand_Docker: Docker uses plain sh -c - TestBackendShellCommand_K8s: K8s uses astonish-shell wrapper - TestBackendShellCommand_NilBackend: nil backend defaults to Docker mode - TestStartBackendRecording_K8sUsesAstonishShell: E2E test of recording flow All sandbox package tests pass. * fix: prevent activity and file-diff cache collisions in TUI render-cache Fixes visual-correctness issues identified in PR review where two activities with identical summaries but different steps (e.g., 'Read 1 file' reading different files) could display the wrong transcript block due to cache-key collisions. Changes: - Add stepsCacheDigest(), argsCacheDigest(), resultCacheDigest() helpers that compute stable FNV-1a hashes of step fields for cache-key inclusion. - Include step digest in activity cache key to prevent collisions on identical summaries. - Include ToolName and args digest in file-diff cache key when DiffVerification is empty (fallback path to DiffFromToolArgs). - Add TestActivityCollisionPrevention regression test verifying two activities with same summary but different steps have different cache keys and render distinct output. - Add TestFileDiffCollisionPrevention regression test verifying two file-diffs with same content but different args have different cache keys and render distinct output. All TUI tests pass; no regressions. * tui: address PR review feedback on render-cache Four changes addressing review comments on #506: 1. De-duplicate agent badge rendering (Medium issue) Extract applyRoutingBadge(md, cw, it) helper method. Both the streaming bypass path and the finalized/cacheable path now call this single helper, guaranteeing the streaming and finalized bubbles render identically. Eliminates 25-line copy-paste that could silently diverge. 2. Bound/evict itemRenderCache after each render pass (Medium issue) Track usedCacheKeys in each renderTranscript call. After the for loop, delete any map entry not referenced in this pass. This prevents unbounded growth from orphaned entries (opposite expand/collapse states, items removed by /compact). The cache is now bounded to at most len(tr.Items) entries after every render. 3. Harden TestWindowResizeClearsMarkdownCache (Minor) Replace the t.Skip("cache key format changed") guard with an explicit prefix scan: any key starting with the old-width prefix ("80\x00") in either mdCache or itemRenderCache after resize fails the test loudly. A future key-format change now errors instead of silently skipping. 4. Fix hash formatting and rune-safe truncation (Minor) - strconv.FormatInt(int64(hash), 16) -> FormatUint(hash, 16): avoids negative-looking hex for high uint64 values. - argsCacheDigest / resultCacheDigest truncation now uses []rune slicing to avoid splitting a multi-byte UTF-8 rune mid-character.
feat: 3-tier shared model routing (strong/medium/weak) Replace binary strong/weak Auto routing with 3-tier architecture sharing one RoutingLLM pool between orchestrator and sub-agents. The MLP classifier's continuous sigmoid score is split into three ranges using configurable high_threshold (default 0.70) and low_threshold (default 0.30). Changes: - ModelRoutingConfig flattened to flat strong/medium/weak + 2 thresholds - RoutingLLM expanded to 3 models with two-threshold selection - Single shared RoutingLLM for both main agent and sub-agents - AutoRoutingConfig reduced from 12 to 8 fields (3 pairs + 2 thresholds) - Model picker auto-config: 7-line UI (3 models + 2 thresholds + confirm) - Routing badges: 🧠 strong / ⚙️ medium / ⚡ weak - Routing summary: '%.0f%% strong X, %.0f%% medium Y, %.0f%% weak Z' - Legacy migration: both pre-4-tier and 4-tier configs auto-migrate - Medium model is optional; if unconfigured, medium scores fall back to weak Files: - pkg/config/app_config.go: flat 3-model config + Migrate() - pkg/provider/routing/routing_llm.go: 3-way selection, RoutingStats.medium* - pkg/tui/backend/backend.go: 3-tier AutoRoutingConfig - pkg/launcher/tui_code.go: single RoutingLLM wiring, summary, emitRoutingInfo - pkg/tui/events/types.go, transcript.go: RoutingMedium* fields - pkg/tui/model_picker.go: 7-line auto-config UI - pkg/tui/app.go: 3-icon routing badges - docs/architecture/model-routing.md: full rewrite Tests: all pass (go test ./..., make lint) Build: clean (go build .)
PreviousNext