Skip to content

fix(desktop): recover when orchestrator agent is missing - #3128

Merged
JAYATIAHUJA merged 2 commits into
Untrivial-ai:mainfrom
Hardik180704:fix/desktop-agent-recovery
Jul 26, 2026
Merged

fix(desktop): recover when orchestrator agent is missing#3128
JAYATIAHUJA merged 2 commits into
Untrivial-ai:mainfrom
Hardik180704:fix/desktop-agent-recovery

Conversation

@Hardik180704

Copy link
Copy Markdown
Collaborator

What changed

  • redirect unconfigured projects to Project Settings before spawning an orchestrator
  • cover the board, top bar, sidebar, and command palette launchers
  • keep the existing one-click flow unchanged when an agent is configured or an orchestrator is already running
  • add regression coverage for every affected launcher

Why

Projects added through the CLI can exist without an orchestrator agent. The desktop still showed an enabled spawn action and sent a request that could only fail with AGENT_REQUIRED. This gives the user a recovery path instead.

Checks

  • frontend typecheck
  • E2E typecheck
  • full Vitest suite: 1,081 passed
  • renderer smoke: 20 passed
  • git diff --check

Closes #3125

@JAYATIAHUJA JAYATIAHUJA left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused fix. The primary launcher surfaces now guard the missing orchestrator-agent case, and the added tests cover Command Palette, Sessions Board, Shell Topbar, and Sidebar. I verified the targeted renderer tests and frontend typecheck locally. Blocking issue: rontend/src/renderer/components/RestoreUnavailableDialog.tsx:25 still calls spawnOrchestrator(session.workspaceId, 'restore_dialog', true) without checking whether the project has a configured orchestratorAgent. That dialog is another orchestrator-spawn entry point, so an archived/unrestorable orchestrator can still hit the same failed/harmful spawn path this PR is trying to prevent. Please guard this path as well, either by passing/resolving the project config and routing to /projects//settings when the orchestrator agent is missing, or by replacing/disabling the recreate action with a settings action. Please also add a focused test for the restore-unavailable dialog path.

Verification run:
pm --prefix frontend run test -- CommandPalette.test.tsx
ShellTopbar.test.tsx S
idebar.test.tsx board-empty-states.test.tsx passed, and
pm --prefix frontend run typecheck passed.

Requesting changes until the restore-dialog spawn path is covered.

@Hardik180704

Copy link
Copy Markdown
Collaborator Author

Thanks for catching this, Jayati. Fixed in b2bd6f723.

  • RestoreUnavailableDialog now checks the project config before recreating.
  • If the orchestrator agent is missing, it closes the dialog and opens Project Settings without calling the spawn API.
  • The configured-agent path still performs the existing clean recreation.
  • Added focused tests for both paths.

Local checks are green: 1,083 frontend tests, both typechecks, and 20 renderer-smoke tests passed. Fresh CI is running now. Could you take another look when you get a chance?

@JAYATIAHUJA JAYATIAHUJA left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The restore-dialog recreation path is now guarded as requested. I rechecked the current PR diff against upstream/main: it now covers the primary launcher surfaces plus RestoreUnavailableDialog, and includes focused restore-dialog coverage for the missing orchestrator-agent case and the configured-agent recreation path. Verification run locally:
pm --prefix frontend run test -- RestoreUnavailableDialog.test.tsx CommandPalette.test.tsx ShellTopbar.test.tsx Sidebar.test.tsx board-empty-states.test.tsx passed (5 files, 99 tests), and
pm --prefix frontend run typecheck passed. Approving.

@JAYATIAHUJA
JAYATIAHUJA merged commit aa73621 into Untrivial-ai:main Jul 26, 2026
3 checks passed
harshitsinghbhandari referenced this pull request in agentlab-in/hosted-ao Jul 27, 2026
* fix(desktop): recover when orchestrator agent is missing

* fix(desktop): guard restore dialog recreation
AronPerez added a commit to AronPerez/agent-orchestrator that referenced this pull request Jul 29, 2026
* feat(frontend): add board copy actions (#3050)

* feat(frontend): add board copy actions

* fix(frontend): keep PR separators next to copy actions

* fix: unify session state and lifecycle presentation (#2980)

* fix: unify session state derivation and presentation

* fix: keep merged session cards on standard surface

* fix: remove merged card border accent

* fix(board): prioritize idle sessions in work lane

* feat(board): pair merged sessions with ready lane

* feat(board): improve merged states and session archive

* feat(board): add archive layout switcher

* fix(board): default archive to aligned grid

* fix(board): tighten archive spacing

* feat(sessions): refine workspace UI and PR activity

* feat(sessions): complete lifecycle and merge handling

* chore: remove redundant session state paths

* feat(sessions): finalize state and recovery flow

* fix: extend missing activity grace period

* fix: restore board smoke test contract

* fix: preserve early activity on agent resume

* fix: address session state review feedback

* style: simplify exited activity guard

* fix: preserve merge-triggered session termination

* fix: align session activity presentation

* style: format session state changes

* fix: make board termination action self-contained

* fix: treat merged terminated sessions as archived

* fix: persist workspace repository metadata

* `chore(ci): enable TypeScript typechecking in frontend CI workflow` (#2978)

* ci: add GitHub Actions workflow for frontend typechecking and tests

* ci: typecheck E2E specs in frontend workflow

* fix(windows): route background agent auth checks through aoprocess to suppress console window flashes (#3006)

* feat(frontend): compact board and review spacing (#3078)

* fix(desktop): minimal, consistent scrollbars across all platforms (sidebar, sessions board, markdown preview) (#2997)

- keep the sidebar header and project controls fixed while its list scrolls
- add minimal scrollbars to Sessions Board regions
- add consistent scrollbar styling to Markdown previews
- include regression coverage for the updated scrolling behavior

* fix: move topbar kill confirmation into dialog (#3072)

Replaces inline kill confirmation with a shared confirmation dialog.
Clears stale errors before retrying a failed kill.
Removes the unused dialog size property.
Standardizes confirmation and report-dialog styling.
Contains frontend-only changes with all tests and CI checks passing.

* feat(terminals): scope shell tabs to the session and allow renaming them (#3077)

* feat(terminals): scope shell tabs per project and allow renaming them

Two fixes to the standalone shell terminal tabs:

- Per-project isolation: shell terminals are an app-wide daemon list, so a
  shell opened in one project showed up as a tab in every project's session
  view. SessionView now filters the strip to the current project's shells;
  project-less shells live only on the standalone /terminals screen. The pane
  falls back to the session's own tab when the active shell belongs elsewhere.
- Rename: double-click a shell tab to rename it inline (Enter/blur commit,
  Escape cancels). New PATCH /api/v1/shell-terminals/{handleId} persists the
  title on the daemon so it survives reload. The two near-identical tab
  components (CenterPane, ShellTerminalsView) are unified into one shared
  ShellTerminalTab.

Backend: UpdateShellTerminalTitle query/store/service with trim + length
validation and 404 on unknown handle; OpenAPI + generated TS regenerated.

* style: gofmt import ordering to go.mod's 1.25.7 (fixes main breakage from #3006)

* feat(terminals): scope shell tabs to the session, not the project

Shells opened inside a session were shared across every session in the
project (they carried only a projectId). Scope them to the session they
were opened from instead, so each session's strip shows only its own
shells.

- New nullable shell_terminals.session_id column (migration 0036, FK to
  sessions ON DELETE CASCADE); threaded through query/store/service/DTO.
- Open request carries sessionId; the shell layout tags new shells with
  the current route's session. SessionView filters the strip by sessionId;
  the standalone /terminals screen shows only session-less shells.

Replaces the earlier per-project filter. OpenAPI + generated TS regenerated.

* fix(terminals): make the shell-tab rename field visibly editable

The inline rename input was transparent with no border, so double-clicking
a tab opened an edit field that looked identical to the static label -
users couldn't tell they were in edit mode. Give it a bordered, ringed,
solid-background treatment and mark the label select-none so a double-click
enters edit cleanly instead of highlighting the text.

* feat(terminals): rename shell tab via native gesture per platform

Use each platform's convention to start an inline rename: double-click on
macOS/Linux, right-click on Windows (native context menu suppressed). The
title tooltip hints the gesture. Replaces the double-click-only trigger.

* feat(terminals): trigger tab rename from anywhere on the tab

Move the rename gesture (double-click on macOS/Linux, right-click on
Windows) from the label button to the whole tab container, so a click
anywhere on the tab - label or padding - starts the rename. The close
button stops propagation so double-clicking it still only closes.

* fix(terminals): detect double-click from two quick clicks for trackpads

Some trackpad configurations deliver a double-tap as two separate click
events that never synthesize a native dblclick, so onDoubleClick alone
never fired. Add a click-timing detector (two clicks within 500ms on the
tab) alongside the native handler so the rename gesture works regardless.

* fix(copilot): forward role-specific model config to launch and restore (#2975)

* fix(copilot): forward role-specific model config to launch and restore

* refactor(copilot): take ports.AgentConfig in appendModelFlag for family consistency string directly. Functionally identical output.

* chore: retrigger CI

* fix(copilot): defer model-on-restore pending verification, add TestGetRestoreCommandDoesNotForwardModelOverride

* feat(browser): search non-URL input, open links in-app, and glow on new links (#3085)

Reworks the session Browser panel so it behaves like a real in-app browser:

- Address bar: non-URL text (e.g. "hi", "how do i center a div") now runs a
  web search instead of coercing to https://hi and dead-ending on a blank
  page. Real hosts, localhost, file paths, and explicit schemes are unchanged.
- Terminal links: a left-click on any http/https link opens it in the AO
  Browser panel (previously only localhost; external links went to the system
  browser). Right-click a link for "Open in system browser". Non-web schemes
  (mailto:) still open externally.
- No focus-stealing: an incoming preview (ao preview, or a link/URL) badges
  the Browser tab instead of auto-opening it. The badge is a glowing beacon
  and clears when the tab is opened.
- Glow on terminal output: when an agent prints a URL (e.g. a pushed-PR link),
  the Browser tab glows so the user can open it when they choose. Detection is
  ANSI-stripped, handles chunk splits, and dedupes (lib/detect-urls.ts), fed
  by a new onOutput seam on the terminal hook.
- fake harness prints a realistic PR URL on its push line so the terminal-URL
  glow is demoable/testable end to end.

Tests: new detect-urls suite; updated browser-view-host, SessionView,
TerminalPane, and XtermTerminal suites for the new behavior.

* feat(frontend): flush Windows center panel and lighter terminal background (#3083)

- Add a platform-windows class on the shell root and drop the framed
  center panel's top inset on Windows, where the custom titlebar already
  supplies top chrome; other platforms unchanged.
- Lighten the dark-theme terminal background token (#15171b -> #101317e7).

* fix(push): don't offer Register before pairing, and report the real failure (#2933)

* fix(push): don't offer Register before pairing, and report the real failure

A TestFlight tester who had not yet connected to a server saw a Register button
and, on tapping it, an alert claiming their build had no push entitlement. Both
were wrong.

- The Register action was gated on `!!config`, which is truthy even when
  unpaired (the default config has an empty host). describePush now takes the
  config itself and derives 'is there a server' from a non-empty host, so the
  rule lives in one tested place instead of at each call site.
- registerForPush wrapped token minting and daemon registration in one try, so a
  server that was simply unreachable surfaced as 'no push entitlement'. The two
  steps are now separate and it returns a typed reason, letting the UI say what
  actually went wrong.
- Extract the decision logic into lib/pushStatus.ts (no RN imports) and cover it
  with vitest; wire the mobile workflow to run the suite alongside typecheck.

* feat(mobile): put Orchestrator before PRs in the tab bar

Reorders the tab bar to Kanban, Orchestrator, PRs, Settings. Route names are
unchanged, so deep links (including notification taps to /prs) still resolve.

* fix(push): gate Enable on pairing, and separate server rejection from unreachable

Two follow-ups from review of the push-registration UX fix.

Unpaired users could still reach a push action. The "is there a server"
guard only covered the Register branch, so a fresh install — permission
never asked, empty host — was still offered Enable. Tapping it spent the
user's one-shot OS permission prompt and then failed registering against
an empty host. describePush now gates Enable on the same rule, and
registerForPush returns a `not-configured` reason before touching the
permission prompt, so no call site can reintroduce it.

Real server errors were labelled as unreachable. Every failure from
registerPushDevice mapped to "Couldn't reach your AO server", including
a 401 from a wrong password, a 429 lockout, and 5xx — sending people to
debug their network instead of their password. api.ts now throws a typed
ApiError carrying the HTTP status (message format unchanged, so existing
call sites that match on it still work), and classifyServerFailure maps
it to server-auth / server-rate-limited / server-error; only a request
that never got an answer is still reported as unreachable.

Mutation-checked both regression tests: reverting either fix fails
exactly the test that covers it, and restoring it passes all 22.

* fix(mobile): switch Android package to aoagents.dev; shift tab animation

* docs(android): fix misleading google-services.json ignore comments (EAS strips it even when committed)

* ci: remove legacy frontend-nightly workflow (#3070)

* fix: support ctrl plus zoom accelerator (#2843)

Co-authored-by: Vaibhaav <user@example.com>

* fix(browser): remove redundant working label (#2821)

Co-authored-by: Vaibhaav <user@example.com>

* fix(storage): apply missing out-of-order migrations (#3092)

* chore: remove partial prettier setup (#3090)

* feat(settings): add Developer Mode section gating Feature Releases (#3039)

* feat(settings): add Developer Mode section gating Feature Releases

Adds a Developer Mode toggle (single Switch, default off) to global Settings,
persisted via the ui-store + localStorage pattern under ~/.ao/electron. When off,
the Feature Releases update channel option and its build picker are hidden entirely;
when on, Updates behaves as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(settings): surface concealed feature pins and refresh docs

Address review on PR #3039:
- Show the Return-to-home banner whenever a feature build is pinned in settings
  (not only when one is already running), so a pin that stays effective in the
  main-process updater is never hidden while Developer Mode is off.
- Disable Return until update settings load, avoiding a clobber of enabled/channel
  prefs with defaults when settings failed to load.
- Rename STABLE_CHANNEL_OPTIONS to NON_FEATURE_CHANNEL_OPTIONS (it includes Nightly).
- Cover persisted-pin gating, the primaryValue guard on toggle-off, and localStorage
  persistence with tests (24 passing).
- Update desktop-release.md: Feature Releases now requires enabling Developer Mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(settings): clear feature pins atomically; scope pin banner to Dev Mode off

Address re-review on PR #3039:
- Return now clears the feature pin via a new updateSettings:clearFeature IPC that
  does an atomic read-modify-write on persisted main-process state, so it preserves
  the home channel and works regardless of renderer form hydration. Drops the
  disabled-until-loaded guard, which could otherwise strand a user on a feature build.
- Restrict the persisted-pin fallback banner to Developer Mode off; with it on the
  visible picker already shows the pin, so Updates behaves exactly as before.
- Rename pinnedPr -> featurePr.
- Add ui-store persistence tests (default off, restoration from storage, write) and
  assert the picker/empty-state are absent for a concealed pin; cover the Dev-Mode-on
  no-extra-banner case. Full renderer suite: 978 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): prettier formatting and add clearFeature to e2e fake bridge

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(settings): make Return-home a single updater-serialized operation

Address AO reviewer + illegalcall [P1]: the previous split of
updateSettings.clearFeature() (settingsOperationQueue) then a separate
updates.check() (updaterOperationQueue) was not atomic across the two queues, so a
concurrent retirement poll or hourly automatic-check could re-persist the pin after
the clear, keeping the app on the pr<N> feed.

- Add auto-updater returnToHome(stateDir, requestId): clears the feature pin and
  resolves the home channel inside ONE runSerializedUpdaterOperation, so no other
  updater op or settings-write can interleave. Cleared against persisted state, so
  it never depends on renderer form hydration.
- Replace updateSettings:clearFeature IPC with updates:returnHome; rewire preload,
  bridge, e2e fake-bridge, and test setup; drop the now-unused clearFeature.
- Re-sync the form to persisted truth if pin/return requests error.
- auto-updater tests: returnToHome clears only feature (preserving channel/enabled/
  nightlyAck), checks the home channel, and no-ops in dev. Full suite: 981 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(settings): make the pinned-build fall-home copy truthful

Address illegalcall [P2]: the banner said automatic updates would return the user
home on the next check, but runAutomaticUpdateCheck preserves a live pin
(reconcileFeaturePin keeps it) and configureFeed keeps tracking the pr<N> feed with
auto-download. The app only returns home on Return or when a retired pin is cleared.
Copy now reads: automatic updates keep tracking PR #N until you return home or the
build retires. Concealed-pin test asserts the truthful message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(conpty): correctly implement pidAlive using processalive on Windows (#3082)

* Fix/tmux isalive agent exited 2802 (#2831)

* fix(tmux): distinguish agent exit from surviving keep-alive shell in IsAlive

Set @ao_agent_exited pane option before entering the keep-alive shell in
buildLaunchCommand, then query it in IsAlive after has-session succeeds.
When the option is "1" the agent has exited even though the pane and
session survive, so IsAlive returns false (dead). Any read error (option
unset, transient failure) falls back to true (alive) - backward-compatible.

Fixes #2802

* fix(tmux): decouple Create from IsAlive to fix integration tests

Create now calls hasSessionArgs directly instead of IsAlive. IsAlive's
agent-liveness check (@ao_agent_exited) triggered false failures in Create
when the launch command finished quickly — the option was already set to
"1" before Create finished, causing it to destroy the session.

The integration test assertion on IsAlive after Create is also relaxed:
a fast agent may correctly report as dead while the keep-alive shell lives.

Relates to #2802, #2822

* fix(tmux): add -q to show-options in exitedOptionArgs for clean three-state probe

* test(tmux): make agent exit integration deterministic

* fix: keep the notification bell at the right end of the topbar on all platforms (#3095)

* fix: pin notification bell to the trailing end of the topbar actions on all platforms

On macOS the bell led the topbar action row (before Kill/Orchestrator)
while Windows/Linux pinned it to the far right, so its position changed
across machines. Remove the macOS-only leading render in ShellTopbar and
always render the bell as the last item of the actions row; in the
SessionsBoard in-panel action row move it after New task/Orchestrator.

* feat: show a ringing bell icon when there are unread notifications

Unread state previously only filled the plain Bell; swap in BellRing so
the unread state is visually distinct at a glance. The count badge is
unchanged.

* refactor: remove unused pr action stub (#2759)

Co-authored-by: Vaibhaav <user@example.com>

* test: stabilize windows quality baseline (#2760)

Co-authored-by: Vaibhaav <user@example.com>

* feat(reviews): two-section Reviews tab (#3053)

* feat(reviews): two-section Reviews tab with PR review summaries

Split the Reviews tab into two sections:
- Pull request reviews: GitHub review summaries (reviewer, verdict,
  submitted body) from the session PR summary API.
- AO code reviews: the existing reviewer pane, relabeled.

Each section renders one collapsible PR row per attached PR. Headers show
PR identity and update context only; verdicts appear in the expanded rows,
so there are no duplicate verdict pills. Both sections sit in the same
bg-surface card the Summary and Files tabs use.

The Pull request reviews section only appears when a PR has review
summaries. All sizes/colors go through existing design tokens.

* feat(reviews): codex reviewer icon, gated terminal, review link, spacing

Address review feedback on the two-section Reviews tab:
- show a codex mark for the AO reviewer harness instead of the generic
  shield (shield stays as the fallback for other harnesses)
- keep the Open terminal button out of the footer until a review has run
- link AO review rows to the posted GitHub review
- lead with AO code reviews, PR reviews below
- open up the AO panel spacing so rows are not stuck together
- seed mock PR review entries so the section renders in preview

* fix(reviews): drop arbitrary-value width from disclosure button

A block-level flex button with -mx-1.5 already resolves its auto width to
the container plus the negative margins, so the explicit w-[calc(100%+0.75rem)]
was redundant. Removing it keeps the same bleed without an arbitrary value.

* Feat/command palette search entry (#3097)

* feat: restyle command palette and add sidebar Search entry

* fix: restyle sidebar Search to match Settings row chrome

* fix: restyle sidebar Search to match Settings row

* fix: drop collapsed Search fill and match rail radius

* fix: polish command palette spacing, tokens, and footer shortcuts

* feat: restyle command palette to categorized like results

* fix: keep sidebar Search and Projects chrome fixed while the list scrolls

* fix: restore Enter to best match in categorized command palette

With shouldFilter:false, cmdk selects the first DOM item, so category
order was winning over score. Rank groups by best member matchScore
while typing, restore the rank order guard, fix light theme selection
and footer contrast, drop duplicated CSS / app wide animation forwards,
and clarify the Search click deferral.

* Fix/macos titlebar panel alignment (#3111)

* fix: align macOS titlebar and panel chrome

* fix: drop setTrafficLightsInset from e2e fake bridge

* fix: align settings center panel with board frame insets (#3112)

Use the default CenterPanelShell titlebar alignment so Settings shares
the same macOS outer frame as board/session, without changing the
centered settings content layout.

* feat: box session inspector sections with settings surfaces (#3114)

Wrap Summary/Reviews sections in settings-row cards so the right rail
matches the boxed inspector refs and settings color theme.

* fix(codex): reconcile stale active sessions (#3117)

* feat(board): revamp task cards with agent logos and cleaner layout (#3121)

* feat(board): revamp task cards with agent logos and cleaner layout

Rework the board/kanban session card:
- new AgentAvatar rendering each harness's real brand logo (20 agents with
  assets; a lettered tile falls back for the few without one)
- title leads as the card hero; drop the redundant agent-name label
- move the status (Working / CI failed / ...) into the footer at a smaller
  size, alongside a relative timestamp
- git-branch icon on the branch row; remove the branch and PR copy buttons
- only show a PR line when a PR exists (no more 'no PR yet' noise)
- vary the preview mock sessions across agents so the logos show in the
  VITE_NO_ELECTRON board

* fix(board): nudge agent avatar to align with the title's first line

* fix(board): restore full-card click target + review nits

- make the whole card the open target again (move role/onClick to the outer
  card div) so status, timestamp, issue chip, and PR row are clickable; stop
  propagation on PR links so they still open the PR, not the session
- correct the AgentAvatar fallback comment (amp/cline have logos now; only
  agy/auggie/autohand/fake fall back)
- drop the now-unused clipboard mock scaffolding from the board test

* feat(landing): swap in the new aoagents-landing app (#3122)

Replace the fumadocs-based frontend/src/landing with the standalone
aoagents-landing Next.js app (from ronishrohan/aoagents): App Router marketing
site plus its @superset/* workspace packages under packages/. Testimonial
permalinks use the canonical x.com host so the secret scan does not
false-positive on the 'Twitter Client ID' rule.

The landing stays a self-contained sub-app (excluded from the frontend
electron/vite build) run with: cd frontend/src/landing && npm install && npm run dev

* fix(ci): track landing package-lock for deploy-landing (#3126)

npm ci and setup-node cache need the lockfile in-repo; it was gitignored after the landing swap.

* Fix/landing ci lockfile (#3127)

* fix(ci): track landing package-lock for deploy-landing

npm ci and setup-node cache need the lockfile in-repo; it was gitignored after the landing swap.

* fix(landing): restore static export for GitHub Pages deploy

The aoagents swap dropped output: "export", so deploy-landing built
successfully but never wrote out/, and upload-pages-artifact failed.

* ci: skip canonical-only workflows on forks (#3120)

* ci: skip landing Pages deploy on forks

Forks copy the workflow but cannot create/enable GitHub Pages with
GITHUB_TOKEN, which fails red after a successful build. Gate both jobs
to the canonical AgentWrapper repo so fork runs no-op instead.

Fixes #3119

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: skip release-latest guard on forks

The hourly schedule checks github.repository for a stable desktop
release. Forks have none, so the job fails every hour. Gate it to the
canonical AgentWrapper repo alongside the landing Pages deploy skip.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: polish windows shell controls (#3100)

* fix(desktop): recover when orchestrator agent is missing (#3128)

* fix(desktop): recover when orchestrator agent is missing

* fix(desktop): guard restore dialog recreation

* fix(linux): match mac shell chrome (framed board + titlebar cluster) (#3129)

* fix(linux): match mac shell chrome (framed board + titlebar cluster)

linux rendered a separate `□ ← → Board` shell topbar while mac framed the
board and put the sidebar toggle + history arrows in a fixed titlebar
cluster. unify linux onto the mac path:

- hidesShellTopbar() now covers linux, so the board/session actions and the
  Board crumb mount in-panel (framed) instead of in a shell topbar
- TitlebarNav renders on linux too, pinned at the sidebar's top-left (no
  traffic lights to offset past)
- the sidebar reserves the same titlebar band (underTopbar) on linux
- drop the inline linux-only sidebar toggle — the fixed cluster replaces it,
  which also fixes the collapse-vanish bug (the fixed cluster survives an
  off-canvas collapse, so there's always a way to expand)

windows is unchanged. supersedes #3124.

* fix(linux): align framed board title with the titlebar cluster

the sidebar reserved a titlebar band on linux but the center panel still
used the default (non-mac) insets, so the board/session title sat at the
wrong height and the fixed cluster crowded the panel's top-left corner.

apply the mac center-panel alignment on linux too (near-flush inset +
36px title bar level with the cluster). linux has no traffic lights, so the
sidebar-closed title clears the cluster from its own left edge via a linux
content-offset token instead of the macOS 79px offset.

* fix(linux): center titlebar cluster on the board title's centerline

the linux cluster sat at top-1.5 (6px), centering its 36px box at y24 while
the framed board titlebar centers at y21 (mac inset 2px + surface border 1px)
— so the toggle/arrows read as misaligned against the project title. drop to
top-0.75 (3px) so both share the same centerline.

measured with a throwaway playwright pass under a linux UA: cluster and title
now both center at y21.

* fix: render bare agent logos on board cards (no tile/border) (#3130)

* fix: render bare agent logos on board cards (no tile/border)

the board task-card avatar wrapped every logo in a neutral tile
(rounded-md border bg-raised), which boxed in brand marks that already
carry their own shape (codex, claude, cursor, …). render the logo bare so
the proper brand mark shows; agents without an asset keep a bare initial.

* fix: use the Claude Code mascot for the claude-code logo

claude-code shared claude.svg (the Anthropic sunburst). point it at the
dedicated Claude Code pixel mascot (lobehub claudecode-color, #D97757) so
the board card shows the product's own mark. the "claude" alias keeps the
sunburst.

* fix(board): make the kanban grid shrink to fit (no horizontal scroll) (#3131)

the 4-column grid had a fixed min-w-[64rem], so any window narrower than
1024px overflowed the pane and showed a horizontal scrollbar. drop the
min-width (and the now-dead xl:min-w-0 / overflow-x-auto): the columns are
already min-w-0 and the cards truncate/wrap, so the grid just shares the
available width and stays responsive at any size.

* fix(ui): fix the disable-report-problem to submit (#3002)

* fix(ui): resolve the merge conflict

* fix(ui): fix disable report problem submit

* test(ui): add regression test for ReportProblemDialog submit button validation

* fix(ui): improve ReportProblemDialog submit button behavior and test structure

* test(ui): enhance ReportProblemDialog validation for whitespace-only inputs

* fix(board): show agent brand logo on archive cards (was text label) (#3132)

* fix(board): show agent brand logo on archive cards (was text label)

archive cards (grid + rows) printed the agent as a text label ("Claude",
"OpenCode", …). swap it for the same bare AgentAvatar brand logo the live
task cards use, so the archive matches the board. drop the now-unused
agentLabel helper; update the archive-card test to assert the logo by title.

* fix(a11y): give agent avatar an accessible name

replacing the archive card's visible agent text with a logo dropped the
agent name from the accessibility tree (alt="", title is only a tooltip).
expose the provider as the accessible name via alt (logo) / role+aria-label
(fallback), so screen readers still announce the agent.

test: assert the logo via getByRole("img", { name }) instead of getByTitle,
and cover the row archive layout too.

* fix: isolate session kill state across worker switches (#3084)

* docs: design session kill state isolation

* fix: isolate session kill state

* style: format kill state implementation plan

* fix: prevent late kill navigation

* feat(landing): moving agents marquee (all 23, real logos) (#3136)

* feat(landing): moving agents marquee (all 23) on the trust section

the "use the agents you already trust" row was a fixed set of 12 logos.
replace it with a seamless horizontal marquee of all 23 supported agents at
the same 32px logo footprint. twenty ship a brand logo (copied from the app's
agent assets); agy/auggie/autohand have no brand mark yet, so they show a
monogram chip in the same size.

the track duplicates the list and translates -50% for a gapless loop, pauses
on hover, and stops under prefers-reduced-motion. edges fade via a mask.

* fix(landing): narrow the agents marquee viewport (~10 visible)

the marquee spanned the full section width, so most of the 23 logos were on
screen at once. cap the viewport at max-w-2xl and center it so ~10 flow
through the window at a time, with the rest of the loop off-screen.

* fix(landing): real logos for all marquee agents + fix invisible marks

- agy/auggie/autohand: replace the monogram chips with real brand marks
  (Google Antigravity favicon; Augment + Autohand favicons, whitened)
- goose/kilocode: their app PNGs are near-black and vanished on the dark
  background — swap in whitened lobehub marks
- drop loading="lazy": marquee items move by transform, so their layout
  position stays off-screen and lazy never fires, leaving blank logos as
  they scroll in

every one of the 23 agents now shows a visible logo.

* fix(landing): marquee reduced-motion fallback, seamless loop, a11y

address review feedback on the agents marquee:

- reduced motion: the clipped marquee only showed ~10 of 23 agents when the
  animation stopped. hide the marquee and render a full wrapping static list
  (all 23 visible) under prefers-reduced-motion instead.
- loop reset jump: the flat duplicated list put a gap between every item, so
  translateX(-50%) didn't line up with the second copy (20px jump). structure
  the two copies as equal-width groups with a trailing gap equal to the inner
  gap, so -50% lands exactly on group two.
- a11y: the duplicate copy exposed all 23 alt texts twice. mark the duplicate
  group aria-hidden so screen readers meet each agent once; also pause on
  focus-within, not just hover.
- react-doctor: drop the per-render loop array (groups map AGENTS directly)
  and key by agent.name instead of the array index.

verified: jump ~0px, 23 logos announced once, all 23 visible under reduced
motion; static export build passes.

* fix(landing): header scroll-to-black not firing on the hero pages

`if (pathname === "/download") return null` sat before the useState/useEffect
hooks — a Rules-of-Hooks violation that broke the client component's
hydration, so the scroll listener never attached and the transparent hero
header (/, /design-partners) stayed transparent instead of turning solid on
scroll. move the early return after the hooks so the effect runs.

verified in a production build (output: export): header is transparent over
the hero and flips to bg-background once scrolled.

* fix(landing): add an operable pause/play control to the agents marquee

:focus-within couldn't pause the marquee because it has no focusable
descendants, leaving hover as the only control (no keyboard/touch). add an
explicit Pause/Play button that toggles the animation via a data-paused
attribute — keyboard, touch and mouse operable (WCAG 2.2.2). hidden under
prefers-reduced-motion, where the static list already shows every agent.

* fix: preserve failed scratch workspaces for retries (#3137)

* fix: preserve failed scratch workspaces for retries

* fix: preserve post-runtime scratch failures

* feat(landing): restore the docs site in the new landing style (#3143)

* feat(landing): restore the docs site in the new landing style

the #3122 landing swap replaced the whole landing wholesale and dropped the
old fumadocs docs site (~40 pages + routes + components). the Documentation
nav still pointed at aoagents.dev/docs, which 404'd.

restore it, ported onto the new landing's next-mdx-remote stack:
- recover all content/docs MDX from before the swap; strip the fumadocs-ui
  imports (the components now come from a component map)
- src/lib/docs.ts: read content/docs recursively, build the sidebar tree from
  the per-dir meta.json (section separators included), page lookup + TOC
- /docs/[[...slug]] route (static export, generateStaticParams): left sidebar
  nav + MDX content + right-hand TOC, styled like the changelog/blog (dark,
  prose-invert)
- landing-styled MDX components: Callout, Accordion(s), Step(s), Tab(s)
  (interactive), Card(s), plus the ported PluginCard/PluginGrid/
  PlatformSupport/Logo/InstallDownloads; heading ids injected for TOC anchors
- point the Documentation nav at /docs (internal); add docs to the sitemap

verified: next build (output: export) generates all 55 doc pages; sidebar,
TOC, tabs, callouts render on the dark landing theme.

* fix(landing docs): scroll-spy TOC, hide sidebar scrollbar, drop footer

- the "On this page" TOC is now a client component that highlights the
  current section as you scroll (scroll listener on heading positions)
- hide the left sidebar's scrollbar (scrollbar-hide) — it still scrolls
- no marketing footer on docs pages; they're full-height with their own
  sidebar/TOC (Footer returns null on /docs like it already does on /download)

* fix(landing docs): visual hierarchy in the sidebar nav

main topics and their sub-pages looked identical (only indentation differed).
now: section labels stay uppercase/muted, a topic that owns child pages reads
as a bright, medium-weight group header, and its children sit lighter/muted
under a vertical guide line — so nesting (Plugins > Agents > Claude Code)
reads at a glance.

* fix(landing docs): tighten the gap above the page title

the content title floated ~88px below the header (article padding + the
typography plugin's h1 top-margin). force the h1 margin to 0 and trim the
article top padding so the title top-aligns with the sidebar (~32px), instead
of a big empty band under the header.

* fix(landing docs): broken download link (404)

InstallDownloads linked to /download, which isn't a route in the new landing
(the header only stubs it out), so it 404'd. reuse the site's canonical
DownloadButton instead — it detects the platform and opens the latest GitHub
release, same as the homepage.

* fix(landing): recenter hero preview after resize (#3146)

* feat(landing): add 0.11.0 changelog entry + restore the detail route (#3140)

* feat(landing): add 0.11.0 changelog entry + restore the detail route

the changelog page showed "No updates yet" because content/changelog was
empty. add the 0.11.0 ("The Floor") release entry as MDX.

the per-entry detail route was also missing from the landing swap (the
list-page titles and the sitemap already point at /changelog/<slug>, and
getChangelogEntry/getAllChangelogSlugs existed unused), so add it with
generateStaticParams so the static export prerenders each entry. without it
the entry title and sitemap urls 404.

verified: `next build` (output: export) generates both /changelog and
/changelog/agent-orchestrator-0-11-0.

* feat(landing): auto-populate changelog from GitHub Releases

the changelog now builds itself from published stable releases instead of
needing a hand-written MDX per version.

- getChangelogEntries fetches stable vX.Y.Z releases from the GitHub API at
  build time, maps each release body to an entry, and merges them with any
  curated content/changelog MDX (curated wins for the same version, so the
  0.11.0 "The Floor" entry stays until v0.11.0 is actually tagged)
- nightlies, per-PR prereleases, package tags and empty-body releases are
  filtered out
- release bodies are plain markdown, so a sanitizer escapes the constructs
  MDX would try to evaluate (<, {, }) outside code spans — an arbitrary body
  can never break the static export. remark-gfm renders the GitHub markdown
- the data fns went async; updated the list page, [slug] route, sitemap and
  changelog.xml accordingly

verified: next build (output: export) generates /changelog plus a detail
page per entry (0.11.0 + v0.10.3 down to v0.2.0); v0.10.0's empty body is
skipped and the v0.7.0 body with raw < / { builds clean via the sanitizer.

note: the deploy-landing workflow also needs a release+schedule trigger so a
new release redeploys the site (the push trigger is landing-path filtered).
that change is applied separately — it needs `workflow` token scope.

* fix(landing): render release bodies as Markdown, not MDX

review flagged that the MDX sanitizer couldn't fully protect arbitrary GitHub
release bodies: a line like `export const metadata = { version: "1.0.0" }`
escapes to a valid MDX ESM construct that compileMDX still rejects, which
could break the static build on a future release.

fix it structurally — a release body is plain Markdown, so render it through
a Markdown-only path (react-markdown + remark-gfm) that never invokes the MDX
compiler. curated content/changelog entries still compile as MDX (they use
custom components). entries carry a `source` field to pick the renderer.

- drop sanitizeMdx entirely (no longer needed)
- getReleaseEntries filters + maps in a single pass (react-doctor nit)
- correct the stale comment that claimed a daily deploy schedule

verified: rendering `export const metadata = {…}`, import lines, raw <tags>,
{expressions} and fenced code through react-markdown does not throw and keeps
the text literal; full static build passes over the real release bodies.

* Feat/project session tabs (#3138)

* feat: add persistent per-session tabs

Let users build independent tab layouts from sessions across projects and scoped terminals, with a searchable launcher for larger workspaces.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: simplify session tab launcher

Place session search beneath the terminal action and remove redundant launcher chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(frontend): remove hidden sidebar controls from tab order (#3148)

* chore(landing): rename workspace scope to @ao/* and align site metadata (#3164)

* chore(landing): rename workspace scope to @ao/* and align metadata

Standardize the internal package scope to @ao/* (package names in packages/*,
~40 imports, and the lockfile; workspaces relinked) and bring the site's
generated metadata, CLI installer, marketplace copy, analytics cookie name, and
structured-data links in line with our brand, domain, and GitHub. Also tidy
stale design-reference comments in the renderer and docs.

Verified: landing static export builds (71 pages).

* docs(design): neutralize the design-lineage references in DESIGN.md

Rephrase the historical aesthetic-direction notes to describe the flat,
hairline-bordered reference generically and drop the external repo link.
The live direction (clone agent-orchestrator verbatim) and every design
spec — palette, type scale, spacing, layout, motion — are unchanged.

* docs: use canonical aoagents.dev domain in README and docs (#3165)

README and the changelog/migration docs still pointed at the hyphenated
ao-agents.com host. The live site and the brand constants use aoagents.dev,
so the install and docs links here 404. Align all 29 references to the
canonical domain.

* test(frontend): reuse sidebar sizing constants (#3168)

* feat(frontend): restyle project settings to match global settings page (#3170)

* feat(frontend): restyle project settings to match global settings page

Replace the card-based project settings layout with SettingsPageShell,
SettingsPanel, SettingsSection, and SettingsRow so it matches the global
Settings UI. Use SettingsOptionMenu for agent, reviewer, and permission
pickers; add compact tokenized inline inputs and a primary save footer.

Cap settings dropdown menus to a fixed width/height with internal scroll,
extract layout dimensions to tokens, and add close-button/Escape navigation
tests for project settings.

* fix: declutter agent select dropdown

Drop the status warning icon and give harness picker rows more width and padding so labels and status text aren’t cramped.

* fix: normalize project settings value typography

Drop mono styling on identity rows so values like kind, id, path, and repo match the Refresh control size and style.

* fix: hide Authorized status on agent select options

Keep agent menu labels clean for authorized harnesses so option names stay
selectable in tests and UI; only surface Needs install / Needs auth / Auth unknown.

* fix(board): legible agent logos in both themes + decluttered task cards (#3169)

* fix(board): keep single-colour agent logos legible in both themes

A few harness marks ship as a single colour — white-only (opencode, cursor,
cline, goose, kilocode) or black-only (copilot) — so they disappeared against
one of the board themes (white on the light board, black on the dark board).

Render those marks as inline currentColor SVGs so they take the board's
foreground colour and stay legible in both light and dark. Full-colour brand
logos are unchanged (still <img>). Also drops the placeholder Copilot octocat
PNG; Copilot now uses its real mark, inlined with the rest.

* fix(board): declutter task cards — drop the redundant status pill

Each column header already names the stage (Working / Needs you / In review /
Ready to merge), so the per-card status pill just repeated it ("Working" under
Working, "Approved" under Ready to merge, etc.). Remove it entirely.

The card now carries only its own identity + code state — agent, title, branch,
PR(s), diff, updated time — collapsed into one footer row instead of the old
two-row split that left blank gaps. Adds the diff size (+adds / -dels), which
was previously only in the review rail.

Tests updated: cards assert no status pill and still route to the right column.

* test(e2e): assert card identity/column, not the removed status pill

The board/session smoke specs asserted card status text ("Working", "Input
needed", "Ready") that the decluttered cards no longer render. Point them at the
card title instead — column membership already proves the status-driven move.

* fix(sidebar): match the search field radius to the cards

The sidebar Search button used --radius-settings-row (16px), noticeably rounder
than everything around it. Drop it to rounded-lg (8px) so it matches the board
cards and the collapsed-icon search state.

* fix(board): shrink agent logos to 16px and even out their visual size

Drop the agent mark from 18px to 16px (size-icon-base) so it sits quieter next
to the task title. The inline mono marks filled their box edge-to-edge, reading
larger than the padded brand logos — inset their viewBox (-2 -2 28 28) so every
agent logo lands at the same visual size.

* fix(sidebar): shrink the search field label to 12px

The Search label sat at text-control (13px), heavier than the compact field
wants. Drop it to text-xs (12px) so it reads as a quiet placeholder next to the
already-11px shortcut hint.

* Revert "fix(board): legible agent logos in both themes + decluttered task car…" (#3172)

This reverts commit 2702e5c8c9df7a660c0662afa20eb78882b778ca.

* feat(landing): refresh design partners and add mobile downloads (#3174)

* feat: refresh design partner page

* fix: simplify design partner exchange table

* feat(landing): showcase mobile companion app

* feat(landing): make mobile preview interactive

* fix(landing): tighten mobile preview proportions

* feat(landing): add platform download page

* feat(landing): link design partners in header

* feat(docs): add dedicated docs site and polish docs navigation (#3149)

* fix(landing): polish docs layout and navigation

Align the docs page with the landing design system and make sidebar navigation feel consistent, responsive, and easier to scan.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(docs): add a dedicated docs site

Mirror the landing docs in a separate Fumadocs app that reuses the shared AO theme, and tighten accordion rendering so FAQs read as answered content instead of a gapped list of closed cards.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(docs): restore search and align branding

Point the dedicated docs app at the site favicon and AO logo, switch search back on with a static exported index, and remove the package-style Superset dependencies in favor of direct local imports.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(docs): tighten sidebar spacing and scrolling

Reduce the docs sidebar horizontal padding and restore a proper scrollable viewport for both the desktop sidebar and the mobile drawer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(frontend): exclude docs app from root typecheck

Treat the dedicated docs site as its own Next app so the renderer frontend's root TypeScript pass does not try to resolve docs-only aliases and dependencies.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* Feat/kanban board grid UI (#3139)

* feat: refine kanban board grid and session cards

* feat(board): tighten session card layout and board chrome

Lead cards with agent icon + title, move status/PR into a footer row with a
branch line, and strengthen kanban dividers. Bump macOS traffic-light clearance
so the titlebar cluster clears cleanly.

* fix: refine kanban board grid and session card layout

* fix(board): restore continuous hairline kanban column grid

* feat: restyle agents demo and fix tab branding (#3179)

* feat: restyle agents demo and fix tab branding

Match the Agents feature preview to settings-row UI with a clearer
open/close cycle, pause on interaction, and Agent Orchestrator branding.

* fix: address React Doctor findings in agents demo

Move ref sync out of render, replace the async loop with a tick
scheduler, build SELECTABLE in one pass, and use next/image for icons.

* feat(landing): hero polish, download fixes, and marquee cleanup (#3183)

* feat(landing): polish hero GitHub button + remove marquee pause control

- Hero: rework the GitHub button into a single clean pill (icon + "Star on
  GitHub" + a subtle yellow star with the tabular star count), and use a real
  <a href> instead of button+window.open for proper link semantics.
- Agents marquee: drop the redundant pause/play control; the marquee still
  pauses on hover and reduced-motion users still get the full static logo list.

* fix(landing): use a plain hyphen instead of an em dash in the mobile feature copy

* feat(landing): update hero tagline to "Stop babysitting agents. Start merging real work."

* fix(landing): don't let analytics break download navigation + label mobile CTA

- track() called posthog.capture() unguarded; when PostHog is uninitialized
  (no NEXT_PUBLIC_POSTHOG_KEY, i.e. local dev) it throws inside the download
  Link's onClick and aborts the navigation. Wrap it so analytics can never
  break the click (mirrors the existing Reddit-pixel guard).
- The download button fell through to "Download for Mac" on phones. AO is a
  desktop app, so mobile now shows "Get AO for desktop" with a monitor icon,
  still linking to /download.

* Feat/onboarding guide (#2647)

* docs: consolidate contribution guides and expand development docs

Merge CONTRIBUTING.md and contribution-docs/Contribution-Guide.md into
a single CONTRIBUTING.md. Expand docs/development.md with clone, branch,
rebase, and commit conventions. Add PR squash-merge policy.

Signed-off-by: Piyush Mudgal <pranyasharma55555@gmail.com>
Co-authored-by: Pyasma <pranyasharma55555@gmail.com>

* docs: clarify development guide

* docs: fix development guide follow-ups

---------

Signed-off-by: Piyush Mudgal <pranyasharma55555@gmail.com>

* fix(notifications): suppress toast for the actively-viewed focused session (#3108)

* fix(notifications): suppress toast for the actively-viewed focused session

In-app notifications fired for a session's needs_input transition even when
that session was already open and focused in the foreground, so the toast
told the user something they could already see on screen.

The daemon is headless and serves multiple clients at once, so it cannot
know which pane the user is looking at; its emission stays unconditional
and the suppression lives client-side.

createNotificationsTransport now takes an optional getActiveSessionId
getter and skips aoBridge.notifications.show() when the notification is
for the active session and document.visibilityState is "visible".
NotificationRuntime threads the router's sessionId param in through a ref
so the long-lived SSE connection sees the current session without
reconnecting on navigation.

Cache merges are untouched, so the bell badge and the notification list
still update for the active session - only the toast is suppressed.

Fixes #3107

* fix(notifications): require window focus and limit suppression to needs_input

Addresses review feedback on #3108.

Visibility alone did not mean the user was looking at the session. On
Windows and Linux an unfocused or fully covered Electron window still
reports visibilityState === "visible", so a needs_input toast could be
swallowed while the user was in another app. The guard now also requires
document.hasFocus().

The guard also applied to every notification type, but #3107 only asks for
needs_input. ready_to_merge, pr_merged and pr_closed_unmerged report
outcomes that are not visible in the terminal pane, so they must still
toast even for the session in the foreground. Suppression is now gated on
notification.type === "needs_input".

The session comparison additionally rejects an empty sessionId, so a
notification without a session no longer matches an undefined active
session on a route that has none.

The suppression logic moves into suppressToastForFocusedSession() with the
reasoning documented next to it.

Tests cover both regressions: a visible-but-unfocused window still toasts,
each PR-outcome type still toasts for the focused active session, and the
unread cache keeps every notification either way.

* fix(notifications): only suppress while the agent terminal is on screen

Addresses the follow-up review gap on #3108.

Being on the session route did not mean the agent's prompt was visible.
CenterPane renders one terminal at a time, so selecting a shell tab or the
reviewer swaps the agent's terminal off screen while the route still names
that session. A needs_input toast was suppressed even though the prompt was
nowhere to be seen, and the tab strip carries no attention marker to make
up for it.

SessionView's terminal target is local state and the notification runtime
lives outside that subtree, so the pane now publishes which terminal it is
showing to the ui-store, keyed by session, and clears it on unmount.
NotificationRuntime reports the session only while its agent terminal is
the one showing. It reads the store imperatively: the SSE connection is
established once and outlives navigation, so the getter needs live state
rather than the value captured when it connected.

The transport parameter is renamed to getVisibleAgentSessionId to say what
it now means, and the guard's doc comment covers all three ways "the user
can see it" can be wrong: route, window visibility, and window focus.

Tests cover the pane publishing its target across tab switches and unmount,
and the runtime reporting nothing while a shell or reviewer covers the
agent, including that a tab switch does not reconnect the stream.

* fix(frontend): drop the stale requestNewShellTerminal selector after the merge

The typecheck job failed with TS6133: 'requestNewShellTerminal' is declared
but its value is never read.

main replaced SessionView's new-shell wiring: the button now calls
addShellTerminal, which opens a shell scoped to the tab owner's session and
project, instead of raising the requestNewShellTerminal nonce. This branch
still carried the old selector, and resolving the merge kept both, leaving
the selector with no reader.

Removing it loses no behaviour. addShellTerminal is what onNewShellTerminal
is wired to, and the store action still serves the standalone terminals view
and the Ctrl+Shift+` shortcut, so only the unused local goes.

Also runs Prettier over the two files the merge left off-style: the mocked
CenterPane in the test was re-indented with spaces, and two call sites were
re-wrapped.

* add customizable keybindings (#3157)

* feat(shortcuts): add customizable keybindings

* test(shortcuts): update e2e bridge fixtures

* test(shortcuts): use platform default inspector chord

* fix(shortcuts): harden customizable keybindings

* test(settings): add keybinding bridge fixture

* test(shell): add keybinding bridge fixture

* fix(shellterm): open session-scoped terminals in the session's worktree (#3106)

* fix(shellterm): open session-scoped terminals in the session's worktree

Opening a terminal from a worker/orchestrator session view resolved the
working directory from the project's registered root only, dropping the
session id and colocating the shell with the wrong checkout/branch.

Adds a SessionWorkspaceLocator alongside the existing ProjectRootLocator
so the shellterm service can resolve a session id to its live workspace
(worktree) and use it ahead of the project root; falls back to the
project root when a session has no workspace of its own, and to the
data dir when neither is given. Project-board and no-context opens are
unaffected.

* fix(shellterm): close scoped shells before worktree teardown, validate stale workspace paths

Kill and Cleanup removed a session's worktree without closing any shell
terminal scoped to it, leaving an open shell pointed at a deleted directory
(and on Windows an open handle on that directory could block the removal
itself). Session Manager now closes a session's scoped shell terminals,
best-effort, right before its worktree is torn down.

Kill/Cleanup also never cleared Metadata.WorkspacePath after a clean
teardown (that field doubles as the restore hint, so it survives on purpose).
The shell-terminal session locator now validates the recorded path exists on
disk before trusting it, falling back to the project root when it's gone,
while a dirty worktree Kill deliberately preserved is still returned as-is.

* fix(shellterm): make scoped-shell shutdown a real barrier, regen sqlc output

BeginSessionTeardown previously logged a Destroy failure, deleted the row,
and let the caller proceed to remove the worktree regardless -- a live PTY
could survive Destroy and be left both untracked and standing on ground
about to disappear. It now confirms death via IsAlive before forgetting a
handle, keeps the row and returns an aggregate error for anything still
alive, and callers (Kill, Cleanup, RetireForReplacement, the reconcile/
shutdown save-and-teardown path) must not touch the worktree on that error.

The snapshot-select-then-destroy sequence was also a race: OpenShellTerminal
could insert a new shell between the read and the worktree's actual removal.
BeginSessionTeardown now holds a per-session gate for the whole span through
EndSessionTeardown, so a concurrent Open blocks until teardown (destroy or
preserve) finishes rather than landing in a directory that is disappearing
underneath it.

Coverage previously stopped at Kill/Cleanup; RetireForReplacement and the
reconcile/shutdown save-and-teardown path (saveAndTeardownOne, reached by
both SaveAndTeardownAll and reconcileLive) also force-destroy worktrees and
now go through the same gate.

Also: the SelectShellTerminalsBySessionID generated code was hand-written in
the previous commit rather than produced by sqlc, against this repo's own
rule. Reverted to the pre-hand-edit generated file and regenerated for real
via `npm run sqlc` (CGO_ENABLED=0 -- the local cc1 toolchain doesn't support
64-bit mode); the output matches byte-for-byte modulo the removed comment.

* fix(lint): scope Cleanup's shell-terminal-gate defer per session, simplify test conditions

golangci-lint flagged endShellTerminalTeardown's defer sitting inside
Cleanup's for loop (gocritic deferInLoop) -- it would only run when Cleanup
itself returned, not per session. Split the per-session body into
cleanupOne so the defer is scoped to one call.

Also collapsed !(A && B) to the De Morgan's equivalent in six ordering test
assertions (staticcheck).

* fix(shellterm): apply confirmed-dead rule everywhere, tie release to its own acquisition

CloseShellTerminal deleted a shell's row before attempting Destroy, and
ReapShellTerminalsFromPreviousAppRuns bulk-deleted every orphan row after
best-effort destroys, regardless of whether each one actually died. Either
path could forget a shell that survived, making it invisible to a later
BeginSessionTeardown scan and letting that session's worktree be removed
out from under it. Both now go through the same destroyConfirmed helper
BeginSessionTeardown already used: destroy, then IsAlive if that errors,
and only delete once death is confirmed (an IsAlive error counts as "not
confirmed", same as alive).

The session gate's release was tracked by session id in a shared map
(held[id] = true/false). An unmatched or duplicate release call for a
session could unlock a DIFFERENT, still in-flight acquisition for that same
id -- there was nothing tying a release to the specific Begin that took the
lock. BeginSessionTeardown now returns a release closure over that exact
lock instead; Manager defers the returned value directly. The held map is
gone.

OpenShellTerminal allocated a permanent gate-map entry for any session id
before validating it existed, so repeated requests naming invalid ids grew
daemon-lifetime memory without bound. The session is now confirmed to exist
first; an unknown id never touches gate state.

Also: the blocking-gate test asserted "still blocked after 50ms", which is
consistent with both a correctly-blocked goroutine and one that simply
hadn't been scheduled yet. Added a test-only hook that fires the instant
OpenShellTerminal reaches the gate acquisition point, so the test can prove
the goroutine got there before asserting it's stuck.

* fix(shellterm): close the gate hole in CloseShellTerminal, clear Kill's restore marker

CloseShellTerminal never took the session gate: it deleted a scoped shell's
row and only then destroyed the PTY, with the destroy failure merely logged.
A BeginSessionTeardown racing that close could run its SELECT after the row
was gone, find nothing to drain, report success, and let Session Manager
remove the worktree while that PTY was still alive inside it -- the exact
invariant the gate exists to enforce, on the path a user takes most often.
It now resolves the row's session id first (via a new store lookup over the
already-generated SelectShellTerminalByHandleID query), takes that session's
gate, and destroys under it. Session-less shells still skip the gate.

Kill's shell-gate refusal returned the same (false, nil) shape as the dirty
-workspace refusal beside it but skipped DeleteSessionWorktrees, so a user
kill that stopped on an unconfirmed shell left the restore marker behind for
the next boot's RestoreAll (#2319). Both refusals now clear it.

Also fixes a Windows failure this change surfaced: `ao session kill` now
destroys a session's scoped PTY and removes its worktree inside one request,
and the PTY's handle on that directory can outlive the call that killed it,
so os.RemoveAll failed with a sharing violation and the kill 500'd (a retry
seconds later succeeded). gitworktree's three removal sites now go through
a bounded retry.

* fix(shellterm): make gate waits cancellable, attribute session-scoped shells to their project

Drops the orphaned statements the main merge left behind in
shell-new-session-shortcut.test.tsx: resolving the conflict kept both the
session-scoping and pinned-tab test bodies but also a stray copy of the
shared tail, which parsed as statements outside any function and broke tsc.
Both tests are intact; only the duplicate tail is gone.

Gate waits are no longer uninterruptible. sync.Mutex.Lock cannot observe a
context, so a teardown whose workspace.Destroy stalled (hung git subprocess,
a Windows handle that never releases) parked every waiter forever, and an
HTTP handler blocked there could not notice its client disconnecting. The
gate is now a capacity-1 channel, so acquire selects over the send, ctx.Done
and a bounded budget -- cancellation propagates, and a wedged teardown
surfaces as a retryable 409 instead of a request that never returns.

A session-scoped open that named no project persisted ProjectID="" even
though the working dir resolved through the session's own project, leaving
the row unattributable. resolveShellTerminalWorkingDir now reports the
project it resolved through and that is what gets stored.

Widens the worktree-removal retry added in the previous commit: a kill with
two shells open was observed still holding the directory past 5s, well past
the original ~1s budget, so it 500'd and stranded the worktree. Backoff now
escalates to a ~7.75s ceiling, measured against a worst case of ~6.2s. This
also fixes the two long-standing gitworktree test failures that turned out
to be the same Windows lock.

* fix(gitworktree): scope the removal retry to Windows and make it cancellable

The retry existed for a Windows handle-release artifact but ran on every
platform, so a genuinely permanent failure on macOS/Linux — EACCES, a real
ENOTEMPTY — slept out the whole budget before returning the identical error.
It is now gated on runtime.GOOS. That gate is an injectable var rather than
an inline check so the retry tests still execute everywhere: a bare GOOS
check would have silently reduced all of them to no-op fall-throughs on the
platform CI actually runs.

time.Sleep is uninterruptible, which is the same class of problem the
session gate rewrite just fixed one package over. removeAllWithRetry now
takes a ctx and selects on ctx.Done(), so a caller that has already given up
stops the loop instead of paying out the remaining budget. That matters
because Session Manager tears a workspace project's repos down serially
while holding the session's shell-terminal gate, so the per-path budget can
serialize into N × it and outrun sessionGateWaitTimeout; the comment now
records that the two budgets are coupled.

Corrects the documented total: 18 attempts is 17 sleeps, 7250ms, not 7.75s.
The sequence was right, the sum was not, and the constant is empirical
enough that someone will tune against that number later.

* `fix(vibe): route keychain auth probe through aoprocess and guard on OS` (#3076)

* feat: implement authentication status detection for Vibe agent via environment variables, files, logs, and keychain

* fix: remove unused gosec nolint directive from Vibe agent keychain auth check

* fix: format imports alphabetically using gofmt in agent adapters

* test: add comprehensive unit tests for Vibe agent adapter functionality

* fix(landing): keep the mobile app preview's proportions on small screens (#3184)

* fix(terminal): write the initial replay as one chunk behind a cover

Fixes #3160 — opening a session showed a mid-session position and then
visibly scrolled down to the tail.

The daemon pumps a pane's attach replay in 32KB reads (attachment.go
copyOut), so the renderer receives N WebSocket frames and issues N
terminal.write() calls. xterm parses each write atomically, but the
browser paints BETWEEN event-loop turns, so every frame boundary is a
painted, further-scrolled state.

Measured against real xterm 5.5.0 on a 1000-line replay: 25 frames at
16ms spacing paint 25 distinct scroll positions over 550ms; the same
bytes as a single write paint exactly 1, for ~2ms of parse. Note there
is no ~15KB internal batching in xterm's WriteBuffer — the batch
granularity is the caller's write() calls, i.e. our WS frames.

Buffer the initial burst and write it once, keeping the pane covered
until that write is parsed:

- XtermTerminal: forward xterm's write callback, which was dropped. It
  fires when the chunk is parsed, giving a deterministic reveal point
  rather than a timer guess.
- useTerminalSession: gate opens in connect(), not on `opened` — the
  daemon fires onOpen from setPTY before copyOut starts, so `attached`
  precedes the first replay byte. A 60ms quiet window ends the burst; a
  750ms cap bounds the cover and still flushes as one chunk, so the
  worst case degrades to a single jump rather than the walk. Exposes
  replaySettled.
- TerminalPane: blank terminal-coloured cover while buffering, label
  delayed 120ms so fast opens never flash a loader. Deliberately not the
  existing empty-state card, which would flash "Starting session" on
  every session switch.
- Hold the resize re-assert while a burst is in flight: the backend
  answers every resize frame with an …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): Spawn Orchestrator has no recovery when project has no agent configured

3 participants