Commit Graph
6132 Commits
Author SHA1 Message Date
Brennan Benson 8757e40063 fix(native-chat): keep a structured agent's tool line between tool calls (#22349)
* fix(native-chat): keep a structured agent's tool line between tool calls

A structured session's status named a tool only while the call was still
running, so the sidebar's tool line went blank whenever the agent was
thinking or writing between calls. Terminal agents keep naming the finished
tool until the next one starts, and clear it after a failure. The structured
status projection now does the same: a running call wins, otherwise the
turn's newest root call if it completed.

* fix(native-chat): bound the structured tool line by the running turn, not the user row

A send made while a turn is running writes its user row into the journal
straight away, and the turn keeps going. Stopping the scan at that row
blanked the tool line while a tool was still running. The scan now runs to
the turn record and names a call only when that record is still running, so
a turn that already ended never lends its last tool to a pending follow-up.

This lookup was the running-only lookup's only production caller, so it
replaces that lookup instead of sitting beside it.

* fix(native-chat): keep naming a structured agent's failed tool until the next one

Clearing the tool line after a failed call brought the blank gap back for
much of a turn: Codex marks any nonzero exit as failed, so a search with no
match or a red test run is enough. The failure already shows on the tool's
own row in the transcript. The running turn's newest running call still
wins; otherwise its newest root call is named whatever it settled to.

* fix(native-chat): name a structured Codex edit on the tool line as the chat draws it

Once a Codex edit's changes exist, its apply_patch call becomes a diff row,
which the status lookup skipped, so the row named the command before the edit.
The chat's tool-call block for a journal row now comes from one shared builder,
and the status lookup reads the same definition: a diff is named as Diff with
its path, and counts as settled since it carries no lifecycle.

* docs(native-chat): describe the structured tool field as running-or-latest

The status summary's toolName/toolInput now name the running turn's
latest tool between calls, not only a running one. Update the wire type
and status bridge comments that still said "the running tool".
2026-09-22 22:42:31 -07:00
Neil 51d3cafc4f feat(editor): add a setting to turn off preview tabs (#22398)
Single-clicking a file in the Explorer, or following a link in Markdown
source, opens it as a preview tab that the next preview open replaces.
There was no way to turn that off, so browsing files kept swapping one tab.

Adds `editorPreviewTabsEnabled` (General -> Navigation, on by default).
A caller's `preview` flag is now an intent that `resolveEditorPreviewIntent`
resolves against the setting, covering every open path - files, diffs,
history diffs, conflicts - in one place.

Preview-ness is derived rather than reconciled: readers treat a tab as a
preview only when the stored flag and the setting agree, so a flag left
over from a saved session, another window, or a host switch is inert while
previews are off. Nothing rewrites stored flags when the setting changes,
so no settings-landing path has to remember to clean up.

Fixes #22397
2026-09-22 22:33:06 -07:00
Neil 83dd047fd9 fix(explorer): find files by name in large local workspaces (#22369)
* fix(explorer): search local workspaces by file name across every file

The Explorer name filter only searched remote workspaces directly; local
workspaces still filtered the first 20,001 listed files, so files beyond
that cap never matched in large repos. Local name queries now rank the
whole workspace on the host, and fall back to an uncapped git listing
when ripgrep is not installed.

* fix(explorer): filter capped local listings on the host with the Explorer word rule

Replaces the Quick Open fuzzy top-32 routing, which dropped multi-word
matches and capped visible results. The Explorer keeps its instant
renderer-side filter; only when the local listing hits its cap does it
re-list on the host with the same word rule applied before the cap.

* fix(explorer): keep capped matches when host name filtering fails

- Fall back to the capped listing (and stop re-listing) if the host scan fails
- Keep primary matches when the ignored-file pass fails during a filtered scan
- Key host scans on normalized filter words; reset capped state per filter session
- Bound nameFilter size at the IPC boundary; drop the double readdir walk

* fix(explorer): match name filters without locale-dependent lowercasing

* fix(explorer): avoid render-time ref writes in the host name filter fallback
2026-09-22 19:39:52 -07:00
NeilandAdrien De oliveira ebed0964a2 feat(agents): add first-class Muse Code harness (#22216)
* feat(agents): add first-class Muse Code harness

Add Muse as a supervised Orca agent across desktop, mobile, session history, source control, local hooks, SSH, WSL, and native Windows. Preserve user settings, support Muse 1.3 hook environment allowlists, and recognize versioned foreground processes. Include question, waiting, completion, resume, and readiness coverage.

Co-authored-by: homesh-dev <300847526+homesh-dev@users.noreply.github.com>

Co-authored-by: jeffhuen <32542276+jeffhuen@users.noreply.github.com>

Co-authored-by: John Cusack <johncusackccm@gmail.com>

Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com>

* test(agents): cover Muse remote hook registration

* test(agents): cover Muse hook and source-control contracts

* test(agents): exclude Muse hook metadata from script mode check

* test(agents): keep Muse skill picker coverage stable

* test(ai-vault): include Muse in every-agent fixture

* test(mobile): repin Muse agent icon closure

* fix(muse): detect questions and approvals from structured Muse signals

Muse 1.3 fires no hook for request_user_input, so a pending question left
the pane "working". Its internal reminder subagents also post hooks with
their own session ids (even after Stop), which surfaced "tool failed" rows
and flipped finished panes back to working.

- Read pending questions from Muse's session log
  (user_input_prompt_requested/settled) via the existing transcript poll,
  now generalized from Codex subagents to Muse on main and relay.
- Drop child-session hooks (SubagentStart ids, or turn_id === session_id).
- Treat Notification permission_prompt as the approval wait; PermissionRequest
  also fires for auto-approved calls, so it only caches the approval card.
- Ignore Notification copy as the prompt; poll replays are not new prompts
  or turn boundaries.
- Allowlist USERPROFILE so Windows cmd AutoRun doesn't fail every hook.

* perf(muse): parse only question events from the session log

Most Muse session-log lines are large model/tool records. Filter raw lines
by the user_input_prompt_ marker before JSON.parse via an optional
readJsonlCursor line filter.

* fix(muse): unwrap batched log records and scope questions to the live turn

Review follow-ups: question events inside retained_frame batches were
skipped, and a question left open by a crash or interrupt stayed pending
for the pane's life. Share the history scanner's retained_frame unwrapper,
and only report a pending question whose run_id matches the hook turn_id.

* refactor(muse): drop type assertion in retained_frame unwrap

* fix(agent-hooks): satisfy exhaustive-switch lint in transcript poll policy

---------

Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com>
2026-09-22 19:13:11 -07:00
Brennan Benson 1b85be67d8 feat(native-chat): notify on every settled structured turn (#22105)
* feat(native-chat): notify on every settled structured turn

A structured chat that finished while you were elsewhere lit the sidebar
but never raised an OS notification, and a notification that did arrive
for one could not open the chat it came from.

Unread and delivery now come out of the single resolveAgentAttention
decision the terminal lane already uses: the structured dispatcher calls
applyAgentAttention instead of applyAgentAttentionUnread, so the same
policy that decides what to light also decides what to deliver, through
the same sound and blocked-permission tail.

Every settled turn notifies, as the CLI lane does. Success says
"finished"; failure and cancellation say "stopped" through the shipped
agentInterrupted flag rather than a second vocabulary. A turn whose
outcome the host never stated stays unknown and lights nothing.

The host now dedupes mobile fan-out by event identity (scope, session,
turn) beside the existing per-workspace burst cooldown, so a completion
two windows both saw reaches the phone once while each window still
decides its own banner. Clicking a structured notification reveals the
chat tab: its pane key's leaf is synthetic, so focusTerminal would hunt
a split-layout leaf that does not exist.

* fix(notifications): spend each mobile gate only when it actually notifies

Two review findings on the structured-chat notification lane, both real.

The mobile event gate consumed its reservation before the per-workspace
burst cooldown ran. Two chats in one workspace share that cooldown key,
so the second chat's completion could burn its event key and then lose
the cooldown to the first chat — never announced, yet permanently marked
as announced, so a later window dispatching it could no longer reach the
phone. The gate now peeks first and records the event at dispatch, which
also keeps a known duplicate from burning the cooldown slot.

A notification id is minted from the status row's stateStartedAt, and the
row re-projects that field as the turn settles: the working episode's
start moves into stateHistory and the settled start takes its place. A
banner raised in the window before that re-projection therefore carried
an id acknowledgement never rebuilt, leaving it on screen for good.
Acknowledgement now collects ids for the row's left episodes too — the
same episodes the unread check beside it already scanned, so the two
halves finally read the same turns. Lane-neutral: the terminal lane
mints its ids the same way and had the same gap.

* fix(notifications): drop the mobile event gate and reveal chats in folder workspaces

The per-event mobile dedupe defended against one completion being
dispatched by several Orca windows. Only one renderer mounts the
structured attention bridge, the completion feed is live-only with no
replay, and any in-process duplicate lands inside the existing 5s
per-workspace burst cooldown, which already collapses mobile and
desktop alike. The gate never acted on a real sequence, so the wire
field, the shared ledger and its tests go; mobile delivery is back to
main's behavior.

A folder workspace id ("folder:<id>") has no "repoId::" prefix, so the
click binding was skipped and clicking a chat notification there did
nothing. The chat route selects its workspace itself through
ui:focusEditorTab, so it now binds without a repoId; the terminal
route is unchanged.

* fix(notifications): retire the banner ids actually dispatched, not ids rebuilt from a moved row

A banner's id is minted from the status row's stateStartedAt at dispatch, and that field moves
afterwards: a completion can outrun the settled re-projection, and a settled structured row is
re-stamped with no history entry by any later journal row (a cancel appends a status note after
the turn settles). Rebuilding ids from the row's episodes at acknowledgement missed the second
case and fanned out up to 21 mobile dismissals per pane for ids never raised.

The shared delivery tail now records each dispatched id per subject; acknowledgement retires
those plus the current-row rebuild it always had. The acknowledgement collector is back to
main's single-field form.

* refactor(notifications): retire announced notifications by subject in main

Main now records, per pane, the ids it actually announced (a desktop banner
shown or a phone alert sent) and an acknowledgement passes the acknowledged
pane keys so main retires all of them. This replaces the renderer-side record
of dispatched ids: main is where the announcement happens, so it records only
real announcements, including phone alerts whose desktop banner focus
suppressed. The id rebuilt from the current row stays as the fallback after
a restart empties the in-memory record.
2026-09-22 18:35:18 -07:00
Brennan Benson 0b6f60ac3b fix(worktrees): a listing that predates a create can no longer retire the new workspace (#22311)
* fix(worktrees): a listing that predates a create can no longer retire the new workspace

Creating a workspace while another one was being deleted could land the user on the
empty Landing screen with the new workspace's Claude launch cancelled. A worktree
listing whose scan began before the create finished (the prepared checkout is hidden
from listings while it is still locked) was applied after the create returned, and the
renderer read the new worktree's absence from that authoritative listing as a deletion:
it purged the workspace's tabs, tombstoned the pending structured launch (so the host
session was retired the moment it published) and cleared the active selection.

Renderer: each provider invocation records the local create sequence when it begins
(shared by every caller that coalesces onto it). The merge no longer drops or retires a
worktree this client finished creating after that point, and the change-event diff
purge defers to what the merge actually dropped. Hydrated rows keep being reaped.

Host: a scan a worktree mutation overtook is published as non-authoritative instead of
authoritative, and the create paths invalidate scans as soon as the new worktree is
listable rather than only at the end of the create.

* fix(worktrees): an overtaken scan re-derives the catalog, and the create fence rides every listing

A scan a worktree mutation overtook was published as non-authoritative with a
failure reason, which lit the sidebar's scan-failed indicator, hid external
worktree cards and emptied the visibility dialog after a routine create/delete
overlap, and its early return skipped the abort, currency and dual-owner guards
every other branch honours. The listing now re-scans a bounded number of times
instead (the invalidation already dropped the cache, so it scans afresh or joins
a sibling's scan) and past the bound answers non-authoritative through the same
guard chain, with no failure reason.

The renderer's create-sequence inception is now captured where the provider
invocation starts and released only when it settles, so a waiter released early
cannot reset it for a later joiner, and the direct-SSH scheduler's refresh --
the only listing on hosts with no scan cache -- now carries it too. A re-created
worktree id is re-inserted at the back of the create-record eviction order so
the bound holds.

* fix(worktrees): the host re-runs a scan a worktree change overtook, for cached and SSH listings alike; the renderer create fence is removed

* fix(worktrees): the runtime listing re-runs a scan a worktree change overtook, and a headless host bumps the generation it witnesses

* test(runtime): the change-invalidation suite lists through the public runtime method instead of casting to a protected one

* fix(runtime): the runtime's own worktree change event bumps the scan generation its listing witnesses

A headless host has no window notifier, so a delete (and every other change the runtime announces) never moved the generation the runtime listing re-runs against; a listing the delete overtook published the removed row as authoritative until the next refresh. The bump now rides the runtime's change event, ahead of the send, as the desktop notifier already does.

* fix(worktrees): a listing that spends its re-scan bound logs the churn on the host

Past the bound the listing answered without a trace, so continuous create/delete churn could not be diagnosed from a host log. One warning names the repo and the number of overtaken scans; a caller that withdrew stays silent.
2026-09-22 18:17:19 -07:00
Jinwoo Hong 4f814c1dd4 fix(browser): scope floating browser shortcuts to their own panel (#22361)
* fix(browser): scope floating browser chrome shortcuts to their own panel

The floating panel rendered its browser with the default 'focused' scope, so
a focused split browser and the floating browser both answered chrome chords.
The floating browser now answers only chords from inside its own overlay, and
a focused split yields chords whose target sits inside the floating panel.

* test(e2e): pin floating browser shortcut scope

* test(e2e): reuse split shortcut fixtures in the floating scope spec
2026-09-22 20:33:53 -04:00
Jinwoo Hong dfff3915c4 fix(browser): scope back/forward/reload/zoom/grab shortcuts to the originating split (#22340)
* fix(browser): scope back/forward/reload/zoom/grab shortcuts to the originating split

With two browser panes visible in a split, Back, Forward, Reload, Hard
Reload, page zoom and Focus Address Bar fired in every visible pane. Main
forwarded these guest chords without the page id, and each split's active
pane subscribed. The renderer-side listeners for the same chords were also
window-wide per pane, so a key pressed in the toolbar (or in a terminal in
another split) reached every active browser pane.

Guest-forwarded chords now carry the originating browserPageId; preload
admits only well-formed payloads and each pane ignores ids that aren't its
own. Toolbar-path listeners use the same focused-split scope Find already
uses. The streamed remote pane's history chord moves onto that scoped hook.

Cmd/Ctrl+C grab (STA-3319) gets the same scope and no longer arms while a
text selection exists outside the browser pane, so copying from the native
chat transcript works again.

* refactor(browser): simplify split shortcut scoping per review

Drop the preload payload admission (main and preload ship together), fold
the three inline scope checks into browserChromeShortcutOwnsEvent, and
replace the outside-overlay selection check with a plain live-selection
rule so Cmd+C copies from surfaces that do not move split focus.

* refactor(browser): share one zoom command type and tidy shortcut comments

BrowserPageZoomEventDetail and BrowserPageZoomCommand were the same shape;
keep one in shared/browser-page-zoom.ts and route guest and local zoom
through a single handler.

* refactor(browser): narrow the zoom event with instanceof instead of a cast

* test(e2e): pin split-scoped browser shortcuts

Two browser splits (and a terminal beside a browser) now prove that Back,
Forward, Reload, Hard Reload, page zoom, Focus Address Bar, and the element
grab chord act only on the split that sent them, from both the guest page and
the browser toolbar. A native chat selection proves Cmd/Ctrl+C copies instead
of arming grab. Split fixtures move to a shared helper so both specs reuse them.
2026-09-22 20:14:19 -04:00
Brennan Benson 7cb4380179 fix(sidebar): hang the subagent chevron in the card gutter (#22331)
A top-level agent with subagents drew its expand chevron inline before the
status dot, pushing the dot, icon, and title right of every other agent row.
The chevron now hangs in the card's left gutter so all top-level rows share
one status column, and leaf rows no longer reserve a spacer.
2026-09-22 15:48:05 -07:00
Brennan Benson 4c696a1e2a fix(agent-status): a structured session with live child work reads as working (#22295)
* fix(agent-status): a structured session with live child work reads as working

An idle native-chat session whose subagent was still running showed a green
check in the sidebar, the collapsed worktree pill, and worktree ps, while a
terminal Claude session in the same situation showed working. The two lanes
folded child work into the parent's status with different code: the hook
listener did, the structured lane did not.

Both lanes now share one child-work liveness vocabulary and one lead-status
fold. Live agent work makes a settled lead working; shells and monitors alone
make it monitoring. The structured lane derives liveness from the background
task list already on the wire, in both its readers, so the sidebar, the CLI,
the dashboard and mobile agree. The Claude task-kind table is one shared file
covering both the hook inventory and SDK stream names, and the renderer bridge
reuses the shared child-work projection instead of carrying its own copy.

* fix(agent-status): a blocked or out-of-contact subagent still holds its session working

Child-work liveness retired an agent-kind child on any state but working/monitoring,
while the shell beside it stayed live on everything except done/idle. A subagent
waiting on a permission prompt, or one whose host lost contact, therefore counted
for less than a backgrounded sleep and let the session read done. Both kinds now
share the settlement rule `resolveAgentChildWorkFreshness` already reads rows by:
only an explicit done/idle retires child work.

Also keep empty task labels out of the shared background-task projection candidate,
so a host that publishes `name: ''` cannot beat the child-row fallbacks.

* test(agent-status): pin the widened hook-inventory agent names, and correct two stale claims

The hook inventory now classifies through the shared kind table, which also maps the
SDK stream's `local_agent` / `local_subagent`. Nothing pinned that widening, so add
cases for all four agent names — including `teammate`, whose pane state stays `done`
under the #8825 idle-squat rule.

Two comments the fold made false:
- the teardown marker rule's comment claimed it could not disagree with what the UI
  calls working; it is deliberately lead-only, so now it says that and why;
- the agent-status store reference still described the structured row's `state` as the
  deleted `structuredAgentSessionStatusState`, and omitted the `workingMode` the ingest
  now writes.

* fix(agent-status): the state clock restarts when monitoring becomes a real turn

`stateStartedAt` carried forward whenever the prior `state` matched, which was sound
while `state` meant "a turn is running". Now that it folds in child work, an idle lead
watching a `sleep 3600` publishes `working`/`monitoring`; the user's prompt 45 minutes
later keeps `state: 'working'`, so the row inherited the watch loop's clock and read
"Working for 45m" the instant the turn began. Monitoring is its own displayed label
(`worktree-card-compact-agent-row.tsx:40`), so the continuity key is now the whole
published work identity — state AND workingMode — in both writers.

Also record two facts the code stated wrongly: the structured lane's `interrupted: false`
is inert (a projected session status has no interrupted member) rather than a decision,
and the child-work liveness rule's escape hatch is the roster's session lifetime, not a
settled state.

* fix(agent-status): a workflow is watch work, and child work dates itself

Two defects the fold introduced.

`isAgentChildWorkKind` counted `workflow` as agent work, so a structured session
whose only live task was a backgrounded `local_workflow` published a full working
spinner while the children projection — which admits `kind === 'agent'` only —
rendered nothing to expand, and the same workflow in a terminal pane showed the
monitoring badge instead. The repo already decides this: `isClaudeSubagentTask`
excludes workflows by name, and MATERIALIZED_TASK_KINDS leaves "the backgrounded
shell command and the workflow" to the non-agent owner. The predicate is now
`kind === 'agent'`, and the three sites that restated the same test route through
it, so a new kind is decided in one place instead of three that merely agree.

`evidenceObservedAt` dated every row by `summary.updatedAt`, the journal's last
activity. The journal cannot date child work: its clock stopped when the lead's
turn did, so a genuinely live roster aged past the 30-minute staleness window and
mobile's dot decayed a running session to idle. The fold now reports whether child
work alone holds the row open, and only then does the host's observation clock
stand in — keeping "a restart's republish is not new evidence" for lead turns.

* fix(agent-status): the sidebar dates child work the same way the host does

`fromChildWork` reached the host ingest but not the renderer bridge, so after ~30
minutes of live child work with no journal activity the sidebar's row aged into
staleness while `worktree ps` and mobile stayed fresh — two writers for one session
answering differently, which is the defect this PR exists to remove. For a remote
host the client's own receipt time is also the more honest clock, since the journal
stamp is the host's and is never comparable against this machine's now.
2026-09-22 13:48:07 -07:00
Brennan Benson 18696bd875 fix(native-chat): hide delete for structured history (#22106)
* fix(native-chat): hide delete for structured history

* test(native-chat): fix duplicate session action suite
2026-09-22 13:30:56 -07:00
Brennan Benson 9ae7a3de14 test(agent-launch): pin per-call-site behaviour of the launch funnel (#22116)
A regression net for the twelve production call sites of
launchAgentInNewTab, written before the agent.launch migration rewrites
the funnel's internals.

Each call site is characterized as a profile — the argument object it
builds — and fed through the REAL funnel, so the assertions are about
the observable contract rather than about internals the migration will
move. A census re-anchors every profile to its call site by source
markers, so a caller that changes what it passes fails loudly instead of
drifting out of coverage.

Dimensions pinned per call site: resolved surface (terminal /
structured / host-published), built command and argv, the
permission-bypass flag inside the arguments string, prompt transport
(argv / post-ready paste / environment), submit bit, delivery-promise
exposure, group placement, activation, tab-bar order, initial cwd, and
telemetry launch source.
2026-09-22 09:33:10 -07:00
Jinjing b57facc5bc Fix stale listings when search query changes (#22173)
* fix(renderer): prevent stale listings when query changes

Bundle files and truncated state with the request key that produced
them. Only display a listing when its request key matches the current
query, preventing previous results from leaking through before the new
request completes.

* fix(renderer): associate loading state with request key

When a query changes, there's a gap between the render and the effect
that starts the new request. During this gap, the listing becomes empty
(because it's scoped to the request key), but the loading indicator was
stale. To fix this, track loading per request and also report true if
this render is about to start a new request — so the empty listing
correctly appears as "loading" rather than "no results".
2026-09-22 01:00:40 -07:00
90d363afc9 fix(renderer): contain Monaco initialization failures (#21555)
* fix(renderer): add defensive error handling for Monaco editor crashes

Analyzed 34 crash reports for v1.4.205 released 2026-09-17. Identified and
added defensive fixes for React error boundary crashes in Monaco editor setup.

- Error: ReferenceError: thũs is not defined
- Location: Monaco editor initialization (editor.api2 bundle)
- Platforms: Linux, Windows, macOS
- Root cause: Undefined variable in Monaco setup or language registration
- Status: Added try-catch to prevent cascade crash

- Pattern: Cascading process deaths (network service + GPU service)
- Platforms: Primarily Windows
- Root cause: Infrastructure/concurrent process failure (not code defect)
- Status: Documented, requires Electron/Chrome infrastructure review

- Pattern: Renderer memory grows to 851MB on low-RAM Windows systems
- Root cause: Memory exhaustion on systems with <2GB free RAM
- Status: Existing memory monitoring detected; needs leak investigation

- Status: Requires minidump analysis with source maps

1. Added try-catch to Monaco editor mount callback (use-monaco-editor-mount.ts)
   - Catches errors during editor initialization
   - Logs file path and error for better diagnostics
   - Prevents crash cascade to React error boundary

2. Added try-catch to Monaco language registration (monaco-setup.ts)
   - Catches errors during Vue/Svelte/Astro/Nim language registration
   - Logs failures without crashing Monaco setup
   - Allows app to continue even if optional features fail

- Analyzed 34 crash reports across 3 categories
- Examined crash dumps, diagnostics, and memory profiles
- Reviewed Monaco setup and editor component code
- Checked git history for recent changes

- Crash breadcrumbs (memory, process state, user actions)
- Process metrics (heap, private memory, system memory)
- Component stacks (React error boundaries)
- Exit codes and system signals

- Error silently continues instead of crashing: Users get degraded experience
  instead of app crash, can still use editor in most cases
- May hide underlying issues: Errors are logged for crash reports, but won't
  be surfaced as prominently

- Type checking: pnpm tc:renderer (passed)
- Changes preserve existing error reporting through crash breadcrumbs
- Defensive coding only adds try-catch, no behavior change for success path

* fix(renderer): keep Monaco mount failures inside error boundary

* fix(renderer): isolate Monaco setup failures

* fix(renderer): contain Monaco mount failures at the editor surface

The try/catch around the onMount body did the opposite of containment: React
already routed that throw to the page boundary, so swallowing it left a
half-wired editor and hid the crash from the reporting pipeline. It also never
saw the reported failure, which is raised inside @monaco-editor/react's own
create effect before onMount runs.

Revert the hook to main and wrap the editor element in
RecoverableRenderErrorBoundary instead, so either throw degrades the file pane
only, still files a crash report, and retries by remounting on the existing
pane+path key.

* refactor(renderer): drive Monaco setup steps from one guarded table

Ten near-identical guarded calls, each repeating its own function name as a
label, become one [label, step] table run by a single loop. Same behaviour: an
optional registration that throws is logged and the rest still run.

loader.config and the editor model registry stay unguarded — they are
load-bearing, so catching there would only move the failure later.

* fix(renderer): breadcrumb swallowed Monaco setup-step failures

A guarded registration that throws was console-only, so a lost language or
behaviour guard never reached crash reports. Record a breadcrumb so the
containment stays visible in the field.

Claude-Session: ab8ff806-4870-4ea8-bbf5-bbd123b1166e

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-09-22 02:57:18 -04:00
Jinjing 0232c03c43 Refactor editor header file rename to breadcrumb morph UI (#21265)
* Refactor editor header file rename to a breadcrumb morph UI

- Display full breadcrumb (repo name + parent dirs) during rename for context
- Separate basename field from extension suffix to clarify what users edit
- Replace blur-to-commit with explicit confirm/cancel buttons
- Auto-attach extension to basename; respect explicitly typed extensions
- Add comprehensive tests for rename scenarios and edge cases

* Fix markdown rename: drop blur-commit, handle IME, track active file

- Blur no longer commits, preventing accidental renames when focus moves
- IME composition keys properly handled for CJK input support
- File switches during rename now cancel the operation
- Extension display improved to show final filename
- Comprehensive test coverage added for edge cases

* Simplify markdown rename to inline field without breadcrumb or buttons

Replaces breadcrumb-morph rename with confirm/cancel buttons with a
simple inline field accepting full filenames. Commits on Enter, blur, or
Escape to cancel, matching tab bar and file explorer behavior. Adds
renameCancelledRef to prevent blur-commit after Escape. Removes unused
i18n strings for buttons and simplifies state by dropping extension
pinning and breadcrumb display.

* Handle blur race when file changes during rename

When switching files mid-rename, React may deliver the old input's
blur event after the new file renders, causing a stale rename commit.

Mark the rename as cancelled when the active file changes, and add
test coverage verifying stale blur events are ignored.

* Test blur-race condition in hook unit test

Move blur-commit-after-file-change test from EditorPanelHeaderPath
integration tests to useEditorHeaderFileRename unit test. Tests the
blur-handling logic at the hook level where it belongs.
2026-09-21 22:55:43 -07:00
Jinjing 9d4039b8c5 Lower filter chip contrast to indicate read-only status (#21751)
* fix(cmd-j): lower filter chip contrast to indicate read-only status

The scope is seeded from the sidebar, not chosen in the command palette, so the chips should read as metadata rather than action pills offering to undo an action the user never took.

- Redesign chips with reduced visual weight: no border, no background color
- Add "Scoped to" prefix and muted text color to clarify metadata role
- Separate chips with subtle dots instead of relying on spacing
- Hide dismiss icon (X) until hover/focus to further de-emphasize the action

* refactor(cmd-j): add visual icon to filter indicator

- Add ListFilter icon anchor for better discoverability
- Improve filter chip button styling with better hover/focus states
- Move label to screen reader only for cleaner interface

* refactor(cmd-j): adjust filter chip appearance and i18n

Refine filter chip styling with adjusted height, padding, and colors to de-emphasize read-only state. Add "scoped to" label and complete internationalization coverage across all locales.

* improve filter row alignement
2026-09-21 22:53:20 -07:00
Jinwoo Hong 7650abe224 fix(macos): tell the user when Orca's terminal service can't read their folder, and walk them through the fix (#21923)
* fix(macos): tell the user when Orca's terminal service can't read their folder

On macOS, a terminal daemon that survived an app update can be refused access to
a workspace under Documents, Desktop, or Downloads while the Orca app itself can
still read it. Terminals opened there die with "Operation not permitted" and
nothing on screen explains why. The daemon has reported `cwdReadableByDaemon` on
every create since #18043 and main has emitted `daemon_pty_cwd_denied` on proven
divergence since then; the field data says 1,438 users hit it in 21 days. What
was missing was the notice.

The verdict itself moves off `access()`. A grant-less probe on an affected
machine showed a TCC mode where `access(R_OK|X_OK)` passes on `~/Documents` and
`opendir` still fails, so the check now does what a shell listing its cwd does:
`opendirSync`, one `readSync`, `closeSync`. Only EPERM/EACCES reads as denial —
a missing path, a non-directory, or an unexpected error still reads as readable,
so a non-permission failure can never masquerade as one. The same probe is what
the app side compares with, through one oracle shared by the telemetry emitter
and the notice, so the spawn path reads the directory once.

Proven divergence now also records evidence in main: one entry, keyed by the
daemon's pid, start time and launch nonce, carrying an opaque digest of that
identity and the folder class. No path leaves main. The existing focus-time
`macTccAttribution` poll carries it to the renderer, which raises a second toast
latched per daemon scope: dismissed stays dismissed, and a restart mints a new
identity so the poll returns null and the toast clears with no post-restart
probe. If the replacement daemon is denied too, about 31% of cases, the next
spawn re-records under the new scope and the notice returns, now with the
re-allow sentence doing the work.

No new IPC channel, no daemon protocol field, no polling change, and nothing new
on the spawn path beyond one `opendir`. `daemon_folder_access_notice` counts
shown, dismissed and open_manage_sessions against `daemon_pty_cwd_denied` as the
denominator; `shown` is emitted from main the first time a scope leaves the IPC
handler, so the renderer carries no telemetry plumbing for it.

* fix(macos): clear folder-access evidence only when the same folder class reads back

A readable spawn in ~/code said nothing about a Documents denial but was
hiding the notice; retire the evidence only when the daemon reads a folder
of the class it was denied on.

* fix(macos): say what a terminal-service restart actually does

The Manage Sessions restart confirmation still described the product as it was
before agents resumed themselves: it promised panes showing "Process exited"
that the user reopens by hand, and mentioned legacy-protocol sessions nobody
outside the daemon code can act on. Open terminals and agents come back on
their own now, so the old copy made a routine remedy sound like data loss.

It also called the thing a "daemon". The same restart is about to be offered
from a user-facing fix dialog, so both surfaces now say "terminal service", and
the confirm button is just "Restart".

The new body adds the one fact the old one never stated: terminals on remote
hosts are not affected. Translations of the two changed strings are dropped so
the five non-English locales fall back to English rather than keep showing copy
that is now wrong.

* feat(macos): give the denied-folder notice a fix the user can follow

The folder-access toast told the user their terminal service could not read
Documents and then handed them a paragraph: restart from Manage Sessions, and
if that does not work, re-allow Orca in System Settings. Both halves were
guesses. Roughly a third of restarts do not fix it, and the user had no way to
know which case they were in before spending every open terminal on finding
out.

Main can now answer that. `daemon-folder-access-probe.ts` forks a short-lived
child of the app binary the same way the daemon itself is forked, runs one
opendir/readdir/closedir against the denied path, and prints a single JSON
line. macOS attributes a TCC grant to the process that forked the child, so a
child of the app running now answers exactly the question the running daemon
cannot: would a replacement daemon get in? The child goes through the shared
child-process wrapper, never a shell, with a 3s deadline, a 1KB output cap and
an environment scrubbed to PATH/HOME/TMPDIR. Every failure — timeout, bad
output, spawn error — reads as `unknown`, never as a verdict.

That answer rides out as `restartWillHelp` on the evidence the existing
focus-time poll already carries, and the toast becomes a title and two buttons:
Fix… and Not now. Fix opens a dialog with the two real steps. When the grant is
already in place, step one is shown as done and Restart is live. When it is
not, step one is open and Restart is disabled until it completes — which it
does by itself, because the poll re-probes while the answer is still no, and
returning from System Settings is the moment that lands. An unanswered probe
never accuses the user of a missing grant; it leaves both steps open.

Restart calls the management API directly rather than stacking the Manage
Sessions confirmation on top, since the dialog already states the consequence.
Success replaces the steps with a done line and takes the toast down; failure
says so inline and leaves the button usable.

System Settings opens through the existing developer-permissions pane opener,
which takes an id rather than a URL, with Files and Folders added to it. The
event's action enum now also counts fix_opened, settings_opened,
restart_clicked and — emitted from main when a replacement daemon's first spawn
lands in the folder class the previous one was denied on — whether the restart
actually worked.

* fix(macos): let the folder-access notice return after a poll that read no daemon

A daemon identity reads as null during any reconnect blip, and the poll reports that as
"no mismatch". The notice dismissed itself and then never showed again for that daemon,
because the once-per-daemon latch still held its scope. Only "Not now" should latch.

* fix(macos): say what the folder-access notice costs the user

One line read like a stray warning. The toast now says who is blocked and what fails,
and still leaves the steps to the fix dialog.

* fix(macos): give the folder-access toast one action and the X, like every other toast

"Fix" is the only button; the X dismisses. Sonner fires onDismiss for programmatic
dismissals too, so the post-restart takedown now goes through the store and the hook,
and only a user's X is counted as dismissed.

* fix(macos): keep the fix dialog's steps a checklist and put the one action in the footer

Buttons inside each step made the list look like a form, and a footer Close duplicated
the X. The footer now carries the active step's action, with a ghost Cancel; a probe
that could not answer says so under step 1 instead of showing a check.

* fix(macos): let the checklist show the fix landed instead of saying so

A hedged sentence addressed to the user read like chat. On success both steps check
off and the footer offers Done; the unanswered-probe helper is a status, not advice.

* chore(i18n): drop the fix dialog's unused close key

* Revert "chore(i18n): drop the fix dialog's unused close key"

This reverts commit 365915df48.

* chore(i18n): drop the fix dialog's unused close key

* fix(macos): tell step 1 what to do when the folder toggle is already on

Users who need step 1 usually find Orca already allowed in System Settings; the grant
is recorded but not honoured for the daemon. Re-toggling re-records it.

* fix(macos): drop the unverified toggle instruction from step 1

Nothing has been confirmed to fix a grant that is already on, so the step says only
what the probe knows.

* refactor(macos): share the tccutil reset and bundle-id read behind one module

Clearing a macOS TCC row is about to have a second caller: the daemon
folder-access fix (STA-7948) needs the exact `tccutil reset` the computer-use
helper already issues. Extract both it and the PlistBuddy bundle-id read into
src/main/macos-tcc-reset.ts so the two remedies cannot drift apart.

The extracted calls go through runProcessSync rather than a fresh
node:child_process import: the spawn chokepoint's ratchet holds the direct
importer count at a pin, and a new module with its own spawnSync would raise it.
Behaviour is unchanged except that both calls now carry a 10s bound, and the
computer-use test asserts the same argv against the chokepoint's options.

* feat(macos): offer a permission reset when restarting the terminal service cannot help

About a third of the users who see the folder-access notice are still denied by
a freshly forked daemon even though Orca itself is allowed under Files and
Folders, so the restart the dialog offers cannot fix anything for them. That
state previously had one action: open System Settings, where the toggle they
would look for is already on.

The denied state now offers "Reset permission". Main clears Orca's TCC row for
that folder class with tccutil, then reads the folder from the app itself so
macOS raises its prompt against Orca rather than the daemon, then forces a
fresh-daemon re-probe that bypasses the poll's reuse interval. The dialog
re-renders from that verdict: allowed turns step one green and offers Restart,
still denied says so, and a refused reset points back at System Settings.

Nobody has confirmed this remedy on an affected machine, which is why main emits
the re-probe's verdict as reset_outcome_allowed/still_denied/unknown. Those
three, plus reset_clicked, are the evidence that decides whether the feature
stays.

* fix(macos): say what the permission reset does, and keep System Settings as the fallback

Step 1 was labelled like a Settings task while the button did something else, with two routes
in the footer for one step. The denied state now names the step for what the reset does,
explains it under the step, and shows System Settings only after a reset fails or leaves
things blocked.

* fix(macos): count a folder-access restart only against evidence that survived

The stored denial is the prior denial, so a second copy of it outlived the
one event that retires it: a daemon that read its own folder back cleared the
entry but left the copy, and the next daemon's first denial was then reported
as a restart that had never happened.

Track the outcome on the entry itself, drop the spawn-path probe (ten denied
terminals forked ten probe children the focus-time poll re-runs anyway), and
stop emitting `shown` from a getter the reset path calls for data. The
renderer's toast latch is what decides a scope is shown, so it emits it.

Both accessors now read one identity-matched entry.

* refactor(macos): name the folder-access verdict instead of encoding it as a tri-state

`restartWillHelp: boolean | null` re-encoded a verdict the probe already
returns as a named union, so every reader had to remember that `false` meant
"Orca itself must be re-allowed" and `null` meant "no answer".

`freshDaemonAccess: 'allowed' | 'denied' | 'unknown'` says it, end to end
through main, the IPC payload, the preload mirror and the dialog. The reset's
outcome event becomes a lookup. No user-visible string changes.

* refactor(macos): give the folder-access notice one latch instead of three

Two refs in the hook and a field in the store tracked the same fact, and the
dialog reached the hook through a store field plus an effect just to take its
own toast down before sonner echoed the dismissal back.

The store now holds the visible scope and the scopes the user closed, and
exposes the three things that happen to a notice: it is shown, someone else
retires it, or the user dismisses it. The dialog calls retire directly and the
effect is gone. `settingsIsFallback` loses an argument that was always true at
its only call site, so it becomes the local it always was.

* refactor(macos): stop blocking main on the tccutil reset

Two spawnSync calls with a ten-second timeout sat inside an async IPC handler,
so clearing a TCC row held main's event loop for as long as either binary took.

Both now run through runProcess. The computer-use caller that shared them was
already async, so it awaits them.

* test(macos): run the folder-access probe script against real paths

Every other test mocks the spawn away, so the minified child script — the one
piece that duplicates enumerateDirectoryOnce's errno mapping — had no oracle.
It now runs against a temp directory, an absent path, a file, and a directory
whose mode withholds it, which is skipped for root and on Windows.

* refactor(macos): read the folder-access entry through one identity match

All four callers that ask "is this evidence still this daemon's?" now go
through the same private accessor, so the rule the canonical path depends on
lives in one place.

* fix(macos): keep folder evidence through a failed health read, and make a forced re-probe always probe

A rejected attribution-health read nulled the folder evidence on the same poll, which the
renderer read as "cleared". A forced refresh after a reset returned early on an older
settled verdict. The dialog also closes when a reset finds the evidence gone, and stops
showing the unverified helper once the restart is done.

* fix(macos): name the folder in the access-notice scope

One daemon denied two protected folders kept one scope, so the toast, the
fix dialog, and the tccutil reset could each be about a different folder.

* refactor(macos): derive the folder-access dialog from the latest verdict

The store held an `open` flag and a mismatch frozen at the moment the toast
was raised, so the dialog could open on a stale verdict and its remedy state
could survive a close. It now keeps the latest verdict and the scope the user
opened, and the dialog is shown only while the two agree.

* fix(macos): offer the permission reset only where there is a row to reset

A workspace symlinked out of Documents or on an external volume can be denied
too, and the dialog offered a reset that main refuses. One shared list of the
TCC-backed folder classes now decides both.

* fix(macos): give the permission prompt's read a deadline

An unanswered macOS sheet blocks the app's folder read for as long as the user
ignores it, and the fix dialog is modal and busy until that read returns. The
wait now ends after a minute and reports an unknown outcome rather than
probing under the sheet.

* fix(macos): count the folder-access notice once per scope

A reconnect blip reports no daemon, which takes the toast down and lets the
same scope raise it again. Both raises counted as separate notices, inflating
the denominator behind the affected-user rate. The two latches are now one
map from scope to phase, and the count follows first insertion.

* fix(macos): drop the restart warning once the restart is done

Step two ticked green while its helper still warned that open terminals and
agents would restart, which had already happened.

* fix(macos): keep the folder-access toast up when the fix dialog opens

Sonner deletes a toast after its action button runs unless the handler
prevents the event, and it does so without calling onDismiss. Clicking Fix
therefore took the notice off screen while the scope stayed latched as
visible, so cancelling the dialog left no way back to it.

* refactor(preload): reuse the shared daemon cwd class instead of copying it

The five folder classes were hand-mirrored in preload behind a comment saying
preload cannot depend on main-only modules. The enum lives in src/shared,
which preload already imports from elsewhere, so the copy could drift.

* refactor(macos): close the fix dialog when its evidence disappears

A null verdict left the opened scope set, so the same scope coming back
remounted a checklist nobody had opened. Clearing it on a null verdict also
makes the dialog's scope key redundant, so it goes.

* fix(macos): let each fix-dialog button report its own work

The footer swaps the reset for a restart as soon as a poll says the grant
landed, which can happen while the reset is still running. Both buttons read
their label off the dialog being busy at all, so the restart button appeared
spinning as "Restarting…" for a restart nobody had started.

* fix(macos): clear the reset failure once the permission is granted

"Couldn't reset the permission" stayed on screen after the user granted it in
System Settings and the probe read allowed, contradicting the ticked step
above it. Its sibling line was already gated on the same verdict.

* fix(macos): end the folder-access remedy with the evidence it is about

Two ways out were missing. A reset that cleared the evidence closed the dialog
but left the toast on screen, because only the poll retired it; the store now
retires the notice whenever a verdict comes back null, so both callers get it
and the hook's own branch goes. And the opened scope survived a verdict for a
different scope, so the original one returning later reopened the dialog with
nobody having asked for it.

* fix(macos): keep the folder prompt off main's spawn path

The app-side readability check moved from accessSync to opendir when the
notice was added. TCC lets accessSync through but gates opendir, so on a
machine that has never granted Orca the folder, spawning a terminal there
raised the macOS sheet and froze main until the user answered it. The read is
async now and the spawn no longer waits for it. The blocking variant keeps a
name that says so, and the reset module's own copy of the read is gone.

* fix(macos): only say a folder is still blocked when something re-read it

Two paths reached "Still blocked after the reset." with no verdict behind it:
an unanswered prompt, where the reset returns the verdict stored before it
ran, and a re-probe that could not answer. The reset now returns the same
access it reports to telemetry, and the line waits for a real denial.

* refactor(macos): let the folder-access refresh decide when to skip itself

The poll handler re-implemented the refresh's own two guards, a null entry
and a settled allowed verdict, so each had to be kept in step by hand.

* fix(macos): stop the daemon blocking on its own folder read

The daemon reads the requested cwd before forking a shell to report whether
it can list it. That read is the one macOS gates, so on a folder the daemon
is refused it could hold the daemon's event loop behind a prompt. It is
awaited now, which leaves the blocking enumerator with no callers.
2026-09-22 01:09:26 -04:00
0677271709 fix(orchestration): reap leaked worker terminals via process-incarnation fallback — stops an unbounded PTY/process leak on Remote Server (OOM / cgroup PID exhaustion) (#18790)
* fix(orchestration): remint live handle from process incarnation on worker release

When a durable terminal handle goes stale (rendererGraphEpoch fence),
inspectWorkerTerminal re-mints a live handle via
resolveTerminalHandleByProcessIncarnation + matchesProcessIncarnation so
release/stop/read act on the still-running PTY instead of reporting
missing and leaking the agent process tree.

- keep main shared host-scope re-exports; add matchesProcessIncarnation
- wire observation.terminalHandle through control/stop/release
- rebuild release-completion on main structured paths
- on missing/unattached + provably exited: settleDead fence first, then
  same-incarnation settleWorker fall back (archive may block settleDead
  mid-request); settle before recovery defer

* fix(orchestration): derive SSH host scope from the reminted handle; reuse fresh-request recovery guidance for structured workers

Addresses two open CodeRabbit review comments on PR #18790.

inspectWorkerTerminal read the dispatch authority with the stale durable
terminalHandle, so after a remint the lookup resolved nowhere and
currentHostScope was always undefined — an SSH worker with no liveness
verdict and no persisted host_scope got classified from terminal.connected
instead of unverifiable. It now reads the same effectiveHandle every other
observation in the function uses.

stopStructuredWorkerForRelease told the caller to repeat the release with
the same --retry-request, which only replays the stale release_unknown
receipt and made a structured-worker close failure permanently unretryable.
It now sources releaseUnknownRecovery from worker-release-completion so the
fresh-request-ID guidance lives in one place.

Pre-commit lint-staged (oxlint + oxfmt) run manually: clean.

* test(orchestration): exercise incarnation recovery through runtime paths

* test(orchestration): pin the incarnation read scenario to the reminted terminal

The read scenario only asserted that the call resolved, so it documented
nothing about which handle the read reached. Assert that the handle
readTerminal received resolves to the registered pane and incarnation, so
the scenario proves the read went through the reminted terminal instead of
passing on the incarnation fence's throw.

* refactor(orchestration): drop redundant incarnation prefix check; require liveTerminalHandle

* feat: add freebuff as a first-class TUI agent (#42)

<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every
commit. -->

| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 0 | 0 | 0 | 0 |
| Prod | 28 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​37 | 0 |
$\color{#1a7f37}{\Huge{\mathbf{+}}}$​37 |

<!-- /orca-pr-loc -->

## ELI5

Add Freebuff (`freebuff`) as a recognized first-class TUI coding agent
in Orca alongside Codebuff and other supported agents.

## What Changed

- Registered `freebuff` across shared TUI agent definitions,
configuration catalogs, display names, and telemetry schemas.
- Added agent icons, favicons, status mappings, and mobile asset
references for Freebuff.
- Added localization strings across supported language packs (`en`,
`es`, `fr`, `ja`, `ko`, `zh`) and updated locale translation policy.
- Documented Freebuff CLI in README agent table (`npm i -g freebuff`).

## Why

Freebuff is a CLI coding agent twin of Codebuff (`npm i -g freebuff`).
Adding it to the catalog enables users to launch worktrees, run
automated sessions, and pick Freebuff directly within Orca.

## Linked Issue

N/A

## Visual Proof

`N/A` - Catalog registration and metadata definition for CLI agent
launch; UI rendering uses existing TUI agent picker and status
components.

## Testing

- Verified TypeScript contracts, schemas, and catalog configurations.
- Tested CLI detection / agent picker integration locally on Linux
(`worktree create --agent freebuff`).

## AI Disclosure

Assisted by AI coding tooling.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why (including ELI5)
- [x] Before/after screenshots or videos attached for UI changes, or
`N/A` with reason
- [x] Self-reviewed for correctness, security, and performance
- [x] Cross-platform, SSH/remote, and path/shortcut impact considered
(or N/A)

---------

Co-authored-by: Lesley Murfin <lesley@revivebusiness.ca>

* test(orchestration): erase method overloads in worker reap fixtures

* test: document worker fixture type boundaries

* test: simplify worker fixture typing

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: svc-orca[bot] <313947298+svc-orca[bot]@users.noreply.github.com>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-09-21 17:23:33 -07:00
Brennan Benson 6ce7208b98 fix(sidebar): stop a workspace with a structured chat reading as asleep (#22098)
* fix(sidebar): stop a workspace with a structured chat reading as asleep

A workspace whose only surface is a structured native chat showed the sleeping
moon, lost its pull-request glyph to it, and would vanish entirely under the
hide-sleeping filter.

hasActiveWorkspaceActivity asked three terminal-shaped questions: a tab in
tabsByWorktree with a live PTY, a browser tab, or a fresh non-done agent-status
row. A structured chat answers none of them. Its tab lives in
unifiedTabsByWorktree, so the PTY term never sees it, and an idle session
projects state 'done', which is exactly what isFreshNonDoneAgentStatus refuses.
Both chats finishing their turn was enough to draw the moon.

Add a fourth term keyed on the chat EXISTING. Not on a live provider child: that
child is held only while the chat's pane is visible and is evicted 15s after it
is not, so keying on it would flip the glyph on every worktree switch and report
a process recycle the user never sees. The transcript, and the session's ability
to take the next send, outlive the child.

The term goes in the shared predicate, not the card, because the moon, the
hide-sleeping filter and the Cmd+J palette all read it and must not disagree
about which workspaces are asleep.

Supporting moves, no behaviour change: the projection sits beside its siblings in
visible-worktree-activity-inputs, and buildVisibleWorktreeOptionsFromState moves
to its own module, which leaves the filter a pure function of its options and
keeps both files under the 300-line cap without raising it.

* fix(sidebar): keep structured chats visible across workspace surfaces

* refactor(sidebar): keep jump palette inputs below lint limit
2026-09-21 17:15:43 -07:00
Brennan Benson 15472cd4c6 feat(native-chat): keep restart recovery available in status bar (#21397)
* feat(native-chat): keep restart recovery available in status bar

* fix(native-chat): source the restart offer from the host and retire it on recovery

Closing the reconnect dialog spent the durable recovery offer, so looking around
before deciding lost the recovery for good. The offer now survives a close, and
the status bar carries it — but a durable offer needs a way to die, and it only
had a reconnect, an explicit dismiss, and a 24h expiry.

The claim's launch-scoped lifecycle moves into its own collaborator, which splits
what the host ADVERTISES from the evidence it holds. A resume-capable hold that
hands a marked chat its provider child back is the recovery the offer existed to
perform, so it stops being advertised and stops being written back at quit, while
the marker stays valid evidence — a user who reopened a chat can still ask the
agent to carry on. Teardown re-derives the snoozed offer rather than round-tripping
raw markers, and this teardown's own witness now outranks the stale claim for the
same chat instead of being overwritten by it, which was silently persisting an old
turn id and making the next launch refuse the chat that was actually mid-turn.

On the renderer the candidate list gets its own producer against
agentSession.restartResumable, so the status entry and the dialog read one
host-owned answer instead of the dialog pushing its local state at a sibling. The
entry re-reads the host before reopening, so a reopened list can never name a chat
the host would now refuse; dialog open becomes the external one-shot request
rather than a flag mirrored into render state, which is what let a reopen replay
the launch answer and re-offer chats already reconnected. Dismiss all is quiet
rather than destructive, saves the preference like every other exit, and reports a
write the host never confirmed instead of trapping the dialog open.

* fix(native-chat): keep a durable offer a launch never read, and settle the one a continuation spent

Teardown replaced the recovery capsule with whatever this launch still owed,
and a launch that never read the offer owes nothing — so a quit after a
failed first read, a disabled flag, or a window that never mounted deleted a
recovery the user was never shown. The write-back now distinguishes "claimed
and still owed" from "never claimed": the first is re-derived as before, the
second carries forward verbatim, because nothing revealed those sessions and
the predicate would refuse every one for want of a journal nobody opened.

Reconnect and continue spent the same claims Reconnect does but never shrank
the offer, leaving the status bar counting chats the host had already handed
back and sending the user to an entry that re-reads, finds nothing and does
nothing.

* fix(native-chat): stop a teardown answering for an offer it could not read

Two ways the write-back deleted a durable recovery offer nobody had seen.

A take that FAILED left the claim holding an empty list and reporting that
this launch had answered for the offer. The markers were still on disk,
unread and unknowable, and teardown then overwrote them with its own empty
list. It now writes nothing at all unless it has a witness of its own.

`owed()` read "has the capsule been touched" where it meant "did anything
here LOOK at the offer" — and its own write-back read counted. Teardown is
retried when a phase fails, so the second attempt re-derived carried markers
against a session map eviction had already emptied, refused every one, and
wiped what the first attempt had just carried forward. The flag is now set
only by the paths that actually read or act on the offer.

The mock guard for the carry could not fail: it indexed the session it
claimed nothing had revealed, so re-deriving passed and the verbatim carry
was never the reason it went green. It now runs against no indexed session,
which is what an unread offer looks like.

Also drops the `Not now` row from the preference table, where it was paired
with a dismiss method it no longer calls, and asserts the same thing where
the snooze is already covered. Splits the marker predicate's journal reader
out of the resume host, which was at its line ceiling.

* fix(native-chat): clear the corrupt recovery capsule the take refused

A capsule whose contents no longer parse made take() throw before it ever
reached the clear, so the bad file survived every launch. Nothing else
rewrites it now that a teardown owing nothing readable declines to write, and
the freshness filter runs after the parse, so the 24h window could not release
it either: one corrupt file refused recovery forever.

Clear it inside the same transaction that failed to read it, then rethrow, so
the poison dies on the next launch while callers still see why the take failed.
A clear that fails is swallowed rather than allowed to mask the parse error.
Refusing to expose partial candidates is unchanged, and a read that fails for
any other reason still writes nothing.

* feat(native-chat): make resume the one restart action, and make it actually resume

The restart prompt offered two actions: "Reconnect all", which reattached
and sent nothing — exactly what opening the chat already does — and
"Reconnect and continue", which reattached and asked the agent to carry
on. The vacuous one is gone, the "Not now" button and the info popover
with it, and the feature is now called resume throughout.

"Don't ask again (resume automatically)" now runs the action the button
runs: the launch calls agentSession.restartContinue instead of
agentSession.restartResume, so the preference means what it says. Several
comments asserted the opposite as a structural guarantee and are
corrected. agentSession.restartResume stays: no in-app caller is left,
but it is a published wire method a non-desktop or older client can call.

* fix(native-chat): label the resume button with the number of chats selected

The button read "Resume all" whenever every chat happened to be ticked,
which described the selection rather than the action. It always acted on
the selected chats only. Now it always names that count, with a singular
variant so one chat does not read "1 chats".

* refactor(native-chat): drop the reconnect vocabulary the resume action left behind

Resuming became one action — reattach and ask the agent to carry on — so the
notification helpers no longer need to be told which action they are reporting.
Every caller passed `continue`; the `reconnect` branch, its helper and its
catalog keys are gone.

The dialog and the launch path had grown two copies of the same call: same RPC,
same response shape, same announce-and-settle. That now lives once in the store
module that owns the offer, which also takes the dismiss call, leaving the modal
presentational. The two copies had drifted — only the dialog's caught a
malformed payload — and the unified one keeps the defensive reading.

No behaviour change. `agentSession.restartResume` stays: it is a published wire
method even though nothing in the app calls it.

* refactor(native-chat): derive the resume selection instead of intersecting it

The modal's selection was intersected back against the host's candidate list
before every action, as a guard against naming a chat the host never offered.
That guard could never fire: the selection was already derived from that same
list, so the intersection was the identity. The array of chosen ids is now the
derived value and the lookup set falls out of it, which makes the property
structural rather than checked. The helper had no other caller and is gone,
along with its three tests.

Three tests mocked the resume response in the shape the old API returned. Two
never reached that branch at all; the third only passed because the unreadable
shape happened to exercise the malformed-payload path. All three now use the
real shape, and the malformed-payload behaviour — report an unconfirmed
delivery, leave the offer standing — gets a test that says so.

Also: the candidate reader took two trailing optional parameters, so one caller
passed a placeholder `false` to reach the second; they are an options object
now. `isFolderWorkspaceId` had no caller outside its own module and is no
longer exported. `RestartActionOutcome` only ever describes a continuation row,
so it is named for that. `dismissAll` set a busy flag that nothing could
render, since it closes the dialog first. Several comments repeated an argument
already made in the module they point at.

Settings: the automatic-resume description is one sentence again.

No behaviour change.

* fix(native-chat): make restart recovery explicitly durable

* fix(native-chat): preserve dismissal fence across new interruptions
2026-09-21 16:38:05 -07:00
Jinjing 0bbaadafa4 Clear website annotations after successful delivery (#22060)
* Clear website annotations after prompt delivery

Capture annotation snapshot at send time and selectively remove only
the captured objects when delivered. Preserves edits and additions
made during in-flight delivery.

* rm design doc

* Clear only delivered browser page annotations

Annotations are now explicitly passed to the clear handler, allowing it
to remove only delivered annotations by identity. This preserves any
annotations edited or added after delivery began.

* Distinguish delivered vs user-cleared annotations

Add removeDeliveredBrowserPageAnnotations to remove only delivered annotations while preserving concurrent user edits. Simplify clearBrowserPageAnnotations to clear all annotations for a page when user explicitly clears.
2026-09-21 16:17:29 -07:00
Brennan Benson 6dc00702d2 refactor(floating-workspace): launch the default agent through the shared launcher (#21390)
* refactor(floating-workspace): launch the default agent through the shared launcher

The floating workspace titlebar agent button drove tab startup itself: it built
its own `buildAgentStartupPlan`, created the tab, queued the startup command and
rebuilt the tab-bar order by hand. That is a second copy of what
`launchAgentInNewTab` already does for every other "start an agent here" button,
so a launch-point change had two places to land.

The button now calls `launchAgentInNewTab` and keeps only its own placement:
selecting the tab inside the floating panel's unified group and focusing it.

`launchAgentInNewTab` gains an optional `activate` so a caller that places the
tab itself can keep the new terminal out of the global selection. The floating
panel needs this — activating would move the main window's active tab to a tab
it does not show — and it matches the other floating tab creators, which already
pass `activate: false` to `createTab` and select via `activateTab`.

Two behaviours change, both fixes:
- tab-bar order now goes through `persistAgentLaunchTabOrder`, which reconciles
  editor and browser tabs. The hand-rolled loop rebuilt order from terminal tabs
  only, dropping the floating workspace's markdown and browser tabs.
- the startup plan now carries the resolved Windows shell, so argument quoting
  matches the shell the PTY actually gets.

* test(agent-launch): pin the floating button as a launch funnel caller

The census exists so a new launchAgentInNewTab caller is a deliberate act. This
entry is a bypass converging, not a bypass appearing: the button previously
hand-rolled the helper's terminal arm against queueTabStartupCommand.

* fix(agent-launch): preserve floating terminal launch boundaries

* fix(agent-launch): honour the chat-view default in the floating workspace

The floating launch button now routes through the shared launcher, so it should
inherit the same launch policy as every other caller. The previous review pass
added a `workspaceKind === 'floating'` guard to `decideInitialAgentTabViewMode`,
which silently dropped `openAgentTabsInChatByDefault` (and the user's model and
effort preferences) for that one button.

That guard was not justified. `canToggleNativeChat` has no workspace-kind gate,
so a floating terminal can already be switched into the chat view by hand, and
`TerminalPaneNativeChatPortal` mounts into the pane's own container — the panel
already renders it. Refusing the setting at launch while the same view sits one
click away in the same panel is an inconsistency, not an invariant.

Structured sessions stay out of the floating workspace on the pre-existing
blocker in `resolveStructuredNativeChatSupport`: those open an `agent-session`
tab, and the floating panel renders no such surface.

* test(agent-launch): record why floating routes to the terminal-backed chat lane

* refactor(floating-workspace): record why the launch does not take the global selection

* refactor(agent-launch): lift launch execution-context resolution into its own module
2026-09-21 16:07:37 -07:00
Brennan Benson da982a4eb0 fix(native-chat): navigate to open history sessions (#21283)
* fix(native-chat): navigate to open history sessions

* test(native-chat): provide structured session predicate
2026-09-21 15:44:19 -07:00
Jinjing 73b726c64f refactor: use generic VirtualizedList in conflict review (#22092)
Replace SourceControlVirtualFileList with a reusable VirtualizedList component,
and update related constants and test IDs to reflect the generic nature of the
component. This extracts the virtualization logic to a shared utility that can be
used across different features.
2026-09-21 15:24:58 -07:00
Jinjing 7047cdc0d4 refactor: virtualize artifacts list with reusable component (#22061)
Extract SourceControlVirtualFileList to a generic VirtualizedList component
and apply windowing to the artifacts table for efficient rendering of large
lists. Add aria-setsize and aria-posinset announcements to windowed rows
when opted in.
2026-09-21 15:05:31 -07:00
297cfe0cf3 fix(usage): price GPT-6 Astra, and declare when the Codex cost total omits a model (#22073)
* feat(usage): price GPT-6 Astra token usage

gpt-6-astra was missing from MODEL_PRICING, so normalizeModelForPricing
returned null and estimateCostUsd dropped every event on that model from
the total. Stats & Usage showed ~$0 for hundreds of millions of tokens
with no unpriced indicator, since hasInferredPricing only covers a
missing model name, not a missing table entry.

Rates are the published ones: $10 input / $1 cached input / $50 output
per 1M, with the >272K long-context tier at 2x input and cache and 1.5x
output, which the existing tier fields already express.

Fixes #22005

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(usage): say when the Codex cost total omits an unpriced model

A daily row whose model has no `MODEL_PRICING` entry gets a null cost, and
`buildSummary` simply skips it. As long as one other row is priced,
`hasAnyBillableCost` is true, so the Codex card prints a confident dollar
figure that silently leaves those tokens out. That is how GPT-6 Astra usage
read as near-$0 before the entry landed, and it is how the next unpriced
model will read too.

`hasInferredPricing` does not cover this: it only fires when a rollout has no
model name at all, and its label ("inferred pricing") describes a guess, not
an omission.

So the summary now carries `hasUnpricedModels`, set when a row has a model
name and no price, and the estimated-cost card appends
"• excludes unpriced models" — the same bullet-suffix idiom the breakdown
rows already use for "• inferred pricing". The number stays; it stops
claiming to be the whole bill.

* fix(usage): caveat the Overview total too, and only when a remainder exists

Review of #22073 found the Codex caveat stopped at the Codex tab. The
Overview tab prints a combined total across providers and already has a
"- some model prices are unavailable" line, but `hasPartialCost` only
noticed a provider whose whole cost was null. A Codex range with one
unpriced model among priced ones kept a real number, so the line stayed
hidden and the Overview repeated the same confident, incomplete figure.
`UsageProviderOverview` now carries `hasPartialCost` — set from
`hasUnpricedModels` for Codex, false for the providers that cannot yet
report it — and the reduction ORs it in. No new string.

Second, the Codex card could read "n/a • excludes unpriced models" when
nothing at all was priced. "Excludes" promises a remainder, and there was
none. The suffix now also requires a non-null total; that case is still
declared, on the Overview, through the null-cost path.

`unpricedCostLabel` becomes `costCardLabel`, since it holds the plain
label whenever there is nothing to qualify.

---------

Co-authored-by: Alfred212121 <58665898+Alfred212121@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 17:37:50 -04:00
Neil 1a3f4e88c0 fix(opencode): support v2 plugins under plain executable name
Supports OpenCode 2 installed as opencode, including plugin loading and quick-command submission.
2026-09-21 14:29:04 -07:00
Neil 72a1b148c1 fix(settings): make terminal theme selection override Ghostty colors (#22069)
* fix(settings): clear terminal overrides when selecting a theme

* test(settings): cover light terminal theme override reset
2026-09-21 14:10:02 -07:00
Brennan Benson 2739246058 feat(native-chat): light the unread indicators when a structured chat finishes (#21924)
* feat(native-chat): light the unread indicators when a structured chat finishes

A structured native chat had no attention producer. The PTY lane reaches the
unread markers through use-notification-dispatch, whose liveness reads PTY
state and whose admission requires terminal panes, so a structured session —
which runs on the execution host with no renderer PTY — could finish a turn
with nothing lighting anywhere. A backgrounded chat was the worst case: with
no mounted pane there was no reader to notice at all.

The host derives the completion, because only the host can. The journal keeps
committing whether or not a renderer holds a reader, so the new feed observes
each commit at StructuredAgentSessionClientDelivery.publishJournal and emits on
every running -> settled transition. That edge runs after the subscriber loop
and independent of it, which is exactly why a chat nobody is watching can still
complete. It is a separate capability-gated stream rather than a field on the
status summary: the summary carries no turn identity and no outcome, and is
re-broadcast on every status change, so folding a completion into it would make
every status consumer a completion consumer.

ONLY `success` LIGHTS ANYTHING. Outcome is A0's provider verdict and is never
inferred: a turn the host merely watched stop carries no outcome and produces
no event, because absent means UNKNOWN. `completed` alone proves nothing — a
provider reports its own API error as a finished turn — so the host emits
nothing for it and the renderer filters again on the way in.

RECOVERY IS LIVE-ONLY. Nothing is retained, queued or replayed on either side.
A subscriber learns what settles while it is subscribed and nothing else; on
reconnect it re-opens an empty stream and whatever landed during the gap is
gone. A retained completion would be a durable "unread is owed" obligation with
nothing to retire it, and a reconnect would then light the dot for work the
user already read. Tests on both sides pin this so a later refactor cannot
quietly turn it into catch-up.

The dot itself reuses the neutral policy in attention/agent-attention-policy
and #21274's structured surface adapter, so suppression, acknowledgement and
addressing keep exactly one implementation and the surface key is never omitted
to evade a check. No second suppression rule is introduced. OS delivery is
deliberately not wired: this calls applyAgentAttentionUnread, not
applyAgentAttention.

Also narrows the completion feed's journal dependency to the newest-turn reader
it actually uses, and adds journal.newestTurn() beside the existing
activeTurnId() on the one shared by-sequence scan rather than a second scan.

* test(cross-version): register the turn-completion subscribe on the wire manifest

The cross-version gate asserts the structured surface's method list by name and
count, so an additive method has to be declared there deliberately. Adding the
entry makes the suite call it in both skew directions and stubs the host side,
which is the statement the gate exists to force.

* fix(native-chat): rebaseline completion feed after rewinds
2026-09-21 13:05:24 -07:00
Jinjing f7955e81ff feat(conflict-review): virtualize large conflict file trees (#21920)
Implement windowing for the conflict review file tree using
SourceControlVirtualFileList to efficiently handle large merge conflicts.
Add comprehensive tests for virtualization behavior including scrolling,
collapsing, and dynamic updates.
2026-09-21 12:22:28 -07:00
Brennan Benson 91e6e1f355 fix(native-chat): collapse a finished turn to its answer (#22029)
* fix(native-chat): collapse a finished turn to its answer

A finished turn's "Worked for N" row hid the turn's tool runs and nothing
else. Every sentence the agent said on the way to its answer stayed in the
transcript, so the resting state of a long chat was the narration, not the
reply — one 16m 56s review turn left 21 assistant messages and roughly
seven screens of scrolling behind a control that reads as if it had put
the work away.

The fold's unit is now the turn. A settled turn draws its prompt, its
duration, and its answer; the narration and activity that produced it sit
behind the caret. The answer is the turn's last assistant row that renders
prose — derived, because the journal carries no marker saying which message
is the reply.

Collapsed stays derived rather than stored: nothing closes the disclosure
when a turn ends, it arrives closed because the turn gained a duration. A
running turn therefore folds nothing and the reader watches the work as it
happens, which is what already happened and is now stated rather than
inherited.

Rows that outlive the turn that started them stay outside the fold — a
spawn roster and a background task are often the only record of how that
work ended. So does the reader's own message, question receipts, and the
turn's diff rollup. A turn that produced no prose folds whole, its status
row standing as the anchor.

Two presentation changes come with it, both about the opened view:

- A settled run's header was a call count followed by a monospace list of
  tool names and arguments. It is now one sentence in the transcript's own
  type — "Read 7 files, ran 17 commands, and searched 4 times" — built on
  the tool-category vocabulary that already picks the row's glyph, so the
  words and the icon cannot claim different things. A run of one command
  keeps that command as its header.
- A tool call now owns its result instead of standing beside a separate
  `Result` row, so an opened run lists the work rather than twice as many
  rows half of which say `Result`. Output is one more click. Pairing is
  positional — a result answers the most recent unanswered call — because
  result blocks carry no call identifier to match on.

Command previews also lose the `/bin/zsh -lc "…"` wrapper they all opened
with. The unwrap happens inside `summarizeToolInput`, before truncation,
because the clip at 80 characters removes the closing quote that proves the
wrapper; one site fixes the header, the rows, and the running label.

Measured on a real session journal at 1200x900: the turn above goes from
6,300px across 51 rows to 452px across 2, the whole session from 8,151px
to 2,138px, and the same turn opened from 18,540px to 11,131px.

The fold derivation lives in `src/shared` so the mobile transcript can read
the same rule; wiring mobile's list to it is not part of this change.

* fix(native-chat): preserve FIFO tool result pairing

* fix(native-chat): keep tools collapsed when opening turn

* test(native-chat): clarify independent tool disclosures
2026-09-21 12:13:30 -07:00
Jinjing 7da9788c83 fix(updater): send the gh token and cache the release picker's build list (#21902)
* fix(updater): send the gh token and cache the release picker's build list

The dev build picker listed releases through api.github.com with no
Authorization header, so it spent GitHub's 60/hour per-IP bucket that every
unauthenticated caller on the same network shares, and it refetched on every
settings mount and channel click. When that bucket ran dry the picker showed
"No builds found" with a rate-limit line even though GitHub was healthy and
the user's own token had its full quota.

Attach the local `gh auth token` when there is one so the request draws from
the user's 5000/hour bucket, fall back to unauthenticated on a rejected token
or a spent token bucket, cache the list per channel for five minutes in the
main process (the refresh button forces a reload), classify 403 by the
rate-limit headers, and say when the limit resets.

Fixes #21898

* fix(updater): don't trip breaker for secondary rate limits

GitHub sends x-ratelimit-remaining: 0 on both primary and secondary
limits. Secondary limits carry Retry-After and shouldn't block all core
gh commands — only the primary limit should trip the shared breaker.

* Scope gh rate limits to execution environment

* Add build list cache hint to release channel settings

Inform users that build lists are cached for 5 minutes and they can
refresh to check for new builds immediately. This makes the cache
behavior visible and explains why a manual refresh is necessary to
bypass the cache.
2026-09-21 11:52:37 -07:00
Brennan Benson 83a031c081 refactor(agent-status): delete two launch-config accessors left with no callers (#22032)
#21844 removed the Codex launch-argument attention suppressor, which was the
last production caller of two launch-config lookups. Both were left in place
for a follow-up; this is it.

`getAgentLaunchConfigForStatusMetadata` (renderer store) looked a launch config
up from a loose metadata bag. Its sibling `getAgentLaunchConfigForStatusEntry`
takes a real status entry and still serves the one live consumer, cold restore
resume startup. Deleting the metadata accessor also orphaned its
`getLaunchConfigForStatusMetadata` helper and the
`AgentLaunchConfigStatusMetadata` parameter type, so those go too.

`getAgentStatusLaunchConfigForPaneKey` (main runtime) returned a pane's launch
config behind a launch-token fence. Its two remaining references were
assertions in the launch-authority retirement test. They were a second view of
a state bit the test already pins: retirement nulls `pty.launchToken`, and the
surviving `verifyOrchestrationCompatibilityCaller` assertion fails when it
does not. Verified by ablation — disabling only the `launchToken` nulling
fails that assertion with the accessor already gone, so no coverage is lost.
Retirement never cleared `launchConfig` itself, so there was no second
property hiding in those assertions.

Test mocks that existed only to satisfy the removed store method are stripped;
the tests themselves are about other behaviour and stay.

No behaviour change.
2026-09-21 11:21:06 -07:00
Jinwoo Hong f07bf8544c feat(session-search): sort search results by newest, and break relevance ties by recency (#21863)
* feat(session-search): sort search results by newest, and break relevance ties by recency

Results were ordered by match score alone with the session id as the
tiebreak, so equally good matches came out in an arbitrary order and
nothing ever favoured recent work. The Sort menu now offers Most relevant
and Newest while the box has text; the engine already knew both orders and
the all-computers merge already honoured the newest one, so only the panel
had to ask. Under Most relevant, equal scores now go to the newer session.
The choice persists with the other view options, separately from the
list's own Last updated / Created sort.

* fix(session-search): label results by the order they are in

The header subtitle and the results group said "best matches" whichever
sort was chosen; under Newest they now say so. The panel's scope state and
its two context effects move to use-ai-vault-panel-scope.ts, which keeps
the panel under the line cap and gives that behaviour a name.

* feat(session-search): move search sort onto a results bar above the hits

Search mode gets a bar in the group header's place: the hit count on the
left, a ghost menu button on the right that names the current order and
opens the two-item radio group. The filter menu's Sort section keeps one
meaning again (Last updated / Created), the header subtitle stops
reporting sort, and search rows run flat with no group header.

* style(session-search): drop the icons from the results-bar sort menu and match its text size

* feat(session-search): one sort bar above the list in both modes

Filters stay behind the header filter icon; sort moves onto the bar
directly above the session list, in browse mode as well as search.
The bar is mode-agnostic: it takes a label, the selected value, a typed
option list, and a callback, and the panel configures it twice.

- rename AiVaultSearchResultsBar to AiVaultSessionListBar and generalize it
- add ai-vault-sort-options for the two option lists and their aria labels
- drop the Sort section from the filter menu and stop counting sort in the badge
- header subtitle now reads "Indexed history" in both modes

* feat(session-search): count sessions plainly and offer Show more when the scan fills its depth

* fix(session-search): step history depth 250 at a time and keep Show more visible while the rescan runs

* style(session-search): let the sort menu hug its two options

* fix(session-search): show more reads the depth its rows came from

The row inferred "a deeper rescan is running" from the selected depth minus one
page, which at the default depth is zero, so every foreground scan with at least
one session painted a disabled "Loading more sessions…" footer the scan had room
for.

The scan now publishes the depth it ran at beside its sessions, and the row
compares the two: it survives the rescan because that depth trails the selected
one until the deeper scan lands. Drops the stepping arithmetic and
nextAiVaultSessionLimit, and moves the row out of the menu file it was sharing.

* refactor(session-search): an untitled group is what hides a header

Search mode said "no group headers" twice, in two files, both keyed off the same
flag: an empty label in the filters hook and a hideGroupHeaders prop on the list.
The label is now the only fact. A null label means the group has no header of
its own, the list renders its rows flat, and the prop is gone.

The shared group type keeps its string label so the mobile sections that map it
are untouched; the nullable label is the renderer list's own type.

* refactor(session-search): plain labels, and a browse bar that can report zero

Three small simplifications around the list bar:

- The browse bar is guarded on the loaded history rather than the filtered rows,
  so "0 of 250 sessions" can actually appear when filters hide everything and
  the sort control stays reachable. Search keeps its own guard.
- The two count labels were components whose whole body was a ternary over
  translate; they are functions returning a string, and the bar's label prop is
  a string.
- The persistence guards stop being exported with no caller outside the file,
  and the search-sort guard reads the AI_VAULT_SEARCH_SORTS list instead of
  respelling the union.
2026-09-21 14:11:40 -04:00
Neil 2a785274ad fix(remote-runtime): stop an unhydrated host graph from reading as a closed terminal (#21967)
A remote pane asks the host two different questions about its terminal, and
they consult different amounts of state. `session.tabs.list` hydrates first —
`listMobileSessionTabs` runs the workspace-session hydrate with
`allowAttachedWindow: true`, then restores live paired-renderer terminals.
`session.tabs.activate` hydrates with no options, which is a no-op whenever an
authoritative window exists, so it answers `tab_not_found` from state the host
never filled in. The client treated that as removal evidence and surfaced
"Remote terminal was closed." over a terminal the same host was still
reporting connected and writable.

Activation's absence answer is now non-authoritative: the bounded inventory
poll below adjudicates. A surviving sibling leaf with this leaf gone still
returns removal evidence and still surfaces the toast; nothing conclusive
inside the window stays unknown liveness, which parks a retry instead of
asserting closure. `null` leaves both return unions.

Fixes #21852
2026-09-21 03:24:52 -07:00
Neil 4feaaf5c5c feat(terminal): configure interactive Unix shell arguments (#21904)
* feat(terminal): configure interactive Unix shell args

* fix(settings): clarify Unix shell argument defaults

* fix(settings): improve Unix shell argument guidance

* fix(settings): simplify shell argument guidance

* fix(settings): clarify empty shell args

* fix(settings): explain empty shell args

* feat(settings): make shell argument modes explicit

* fix(settings): keep no args inside custom mode

* fix(terminal): apply configured shell args on the renderer spawn path

The renderer's pty:spawn handler builds options in ipc/spawn-options, not
the runtime controller, so the configured profile never reached a terminal
pane. The local launch plan also dropped the args whenever shellOverride
was set -- which the spawn path always fills from terminalDefaultShell.

Both spawn paths now share one resolver.

* chore(i18n): allowlist the new terminal shell argument strings

Matches how the sibling Terminal shell settings strings are already handled.
2026-09-21 01:35:58 -07:00
Neil ac4d6b407a fix(devin): skip workspace trust for Orca launches (#21925)
* fix(devin): skip workspace trust in yolo launches

* fix(devin): migrate existing workspace trust defaults

* fix(devin): persist migrated launch arguments

* fix(i18n): include required source control stop label
2026-09-21 00:43:27 -07:00
Jinjing 1b77838d1c Consolidate source control tooltips to eliminate redundant hover text (#21733)
* refactor(source-control): consolidate tooltips to avoid duplicates

- Remove native title attributes from buttons that also render Radix tooltips
- Introduce PrimaryActionTooltip wrapper that decides whether to show a tooltip based on context
- Hide pure repeats: enabled Stage All and Create PR have no tooltip since the label already states the action
- Show tooltips only when they add information: disabled reasons, commit shortcut, and remote counts

* Show Create PR intent tooltip to explain the prepare step

The Create PR intent label doesn't convey that clicking it stages,
commits, and pushes before opening the PR dialog. Keep the tooltip
to surface this multi-step operation that users might not expect.
2026-09-21 00:05:13 -07:00
Brennan Benson afb618f2b3 refactor(agent-status): drop two superseded Codex attention workarounds (#21844)
* refactor(agent-status): drop two superseded Codex attention workarounds

Codex fires its PermissionRequest hook as decider #1, before its own
auto-reviewer and before the user, so the event never meant "a human is
blocked". #21389 fixed that at the source: the execution host reads the
turn's approvals_reviewer from the rollout at write time and keeps a
reviewer-owned approval in `working`.

Two older reader-side workarounds for the same bug are now redundant.

The launch-argument suppressor guessed auto-approve mode by string-matching
the launch args, then dropped the status row in the reader. It only matched
Codex's bypass flag, and under that flag Codex's approval policy is `Never`,
which takes the Skip path and fires no PermissionRequest at all. When the
user turns on "Approve for me" inside a live session the args never change,
so it never fired for the actually-reported case either.

The Codex-only 1.5s notification quiet window could not do its job: measured
auto-reviews take 3-20s and a human can answer in under a second, so no
fixed constant separates them. Its deferred callback also re-checked
liveness and returned without notifying, so a genuine prompt whose pane went
non-live inside the window was dropped rather than delayed. Codex now
notifies synchronously like every other agent.

Also types the coordinator's completion state from the controller's exported
CompletionState instead of asserting each field, which the changed-lines
casting gate required once those lines moved.

* fix(agent-status): settle transient process-exit evidence
2026-09-20 23:32:00 -07:00
Brennan Benson 4da3a95d50 fix(native-chat): scope a tool row's hover reveal to that row (#21918)
Hovering one tool call in a chat turn revealed the expand chevron on every
other row in the same message at once, so the whole message lit up and nothing
said which row the click would open.

The rows were reading a hover they do not own. Tailwind's unnamed
`group-hover:` is not nearest-ancestor scoped — it compiles to
`:is(:where(.group):hover *)`, which matches a hover on ANY `.group` ancestor.
`NativeChatMessageRow` wraps the whole assistant message in a bare `group` for
its own copy/timestamp reveal, so every collapsible row nested inside it
answered to that wrapper as well as to itself.

Each row now names its own group — `group/tool-line`, `group/tool-run`,
`group/subagent-run`, `group/diff-card` — which compiles to
`:is(:where(.group\/tool-line):hover *)` and reaches that row alone. The
message-row reveal is left bare on purpose: its copy button and timestamp are
meant to answer to a hover anywhere in the message.

`NativeChatDiffCard` is included for the same defect, not as extra scope: its
verb label was brightening on any hover in the message.
2026-09-20 23:26:29 -07:00
Neil 8cf1c8594e docs(opencode2): clarify quick command delivery (#21929) 2026-09-20 23:22:27 -07:00
Neil 70c4f20466 fix(opencode2): auto-submit quick command prompts
OpenCode2 quick commands now submit through the ready-state delivery path. Focused regression coverage and full CI pass.
2026-09-20 23:08:36 -07:00
Neil 98299d879b fix(terminal): persist a parked remote pane's scrollback across a hard restart (#21295) (#21367)
* fix(terminal): route a parked pane's scrollback patch to the remote host's partition

A park capture changes only terminalLayoutsByTabId, so its debounced session
patch carries no tabsByWorktree. splitWorkspaceSessionByHost built its
tab->worktree index from the patch alone, resolved nothing, and routed every
layout to the 'local' partition, where main's pruneLocalTerminalScrollbackBuffers
strips scrollback it cannot attribute to a remote worktree. The remote host's
runtime:<id> partition never received the capture, so anything parked since the
last clean checkpoint was lost on a crash, SIGKILL, or a forced kill during an
app update (#21295).

Route tab-keyed patch fields with the renderer's live tab catalogs as a fallback
when the payload names no tab rows. Payload rows still win, so full-payload
writes are byte-identical. Once routed to runtime:<id>, main merges the
partition's own prior tabsByWorktree and the prune preserves.

Proven by tests/e2e/paired-remote-terminal-parked-scrollback-restart.spec.ts: a
hard kill (no checkpoint) then relaunch, asserting the capture is in the remote
host's partition on disk. Mutation: reverting the routing fix turns that
assertion red and fails the 3 catalog-dependent unit routing tests.

(cherry picked from commit 58a344c1d4)

* test(terminal): read both scrollback homes in the restart spec, and ratchet the resolver to the cap's home list

The restart spec read only buffersByLeafId, but the ordinary park now writes
localOnlyScrollbackByTabId, so its own proof reported a false zero and both tests failed for the
wrong reason. Both readers now go through resolveLeafScrollbackBuffers: the on-disk reader calls
it directly (it is a pure function), and the store reader — which runs inside page.evaluate —
reaches it through a new window.__terminalParkingDebug.resolveLeafScrollback(tabId) handle.

resolveTabScrollbackBuffers is typed off TERMINAL_SCROLLBACK_SESSION_HOMES and its unit test
enumerates that constant, so adding a third home fails to compile and fails a test until the
resolver reads it — the 'no consumer reads a home directly' invariant becomes enforceable.

The clean-quit control no longer asserts tokenAfterReveal (measured true, true, false on identical
product code; the live host can serve the reveal from its own tail). It keeps the five
deterministic fields and logs the reveal; the hard-kill test still asserts it, because there the
host is forced unavailable and the reveal must come from the client copy.

(cherry picked from commit 0cd3db1489)

* docs(persistence): pin why the local-only scrollback home stays outside full normalization

The two scrollback homes look symmetric (TERMINAL_SCROLLBACK_SESSION_HOMES), so the missing key
reads as an oversight. It is load-bearing: adding it would route the field through the fail-closed
strip and reintroduce the loss this branch fixes. The renderer prunes it with attribution before
the patch is sent, so the cap still holds without main as a second line.

(cherry picked from commit 1092e35b0a)
2026-09-20 22:57:33 -07:00
Neil f492054bf0 fix(runtime): an outage is not a handle-gap verdict (#20059)
* fix(runtime): an outage is not a handle-gap verdict

The per-pane handle-gap wait releases at a 15s deadline and records that
expiry as a verdict, which authorises the sleeping-agent resume. The
connection generation was the only thing voiding that verdict, and a plain
disconnect never advances it — runtime-status.ts advances on the reconnect,
under a new runtime id. So a network drop mid-turn expired the wait with a
generation that still matched, and the replay forked a second `--resume`
onto the transcript the host was still writing: #19735 through the
disconnect door.

Suppress the verdict while the client positively knows it is out of contact,
reusing the shared runtime-host connection derivation. The waiter still
releases and re-parks, so contact returning gets a full fresh budget and the
pane is still decided on real silence.

Not redundant with the landed-handle drain that follows this commit, nor with
the read-time pane identity from adv2-skew (cdafc90d8f). Mutation on the
composed tree gives three disjoint kills: dropping this guard fails only "does
not turn an outage into a verdict"; dropping the landed-handle drain fails only
the two landed-handle cases; forcing this guard always-true fails 16 across every
suite. Three guards, three holes.

* fix(runtime): an outage inside the budget is not a handle-gap verdict either

The contact check at the deadline is a snapshot of now. An outage that began
and ended inside one wait, on the same runtime, leaves contact restored and the
generation untouched, so the deadline recorded a verdict after milliseconds of
real contact. Capture hostContactEpoch at park time and refuse the verdict when
it moved; the waiter still releases and re-parks for a fresh budget.

Also pins the transport-down snapshot shape (reads 'reconnecting'), which is
what main's status channel actually publishes for a dropped link and was the
one arm of the guard no test exercised.
2026-09-20 22:56:43 -07:00
Neil 4feca6baa1 Keep new worktree dialog actions visible while scrolling (#21915)
* Keep new worktree dialog actions outside scrolling content

* Trigger missing PR checks
2026-09-20 22:54:03 -07:00
SahilZ0810andNeil 9324bb8137 fix(editor): preserve Markdown preview when following wiki links (#19790)
* fix(editor): preserve preview when following wiki document links

* test(editor): avoid cast in markdown navigation fixture

---------

Co-authored-by: Neil <neil@stably.ai>
2026-09-20 22:52:45 -07:00
Neil 2872c3fccc fix(claude): prefill continuation context for manual submission (#21912)
* fix(claude): prefill continuation context for manual submission

* fix(agents): force draft paste when inline prefill falls back
2026-09-20 22:45:05 -07:00
Neil 253f0e3946 Fix Antigravity source-control model discovery and retired defaults (#21606)
* fix(antigravity): discover current source-control models and use CLI defaults

* fix(antigravity): gate configured models on remote runtime support

* fix(runtime): forward default TUI agent for remote git generation

* test(runtime): cover inherited agent forwarding
2026-09-20 22:12:02 -07:00
Jinjing 00da5fd556 test(worktrees): add comprehensive nested lineage coverage (#21903)
- Add 10 test cases for nested worktree rendering and collapse behavior
- Handle edge cases: cycles, uneven siblings, multiple depth levels
- Extract stopNestedWorktreeCardBubble to shared header-event-guards module
2026-09-20 22:11:08 -07:00
SahilZ0810 ffb79c71e0 fix(editor): open plain details blocks in rich markdown mode (#19784)
* fix(editor): allow plain details blocks in rich markdown mode

* fix(editor): preserve case-sensitive details class values
2026-09-20 22:05:30 -07:00