feat(frontend): rebuild Electron desktop UI as a React + Vite renderer#156
feat(frontend): rebuild Electron desktop UI as a React + Vite renderer#156ashish921998 wants to merge 22 commits into
Conversation
Rework the placeholder Electron shell into a clean, minimal Codex-style desktop UI. The single-file renderer (src/main.ts) builds the whole HTML/CSS/JS, so this rewrites only the three render functions; the data model and Electron lifecycle are untouched. - Replace every placeholder glyph (folder, sidebar toggle, search, the literal "S" settings icon, stop, close, chevron) with real inlined Lucide SVGs via a small icon registry + icon() helper at a uniform 1.75 stroke. No more programmer-art glyphs. - Lock a cool-neutral zinc palette, semantic green/amber/red status, a near-black primary button, and one documented radius scale. - Add per-worker status dots; long titles now ellipsize cleanly. - Drop dead cruft (invisible window-dot buttons) and dev-jargon copy leaking into the terminal placeholder text. - Drop Inter from the font stack in favor of the native SF Pro system. Fixes two layout bugs found while verifying the running app: - Sidebar worker labels were clipped (CSS grid min-width:auto); list tracks now use minmax(0, 1fr) so titles truncate and trailing status labels stay visible. - Settings modal overflowed its fixed-width <dialog>; the dialog now sizes to its content and width lives on each panel. Also exclude src/landing from the frontend tsconfig so the Electron shell typecheck/build do not pull in the separate Next.js landing app. Verified in the running app across the orchestrator, worker, new-task, and settings surfaces; npm run typecheck and npm run build pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR rebuilds the Electron desktop frontend as a full React + Vite + TypeScript renderer, replacing a single-file
Confidence Score: 4/5Safe to merge with one form-logic fix: the SpawnWorkerModal worker-name field gates submission via a regex validator but the value is never forwarded to the API. The core Electron process, terminal mux, event transport, and API rebasing are all well-structured and correctly guarded. The one concrete defect is in SpawnWorkerModal where name validation silently blocks form submission for a field whose value is discarded before the API call. All previously-reviewed issues have been addressed. frontend/src/renderer/components/SpawnWorkerModal.tsx — the worker name validation gate should be reconciled with whether the field is actually submitted. Important Files Changed
Sequence DiagramsequenceDiagram
participant M as Electron Main
participant P as Preload (contextBridge)
participant R as Renderer (React)
participant T as EventTransport
participant MX as TerminalMux (WS)
participant D as Daemon (loopback)
M->>M: spawn daemon (AO_DAEMON_COMMAND)
M->>M: scan stdout/stderr for listen port
M-->>R: "daemon:status { state:ready, port }"
R->>P: daemon.getStatus() via IPC
P-->>R: DaemonStatus
R->>T: createEventTransport.connect()
T->>D: EventSource /api/v1/events (SSE)
D-->>T: CDC events (session_created, etc.)
T->>R: invalidateQueries([workspaces])
R->>D: GET /api/v1/projects + /api/v1/sessions
D-->>R: workspace data
R->>MX: createTerminalMux(ws://127.0.0.1:PORT/mux)
MX->>D: "open{id, cols, rows}"
D-->>MX: "opened{id}"
MX-->>R: onOpened - state:attached
D-->>MX: "data{id, base64}"
MX-->>R: onData - xterm.write(bytes)
Reviews (7): Last reviewed commit: "Merge origin/main into feat/electron-ui-..." | Re-trigger Greptile |
…sign - api-client: drop duplicate /api/v1 from baseUrl; openapi-fetch concatenates baseUrl + the schema path (which already includes /api/v1), so every real daemon call was resolving to /api/v1/api/v1/* and 404ing. - main: enable the renderer sandbox and add setWindowOpenHandler (deny + shell.openExternal) plus a will-navigate guard, before daemon stdout is wired. - useWorkspaceQuery: only fall back to mock workspaces under import.meta.env.DEV; in production surface the real empty/error state instead of dev fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the e2e spec, playwright config, the test:e2e script, the @playwright/test and playwright devDependencies, and their tsconfig/vitest references. Vitest unit tests remain the frontend test surface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Greptile triage (all four resolved):
main.ts is now 181 lines (was ~900); the three stale comments reviewed the pre-pivot file. |
App took queryClient as an optional prop and guarded updateWorkspaces behind queryClient?.setQueryData, but router.tsx renders <App> on every route without passing it, so the prop was always undefined and createProject/createTask never updated the sidebar in the real app (the dummy-session fallback was permanently invisible). Use useQueryClient() from the surrounding QueryClientProvider instead. Tests now exercise the same context path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… deps Also adds the electron-builder build.publish (GitHub) config and the test:e2e script used by the auto-update and e2e layers below. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New terminal-mux client speaks the daemon's /mux protocol (open/data/resize/close, base64 frames, ch "terminal") matched to backend/internal/terminal/protocol.go. TerminalPane attaches the selected session's PTY, forwards keystrokes/resize, and renders with the WebGL addon (canvas fallback). Opens before resizing since the backend drops a resize for an unregistered pane. Framing is unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EventTransport now subscribes to GET /api/v1/events (named CDC events from backend/internal/cdc) and the daemon IPC status, invalidating the ["workspaces"] query (debounced) instead of a dead ["daemon"] key. EventSource is guarded for jsdom/preview. Test mock updated for the new apiBaseUrl export. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
App uses react-resizable-panels (Group/Panel/Separator) for a draggable sidebar split, with sizes persisted to the Zustand store. The main pane gains shadcn Tabs (Terminal/Details, terminal forceMounted so its PTY survives tab switches) and a status Badge; the composer uses Button and a free-text branch Input. All five shadcn ui components are now in use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires autoUpdater.checkForUpdatesAndNotify() (packaged builds only) and a desktop-release workflow that publishes installers + the update feed. Going live needs Apple signing + notarization + CI secrets, documented in frontend/docs/desktop-release.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Config + workbench spec assert the current React UI (mock-data driven), with ELECTRON_STARTUP_PREVENT so the dev server doesn't launch Electron during e2e. Re-adds the tsconfig include and vitest e2e exclude. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running the Electron app surfaced a WebglAddon dispose crash (reading '_isDisposed' on a half-initialised addon) that threw <XtermTerminal> into the router error boundary. Probe for a real WebGL2 context before loading the addon, and guard terminal.dispose() so a renderer-addon teardown error can't crash React. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or handling Computer-use QA of the running Electron app surfaced and fixed: - collapsed sidebar no longer drops the main-header tabs/status badge (WorkbenchMain now fills width in both the resizable and rail layouts) - task composer closes on Escape - "needs input" sessions render an amber badge (was muted gray); failed uses outline - terminal shows an "Attaching to <session>…" state instead of a blank void - session-row title truncates cleanly to the left of the hover actions (in-flow) Also folds in concurrent refinements to the same files: createTask surfaces API errors instead of fabricating a dummy session, the default daemon port is 3001, the default selection starts empty, and the terminal theme updates in place without rebuilding the pane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon exit/error/spawn listeners closed over the module-level daemonProcess. If stopDaemon nulled it and startDaemon spawned a replacement before the old process's exit fired, the stale handler would null the new reference, leaving an orphaned untracked daemon (stop() then no-ops, starts accumulate). Capture the spawned handle locally and guard every listener with daemonProcess === child. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| @@ -0,0 +1,51 @@ | |||
| import { createHashHistory, createRootRoute, createRoute, createRouter, Outlet } from "@tanstack/react-router"; | |||
There was a problem hiding this comment.
✅ Matches the locked design (docs/designs/desktop-app-design.html §4.2): TanStack Router is exactly the chosen router, paired with the TanStack Query you're already using. Flagging explicitly because this is a real @tanstack/react-router setup (hash history + typed Register), not a hand-rolled router — so there's no routing divergence to reconcile here. The RootLayout→App param-wrapper pattern is clean.
| @@ -0,0 +1,84 @@ | |||
| export type SessionStatus = "running" | "needs_input" | "stopped" | "failed"; | |||
There was a problem hiding this comment.
SessionStatus (running|needs_input|stopped|failed) is a third status vocabulary: it's neither the daemon's 12-value derived set (docs/architecture.md:40-49 — terminated/merged/needs_input/ci_failed/draft/changes_requested/mergeable/approved/review_pending/pr_open/working/idle) nor the legacy StatusTone spectrum. The agreed plan (design doc §7 status-spectrum audit) keeps the Go set authoritative and maps Go→StatusTone via a client-side shim. useWorkspaceQuery.ts:sessionStatus() already hand-collapses Go statuses, so every PR/review state (ci_failed, changes_requested, approved, …) is silently lost here. Recommend aligning to the Go set + a shim rather than a new 4-value enum.
| connect: () => () => void; | ||
| }; | ||
|
|
||
| const EVENTS_URL = `${apiBaseUrl.replace(/\/+$/, "")}/api/v1/events`; |
There was a problem hiding this comment.
✅ Strong, and a borrow-back for our plan. I verified this against the backend: /api/v1/events is a real SSE endpoint (backend/internal/httpd/events.go:74-80 — text/event-stream, emits id:/event:/data: and honors Last-Event-ID), and all 8 CDC_EVENT_TYPES here match backend/internal/cdc/event.go:23-30 exactly. The named-event + auto-reconnect approach is correctly matched to the contract.
This is cleaner than the 'CDC over the /mux sessions channel' route our design doc assumed (§7 mux audit) — separate transports, free EventSource resume. We should adopt this SSE pattern in the doc and retire the sessions-channel-for-CDC idea.
One caveat: events.go:40-43 returns 501 when the controller deps aren't wired — worth confirming Source/Live are mounted on this PR's base main (they're stubbed in the worktree I reviewed against).
| import createClient from "openapi-fetch"; | ||
| import type { paths } from "../../api/schema"; | ||
|
|
||
| export const apiBaseUrl = import.meta.env.VITE_AO_API_BASE_URL ?? "http://127.0.0.1:3001"; |
There was a problem hiding this comment.
- Port discovery —
apiBaseUrlhardcodes:3001, but the daemon binds whatever port it's given (and may use0→OS-assigned). The supervisor should read the actual port fromrunning.json(backend/internal/runfile/runfile.go:20-26) and hand it to the renderer rather than assume the default. - Token — the remote model (§5) needs a bearer token; on the WS it must ride the
Sec-WebSocket-Protocolsubprotocol since browsers can't set headers on a WS handshake. - CORS — packaged renderer origin is
file://but it fetcheshttp://127.0.0.1:PORTdirectly, and the daemon has no CORS (confirmed: no CORS middleware inbackend/internal/httpd;README.md:94), so those requests will be blocked.
Your own frontend-shell-decision.html landmines flag (1) and (3) too. None block this PR — they're the next real work, and mostly daemon-side.
| @@ -0,0 +1,476 @@ | |||
| <!doctype html> | |||
There was a problem hiding this comment.
📄 Doc reconciliation — for the team, not a change to this file. This decision record lands on Electron, same as our design doc (docs/designs/desktop-app-design.html) — good, we agree on the conclusion. But the bases differ in ways that matter:
- It drops Linux ('macOS + Windows'); our doc commits to Linux binaries as a hard requirement → 'is Linux a target?' is now an open conflict.
- Its deciding axis is language count, and it never mentions the agent-controllable in-app browser (multi-tab, CDP-driven) that our doc treats as the reason Electron wins and as a locked Phase-3 feature. If that requirement still holds, Electron is locked for a stronger reason and Linux likely has to stay.
- Two decision docs at two paths (this one at repo root, ours under
docs/designs/) = no single source of truth.
The '~0% renderer built' landmine in this doc is also now superseded by this very PR. Suggest the orchestrator merges the two into one record.
AgentWrapper
left a comment
There was a problem hiding this comment.
Architectural review — alignment with the locked frontend design doc
Reviewing for architectural alignment with docs/designs/desktop-app-design.html, not generic correctness (Greptile's two correctness bugs stand — see the bottom). Backend claims verified against the daemon source; inline notes are on the specific lines. Net: comment-only — the stack is right and cleanly built, but the PR answers two questions the team had already answered differently, and those need a ruling rather than a code change forced on the author.
✅ Strong alignment (validates the design doc)
Electron · React 19 + Vite + TS · TanStack Query and Router · Tailwind · xterm.js + WebGL (probe → canvas fallback) · electron-updater · Zustand — all exactly the locked stack, implemented cleanly. Electron security posture is right (contextIsolation/sandbox/nodeIntegration:false, contextBridge with unsubscribe, navigation hardening). The /mux client (terminal-mux.ts) matches protocol.go and is framing-tested.
Notably good: the SSE event wiring is precisely matched to the real backend — /api/v1/events is a genuine SSE stream (events.go:74-80) and the 8 hardcoded CDC event names match cdc/event.go:23-30 exactly. Real backend research, not guesswork.
⤴️ Borrow back into our plan
- Promote OpenAPI codegen from phase-2 to phase-0 —
openapi-typescript+openapi-fetchoffopenapi.yamlis strictly better than hand-porting types for bootstrap. - Adopt the SSE
EventTransportsplit (terminal=/mux, CDC=SSE, refetch-on-event) and retire the doc's "CDC over the mux sessions channel" idea — theirs is cleaner. react-resizable-panels, the WebGL-probe terminal renderer, and the electron-updater release CI + signing checklist are all worth keeping.
⚠️ Divergences (calls in the report)
- D1 — this is a stack scaffold, not the 1:1 legacy port (team decision). New mock-data components, a new light-default design system (Sidebar even hardcodes light hex, ignoring the dark theme), and a new 4-value status enum — vs the locked dark-only Mission Control + 12-status-via-shim. Legitimate as a scaffold; doesn't fulfil the 1:1-port mandate.
- D2 — single npm app, not the
packages/core+packages/runtimepnpm workspace (defer). Seam is clean, mobile is OPEN, so extraction later is cheap — but track it. - D3 — daemon supervisor is a stub (we align): spawns
AO_DAEMON_COMMAND, norunning.jsondiscovery, no/readyzpoll, no token, no/shutdown. Your own decision doc specifies all of these. - D4 — no CORS/token for direct renderer→daemon (daemon-side, latent):
file://→127.0.0.1is cross-origin and the daemon has no CORS/token. Next real work. - D5 — second decision doc (
frontend-shell-decision.html) lands on Electron too 👍 but drops Linux (our doc requires it) and never mentions the agent-controllable in-app browser that our doc treats as the deciding factor + a Phase-3 feature. Two docs, two paths → reconcile to one.
Mux parity
Our audit found the legacy mux can't port 1:1; this PR sidesteps it well — sessions channel unused, CDC via SSE. Two residual gaps: no terminal reconnect (the backend supports reconnect-resubscribe; a blip kills the pane) and one-WS-per-pane forgoes multiplexing (fine for the single-pane tab UI now).
Deferred to Greptile (correctness, not this lens)
useWorkspaceQuery maps path to the wrong field (every project shows its UUID); createTask swallows the API error and closes the composer silently; global.d.ts types window.ao as non-optional. Worth fixing before merge.
Full write-up with file:line and per-divergence resolutions: docs/reviews/pr-156-review.md (in the ao-15 worktree). The two team decisions (D1, D5) are headed to the orchestrator.
Adopt the emdash design system for the Electron renderer and document it as the source of truth in DESIGN.md (CLAUDE.md routes to it before any UI work). - Theme: system font + flat near-black neutral ramp, refined-blue accent, light/dark - Orchestrator-led shell: pinned orchestrator (orchestrator-first), Projects with name-only worker rows, and a per-project "+" that opens the spawn-worker modal scoped to that project - Worker workspace: agent terminal as the conversation + Git changes/commit rail; contextual topbar (PR/CI pill + Changes/Files/Terminal toggles + session menu) - Spawn-worker modal mirroring emdash's Create Task, mapped to `ao spawn` - Session status as a single glyph (spinner while working / PR icon / dot), no text badges Verified: tsc --noEmit clean, vitest 12/12 (playwright e2e not run here). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ew hardening Connects the emdash redesign to the running backend instead of mock data: - session terminalHandleId + project path plumbed through the Go DTOs/OpenAPI and the generated frontend schema, so the renderer can attach PTYs and show real project paths - dynamic API base URL keyed off the daemon's reported port, with the CDC SSE event transport reconnecting when the port changes - drop the renderer mock-data module Pre-landing review fixes (cheap/mechanical, per /ship triage): - main.ts: spawn the daemon detached and kill its whole process group so the signal reaches the real daemon behind the /bin/sh wrapper, not just the shell - TerminalPane: drop the unused session.provider effect dep that needlessly recreated the terminal + mux socket - remove dead code: ui-store setView/setSidebarOpen, api-client apiBaseUrl export, terminal-mux ServerFrame.session field - add unit coverage: workspace status helpers, event-transport reconnect logic, api-client empty-base path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Format-only pre-commit: Prettier on staged frontend files, gofmt on staged Go files. Prettier config tuned to the existing frontend style (2-space, double-quote, semicolons, trailing commas, printWidth 120). Generated artifacts (schema.ts, openapi.yaml) are ignored so the api-drift CI job stays authoritative. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One-time formatting pass so future per-file diffs stay clean under the new pre-commit hook. Applies the committed .prettierrc to tracked frontend/src files that had no in-flight changes; files with active WIP are left to normalize when next committed. Generated artifacts (schema.ts) remain ignored. Formatting-only — no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rigin, CORS) The desktop build's network path was never exercised — dev worked only because Vite proxies everything same-origin. Three independent breakages: - runtimeFetch rebuilt requests via `new Request(target, input)`, which needs the source request's `duplex` getter; Electron's Chromium lacks it, so every request with a body threw. Copy fields explicitly and buffer the small JSON body instead. - The renderer was loaded from file://, whose opaque "null" origin can never be trusted by the daemon (sandboxed iframes on any website share it) and which also breaks absolute /assets URLs. Serve the packaged renderer from app://renderer, an origin only this app can present. - The daemon had no CORS support, so even a trusted origin could not read responses. Add an origin allowlist (default: app://renderer, plus loopback-served content, which can already reach the daemon directly). Per Codex review: requests bearing a non-allowlisted Origin are rejected with 403 before any handler runs — omitting CORS headers alone would hide the response but still execute "simple" no-cors cross-origin POSTs on a no-auth daemon that can spawn agent sessions. This closes a pre-existing drive-by CSRF hole, not just a regression. AO_ALLOWED_ORIGINS overrides the allowlist; "null" and "*" are config errors. Verified live: hostile-origin POST 403s, app://renderer and loopback dev origins are granted, no-Origin CLI traffic is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
React Doctor found no issues. 🎉 Reviewed by React Doctor for commit |
yyovil
left a comment
There was a problem hiding this comment.
Review note: please split formatter setup from feature changes
The PR bundles three distinct concerns and it's making review harder than it needs to be:
- Prettier tooling setup —
.prettierrc,.prettierignore,.lintstagedrc,.husky/pre-commit. These are achore:concern and unrelated to the renderer. - Mass reformat of existing files — all
frontend/src/landing/components (~20 files),globals.css, etc. These are 100% Prettier-driven reflowing (JSX attribute wrapping, CSStransitionexpansion) with zero logic changes. They're drowning out the real diff. - The actual feature — the React renderer, CORS middleware, daemon discovery, OpenAPI updates, terminal mux. This is the meat, and it's buried.
Suggested split:
chore: add prettier config and format existing files— items 1 + 2, reviewable in secondsfeat(frontend): rebuild Electron desktop UI as a React + Vite renderer— item 3 only, focused and reviewable
Either a rebase to separate the formatting commit, or extracting items 1+2 into their own PR first works. The formatting-only files can be reviewed with git diff -w in the meantime if you need to unblock.
|
@ashish921998 plz keep the PRs tightly scoped with the PR title. |
Resolves conflicts with main's prettier config + CI auto-formatter (#166): - Adopt main's .prettierrc (tabs) — CI reformats every push, so the branch's spaces config would just get rewritten - Take main's versions of frontend/src/landing/** (branch only had formatting changes there; now byte-identical to main) - Keep the branch's frontend/package.json, tsconfig.json, main.ts (the Electron redesign), root package.json (husky/lint-staged), and .gstack/ gitignore entry; keep main's .envrc.local entry - Reformat the branch's own files with prettier@3 exactly as CI does, so the auto-formatter has nothing to commit after push Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| const nameValid = name === "" || NAME_RULE.test(name); | ||
| const canSubmit = prompt.trim().length > 0 && projectId !== "" && nameValid && !isSubmitting; |
There was a problem hiding this comment.
Worker name blocks submission but is silently discarded
canSubmit gates on nameValid, so a user who types something like "My Worker" (spaces or uppercase letters violate NAME_RULE = /^[a-z0-9-]+$/) cannot submit the form. But the name value is never passed to onCreateTask — it only drives the worktree-path preview in the "Workspace" tab and is reset after a successful submit. The default "" is valid, so a user who doesn't touch the field is unaffected, but anyone who types an entry that doesn't satisfy the regex is silently blocked from spawning a worker for no functional reason.
Either remove nameValid from canSubmit (and keep the visual hint as a non-blocking style signal), or thread name through onCreateTask and the createTask API call so the validation actually enforces a real constraint.
…ass, no_signal watchdog (#170) * fix(codex): deliver activity hooks via -c session flags, trust worktree at launch Codex (0.136+) never loads hook config from AO's per-session worktrees: project-local .codex/ layers only load from trusted directories, and for linked git worktrees codex sources hook declarations from the matching folder in the root checkout — so the workspace-local .codex/hooks.json AO wrote was dead config and codex sessions never reported activity. Deliver the hooks on the launch/resume command instead: - -c 'hooks.<Event>=[...]' session-flag config for SessionStart, UserPromptSubmit, PermissionRequest, and Stop; the session-flags layer is not trust-gated and aggregates with the user's own hooks. The existing --dangerously-bypass-hook-trust flag lets them run without a persisted trust hash. - -c 'projects={"<worktree>"={trust_level="trusted"}}' (inline-table form; the dotted projects."<path>".trust_level key is corrupted by codex's naive -c dot-split) so spawns into never-trusted repos don't hang invisibly on the interactive directory-trust prompt. Both the literal and symlink-resolved worktree paths are trusted. - -c notice.hide_rate_limit_model_nudge=true so the "switch to a cheaper model?" dialog can't hang a headless pane and swallow the spawn prompt. GetAgentHooks no longer writes workspace files (worktrees stay clean); it only strips entries older AO versions left in .codex/hooks.json, preserving user hooks. UninstallHooks/AreHooksInstalled now operate on those legacy files only. Verified with a real spawn into a fresh untrusted repo: activity transitions idle -> active -> idle hands-free, no .codex dir in the worktree, no hook delivery failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sessions): activity-signal watchdog + hook delivery hardening A codex upgrade broke activity tracking silently: sessions showed a confident "idle" forever while the agent worked. This bundle makes hook delivery verifiable end to end and makes any future breakage loud instead of invisible. Watchdog (no_signal status): - sessions.first_signal_at (migration 0010) records the FIRST hook callback per spawn/restore — raw signal receipt, independent of the derived activity state. lifecycle.ApplyActivitySignal stamps it (and writes through same-state repeats until stamped, e.g. Codex SessionStart reporting idle on an idle-seeded row); MarkSpawned clears it so every relaunch re-proves its hook pipeline. - deriveStatus downgrades a live session with no receipt to the new no_signal display status after a 90s grace, instead of idle. Terminated/PR-derived statuses still win. The sessions CDC update trigger now also fires on first-signal receipt so the dashboard transition is pushed live. - frontend maps no_signal -> needs_you (a human should look at the pane). Hook callback hardening (re-landed from the closed redesign PR #156): - the session manager pins each spawned session's PATH with the daemon executable's directory first, so the bare `ao` in hook commands resolves to the daemon that installed them, with a spawn-time warning when the pin cannot apply. - `ao hooks` failures append to $AO_DATA_DIR/hooks.log (size-capped); `ao doctor` gains a hooks-log check that warns on failures from the last 24h, and an ao-binary identity check. Codex launch-surface canary: - `ao doctor` gains codex-launch-flags: it runs probes exported by the codex adapter (built from the same flag builders as the real spawn argv) against the installed binary, warning when codex rejects the hook-trust bypass flag or AO's -c session-flag overrides. - codex hook callback timeout drops 30s -> 5s so a hung daemon cannot stall the agent's turn. Docs: the agent PRD callback section now describes the implemented flow (derive state, POST /sessions/{id}/activity, hooks.log) instead of the unbuilt SQLite/metadata merge, and notes that hook-derived metadata persistence (codex resume) is still not implemented. Frontend note: main's renderer test suite has 7 pre-existing failing files and a vite-config typecheck error unrelated to this change; workspace.test.ts (the only frontend file touched) passes 26/26. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(store): restore TestSessionWorktreesRoundTrip lost in the re-landing port The branch ported store_test.go wholesale from the closed redesign branch, whose copy predates #165 — silently dropping the session-worktrees round-trip test #165 added. Restore main's file and re-apply only this branch's addition (TestSessionFirstSignalRoundTrip). No other ported file lost main-side content (audited per-file against main; the remaining deletions are this branch's intended refactors). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(status): only derive no_signal for harnesses that have a hook pipeline Review finding: the no_signal downgrade had no harness-capability gate, but first_signal_at can only ever be stamped by an `ao hooks` callback. Ten spawnable harnesses (amp, aider, crush, grok, kimi, devin, auggie, continue, vibe, pi) install no hooks at all, so every live session of theirs would have flipped from idle to a permanent no_signal -> needs_you after the 90s grace. The session service now takes a SignalCapable predicate; daemon wiring injects activitydispatch.SupportsHarness (the deriver registry is the source of truth for "this harness can signal"). Left nil, the service never claims no_signal. A new dispatch test pins that every deriver token is a known harness name. Also from the same review: - lifecycle/manager.go and the 0010 migration claimed Codex's SessionStart reports idle as the first signal; both codex and claude-code derivers deliberately return no signal for session-start, so the comments now cite a real case (a lost "active" POST followed by a Stop hook landing idle). - docs/agent/README.md documents the gate and the restore caveat: a restored session the user never prompts has nothing to signal, so it shows no_signal after the grace until a receipt-only session-start signal exists. - 0010 migration uses DROP TRIGGER IF EXISTS per house style. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ker name (#171) * fix(spawn): stop sending branch on spawn, render API errors, wire worker name Three spawn-modal bugs, re-landed from the closed redesign branch (#156): - createTask no longer sends `branch`: the API field names the session's NEW worktree branch, so submitting the modal's default ("main") made the daemon 409 with BRANCH_CHECKED_OUT_ELSEWHERE on every spawn. The "Based on" pane is informational — workers branch off the project's default branch in a fresh worktree. - API errors render their envelope message instead of "[object Object]": openapi-fetch resolves non-2xx responses to a plain {code,error,message} object, not an Error; new apiErrorMessage unwraps it (message + code). - The "Worker name" field is actually used: after spawn, a best-effort PATCH rename sets the displayName (a failed rename must not look like a failed spawn — the worker is already running). Also revives the renderer test suite, which made these tests (and 7 of 9 suite files) impossible to run on main: - vitest.config.ts re-exports vite.renderer.config so `vitest run` actually loads the jsdom environment + setup file. Forge's per-target vite.*.config.ts names are invisible to vitest, so the existing `test` block was dead config and every DOM-touching test died on "window is not defined". - vite.renderer.config.ts imports defineConfig from vitest/config so its `test` key typechecks. - routeTree.gen.ts + the session route are regenerated by the pinned @tanstack/router-plugin (it runs as part of loading the renderer config; the checked-in tree predated the installed plugin version and its route-ID drift caused 3 of main's 7 typecheck errors). - App.test.tsx wraps App in TooltipProvider, mirroring routes/__root.tsx. Frontend: 9/9 test files, 99/99 tests pass (was 2/9 files). Typecheck is down from 7 errors to 3 — the survivors (forge.config notarize/maker types, update-electron-app call signature) predate this branch and are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: format with prettier [skip ci] * fix(gitworktree): base new session branches on the local default branch when no remote exists Re-lands the remoteless fallback from the closed redesign branch (archived in 641b712). Creating a session worktree resolved the base for a NEW branch only via the remote-tracking ref (origin/<defaultBranch>), so a registered repo with no remote failed every spawn with BRANCH_NOT_FETCHED — an error that misleadingly names the new session branch and suggests `git fetch`, which is impossible without a remote. refs/heads/<defaultBranch> now follows origin/<defaultBranch> in the candidate list: remote-tracking still wins whenever it exists, and a remoteless repo bases session branches on its local default branch. Verified live: a plain `git init` repo (no remote) that previously failed now spawns, and the integration suite covers it (TestWorkspaceIntegrationCreateInRemotelessRepo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
What
Rebuilds the Electron desktop frontend as a React + Vite + TypeScript renderer and implements the full intended stack boilerplate (see the layer table below). Started as a restyle of the single-file
main.ts, pivoted to a real renderer architecture, then filled in every stack layer.Stack status
/api/v1/eventsCDC SSE + daemon IPC/muxWebSocket + WebGL (canvas fallback)Highlights
terminal-mux.tsWebSocket client speaks the daemon/muxprotocol (open/data/resize/close, base64 frames) matched tobackend/internal/terminal/protocol.go;TerminalPanestreams real PTY output, forwards keystrokes/resize, and renders via the WebGL addon. Framing is unit-tested.electron-updater+ publish config + release CI + signing checklist infrontend/docs/desktop-release.md).Verification
tsc --noEmit✅ · Vitest 12/12 ✅ (incl. mux framing) · Playwright discovery ✅.Needs interactive / live verification (wired + type-safe, not runtime-proven here)
/muxterminal manager mounted; the WebSocket client + framing are tested but live PTY behavior is unproven in CI.🤖 Generated with Claude Code