Commit Graph
7629 Commits
Author SHA1 Message Date
Neil 7ce58ec47e refactor(terminal): one PTY attribution for teardown, close and identity; delete the deprecated owner resolver (#30626)
* refactor(terminal): one PTY attribution for teardown, close and identity; delete the deprecated resolver pair

- strict ssh: route: a proven direct listing beats a same-id server proxy; other contradictions refuse
- terminal spawn's git branch takes the strict route when a host is named (rivals refuse)
- resolveTerminalPtyOwner + isUnattributedTwinWorktree replace resolveRuntimeOwnerForWorktree in
  shutdown, pane and agent hibernation, and PTY identity; close logs unroutable runtime ids
- delete getRuntimeEnvironmentIdForWorktree/resolveRuntimeOwnerForWorktree (legacy branch moved
  into the adapter), the alias golden test, the importer ratchet, its baseline and pr.yml step
- hasHostEvidence reads the owner indexes instead of scanning repos and groups

* fix(terminal): close and identity read spawn's attribution for unscoped remote ids (Opus P2)

- an unattributed twin's unscoped remote: id is skipped and logged on close, never closed locally
- setup terminals: identity and close follow spawn's runtime; raw-id teardown stays local
- drop the last stale getRuntimeEnvironmentIdForWorktree mock (tests/e2e unit suite)

* refactor(terminal): one ownership read in resolveTerminalPtyOwner

* test(terminal): pin the close-path twin skip and the two-proxy direct listing (Opus P3)

* test(terminal): keep the close-provider owner fixture's full union type (Codex P1)
2026-10-19 01:50:57 -07:00
Neil 699b9a0637 feat(files): guarded file writes that refuse to overwrite a changed file (#30605)
* feat(files): guarded file writes that refuse to overwrite a changed file

Adds an optional disk precondition to file writes: the writer compares the file
on disk with an expected signature inside the write, serialized per file (path
then inode), and refuses with a typed changed/missing result instead of writing.
Remote hosts serve it as files.writeIfUnchanged behind a capability; older
hosts get a client-side compare that fails closed, pinned to the probed runtime
and connection. Undo of a delete and the MCP starter config use it to write only
into a missing or identical file. Groundwork for guarding editor saves.

* fix(files): pin guarded writes on the sending socket; keep refused undo; narrow not-found and pinning

* fix(files): pin a guarded save to one pairing; keep only refused undo steps

* fix(files): ENOENT by code only; resolve symlink chains fully; pin lost-reply recovery to the save's pairing
2026-10-19 01:27:53 -07:00
Jinwoo Hong cf13e0cf9d fix(sidebar): phone feed list leftovers before the flip (dark) (#30631)
* fix(sidebar): phone feed list leftovers before the flip (dark)

Bump the feed's semantics version for P11's desktop facts; mark switch-on phone page bundles so packaging and orcad staging refuse them; show the page's last-listed rows before the first feed read; draw the host's pending creations; pass the listed host's own label into host sections instead of the running device's platform.

* fix(sidebar): review fixes for the phone feed leftovers (dark)

A dark orcad may stage the dark page (only a marker of an off switch is refused); the bundle tree writer leaves the marker; the pre-feed list keeps the view's filters; a creation names its project only outside that project's header and survives search when its project matches; the feed view and row labels read one resolved desktop platform.

* fix(sidebar): second review fixes for the phone feed leftovers (dark)

Test the off-switch marker check across switches; resolve the desktop platform once for rows and headers; a section search narrows keeps only matching rows.
2026-10-19 04:27:09 -04:00
Brennan Benson 2db7d5aac4 Refuse chat resume until terminal writers have exited (#29242)
* fix(native-chat): identify the owner when resume create is refused

* test(native-chat): consolidate transcript refusal cases

* test(native-chat): keep ownership coverage at the launch boundary

* test(native-chat): update adoption fixture for early ownership admission

* test(native-chat): align resume admission assertions with host ownership

* test(native-chat): keep RPC coverage focused on ownership proof

* test(native-chat): assert blank request presence before optional source

* fix(native-chat): admit the actual CLI source during chat creation

* fix(native-chat): satisfy source admission lint checks

* test(native-chat): provide the waiting hook prompt

* test(native-chat): isolate source admission fixtures from host homes

* test(native-chat): canonicalize adoption fixture directories

* test(native-chat): preserve early ownership admission in source stack

* Resume CLI conversations directly in native chat

* fix(native-chat): preserve resume refusal behavior and honest transcript feedback

* fix(native-chat): retain import read evidence in typed result

* Use the header test fixture record type

* Consolidate launch owner imports and correct feedback assertion

* Keep existing resume translation in runtime catalog

* fix(native-chat): preserve admitted source and prepared execution home

* fix(chat): retain source evidence for dispatched adoption

* fix(chat): keep source capture within module limits

* Keep deferred chat publication in the shared launch owner

* test(chat): allow host evidence in create routing

* Satisfy launch publication checks and scope slot cleanup

* Preserve canonical tab membership on projected reorders

* Use recorded launch ownership for surface recovery

* Reserve deferred chat order through existing group actions

* Pass the active layout owner into launch cancellation

* Stop terminal writer before adopting conversation in chat

* Guard chat adoption completion and satisfy static checks

* Switch completed terminal turns without confirmation and test handoff

* Recheck terminal writers on reserved adoption retry

* Split session entry delegates and fix linted test types

* Keep chat opening send below create module line limit

* Require explicit terminal stop consent before chat adoption

* Use direct process-table test mock

* Fix chat adoption types and runtime localization

* Localize terminal takeover refusal guidance

* Keep CLI stop admission scoped to active source panes

* Preserve ordinary history resume through attach

* fix(native-chat): admit an unsaved transcript under a linked account folder

A missing transcript stayed unresolved while its account root resolved through
symlinks, so containment failed and a not-yet-saved conversation showed a load
error instead of the unsaved notice.

* Update adoption replay test for at-rest creation

* refactor(native-chat): move the newest-item import window into its own module

Keeps journal-legacy-import.ts under the line limit after the source
identity observer landed on top of the streaming import.

* fix(native-chat): restore the refusal error import lost in the main merge

* Remove duplicate account resolver and condense terminal admission

* Refuse conversation adoption until terminal writers exit

* Keep conversation inspection within lint limits

* Preserve conversation owner in launch feedback

* Expect unavailable writer scan to refuse fresh adoption

* Keep terminal conversation evidence tied to its execution

* Correct writer continuity test refusal and event shapes

* Prove original terminal writers exited before chat adoption

* Scope saved conversation holds to their execution host

* Keep child-inspection fixture verdict literal

* Recheck awake saved conversation holds against their owned pane

* fix: inspect owned Windows jobs independently of foreground naming

* test: use braces in Windows inspection fixture

* test: complete ordinary Windows terminal inspection fixture

* test: explain partial terminal inspection fixture

* fix: inspect the terminal a conversation is switched out of

* test: separate OpenCode writer observation coverage

* fix: tie terminal conversation links to the PTY execution

The conversations a terminal reported now live on its PTY record for that
incarnation, so moving the pane or running many other terminals can no longer
drop them, and a link whose terminal can't be inspected refuses.

- Each link keeps the agent process its hooks last proved; a live or
  suspended process refuses even when the terminal's tree is shell-only
  (detached tmux, nohup).
- tmux-published events are never pane-owned, and an unmarked binding copy no
  longer revokes an earlier pane-owned proof.
- A restored row or saved checkpoint is censused against the pane's current
  local PTY instead of requiring this runtime's observed identity.
- The refusal now says Orca can't confirm the agent has exited and asks the
  user to stop what's running there or close the terminal.

* test: drop the stub-host admission calls from OpenCode observation coverage

* test: census suspended children through the real terminal host

* test: drop writer checks the real-terminal flow already covers

* fix: let prompt workers outside the terminal's session pass the exit check

- The POSIX shell-alone proof skips a session leader below the shell and
  everything under it: a prompt's zpty worker has left the terminal, while
  suspended, background, nohup and disowned jobs still refuse.
- Windows steady-state polls read the cached process table; every read that
  decides adoption or close stays fresh.
- The pane header and tab menu point back to Session History, since their
  action is gone once the agent exits.

* fix(terminal): count only Windows job members still linked to the shell

A pane's job keeps every process it ever started, including daemons whose
parent exited (an rc-started ssh-agent), so such panes never read as an idle
shell and the chat switch refused forever. Walk descendants of the shell along
live parent links within the job, for every shell; the Git Bash launcher keeps
its bash-chain allowance and a child older than its parent still refuses.

* Revert "fix(terminal): count only Windows job members still linked to the shell"

MSYS exec and fork leave intermediate Win32 parents exited, so a live Claude
started through a script (and 'sleep 60 &') became unreachable and the chat
switch admitted while it ran. Back to the job census until a console-based
design is measured.

* fix(terminal): set aside detached daemons on a Windows shell-alone decision

A pane's job keeps every process ever started in it, so an rc-started
ssh-agent made a Git Bash pane read as busy forever and the chat switch
refused. MSYS fork/exec leaves live work under exited Win32 parents, so parent
links alone can't tell the daemon apart; it is the one that also left the
console. On a scanChildProcesses decision read (switch admission, run-terminal
close, close guards), a member neither linked to the shell nor attached to its
console no longer counts. Polls and shell confirmation keep the job census.
The console-list helper now forks through the process-host chokepoint.

* refactor(daemon): name the process inspection options once

Keeps the daemon router and terminal host within their line budget now that
the options carry scanChildProcesses.

* fix(terminal): set aside detached daemons only for adoption, never for close

Closing a Windows pane kills its whole job, so a close check that ignored a
detached process (gitk &, cmd /c start ...) would kill it without asking.
Adoption asks whether anything is still in the terminal; close asks whether
closing destroys something. Only the adoption read now sends ignoreDetached;
close guards and the run-terminal sweep keep the job census. The win32 test
also stops the ssh-agent it starts.

* fix(native-chat): check the terminal being switched out of before opening its chat

When the conversation already had a native chat, the switch hit the
ownership conflict first and the header opened that chat, even while the
terminal's agent (a later claude --resume) was still running. The switched-out
terminal is now checked before the conflict, so a live one refuses as usual
and one at rest still opens the owning chat.

* test(terminal): assert close reads never call an ssh-agent pane idle

A plain read can answer unverifiable while the eval's short-lived processes
exit; either way it must not read as no-children.

* fix(native-chat): check terminal writers before naming an owning chat for any switch

Session History's Resume in New Native Chat carries no source pane, so with an
earlier chat still holding the conversation it skipped the terminal check and
the owning chat's first send would start a second writer beside a live
terminal Claude. Every adoption request now runs the writer check before the
owner conflict, as the no-owner path already does.

* test(reliability): run the chat-switch writer oracles in the provider-ownership gate
2026-10-19 01:06:36 -07:00
Jinwoo Hong 5f2bb930d1 fix(sidebar): Hide sleeping keeps a paired server's active workspaces (dark) (#30624)
* fix(sidebar): Hide sleeping reads a paired server's own terminal report (dark)

With ORCA_SIDEBAR_INPUTS=main, main's activity domain now lists a paired
server's rows: their terminals and agents as the server's worktree.ps
reports them, plus the browser and chat tabs this desktop's saved session
holds there. A server out of contact keeps live terminals unverifiable.
Switch-off builds are unchanged.

* fix(sidebar): keep server activity out of the published paired rows

Review: a strict field on paired rows could drop a whole row for an older
reader; the activity domain already carries it.
2026-10-19 03:49:36 -04:00
Brennan Benson 7dcb7d4439 Sleep stops a workspace's native chat agents (#28445)
* fix(sidebar): Sleep stops a workspace's native chat agents

Sleeping a workspace stopped terminals and browsers but never its structured (native)
chats, so their claude/codex process kept running, and Sleep was disabled for a
workspace whose only content was a chat.

Sleep now stops each chat's agent through a new agentSession.sleep (the host's close,
keeping the tab), routed to the host recorded on the chat's tab; an older host without
the capability fails Sleep with a clear message. The Sleep menu counts a chat.

* Fix native chat Sleep ownership and delayed teardown

* Stop native chat sessions when sleeping their workspace

* Fix sleep teardown wake and host-qualified receipt races

* Verify mobile Sleep sends the selected desktop host

* Keep worktree Sleep RPC within method file limit

* Use sleep intent as attempt identity

* Gate workspace sleep on structured client support

* fix(native-chat): preserve sleep stop receipts and send timing

* fix(native-chat): keep superseded workspace sleeps quiet

* fix(native-chat): ignore superseded failed sleep stops

* Fix sleep lifetime test resolver types

* Accept workspace Sleep for already ended chats

* Sleep native chats through the shared no-toast sleep core

Main split runSleepWorktrees into sleepWorktreesReportingFailures (no toast) and
a toasting wrapper. Move the native chat stop, host routing and typed failures
into the shared core so the WSL move's Sleep request also stops chats, and
report every workspace not verifiably slept (an unconfirmed chat stop included)
in failedWorktreeIds. Fold one ACP adapter field onto a line so main's file plus
this PR's ownership check stays within the file length limit.

* Import isStructuredTab from its shared module

* fix(native-chat): leave chat rows to their host through Sleep

Sleep's worktree status drop deleted the chat's row, which the host feed then
rewrote, so the outcome depended on which write landed last. Open chat tabs
own their rows (same rule as terminal-tab close), and a chat only makes a
workspace sleepable while its host holds the agent's process.

* fix(native-chat): judge the Sleep receipt by facts, not lane occupancy

A running chat's stop queues the provider's own requested-close end report
onto the chat's lane, so the lane-occupancy check read every proven stop as
superseded and dropped its receipt. Later work now counts only once it
exists: a moved fence, a child, delivery, child work, a pending submission,
a pending create or attach, or an open dispatch.

* test(native-chat): report a fake close's end only for an acquired child

* Preserve canonical workspace identity when sleeping

* Propagate optional sleep host through file command contract

* Move the deliberate-sleep PTY tests to main's spec file

Main grouped the PTY connection suites (#29193) and moved this file to a .spec.ts registered from pty-connection-spawn-and-startup.test.ts. Taking main's copy here keeps the following main merge free of a modify/delete conflict; this PR's test changes are reapplied after the merge.

* Reapply Sleep's teardown waits to the moved PTY spec; keep acquisition under its line limit

The deliberate-sleep spec moved in #29193; this carries the PR's changes into it: a pane mounted after wake waits for issued teardown, an unqualified pane waits for every same-id host's teardown, and a queued startup waits too.

The merge left claude-structured-session-acquisition.ts over 300 lines. resolveClaudeAcquisitionError, which already builds the exit-unproven error, now decides whether to release the acquisition.

* test: keep the Sleep receipt checks on the closed handle and kept record

* fix(sidebar): a single-workspace Sleep failure says workspace, not workspaces

* fix(sidebar): Sleep stops a workspace's chats on every host that has a copy

* test: drop redundant casts from the Sleep flow test state

* fix(sidebar): stop a workspace's chats after its terminals, leaving main's teardown owner check as is

* fix: admit workspace chat stops before teardown waits

* fix: retain browser teardown caller sequencing

* fix: retire failed acquisitions after observed root exit
2026-10-19 00:33:01 -07:00
Brennan Benson 503d919e64 feat(native-chat): + menu shows chat, disabled chat and CLI rows (#29074)
* feat(native-chat): choose which agents open as a chat

Settings → Chat gets a Providers list. Each agent the host can open as a chat
has a switch; off opens that agent in a terminal instead. Only choices the user
makes are saved. Unchosen: Claude and Codex on, others on when their CLI is
installed on this computer.

The renderer's route and the host's launch-mode decision both read one rule, so
the tab menu, shortcuts, CLI, orchestration and relayed phone launches agree.

* fix: keep chat provider settings with the other chat settings types

* test: route input carries this computer's detected agents

* feat(native-chat): the + menu shows chat, disabled chat and CLI rows

With Chat UI on, each agent whose chat provider is on gets a chat row, disabled
with the reason where the workspace's machine can't run that chat (not
installed, SSH, WSL, not on this host). Every other installed agent gets a row
marked (CLI) that opens its terminal. With Chat UI off, or on an older server
that opens no chats, the menu is unchanged.

* fix: new tab menu rows use a launch variant instead of restyling the menu item

* fix: name every route blocker the menu rows map

* fix: menu row subscriptions render on the server too

* feat(native-chat): open a chat agent's CLI from beside its + menu row

A chat row whose CLI is installed on the workspace's machine gets a › beside
it. Its side menu offers '<Agent> (CLI)', which opens that agent's terminal for
this launch only; nothing is saved and the row itself still opens a chat.

* fix: keep new tab agent surfaces authoritative

* fix: handle unknown host support reasons

* fix(chat): resolve provider choices from their owning computer

* fix: include chat launch feedback translations

* test(chat): use checked native inventory fixtures

* chore: retrigger PR checks

* test: pin pending chat row behavior

* chore: retrigger PR checks after cancellation

* Fix launch preparation lifetime and execution host startup

* fix: keep agent menu choices bound to their launch

* Simplify agent menu support reads and preserve explicit surfaces

* Keep focused menu behavior coverage within its budget

* Keep menu host fixtures typed as execution hosts

* Assert notes payload on the current launch action

* Combine agent session launch type imports

* fix(chat): keep unchosen providers on without discovery

* fix(chat): use workspace installation for menu defaults

* test(chat): type provider routing fixtures against store contracts

* fix(tab-bar): keep + menu chat rows clickable while the host answers

Each menu open discarded the host's answers, so every open showed the chat
rows disabled with 'Checking chat support' until the host replied. Answers
now outlive the menu and refresh in the background, rows use this client's
facts until the host answers, and a slow host no longer reads as a refusal.

* test(chat): align shared launch fixture with private settings reader

* fix(tab-bar): keep known + menu refusals and never treat a silent host as one

Closing the menu dropped the workspace's agent list, which changed which agents
the menu asks about and threw away the host's earlier answers, so a reopen showed
a refused chat as clickable. A support read that could not answer was stored as
"No chat on this host", disabling chats the client knows are supported.

The menu now keeps the stored agent list while closed (only fetching waits for
it to open) and only a real host answer relabels a row. The workspace's
detection target is resolved where main already resolves it (QuickLaunchButton
and the tab-bar runtime model) and passed in, so no new focus read is added.
Tab-bar test mocks now stub the QuickLaunchButton exports the menu imports.

* fix: sync runtime English catalog for agent menu

* test(chat): give the structured-session RPC fixture the private settings reader

The host now reads saved chat choices through requireStore().getSettings(); the
shared RPC fixture only had getClientSettings, so the launch-mode reader saw no
settings and create support refused Claude. The fixture now follows each test's
client settings.

* test(chat): pin create support's options argument and kept decline reason

* fix(native-chat): + menu chat rows wait disabled for the host and share its answer across tab groups

A chat the host refuses showed clickable for up to the 10s host read, then flipped to disabled,
once per tab group. Rows now stay quietly disabled until their host answers (or the read times
out), and every tab group on the same workspace host reads the same answers.

* fix(native-chat): + menu rows also wait before the host's chat list loads, and on every reopen of a host that could not answer

Asking the host no longer waits for its chat list, so Claude and Codex no longer show clickable,
then disabled, while it loads. A read that times out releases rows for that open only, so the
next open waits again instead of flipping on a late refusal. Agents not installed skip the read.

* fix(native-chat): typed + menu search puts a launchable row ahead of a disabled chat

In a WSL workspace, typing an agent's name and pressing Enter highlighted its disabled chat row,
so nothing launched where main opened the CLI. Search now ranks rows that can launch first.

* fix: preserve provider choice and cached native inventory

* feat(native-chat): hosts publish each workspace's chat availability as one stream

A new negotiated stream, agentSession.subscribeAvailability, advertised as
agent-session.structured.availability.v1. The host keeps one in-memory resource per
workspace, shared by every reader, that asks the existing createSupport check for every
registered agent in that workspace and publishes only complete generations. It answers
again when a launch setting changes, after an explicit agent refresh, and (for answers
that could not be checked) when a workspace appears; the last reader leaving retires it.
A check that could not answer is unknown, never a refusal. agentSession.agents stays
static and unchanged.

* feat(native-chat): + menus read a publishing host's workspace availability

The renderer holds the workspace's availability stream while its tab bar is mounted
(not while a menu is open), shared by every tab group, with runtime and contact fences.
It exposes a readiness model, { ready: false } | { ready: true; byAgent }, from
useHostStructuredAgentAvailability and the menu hook. On a host advertising the
capability the menu reads its answers from that model and makes no per-agent
createSupport reads; an older host keeps the per-agent reads, answer cache and deadline
unchanged. Row building and presentation are unchanged.

* fix(native-chat): workspace availability checks only what the shown workspace needs

Only the shown worktree (or an open menu) holds its host's availability stream, and only with Chat
UI on. Display checks no longer force a fresh login shell for an agent that isn't installed, so
Claude and Codex answer without waiting on it. A change to the Windows runtime default or a project's
WSL choice re-checks, and a workspace still on its first check answers again when one appears.

* fix: keep main's bytes for the create-support file the merge reformatted

The pre-commit formatter rewrapped main's unformatted lines during the merge and pushed the file past its line limit.

* test: type the + menu rows hook mocks without assertions

* test: match the availability stream's types in three tests

* test(cross-version): declare the workspace availability stream in the agent-session surface

* feat(native-chat): + menu holds a placeholder until its agents are known, and learns them while the workspace is shown

* feat(native-chat): + menu reads one agent health snapshot per machine

The + menu no longer checks chat availability per workspace. Each Orca
runtime keeps one agent health snapshot: every agent's location rule on
this machine plus a typed observation of its installed release, probed
in the server's own home. It is taken on first use and replaced only on
a launch setting change, an explicit agent Refresh (after a fresh shell
capture), or a restart, and kept without readers.

- The version probe reports supported, unsupported or inconclusive; a
  launch still refuses an inconclusive run, while the snapshot reads it
  as unknown rather than a refusal.
- agentSession.subscribeAvailability takes no params and publishes that
  snapshot; a replay that closes the transport releases its reader.
- The renderer keeps the snapshot per host, runtime and pairing, through
  lost contact and across workspace switches on the same host.
- The menu hook drops the per-open createSupport display reads, their
  deadline and answer cache; older hosts show client facts and the
  click's admission, unchanged, states any refusal.

* fix: a failed shell recapture still publishes the agent health snapshot

* fix(native-chat): capture fresh shell facts for every health snapshot

* fix(tab-menu): share settled agent actions across menu surfaces

* fix(tab-menu): preserve loading row line height

* feat(native-chat): the whole chat row opens its CLI submenu

Hovering a chat row in the + menu, or pressing ArrowRight on it, now opens
the "<Agent> (CLI)" submenu to the right instead of needing a separate
chevron button. Clicking the row, or Enter/Space, still starts the chat
and closes the menu; hosts pass closeMenu since a submenu trigger never
closes the menu on its own.

* fix(tab-menu): line the CLI submenu up with its chat row

* feat(native-chat): a chat row's terminal button opens its CLI

Clicking a chat row in the + menu starts a chat. Hovering the row (or
pressing ArrowRight on it) reveals a terminal button at its right edge that
opens the agent in a terminal instead. This replaces the submenu, whose
chevron made the row look like it only opened a submenu. The button's click
bubbles into the row, so the menu closes as for any item and the hosts no
longer need a closeMenu callback.

* fix(native-chat): the chat row's terminal button handles drags, arrows and screen readers

A press on + dragged onto a chat row's terminal button and released there
now opens the CLI: the row reads whether the pointer or focus is on its
button instead of the click target, which a drag-release never sets.
Arrow, Home and End keys pressed on the focused button move between rows
as they do on the row. The row is announced as "Start a <Agent> chat"
instead of including the button's label.

* fix(native-chat): chat rows read "Start a chat with <Agent>"

"Start a OpenCode chat" was ungrammatical for agents whose name starts with a vowel.
2026-10-19 00:27:17 -07:00
Jinwoo Hong c68c84f157 fix(automations): headless reuse-session sends the prompt into the existing agent (#24278) (#30594)
* fix(automations): headless reuse-session sends the prompt into the existing agent (#24278)

A headless host now continues the agent a previous run left idle, using the
desktop's selection rule (moved to src/shared/automation-session-reuse.ts) and
the launch's reused-terminal prompt writer; it launches fresh when no agent
takes the prompt. Runs of one automation now claim, precheck, pick, prompt and
record one at a time, and headless retention closes that automation's
terminals in the same lane, so no two ever claim one pane. A pane belongs to
its newest run by dispatch order. A run completes on a done reached after it
was dispatched; an idle pane alone completes it only when the pane never
reported agent status, measured from dispatch.

* fix(automations): keep the shared reuse rule Hermes-safe
2026-10-19 03:11:00 -04:00
Neil d03d731ea6 refactor(renderer): one twin check, one worktree-copy scan, drop stale resolver mocks (#30579)
- isTwinWorkspace replaces four inline resolveOwner(...).kind === 'ambiguous' checks
- someWorktreeCopy shared by isUnprovenServerSshRoute and hasHostEvidence
- file-explorer owner: drop the unreachable second unresolved guard and the stale comment
- native chat owner: drop a null ternary the helper already handles
- tests: delete stale getRuntimeEnvironmentIdForWorktree mocks (29 suites), one shared
  local+ssh twin fixture, file:// open-target tests next to their module, reset mocks.twin
2026-10-19 00:10:41 -07:00
68ed9db434 fix(terminal): relay-era SSH panes the bridge serves check printed file paths on the workspace's server (#30240)
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-18 23:59:58 -07:00
OrcaWinandm4air 2b3b64645a fix(editor): opening a quoted lettered list with an unquoted continuation no longer crashes (#30247)
* fix(editor): opening a quoted lettered list with an unquoted continuation no longer crashes

Marked's blockquote re-reads a lazily continued list with its built-in list(),
which rejects Tiptap's extended markers (a., iii)) and returns undefined; the
caller then reads .raw and the rich editor hits its error boundary. A per-lexer
tokenizer falls back to the registered ordered-list tokenizer, cut where
Marked's own list would end at an outer quote line.

* test(editor): guard extended-marker blockquote fallback against dependency drift

Source re-review against Marked 17.0.6/18 and Tiptap 3.31.3: pin the override
to Marked's lazy-list call site, build the quote-break regex once per item,
and cover more item starts Tiptap accepts so a bump can't reopen the crash.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-18 23:49:23 -07:00
Neil 5f126af9f8 fix(watcher): flush file changes on a steady cadence instead of waiting for quiet (#30270)
* perf(watcher): flush file changes on a steady cadence instead of waiting for quiet

Watcher batches used a 150 ms trailing debounce capped at 500 ms, so a worktree
with agents writing continuously always waited the full cap. The git status
nudge had no cap and could starve until the 60 s safety poll. Batches now flush
25 ms after the first change following a quiet period, then at most every 100 ms;
new events join an armed flush instead of pushing it back.

* fix(watcher): keep atomic saves in one batch and survive clock jumps

Raise the first-flush delay to 75 ms so the native watcher's split burst lands
in one batch, cap the flush delay at one interval so a backwards clock jump
can't stall delivery, and widen the editor delete debounce to cover a split
across one flush interval.

* test: wait past the widened editor delete debounce

* fix(watcher): keep markdown list refreshing under sustained writes and widen delete grace

Markdown document refresh used a resetting 125 ms debounce that never fired
once watch batches arrive every 100 ms. Delete tombstones now wait two batch
intervals so slow stat calls (WSL, AV scans) can't split a save into a flash.

* fix(watcher): bound watcher-driven reads and scans under sustained writes

One watcher-triggered editor read and one markdown scan per target in flight,
with a single follow-up; markdown scans honour the latest change time; the
headless Windows watcher no longer resets its refresh timer on every event.

* fix(watcher): make the faster remote cadence opt-in and harden watcher-driven reloads

files.watch takes an optional batchCadence; hosts keep legacy trailing batching
unless the client opts in. Coalesced editor follow-ups re-check the tab and
replace a hung read only once; Markdown mounts skip pre-change scans; the
Windows fallback refreshes the whole tree at most every 500 ms.

* fix(watcher): stop restarting the remaining watch-driven timers

Combined-diff section reloads and per-file editor reload dispatch no longer
push an armed timer back; a queued editor follow-up for a tab that left the
screen marks it stale instead of dropping the change.

* fix(watcher): let combined-diff and worktree notifications land under sustained writes

A combined-diff section discards an invalidated in-flight load at most once in a
row; worktree base-directory notifications no longer restart their timer.

* fix(watcher): never overwrite an edited diff section and unstick hung reloads

A combined-diff load never commits over a section with unsaved edits; a reload
queued behind a hung read starts after the stale limit without needing another
change event.

* fix(watcher): recover from hung markdown scans and keep evidence for edited tabs

A Markdown scan that never settles is abandoned after the join window so later
changes scan again; a queued editor follow-up for a tab edited meanwhile marks
it changed-on-disk and stale instead of dropping the change.

* chore(watcher): use a numeric markdown scan id

* fix(watcher): don't let later events join a stale echo-verification read

Payloads from the same burst still share the in-flight read; a payload from a
later task gets exactly one fresh read issued after the current one settles.
2026-10-18 23:44:18 -07:00
NeilandClaude 6d8e4cfef2 refactor(shell): one shell-argument-quoting module for the duplicated quote copies (#30564)
* refactor(shell): one shell-argument-quoting module for the duplicated quote copies

Each former copy now calls the canonical function with its exact spelling; a table test pins
byte-identical output for every copy over hostile inputs. Distinct spellings stay distinct
functions: '\'' vs '"'"', the two plain-word sets, and the cmd.exe doubled quote.

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

* style: keep the quoting import at the top of two import-less files

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 23:12:27 -07:00
Neil 59b03cab00 fix(ssh): rehome a cold-start converted workspace the scan missed, and notify replayed background-turn completions (#30578)
* fix(ssh): rehome a converted workspace after a cold-start scan misses it, and notify replayed background-turn completions

A cold-start conversion now lists the server's worktrees itself when startup's scan finishes
without the destination row (the scan predated the server's projects or failed for them).

A reconnect replay of a working row carrying a new turnCompletedAt is now observed instead of
seeded; the completion observer already dedupes the stamps it handled.

* test(ssh): drive the cold-start rehome retry through the server RPC
2026-10-18 23:08:05 -07:00
Neil e5184c4c78 refactor(renderer): fold browser creation, file actions and file preview off the deprecated owner resolver (#30562)
* refactor(renderer): fold browser creation, file actions and doc-preview callers off the deprecated owner resolver

The creation-action policy and browser creation (#3, #48) fold together, so the provider the
policy offers is the one creation uses. Browser panes, page lifecycle and conversion, tab
duplication, the explorer tree pane, doc-preview actions and editor path moves (#21, #43, #45-#47,
#49, #50) read runtimeEnvironmentIdWithLegacyFallbackUnlessTwin. File-explorer writes on a folder
(#51) take the owner from the rows alone, never from the focused server. Deprecated importers
16 -> 6.

* refactor(renderer): fold file preview off the deprecated owner resolver; Open in Orca Browser refuses a twin

A project two hosts publish reads as no server for the doc-preview plan, but the file:// path now
refuses it with its own message instead of opening this machine's copy (#7). Deprecated importers
6 -> 5.

* fix(renderer): a twin's file preview and open-externally refuse instead of opening this machine's copy

Codex P2 on the F7 folds: a folder twin read as no server, so the preview plan built a local
file:// tab and open-externally handed the path to this machine's OS. Both now check the owner
for ambiguity first. Also fix two test fixtures that broke the renderer typecheck.

* refactor(renderer): move the file:// open target out of file-preview.ts

file-preview.ts crossed its line limit after main grew it; the file:// target, its type and its two
refusal messages move to workspace-file-browser-open-target.ts, which they never depended on
file-preview for.

* test: mock the replacement resolver in the quick-action availability suite (Codex P2)
2026-10-18 23:03:33 -07:00
Neil 767b55b1b6 fix(terminal): client pane names reach a desktop host's window, and open panes follow the host's (#30496)
* fix(terminal): client pane names reach a desktop host's window, and open panes follow the host's

- A host with its own desktop window ignored client pane-layout writes, so a
  client's pane rename never reached the window. updateMobileSessionPaneLayout now
  passes the client's pane names to the window one pane at a time through the
  set-pane-title path, and still leaves the window's geometry and the tab's name
  alone. A single pane named after its own tab is dropped, not forwarded.
- Open panes on a client only read host pane names when they mounted. They now
  follow the host's published titlesByLeafId live. A local rename is held until
  the host echoes it, so a snapshot taken before the host saw it cannot undo it.

No wire change: hosts already publish titlesByLeafId, and old peers keep their
current behaviour.

* fix(terminal): hold a client's pane rename over host snapshots until echoed, and fail a dropped window delivery

* test(e2e): read the created host tab id without a type assertion

* fix(terminal): only an explicit client rename reaches a desktop host's window, and local rename holds are bounded

- A client push now names the panes it renamed (optional `renamedLeafIds`), and
  the host forwards only those. A resent title map, which older clients and every
  divider drag send, can no longer revert or clear a name set on the host.
  Older hosts strip the field. Older clients send none, so their renames stay
  local as they do today.
- A held local rename now gives way to a newer host name, a failed push, or a
  15s expiry, and expired holds are pruned. A host that never echoes (an older
  desktop host, or a single-pane name dropped as the tab's own) can no longer
  hide host titles forever.

* fix(terminal): a failed rename push retries with its rename, and an earlier echo cannot undo a newer rename

* fix(terminal): a pane rename bypasses the unchanged-layout push dedupe
2026-10-18 23:03:00 -07:00
Neil df7259e2ae fix(terminal): reattach a restored terminal while an older daemon cannot answer (#30486) (#30499)
* fix(terminal): reattach a restored terminal while an older daemon cannot answer (#30486)

A window save erased the PTY incarnation main persisted for each pane, so
after a restart every reattach lacked the generation proof the owner
resolver needs. With any older daemon generation that could not answer,
the inventory stayed incomplete and every restored terminal was refused as
unverifiable, on every Retry.

- keep each pane's incarnation across a window save that still binds it
- accept an exact incarnation match from a reachable daemon even when
  another daemon's inventory fails (incarnation ids are unique per spawn)
- drop a provably exited older daemon from the reattach inventory
- offer Open new terminal beside Retry on the unverifiable toast

* test(daemon): build the unanswering daemon endpoint with the platform socket path

* fix(session): keep each pane's incarnation when a save carries only some of them

* fix(daemon): judge a replacement legacy daemon's exit by its own pid

* test(daemon): router fixture reports a pid that exists, so exit proof never sees a dead daemon

* test(daemon): the wedged older daemon names a live pid instead of the shared fixture's arbitrary one

* fix(daemon): a spawn treats an older daemon as exited only when its socket and pid are both gone
2026-10-18 23:02:26 -07:00
Jinwoo Hong e8d7da029c An e2e test cuts the window's layout feed mid-session and checks it recovers and survives a restart (dark, layout-owner build only) (#30574)
* test(layout): an e2e ends the window's layout stream mid-session and checks the reopen and a relaunch (dark, layout-owner build only)

* test(layout): the stream-loss e2e covers main's change during the gap deterministically and reuses the shared pane-tree and frame readers (dark, layout-owner build only)

* test(layout): the stream-loss e2e names how long main's create took when it misses the reopen window (dark, layout-owner build only)
2026-10-19 01:45:27 -04:00
Neil f48123e89a feat(agent-status): show OSC 7501 program status in rows and tabs (#30262)
Builds on the program-status core:

- A determinate progress ring replaces the working spinner when a program
  reports progress (rows and tabs; percent only in tooltip/aria), in the
  attention tone while blocked. The working color becomes the
  --agent-working token.
- Non-agent programs (brew, terraform, …) get their first letter as icon.
- The program's own one-line message shows as the row preview, a failed
  program reads "Failed: <message>", and a block such as sign-in that the
  row's state does not show raises attention.
- A finished program's result outlives its exit until the next keypress or
  command in that pane, as the protocol asks; real agents are unchanged.
2026-10-18 22:36:33 -07:00
Brennan Benson 62975d6bfe fix(sidebar): run previous and next worktree shortcuts from a text field (#28891)
* fix(sidebar): run previous and next worktree shortcuts from a text field

The worktree list swallowed every key while a text field was focused, so the bound shortcut did nothing in chat, markdown, and the code editor.

* fix(sidebar): keep the default worktree chord with text fields

Previous/next worktree now carries a text-field rule in its keybinding definition. In a text field the default Cmd/Ctrl+Shift+Up/Down keeps its select-to-start/end meaning, while a chord the user bound still switches worktrees from chat, markdown and the code editor. Option and AltGr chords that type a character stay with the field.

* fix(sidebar): let user-bound Option arrow chords leave text fields

On Mac only Option chords that type a character or dead key stay with a focused text field, so a bound Option+Down now switches worktrees from chat, markdown and the code editor.
2026-10-18 22:26:24 -07:00
Brennan Benson 8250c93f3e fix(sidebar): show the project name on ungrouped compact cards (#28890)
* fix(sidebar): show the project name on ungrouped compact cards

Compact and new-card layouts kept the project in an icon chip, with the
name only in a tooltip. The chip now shows the project name when the
sidebar is not grouped by project.

* fix(sidebar): show the project name on pinned chips too, muted and lowercase

* docs(sidebar): describe repo identity placement, not the chip's contents

* fix(sidebar): keep emoji project icons inside the chip
2026-10-18 22:24:47 -07:00
Neil d45894c280 test(floating-terminal): mock the bulk-close confirmation the panel suite cannot render (#30568) 2026-10-18 22:13:10 -07:00
Neil e3c9370da4 feat(agent-status): read OSC 7501 program status beside hooks (#30261)
Programs can tell Orca what they are doing through the Program Status
Protocol (OSC 7501); Claude Code, Codex, Grok, pi and oh-my-pi already do.
Orca answers the protocol's feature-detection probe (visible xterm and the
hidden-pane emulator), parses reports from PTY output on the execution host
(src/shared/program-status/), and keeps two lanes:

- Lifecycle state keeps one owner per pane. Hooks win; OSC 7501 drives the
  row when hooks are off, missing, or silent on a state the program has
  left (server/program-status/hook-precedence).
- The program's own report (progress, its one-line message, blocks such as
  sign-in) is a live-only `programStatus` facet kept per pane
  (server/program-status/facet-store), stitched onto rows at publish time
  and carried to the renderer store, mobile, paired clients and
  `worktree ps`. Facet-only changes patch just the facet.

Title-driven readers (idle edges, delivery, tui-idle, exit inference)
classify by the program's OSC 7501 activity while it reports one and fall
back to title glyphs only for agents that report nothing explicit, so they
keep working when Claude stops painting its title glyph after the probe.
2026-10-18 22:12:37 -07:00
Brennan Benson fc0d3d2f8f feat(native-chat): /resume in a structured chat opens Agent Session History (#28133)
* Give Session History's rows one decision for open and resume

A session row decided "can ▶ resume this" twice: once for the button's
disabled state and again inside its click handler, and the chat/CLI and
switch-to-pane offers were each gated inline. A second caller of those
offers would have had to copy the same conditions again.

- One projection, resolveAiVaultSessionRowChatActions, now decides every
  open/resume offer a row makes; ▶'s greyed-out state and its target come
  from the same value.
- The panel's host-scope menu and per-session resume lookups move into
  their own hooks unchanged, and the session-list request reads the
  panel's workspace through one selector.

The sidebar behaves exactly as before.

* Let Session History host a chat-bound panel with keyboard selection

A second Session History panel (the /resume dialog that follows) needs
to describe a chat's own workspace rather than the active one, and to
let ↑/↓/Enter pick a row. Mounting it beside the sidebar also exposed
a place where two panels stepped on each other.

- AiVaultPanel takes an optional workspace, an initial query, a header
  action slot, a small search handle, and onSessionOpened, which also
  turns on row keys. A thin sidebar wrapper now owns Settings' "focus
  the search" request, so only the sidebar widens to All.
- Keyboard rows store the chosen row, its session and the search text;
  a session listed twice no longer traps the arrows, and editing the
  search starts again at the top match. Enter takes the row's best
  offer: switch to it if open, else open it as a chat, else the row's
  own ▶, else show details. A clicked row moves focus to the panel, so the
  keys keep working, and a row scrolled into view stops below the
  pinned group header.
- Switching to an open session reports whether it found something to switch to.
- Mounted panels share one copy of the saved view options, so a filter
  changed in one is not undone by the other.

* Make /resume in a structured native chat open Agent Session History

Resuming a past session was only reachable from the Agents sidebar, so
people in a chat had no way to find it. In a structured native chat,
/resume [search] now opens Agent Session History for the chat's own
workspace in a dialog, with the search filled in.

- The dialog lives in the app's modal slot: a dialog rendered inside a
  chat would bubble its key and pointer events through the chat's own
  capture handlers. It closes once the tab on screen changes or a
  switch to an open session succeeds; a failed action leaves it open.
  Focus goes back to the chat unless the user was taken elsewhere.
- /resume is an ordinary row in the structured chat's / menu, in place
  of an agent command of that name, or last when there is none. A skill
  named resume keeps its own row and a picked skill still goes to the
  agent. Typing it in any case opens the dialog; attachments stay.
- Terminal-backed chats (SSH, WSL runtimes, custom start folders,
  reused terminals, older hosts) are unchanged: /resume reaches the
  agent as before. The phone is unchanged too.

* Start /resume's Session History on the chat's own agent

The dialog's agent filter starts on the agent recorded for the chat, the
same one its tab icon shows, and stays local to the dialog: the sidebar's
saved filter is never read or written by it. Sort, grouping and limit stay
shared. An agent the filter doesn't list leaves every agent shown.

* Start an OpenCode chat's Session History on both OpenCode names

A structured OpenCode chat records its provider as opencode, but the
scanner lists current OpenCode sessions as opencode2, so filtering on the
recorded name alone hid every OpenCode row, the chat's own included.

* feat(native-chat): /resume rows resume the conversation in place of the chat

In the /resume dialog a row is now only a "resume here" target: clicking it
or pressing Enter opens the picked conversation as a chat right after the
chat /resume was typed in, and closes that chat once the new one is
confirmed and the old one is idle. A conversation already open elsewhere is
switched to, the chat's own conversation just closes the dialog, a closed
chat of this workspace and agent is reopened in its place, and anything else
only expands its details. A failed resume closes its own tab and leaves the
old chat as it was.

Dialog rows drop every other action (overflow and context menus, title drag,
hover buttons, detail and subagent buttons); the chevron still shows the
details. The dialog widens from 480px to 560px. The sidebar's Session
History panel is unchanged.

* test(native-chat): type the launch-plan mock in the /resume flow test

* fix(native-chat): find the reopened chat by its tab, typed test settlement

* fix(native-chat): /resume picker review fixes (R5)

- Subagent lines in a picker row offer no View Log either (resume null switches them off).
- An unsaved row in the picker drops the "open the log" hint, since the picker has no log button.
- The sidebar title drag handler is restored, so a row whose resume is disabled still blocks text drags.
- Only a failed resume closes its new tab; an unknown outcome keeps both tabs (the new one has its own badge and Retry).
- The replaced chat closes only when its last turn finished, its composer draft is empty, and it is not pinned.
- One pick at a time: a pick is ignored while an earlier one is still settling.

* fix(native-chat): a new, empty chat is idle for /resume replacement

A chat with no status entry closes when its launch has finished and its host,
observed live, reports no message and no provider still starting. Still-starting
and unobservable chats stay open.

* fix(native-chat): /resume knows a stale row's open chat; no Copy in picker details

- A row listed before its chat took the conversation over has no ownership link.
  The pick now also reads each open chat's own record (its host status feed's
  provider session, else the conversation its launch resumes): the chat /resume
  was typed in just closes the dialog, and another open chat is switched to,
  instead of trying the takeover again and failing.
- The first prompt's Copy button is hidden in picker rows (same switch).

* fix(native-chat): act on the host's ownership verdict for stale /resume rows

Drop the renderer's own "which chat holds this conversation" lookup: it keyed on
the record's head handle, which differs from the host's ownership key (Pi rows,
older chain links). Instead, when a resume-here launch is refused because the
conversation is held elsewhere, the refusal's owner decides: this chat closes the
picker, another chat is switched to, the temporary tab closes, and no failure
toast is shown. Rows with an ownership link keep the existing fast path.

* refactor(native-chat): share the composer editor setters with the /resume hook

Keeps NativeChatComposer under its line cap once current main is merged in.

* feat(native-chat): /resume replaces its chat, asking first only when the agent is busy

Picking a row now always closes the chat /resume was typed in once the picked
conversation's chat is confirmed, pinned or not; the unsent-draft and
finished-turn checks are gone. When that chat's agent is replying, waiting on an
approval or question, or still starting, Session History closes and a small
"Replace this chat?" prompt asks first; Cancel or Esc changes nothing.

The prompt reuses the shared confirmation dialog with a new compact look
matching the close prompts.

* fix(ui): compact confirmation look through dialog title/description sizes

The primitives own their typography, so the short close-prompt look is a size
variant on DialogTitle and DialogDescription rather than class overrides.

* fix(native-chat): command picker takes the full structured transport

* fix(ui): compact confirmation width through a DialogContent size

The /resume replace prompt matches the close prompts width (max-w-sm) via a
compact size on the primitive; callers that pass no size render as before.

* fix(editor): pan-zoom dialog props keep their own image size

DialogContent now has a size prop; the pan-zoom dialog s size means the image, so
it is omitted from the inherited props.

* fix(native-chat): /resume asks when the chat's state is unknown; prompt ignores the opening gesture

- R8-1: a chat whose host isn't observed live (old host without the status feed,
  lost contact, no host stamped on the tab) counts as busy, so replacing it asks.
- R8-2: the confirmation dialog takes ignoresOpeningGesture: an outside press
  doesn't dismiss it, a repeated Enter doesn't confirm, and a double-click's second
  click doesn't press Replace. Other callers are unchanged.

* Point the resume-command test at the moved picker-items module

* Reopen a closed owner in place of the chat on a refused /resume

A row listed before its conversation was adopted has no structured link, so /resume launches a new chat and the host refuses it, naming the closed chat that holds the conversation. That branch reopened the owner but never closed the source chat. Both the reveal path and the refusal path now share one reveal-in-place step; an owner open in another tab is still switched to and the source kept.

* Put a reopened chat in the replaced chat's slot

Reveal appends the reopened tab to the end of its group, so a /resume that reopened a closed chat left it after unrelated tabs. Move it right after the source with the existing anchor-insertion helper before the source closes, the way a new chat lands.

* Keep the typed search when the /resume row is picked

With the caret right after /resume and a search typed past it, picking the menu row opened Session History with an empty search. Read the search from the draft, as a typed send does.
2026-10-18 21:38:28 -07:00
Jinjing 43a6cfe538 Confirm before bulk-closing tabs (#30551)
* Confirm bulk tab closes before closing tabs

Close Others, Close Tabs to the Left/Right, and Close All Editor Tabs now
show a confirmation dialog first, so one misclick or shortcut no longer
closes a group of tabs at once. Pinned tabs are still skipped.

Tabs are selected again after the user confirms. If a tab was pinned, moved,
or removed while the dialog was open, it is left alone. Tabs created after
the request are not closed.

The shared confirmation lives in use-bulk-tab-close-confirmation.ts and is
used by both the tab bar and the tab group. The tab group hook now reads
the store directly instead of taking group and groupTabs as props.

* Confirm bulk tab closes before closing tabs

Close Others, Close Tabs to the Left/Right, and Close All Editor Tabs now
show a confirmation dialog first, so one misclick or shortcut no longer
closes a group of tabs at once. Pinned tabs are still skipped.

Tabs are selected again after the user confirms. If a tab was pinned, moved,
or removed while the dialog was open, it is left alone. Tabs created after
the request are not closed.

The shared confirmation lives in use-bulk-tab-close-confirmation.ts and is
used by both the tab bar and the tab group. The tab group hook now reads
the store directly instead of taking group and groupTabs as props.
2026-10-18 21:33:03 -07:00
Jinjing 81730c0b3d Track usage of closing tabs to left and right (#30557)
* Add telemetry for directional tab close requests

Measure how often people use Close Tabs To The Left and Right, without recording tab titles, paths, or workspace identifiers.

- Sample a stable 10% of installs by hashing the install ID, so membership survives restarts. The app-open event carries the sampled flag so the group can be analysed.
- Cap each direction to one event per app session, tracked in main, so repeated clicks or extra windows cannot inflate volume.
- Count the request before the close flow runs, so cancelled save prompts still count.
- Route both directions through one helper that counts eligible unpinned tabs in visible order.

* Count settings opt-in sessions in tab-close sampling denominator

Enabling telemetry from settings now fires `app_opened` for that session, not only when the opt-in came from the pending banner. Before, settings opt-ins never emitted `app_opened`, so those sessions were missing from the adoption denominator for directional tab close events.

- `trackAppOpenedOnce` now marks the session as tracked only when `track()` accepts the event. An opt-out or environment consent override no longer uses up the session's denominator.
- Tests cover sampling for settings opt-in and the `DO_NOT_TRACK` override.
2026-10-18 21:29:29 -07:00
Brennan Benson 46be7e23ee feat(settings): sign Antigravity chats in from Accounts (#28705)
* feat(accounts): sign Antigravity chats in from the Accounts page

Adds an 'Antigravity chats' row to the Antigravity section of Accounts
(behind the native chat setting, this device only): Sign in starts the
host's chat sign-in, follows it with the Link poll, opens the Google page
once, offers Copy link and Cancel, and shows one status line per phase.
The saved line reflects the sign-in file only, never that it works.

* Keep Antigravity chat sign-in bound to the current view

* feat(settings): put Antigravity chat sign-in under its install row in Chat > Providers and Accounts

The chats' sign-in row now sits directly below the Antigravity install row,
in Settings > Chat > Providers and under the agy section of Accounts, with copy
that says it is separate from the agy account. Its button reads Sign in, Change
account or Retry sign-in by state, and stays visible but disabled while the host
has no runnable server; the install row above says why.

* fix(settings): both Antigravity chat sign-in rows share one sign-in per host and pick up one already running

Settings shows the sign-in row under Chat > Providers and under Accounts at once; each kept its own
flow, so starting one silently cancelled the other and a sign-in nobody watched could not be seen or
cancelled. One entry per host now holds the flow and the saved sign-in for every row, follows the
host's Link poll while a row shows it, and on mount adopts the running flow the host names. Sign in
also waits while an install or Remove runs, read from the same install status.

* fix(settings): a failed sign-in poll re-reads the host so a running sign-in stays cancellable, and only a Remove (not an Update) holds Sign in

* fix(settings): the sign-in rows follow the host's sign-in status through the same loop as install, so an idle page sees another device's sign-in

* refactor(settings): install and sign-in rows share one subscription store; Install and Remove wait for a running sign-in, and Sign in waits for the host's answer

Both stores now follow the host through one generic subscription store
(snapshot, then a frame per newer revision, resubscribe with backoff on loss),
replacing the sign-in store's long-poll of a method the host no longer has.
While a sign-in runs, Install, Update and Remove are disabled (Cancel stays).
The sign-in row keeps Sign in disabled until the host answers, says when the
status could not be read instead of showing signed out, and sends one start or
cancel at a time.

* feat(settings): paste the last sign-in page's address to finish an Antigravity chat sign-in

While a chat sign-in waits on the browser, its row shows a field for the address of the last page after signing in, for when that page doesn't load because the browser is on another computer than the host. Continue sends it to the host, which passes it on to Antigravity; the host's refusal shows under the field. With that, remote hosts offer chat sign-in on the Accounts page too, and the row reads "Finishing sign-in…" while the host completes it.

* test(settings): give each host its own fake sign-in status and stub the new subscription export

The sign-in row tests shared one fake host across every owner, so the first test's finished sign-in made the next row show "Change account" instead of "Sign in", and rows left mounted after that failure doubled the later tests' matches. Each owner now has its own fake host behind the subscription, and every test cleans up its render. The Antigravity accounts section test's client mock also lacked the sign-in exports the status store now reads at import.

* feat(settings): the Antigravity chat sign-in row follows the host's sign-in answer

The host no longer reports whether a sign-in file exists; it reports what the
server last answered (signed in, signed out, or not known yet), and a running
sign-in started by another client arrives without its id.

The row now reads that answer:
- "Signed in on {host}." or "Not signed in on {host}."; no line until the server
  has answered, with Sign in still enabled. Change account shows only when the
  server said signed in.
- A sign-in another client is running reads "Sign-in is in progress in another
  client.", with Sign in disabled and no Cancel, copy link or paste field, since
  only the client that started it can act on it.
- "Finishing sign-in..." also follows a browser sign-in while the host checks it.
- Cancel and paste send only this client's own sign-in; the host's refusals
  show under the row.

* fix(chat): a signed-out Antigravity chat says to sign in under Settings, not what the agent printed

The notice read "Sign in to Antigravity. The agent asked to sign in in a
browser": the second sentence was Orca's own log text for the sign-in prompt the
agent printed, shown as if it were the agent's words, and the notice never said
where to sign in.

The connection now closes with a sign-in prompt error whose message stays in the
logs, so the chat shows only Orca's sign-in sentence. Antigravity gets its own
sentence, in the shape Claude's already has: "Antigravity isn't signed in. Sign
in under Settings → Accounts → Antigravity." Other agents' sign-in words, and
the agent's own words in a sign-in answer, are unchanged.

* fix(chat): a sign-in prompt at chat start keeps Orca's words out of the signed-out notice

A signed-out Antigravity chat still read "... The agent asked to sign in in a
browser". The connection closes with a sign-in prompt error whose message is
Orca's log text, but the request wrapper and session setup re-typed every
sign-in answer by copying its message into a new error. The copy lost the
sign-in prompt type, so its message was quoted as the agent's own words.

Re-typing now keeps a sign-in prompt close as it is, at both the request
wrapper and session setup, so the notice says only where to sign in.

* fix(settings): the Antigravity sign-in row shows the host's answer once a sign-in is cancelled or timed out

After a cancelled or timed-out sign-in the row kept reading "Sign-in
cancelled." even when the host had answered that this computer is not signed
in. A finished sign-in now only stands in until the host answers; then the row
reads "Not signed in on <host>." or "Signed in on <host>.". A running sign-in
still shows its step.

* fix(settings): a wrong pasted sign-in address shows Orca's reason, not the browser's URL bubble

The paste field is a URL input, so the browser's own "Please enter a URL."
check ran before Orca could explain what was wrong with the address. The form
now skips the browser's check and shows the host's refusal sentence.

* style(settings): the Antigravity chat sign-in row reads as a plain row like the install row

The sign-in row sat in its own bordered box, the same bright outline the install
row lost when it became a single grey status line. It now has no box: a
medium-weight "Antigravity chats" title, the description and the grey status
line under it (errors inline in the destructive colour), and its action at the
row's right end. Sign in and Retry are primary buttons, so they stand out like
Install; Change account stays outline and Cancel stays ghost. The paste-the-
address form sits under the row without a box. Copy, keys and behaviour are
unchanged.

* ci: run the PR checks against main after the retarget

* fix(settings): list every sign-in phase in the status line switch

* test(chat): name Antigravity as the chat's agent in its signed-out sentence test

* feat(settings): Antigravity provider row with a ⋯ menu and its own sign-in area

The Providers row's status line now says only the install state. Update,
Reinstall and Remove move into a ⋯ menu beside the switch, shown for a copy
on disk; Remove keeps its confirmation. Sign-in moves into a quiet inset
area under the row, shown once a copy runs, with one sentence and one
action per state. Accounts keeps its sign-in row; its install row gets the
same menu.

* fix(settings): a damaged runtime offers Reinstall as its row's button

A damaged copy's repair is the next step, so it sits where Install does instead of inside the ⋯ menu, which then offers only Remove.
2026-10-18 21:28:43 -07:00
NeilandClaude 24e218c101 refactor(shared): move orcad-migration-* into src/shared/orcad-migration/ (#30536)
Pure git mv plus import and path-literal rewrites. Two files #29339 edits stay put:
orcad-migration-dormant-state-validation.ts and orcad-migration-manifest-contract.test.ts.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 21:28:03 -07:00
Neil b117a33c1b refactor(renderer): fold launch, paste and terminal-create callers off the deprecated owner resolver; spawn refuses folder twins (#30497)
* refactor(renderer): fold launch, resume, note-send, paste and terminal-create callers off the deprecated owner resolver

Agent launch, AI-vault resume and reveal, note target and send, wake and creation seeds, native chat
owner, the shell menu and active-workspace terminal creation (#22, #31-#42, #44) read the owner
through runtimeEnvironmentIdWithLegacyFallbackUnlessTwin. Paste uploads images to the server its
PTY id names before falling back to the workspace owner. The folder and floating branch of the
terminal spawn route (#53) reads resolveOwnerWithLegacyFallback: spawn refuses a folder twin
instead of following the focused server, and teardown keeps tearing raw ids down locally.
Deprecated importers 31 -> 16.

* test: give the paste-listener and tab-bar probes the fields the folded callers read

* test: note why the folder-twin route fixture is cast

* test: type the paste-owner twin fixture
2026-10-18 21:26:52 -07:00
Jinjing a7e60f2165 Fix thinking status translations in zh, ja, ko, and fr (#30558)
The live "thinking" status was translated as bare nouns (思维, 考え, 생각, Réflexion) that read as a faculty or a thought rather than an ongoing state. Use present-progressive wording for the status, and add key-specific overrides so the status and the composer option keep distinct labels.
2026-10-18 21:23:49 -07:00
Jinjing a282409d20 fix(i18n): clarify closing all tabs to the left and right (#30535)
* fix(i18n): clarify bulk tab closing in all languages

* Update tab context menu tests for "Close All Tabs" labels

The menu labels were renamed to "Close All Tabs To The Left/Right" in the i18n change, but these two test files still expected the old "Close Tabs To The ..." text. Update the assertions so the tests match the current labels.
2026-10-18 21:01:26 -07:00
Jinjing 4d65c7617d Open HTML file URLs as document previews (#30512)
* Open pasted HTML file:// URLs as document previews

Pasting a file:// URL to an HTML file into the new-tab box used to fall through to the generic URL path, which does not show the document's relative assets. It now routes to the workspace document preview, so the file renders with its local scripts and styles.

- Classify HTML file URLs before the file list loads, so the "Open file" option appears immediately.
- Resolve file URLs and absolute paths only against workspaces on the same host, so an SSH target is never matched to a sibling on another host.
- Reuse an open document on any page of its workspace instead of opening a duplicate preview.
- Keep the preview in the group the user targeted, including for remote hosts.

* Open HTML file:// previews without resolving file-operation context

Opening an HTML file:// URL used to need the worktree and file-operation context before the preview could open. In remote or not-yet-loaded states that lookup can fail, so the preview never appeared. Pass the context as a lazy getter, so it is read only when a file operation actually needs it. The preview path now runs without it, and the dispatch in openTabBarEntry no longer special-cases HTML before the network and file paths.
2026-10-18 20:57:50 -07:00
Neil 8908fdeb0c refactor: extract helpers that agent-status files need room for (#30260)
No behavior change. Moves cohesive pieces out of files at their line limit
so the OSC 7501 program-status work on top reads as additions:

- pane-agent-identity: the confirmed-shell tab-title reset
- SortableTab: its activity-status selector, into useTerminalTabActivity
- agent-status-event-applicator: the Pi provider-session partial update
- terminal-derived-agent-facts: resolving a PTY's status target panes
- server-row-ownership: notifying row-mutation listeners
2026-10-18 20:44:24 -07:00
Jinjing 5ee118a908 fix(source-control): clear unavailable branch line totals (#30475) 2026-10-18 20:44:00 -07:00
Jinwoo Hong 19b7a6c838 A first visit resumes a sleeping agent once, in its own pane (dark, layout-owner build only) (#30468)
* fix(terminal): a first visit resumes a sleeping agent once, in its own pane (dark, layout-owner build only)

Main retires a gone terminal's binding before the pane's fresh start, so on
main-owned layouts the activation sweep read the unbound pane as unable to
resume and launched the agent again in a new tab while the pane resumed it in
place. On main's layout the pane that holds the record owns its resume.

* fix(terminal): the sweep defers only to a pane whose own resume is in flight (dark, layout-owner build only)

Counting every unbound pane in main's layout as the record's owner also
covered a mounted pane whose agent died: it never restarts on its own, so
its agent was never resumed. The pane now marks its in-place resume while
the reattach or fresh start that carries it runs, and the sweep defers only
to that mark.

* test(terminal): the in-place resume hold reads only whether the pane is disposed

* refactor(terminal): the in-place resume hold takes the pane's liveness, not the session

* test(e2e): skip the single-resume spec on Windows, as its sibling resume specs do

* test(terminal): both starts that resume a record set the in-place mark

The e2e that would catch a dropped call needs a layout-owner build CI never
makes, so a switch-on unit test drives each start and checks the mark while it
runs and after it settles. Also corrects the mark's comment: a hidden pane's
background wake sets it too.
2026-10-18 23:41:55 -04:00
Jinwoo Hong 88241bcd1e feat(sidebar): the phone reads its workspace list from the sidebar inputs (dark) (#30462)
* feat(mobile): the phone page lists workspaces from the host's sidebar feed (dark)

* fix(mobile): settle pending option changes by latest intent; feed carries the desktop's focused host and host names (dark)

* fix(sidebar): the feed carries the desktop's pinned placement; e2e compares the phone list to the desktop sidebar (dark)

* test(sidebar): the phone list e2e also expands and unpins

* fix(sidebar): phone feed list review fixes (dark)

A collapsed or worktree.ps-failing feed list no longer shows an empty or error state; a queued worktree read uses the latest client; a superseded option change does not report a refusal; the window shadow leaves the focused host to main's test (focus-read ratchet).

* fix(sidebar): phone feed tests typecheck; e2e helper no longer pulls the native runtime types

Mobile test fixtures fill the fields the test project's types require; the feed trigger type lives with the feed reader, so the e2e helper's imports stop dragging react-native globals into the e2e program; React Doctor's two blocking findings.
2026-10-18 23:41:32 -04:00
Jinwoo HongandClaude 61b30a4933 Main keeps an agent's resume record from the status it receives (dark, layout-owner build only) (#30455)
* Main keeps an agent's resume record from the status its hook server accepts (dark, layout-owner build only)

In a layout-owner build, the window's recorder and quit capture kept only
records of tabs the window holds, so an agent in a workspace the window
never opened lost its resume record at quit. Main now applies the window's
rule (moved into src/shared) to every hook status for a pane it holds, and
captures its own panes' agents at quit; the window's quit capture is gone.

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

* A snapshot replay looks up a pane's tab only when its status can leave a resume record

Restores the lookup order the window had before the record builder moved to shared code, and
updates the two handler tests for the quit-records option.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 23:18:51 -04:00
Neil f951cf44bb refactor(renderer): fold activation mirroring and sort-order persistence off the deprecated owner resolver (#30484)
The eight mirroring callers (#23-#30) read the owner through
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin, so a twin is still not mirrored. Sort-order
persistence (#2) leaves out ids that two hosts publish, and ids whose only server proof is a repo
row (isUnprovenServerSshRoute), instead of writing them to this desktop's or a guessed server's
meta. Deprecated importers 40 -> 31.
2026-10-18 20:16:50 -07:00
Jinwoo HongandClaude 19e45b7921 feat(sidebar): the desktop window lists main's sidebar inputs behind the build switch (dark) (#30461)
* feat(sidebar): the desktop window lists main's sidebar inputs, with its own writes shown until main does (dark)

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

* fix(sidebar): a newer write wins in the pending layer, created rows hold the stamped record, writes name their host (dark)

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

* chore(sidebar): move the focus-read allowance with the window list model it moved into (dark)

* perf(sidebar): main's sidebar feed publishes a change after a quiet spell at once, not 50 ms later (dark)

* test(e2e): the desktop reads-main spec skips on a switch-off build and waits for a frame after collapse

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 22:49:43 -04:00
Brennan Benson 0322185bdc fix(native-chat): a Claude chat keeps a model its CLI stopped listing (#30436)
* fix(native-chat): a Claude chat keeps a model its CLI stopped listing

Claude Code 2.1.302 stopped listing opus[1m], which still runs, and chats on
it moved to the list's default on the next send, effort remapped. The picker
no longer derives a replacement from the list: the chat's own model shows as
a selected-only entry, its effort read-only, and every send carries both.

A real model_not_found reply still recovers: the options answer shows what
the reset child runs, so the next send records it. A reopened chat's catalog
read lists its own model so older clients that replace one keep it too.

Tracked in STA-10947.

* test(mobile): drop the send-options capability the options hook no longer reads

* test: an unlisted model's saved effort shows read-only at rest, as live, for every agent

* fix(native-chat): round-1 review fixes; a new chat's unlisted remembered model falls back as on main

- The read-only value predicate keys on the effort row, so the phone's legacy
  host Permissions row opens as before.
- The desktop read-only effort row honours a row kept from the previous model.
- A live child with no listing of its own answers with the saved listing's
  ids, so the read-only effort reads the same live and at rest.
- The read-only effort borrows listed rows' effort label and level names.
- The host seeds a new Claude chat from a remembered default the saved listing
  no longer names on that listing's default, effort nearest, as main did; a
  chat's own saved or running model is never replaced.

Tracked in STA-10947.

* test: pin the new-chat seed fallback on its typed helper, with no casts

* fix(native-chat): round-2: a carried effort moves with a fallen-back model; one guard

- An at-rest create carrying an effort but no model, on a remembered model
  the saved listing no longer names, now moves that effort to the nearest
  level the fallback model offers, as main did.
- A create whose own options name no model, and whose merged options carry
  none, reads no listing.
- The seed fallback keeps one guard and no unreachable branch.
- The read-only effort finds its level's name in any listed model's choices.

Tracked in STA-10947.

* fix(native-chat): a live options read with no record borrows no saved ids and never throws
2026-10-18 19:49:22 -07:00
Neilandstevelliu 347d0e46a1 feat(cli): set the title of one terminal pane (#30485)
* feat(cli): set the title of one terminal pane

Adds `orca terminal set-pane-title --terminal <handle> --title <text>`; `--title ""`
clears it. The runtime resolves the live handle to its saved leaf and asks the
desktop window to name that pane through a new optional notifier hook
(`ui:setPaneTitle`). Mounted panes apply it through the same local setter the GUI
uses; parked panes get it in the saved layout their next mount reads. The tab's
own name is never touched.

Rebuilt on current main from #24755.

Co-authored-by: stevelliu <stevelliu@tencent.com>

* fix(renderer): a main-owned layout takes a CLI pane title before the window shows it

* fix(renderer): a CLI pane title reaches a never-mounted tab with a rootless layout

---------

Co-authored-by: stevelliu <stevelliu@tencent.com>
2026-10-18 19:48:36 -07:00
Neil bf866505e8 refactor(renderer): fold the right-sidebar, hook, native-chat and doc-page readers off the deprecated owner resolver (#30478)
Thirteen read-routing callers (#4, #5, #10-#20 of the round-3 caller list) read the owner through
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin. A workspace two hosts publish is no longer
searched on either host. The focus-read ratchet now counts the legacy-fallback adapter, which
reads the focused runtime inside an object literal its discovery skipped; the five F1 entries
that blind spot pruned come back. Deprecated importers 53 -> 40.
2026-10-18 19:01:49 -07:00
Jinwoo Hong 613c5986e3 Editor records keyed by owner, phone active file, and a bound on frame holds (dark, layout-owner build only) (#30473)
* fix(layout): editor file records keyed by path and owner, so two hosts' files at one path stay apart (layout-owner build only)

Opening a runtime-environment file at a path a local file already held replaced the local record,
the stored-session patch and unsaved drafts matched by path alone, a move took every owner's record
of the path, and closing a bare-path tab removed both owners' records.

* fix(mobile): a windowless host's editor tab under its owned id shows as active, and retiring it clears the active file (layout-owner build only)

* fix(layout): a layout change's tab and pane holds end 15 s after it was sent when main never answers (layout-owner build only)

* fix(layout): main's view reads and prunes keep an active file under its owned id (layout-owner build only)
2026-10-18 21:34:37 -04:00
Neil 1ad8755fa8 refactor(renderer): fold four lib callers off the deprecated runtime-owner resolver (#30458)
Open-in owner, execution-host platform facts, doc-preview grants and the activation PTY inventory
read the owner through resolveOwnerWithLegacyFallback. Open in now refuses a twin instead of
launching it as this desktop's checkout; the others keep reading a twin as not server-owned. A row
whose only repo row is a server proxying its SSH target routes through that server. The
creation-action policy stays until browser creation folds with it. Deprecated importers 57 -> 53.
2026-10-18 18:09:07 -07:00
NeilandOliver Mee cf460aba5a fix(browser): switch client-hosted profiles by staging the replacement guest (#30452)
* fix(browser): switch client-hosted profiles with staged guest replacement

Squashed from #30351 by @oliver-mee onto the current main.

Fixes #30350

* fix(browser): fence staged profile guests, release switch grants, carry host profile metadata

- A fence during automation retirement now revokes and stops the staged guest.
- A failed or late-settling switch releases its execution-host grant so the page can be recreated.
- Staging cleanup failures before handover stay visible to executor close.
- createPage/restorePage carry host-owned profile metadata to profile-switch-v1 desktops,
  so recovery and restore can rebuild a switched page.

* fix(browser): roll back a switch whose replacement died, and report a lost old guest

* fix(browser): resolve client-hosted pages before the server-local target path when switching profiles

* fix(browser): a staged profile-switch guest owns downloads it starts while loading

* fix(browser): keep the original placement until a timed-out switch settles

* fix(browser): rebind the original guest before releasing a rolled-back staged guest

* test(browser): type the harness guest release mock

* fix(browser): release the replaced generation's download transfers when a switch commits

* fix(browser): admit file transfers from a staged profile-switch guest while its switch is pending

* fix(browser): release only the replaced generation's download transfers after a switch

* refactor(shared): import BrowserNetworkExecutionHost from its own module, no re-export

* fix(browser): adopt a profile switch the client finished after the caller timed out

---------

Co-authored-by: Oliver Mee <102673257+oliver-mee@users.noreply.github.com>
2026-10-18 17:48:57 -07:00
Neil a8e90ddc30 refactor(renderer): one owner adapter for the deprecated-resolver folds, pinned by an acceptance table (#30451)
* refactor(renderer): one owner adapter for the deprecated-resolver folds, pinned by an acceptance table

resolveOwnerWithLegacyFallback answers as resolveOwner for a named host and for rows that carry a
host stamp, and keeps the deprecated answer (single focused runtime included) for missing and
unstamped rows. A full-owner acceptance table pins all matrix cases and asserts the only changes
from the deprecated resolver are the designed ones: twins and contested SSH hosts become ambiguous,
and a stale hub stamp routes through the server that names the target.

* refactor(renderer): the fold adapter shares resolveOwner's named-host rule

Clean-code pass: one namedHostForOwnerRef for both resolvers, floating counts as host evidence,
and the acceptance test drops non-null assertions.
2026-10-18 17:36:53 -07:00
Kelvin Amoaba d4dfcf2694 fix(mobile): page GitHub tasks by number and drop pages from an old selection (#30140) 2026-10-19 00:23:10 +00:00
Jinwoo Hong 3ad036551e fix(native-chat): clicking Send or Stop keeps focus in the message box (#30456)
* fix(native-chat): clicking Send or Stop keeps focus in the composer

A click focused the Send/Stop button; when it disabled (empty draft after the
turn, or while a Stop is in flight) Chromium dropped focus to <body>. A pointer
press on it no longer moves focus, matching Enter. Adds an e2e regression with
a turn-capable Claude stub. Fixes STA-10897.

* Tighten comments
2026-10-18 20:21:59 -04:00
Jinwoo Hong 062e06d982 feat(sidebar): the owning host raises the workspace unread flag (dark) (#30450)
* feat(sidebar): the owning host raises the workspace unread flag (dark)

* fix(sidebar): phone session open clears and spares the flag; refresh views; blur rescan

* fix(sidebar): throttle failed view retries, resend failed empty views, hold presence one refresh longer

* test(sidebar): switch-on window writes neither the unread flag nor a stamp
2026-10-18 20:01:28 -04:00
Jinwoo Hong 04ce3d3891 fix(usage): price Claude models by exact version, add Sonnet 5.5, flag unpriced models (#30421)
normalizeModelForPricing was an ordered chain of regexes in which a new point
release fell through to its major: claude-sonnet-5-5 was billed as Sonnet 5
(cache reads at $0.20 instead of $0.10), and any future opus-5-x / fable-5-x
would be too. claude-haiku-5-5 matched nothing and silently added $0 to a
total that showed no caveat.

The resolver now parses family + major + optional minor (family-first and
legacy version-first IDs) and only accepts an exact MODEL_PRICING key, so an
unknown version is unpriced rather than borrowing another model's rates. The
deliberate opus-4.x -> 4.8 and sonnet-4.x -> 4.6 fallbacks go with it.

Adds Sonnet 5.5 and Mythos 5/5.1 rows. Haiku 5.5 stays unpriced: it reprices a
whole request above 100K prompt tokens, which daily totals cannot express.

The Claude summary now carries hasUnpricedModels (as Codex has since #22073),
and both panes share one unpricedCostLabel helper for the caveat.

Fixes #30338
2026-10-18 19:44:51 -04:00