Skip to content

Tags: SAP/astonish

Tags

beta

Toggle beta's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

v3.8.4

Toggle v3.8.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

v3.8.3

Toggle v3.8.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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

v3.8.2

Toggle v3.8.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

v3.8.1

Toggle v3.8.1's commit message
feat(oauth): default unscoped clients to built-in resources

v3.8.0

Toggle v3.8.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

v3.7.2

Toggle v3.7.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

v3.7.1

Toggle v3.7.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

v3.7.0

Toggle v3.7.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

router-weights-v1.0

Toggle router-weights-v1.0's commit message
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 .)