Commit Graph
14808 Commits
Author SHA1 Message Date
Jinwoo Hong 6fcc183ab9 feat(sidebar): put main's sidebar inputs behind the ORCA_SIDEBAR_INPUTS dark build switch (#30050) 2026-10-17 04:06:43 -07:00
Jinwoo Hong 90c1c0ac50 feat(mobile): draw the host Accounts screen from the desktop's page (#30020)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test

* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch

The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): delete native screens the OTA shell no longer reaches

With every build the OTA shell, these native siblings run nowhere: the
native-executed closure (index.js plus every non-web route) does not reach
them, and the page resolves their .web siblings. Each .web sibling takes the
plain name, its web-overrides entry goes, and the native-only rich Markdown
editor and Mermaid WebView generators and their outputs go with them.

Kept: native siblings the node test suite still renders shared screens
through (terminal WebView, browser frame path, clipboard, dictation, media
picker, live input, snapshot budget, input styles).

Claude-Session: https://claude.ai/code/session_6cea92f6

* test(mobile): drop the phone-preview comparison the deleted native sibling held

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind

The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): one host for the rich editor, plain Text, and drop two unused native deps

MobileSelectableText was react-native's Text, so callers use Text. The rich
Markdown editor's host-facing seams (messages, URL prompt, keyboard inset) are
required now that the page is its only host; the WebView defaults go. Comments
that described deleted native components are rewritten, and
expo-image-manipulator and react-native-uitextview leave package.json and the
lockfile.

Claude-Session: https://claude.ai/code/session_6cea92f6

* feat(mobile): draw the host Accounts screen from the desktop's page

Accounts was the last host screen the shell still drew natively, so its
changes needed a phone build. It is now a page route like Tasks: the
screen moves to src/accounts/MobileAccountsScreen.tsx, accounts.web.tsx
renders it on the page, and accounts.tsx mounts the shell for it.

What React Native Web needed: Alert.alert is a no-op there, so the
reset confirmation, its outcome and a failed account switch are
ConfirmModal drawers (ConfirmModal can now drop Cancel for a notice);
Back goes through the route handoff, which pops the shell's stack; and
the reset idempotency key is built from getRandomBytes, since
randomUUID needs a secure context the iOS custom scheme is not.

edit.tsx edits the phone's own pairing and never renders on the page,
so the route manifest leaves it out and the page's host stack does not
declare it. The script sweep is re-measured for the 14-route tree.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(accounts): answer a same-offer Codex reset retry with its recorded outcome; mobile mirrors the desktop

The desktop keeps no client journal for a reset credit: the host's ledger
owns the attempt. A remote client that lost a reply and retried the same
offer under a new key was refused with "already attempted" (or "unknown
outcome" while the provider call was unresolved), which is why the phone
carried its own durable journal to replay the old key.

The coordinator now answers a request for an offer an earlier key already
claimed as that attempt: the settled outcome, the running attempt, or a
resume of the pending one under its original key. A different offer for
the account is still refused while an outcome is unknown, so nothing can
spend twice. The reply shape is the one a same-key replay always had, so
installed clients with their journal read it unchanged.

With that, the Accounts page sends a fresh key per attempt like the
desktop and keeps nothing on the phone: the AsyncStorage journal, its
client singleflight and the retry-record warnings are deleted, and the
journal-resume recording goes with them.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(accounts): resume an account's unresolved Codex reset when the retry names a newer offer

Review follow-ups:
- Host: after an unknown provider outcome the usage refresh moves the
  offer, so a retry names a newer one. The coordinator now answers it as
  the account's unresolved attempt (resumed under its own key and scope),
  as the desktop's consumeCurrent does, instead of refusing forever. One
  ledger lookup finds own key, offer claimant, then unresolved attempt.
- Mobile: a reply is matched on target, account and account revision,
  not the offer, so the resumed attempt's scope is accepted.
- The key is minted inside requestCodexResetCredit from randomV4Uuid, the
  session helper moved to src/platform/random-v4-uuid.ts.
- RefreshControl goes; the header refresh stays. ConfirmModal's
  onConfirm is optional for an OK-only notice.
- Stale journal comments and the mobile docs line are corrected.
- The edit.tsx page exclusion is dropped from this PR; the script sweep
  is re-measured for 15 routes.
- Root oxlint: escape the apostrophe; zod v4 strictObject.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): secure UUID fallback from expo-crypto; pin reset retry routing

- randomV4Uuid never falls back to Math.random: without crypto.randomUUID
  it builds the v4 UUID from ExpoCrypto.getRandomBytes, as the old reset
  key did. Tests get Node's secure bytes for expo-crypto from the setup
  file, since the native module has no Node build.
- Host tests: a new offer after a settled attempt spends again rather
  than replaying, and a retry naming a newer offer joins a resumed
  attempt in flight (one provider call, original key).

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* chore: restore main's bytes for files the merge's pre-commit format touched

The merge commit's lint-staged run reformatted 27 files this PR does not
change. Committed without the hook so they match origin/main exactly.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): stand in for expo-crypto with a Node alias, not a setup-file mock

Anti-slop forbids module mocking outside test files. The vitest config
now aliases expo-crypto to a faithful Node implementation of the names
shared code reaches; a test file's own mock still wins.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): match the bridged-parity comments to the measured 795-golden tally

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-17 03:11:24 -07:00
Jinwoo Hong 609f3425ad Record simulator tab opens and closes in main (dark, layout-owner build only) (#30034)
* feat(layout): record simulator tab opens and closes in main (dark, layout-owner build only)

* chore(layout): note why sleep/wake params copy OnTab
2026-10-17 02:48:46 -07:00
Jinwoo Hong c3f52428dd Write no false preview flag on a new or promoted tab (dark, layout-owner build only) (#30041)
Switch-on builds only; switch-off behaviour is unchanged.

Main stores a tab's preview flag only while it is true. A non-preview
editor open created its tab-bar entry with `isPreview: false`, and making
a preview permanent by an edit, by "keep open" or by pinning wrote the
same, so the window's next save changed that tab in main's model (the
save-divergence check's `tab <id> changed: isPreview` after a CLI file
open). New tabs now carry the flag only when true, and those writers drop
it, by the rule E2-4 gave activation, now in one place (tab-preview-flag).

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125
2026-10-17 02:38:38 -07:00
Brennan Bensonandm4air 3d2e0bd26d fix(native-chat): a running Claude chat keeps its model when two Claude Code versions list differently (#30022)
* fix(native-chat): a running Claude chat keeps its model when two Claude Code versions list differently

A send's stand-in for a model the list lacks took its rows and default from the
chat's own answer but kept only rows the account's saved list names, and every
running chat rewrote that list whatever CLI version it ran. After Claude Code
2.1.280 -> 2.1.302 the filter dropped the default row and sends moved to Sonnet.

- The options answer names the ids its agent listed (listedModelIds); a live
  catalog is verified only by its own listing. An older host's answer may move
  only to the chat's own default row, and only where the host's list names it.
- Only a session's start (and the probe) writes the account's list; later reads
  no longer do, so an older running CLI can't replace a newer start's list.

Tracked in STA-10947.

* fix: keep running chat model selection within its own listing

Preserve learned model menus on later reads and check launch-added model membership before saving.

Tracked in STA-10947.

* test: validate ACP menu regression fixtures

Use the protocol schema for config options and supply the catalog row default flag.

Tracked in STA-10947.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-17 02:38:19 -07:00
Jinwoo Hong 86f6258d54 refactor(mobile): delete native screens the OTA shell no longer reaches (#29979)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test

* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch

The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): delete native screens the OTA shell no longer reaches

With every build the OTA shell, these native siblings run nowhere: the
native-executed closure (index.js plus every non-web route) does not reach
them, and the page resolves their .web siblings. Each .web sibling takes the
plain name, its web-overrides entry goes, and the native-only rich Markdown
editor and Mermaid WebView generators and their outputs go with them.

Kept: native siblings the node test suite still renders shared screens
through (terminal WebView, browser frame path, clipboard, dictation, media
picker, live input, snapshot budget, input styles).

Claude-Session: https://claude.ai/code/session_6cea92f6

* test(mobile): drop the phone-preview comparison the deleted native sibling held

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind

The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): one host for the rich editor, plain Text, and drop two unused native deps

MobileSelectableText was react-native's Text, so callers use Text. The rich
Markdown editor's host-facing seams (messages, URL prompt, keyboard inset) are
required now that the page is its only host; the WebView defaults go. Comments
that described deleted native components are rewritten, and
expo-image-manipulator and react-native-uitextview leave package.json and the
lockfile.

Claude-Session: https://claude.ai/code/session_6cea92f6
2026-10-17 02:23:51 -07:00
Jinwoo Hong 4f5d464ca6 Open into the window's emptied pane beside a split branch in one layout command (dark, layout-owner build only) (#30039)
* Open into the window's emptied root beside a split branch in one layout command (dark, layout-owner build only)

A tab opened into a group the window kept empty reaches main with the
split that places it, now also when the group's neighbour is itself split
(named by the branch's first and last groups) or is another empty group
main lacks. Main makes the group with its tab beside that branch. Switch-off
builds send no such command.

* Comment the refusal of a branch main's tree does not hold (dark)
2026-10-17 02:15:39 -07:00
Neil 7ad3d49e89 fix(mobile): allow Markdown refresh during tab snapshots (#30040) 2026-10-17 02:15:13 -07:00
Jinwoo Hong 37f23b9445 refactor(mobile): every build is the OTA shell; drop the native/OTA build switch (#29969)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test

* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch

The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind

The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.

Claude-Session: https://claude.ai/code/session_6cea92f6
2026-10-17 02:12:06 -07:00
Jinwoo HongandClaude 345ef81bfa feat(accounts): offer to restart running Claude sessions on the new account after a switch (#29898)
* refactor(terminal): let account-switch pane restarts name their agent

The queued account-switch restart only knew how to relaunch Codex. The
queue now records which agent each pane restarts as, and the resume
startup builder takes that agent, so the same mounted and detached
restart paths can move Claude sessions too. Codex behaviour is unchanged.

* feat(accounts): offer to move running Claude sessions after an account switch

Switching the selected Claude account only reached Claudes started later,
so users had to /exit and resume every running session by hand. After a
switch from the status bar or Settings, Orca now finds local terminal
panes running a resumable Claude session in the switched lane and shows
one toast offering to move them. Confirming restarts idle sessions with
claude --resume <id> in place; sessions mid-reply move when their turn
ends, and are dropped if the pane closes or the user switches again.

* fix(accounts): wait out permission prompts and keep hand-started Claude args

A Claude session at a permission prompt is mid-turn, so a move now waits
for it like a session that is replying. Resuming a Claude pane with no
recorded launch config no longer adds the user's default agent arguments
or environment, so a hand-started claude is not resumed with a
permission bypass. Codex keeps its existing fallback.

* feat(accounts): track which Claude account each terminal pane runs on

Orca had no record of the account a running Claude used, so an account
switch could only assume every session was on the old one. A renderer
tracker now attributes sessions that start after launch to their lane's
selected account and records the account Orca restarts a session on.
Sessions it cannot attribute stay unknown and keep today's behaviour.

* feat(accounts): ask in a dialog before restarting Claude sessions on a switch

Replaces the move toast. After a Claude account switch, a dialog names the
new account and how many sessions are still on the old one, with Keep as
is and Restart, plus Remember my choice. Closing or Escape keeps sessions.
The remembered choice lives in Settings > Accounts as When you switch
accounts while Claude is running: Ask, Always restart, Never restart.

* refactor(accounts): read Claude session accounts from their transcripts and use one restart queue

Which account a running Claude uses now comes from the transcript path its
hooks already report (<CLAUDE_CONFIG_DIR>/projects/...), so the renderer
tracker and its guesses are gone and unknown sessions are never counted.
Restarts queue straight into the store's pending map through one
queuePaneAccountRestarts action; the pane effect and the detached
scheduler hold a Claude restart while its turn runs and skip it when the
session can no longer be resumed. Codex behaviour is unchanged.

* style(accounts): left-align Keep as is in the Claude account switch dialog

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

* copy(accounts): say busy rather than replying, and drop the menu's switching note

The deferred restart waits on any working or blocked turn, not only a reply. The
switch dialog now covers running sessions, so the menu footer note is redundant.

* copy(accounts): say busy sessions wait for idle before restarting

* test(accounts): type the switch-session store stub without broad object

* fix(accounts): keep a queued Claude restart when Codex restart notices are cleared

Codex notice cleanup shares the pending-restart map with Claude switch restarts;
it now drops only Codex entries.

* fix(accounts): wait out Claude questions and keep accepted restarts across switches

- A Claude permission prompt or question reports 'waiting'; a queued restart now
  waits for 'done' instead of only 'working'/'blocked', so it no longer drops them.
- A later switch keeps accepted restarts still waiting on a turn (they relaunch on
  whatever is selected) and drops only those whose session is already on the new
  account, scoped to the switch's lane. The blanket replaceQueued option is gone.
- A server account switch returns before touching the local dialog or queue.
- Pane restart state reads through one selector to stay inside the subscription
  budget, and the focus-read baseline follows the restart-startup rename.

* test(accounts): feed the account-restart e2e fixture the shared restart queue

* feat(accounts): make the switch dialog's checkbox one-way, like native chat's resume prompt

"Don't ask again (restart automatically)" turns on automatic restarts however the
dialog is left, including Escape; Settings → AI Provider Accounts turns it off.

* feat(accounts): choose which Claude sessions restart after an account switch

With more than one session, the switch dialog lists them by workspace using the
native-chat resume tree rows, all ticked; Restart covers only the ticked ones.

* Merge origin/main into claude-sign-in and rename the host-twin teardown's restart queue

* fix(accounts): localize the session tree's agent label

* fix(accounts): restart a pane on the PTY it has when the dialog is answered

A reattach can rebind a pane while the dialog is open and a closed pane has no
PTY, so the ids it listed may name nothing.

* fix(accounts): read a layout's pane PTYs optionally

* fix(accounts): offer to move sessions of a saved account that runs on System default

A saved account with no login of its own runs on System default, so its sessions'
transcripts read as System default. The switch now compares where launches run
(runsOnSystemDefault on the account summary) and asks nothing when that is unchanged.

* refactor(accounts): give the Claude account switch one boundary and key the whole prompt

The post-switch coordinator now takes the before/after account snapshots and
derives launch folders and labels itself, so the status bar and Settings stop
assembling the same request. The status bar's settings projection reuses the
host's row, which keeps runsOnSystemDefault: a covered account switched from
the status bar now finds its System default sessions. Never restart returns
before any session scan. The outcome collapses to show-confirmation | handled.
The dialog keys one component that owns the whole Dialog with a plain boolean.

* refactor(terminal): subscribe mounted panes to this tab's ready account restarts

Replaces claudeTurnStatus, the whole agent-status map passed through the
controller as a re-run signal, with a shallow selector of the PTY ids whose
queued restart can run now. Other tabs' status updates no longer re-render or
re-run every mounted pane once a Claude restart is queued. The effect re-checks
readiness against the current store before consuming. The detached Claude test
now runs through the scheduler's real subscription instead of manual sweeps.

* docs(accounts): shorten Claude account-move comments to their whys

* test(terminal): give the account-restart e2e a callable store and a queued restart

The mounted pane now reads its ready restarts from the store through useShallow.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 02:11:10 -07:00
Jinwoo Hong b2419a58ad fix(mobile): show the update screen instead of native screens when the desktop has no page (#29956)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test
2026-10-17 02:01:20 -07:00
Jinwoo HongandClaude 30e3119fc7 fix(accounts): follow an Orca Claude account switch when the shell sets CLAUDE_CONFIG_DIR (#29890)
* fix(accounts): follow an Orca Claude account switch when the shell sets CLAUDE_CONFIG_DIR

A user whose shell config or environment sets CLAUDE_CONFIG_DIR switched Claude accounts in Orca,
but `claude` relaunched in an open terminal kept running the old account: the terminal's claude
function treated any CLAUDE_CONFIG_DIR it did not inject as a deliberate per-terminal override.

Host terminals now always carry the user's own folder (ORCA_CLAUDE_USER_CONFIG_DIR, from the login
shell's env), also on System default and before that env arrives. The claude function (POSIX, fish,
PowerShell) treats a CLAUDE_CONFIG_DIR equal to it, ignoring a trailing separator, as System
default's folder and follows the selected account; any other value set in the terminal still wins.
WSL panes carry no user folder, so they behave as before.

The terminal daemon protocol moves to 49: hourly builds already run v48 daemons whose wrapper has
the old function. v48 stays attachable.

* fix(accounts): match the user's Claude folder on PowerShell whatever its separators

Orca records the user's CLAUDE_CONFIG_DIR resolved (backslashes on Windows), so a
pane value written with forward slashes read as a deliberate override and stopped
following the account switch.

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

* fix(claude): pass the user's own CLAUDE_CONFIG_DIR to terminals exactly as set

Claude keys its login by the literal folder string, so a resolved spelling (a
dropped trailing slash) could switch System default to another saved login, and
Git Bash on Windows compared a backslash spelling against its rc's forward one.

* refactor(claude): one reader decides whose CLAUDE_CONFIG_DIR it is; one base pane env

The resolved reader now derives from the as-set one, every host pane env builds on
one base patch, and the router tests share one shell probe.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 01:54:24 -07:00
Jinwoo HongandClaude c3e3b5241a Record the chat pane the window picks in main (dark, layout-owner build only) (#30036)
In a layout-owner build the window's chat-pane pick (chatLeafId) reached
main only through its session save, so a frame could undo it and the save
changed main's layout. The window now records it with main's existing
setChatPane command. Codex restart and background-agent pane trees are
already written by main; tests pin that.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 01:50:41 -07:00
Neil 6743353116 fix(remote): make the host's tab name the one source for CLI and desktop renames (#30037)
A desktop connected to a remote Orca server kept its own copy of a tab's
manual name, and ranked its saved-session title (a resumed Codex chat)
above the live title the server publishes. So orca terminal rename on the
server never showed on a resumed tab, and a desktop rename never reached
the server.

The server now publishes the tab's manual name as an optional customTitle
on each terminal surface, and the desktop mirror adopts it. Desktop renames
go to the server: terminal.rename by handle, plus session.tabs.setTabProps
customTitle by tab id for panes that are asleep or starting. Older servers
omit customTitle and strip the new param, so the desktop keeps its own
name with them, as before.

Fixes #29975
2026-10-17 01:44:01 -07:00
zhangxaochenandNeil 92999b0e5f fix(mobile): prevent markdown and file tab reload and screen flicker on retap and revisit (#30033)
* fix(mobile): prevent markdown and file tab reload and screen flicker on retap and revisit

* fix(mobile): preserve newer saves and refresh document metadata

* test(mobile): supply current document state in recorded reads

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-17 01:35:34 -07:00
Jinwoo HongandClaude e6b7c77e1a feat(sidebar): main produces per-workspace PR facts, checked against the window in dev and test (#30026)
* feat(sidebar): main produces per-workspace PR facts, checked against the window in dev and test

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

* refactor(sidebar): share the PR cache key candidates instead of splitting an import

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 01:27:27 -07:00
NeilandClaude 0ba5cccf9d fix(mobile): keep a headless host's chat tab through snapshot rebuilds (#30028)
On a host with no window, chat tabs live only in the runtime's session snapshot. A forced rebuild from the saved session (closing a saved editor tab, an expired SSH reattach) dropped them, and every terminal-only group merge removed them from the tab order.

Refs #29968

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 01:23:10 -07:00
NeilandClaude 7adcefb078 fix(ai-vault): hide Orca's chat-naming requests from Session History (#30029)
Naming a chat runs a one-shot agent CLI request in the chat's folder, and the CLI saves it beside the user's chats. Session History listed it, and opening it from a phone resumed the naming request instead of the chat.

Fixes #29973

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 01:22:15 -07:00
Brennan Benson ee4fd06593 fix(mobile): the chat's Working line uses desktop's spinner (#30008)
* fix(mobile): the chat's Working line uses desktop's spinner (turning circle, hourglass under reduced motion)

* fix(mobile): keep working animations off interaction queue
2026-10-17 01:17:28 -07:00
Brennan Benson a5b96a1f8d feat(acp): an agent can run each chat's own permission mode itself (#28455)
* feat(acp): an agent can enforce the permission mode itself through a session config option

An optional launch-spec field names the session config option and its ask/full values. Every new or
reopened session gets the value for the chat's full-access posture before it is exposed, and a refusal
fails the start. With the field present Orca no longer answers permission requests for the person.
No agent sets it yet.

* fix(acp): reject unconfirmed permission mode during acquisition

* feat(acp): an agent enforcing the permission mode itself runs the chat's own mode

The launch spec maps each chat permission mode to its session option value; a start sets the
value for the mode the host launches with (ask when it names none), replacing the global Agent
Permissions full-access read. A mid-chat change takes effect through the host's existing restart
of a child launched with another mode before the next send.

* Keep ACP session acquisition within the line limit

* fix(acp): a chat's permission mode goes to the agent's mode-category option, whatever the agent names it
2026-10-17 01:16:57 -07:00
Jinwoo Hong 03cefcb84c feat(sidebar): main keeps one live set of paired-server rows, checked against the window in dev and test (#30027)
* feat(sidebar): main keeps one live set of paired-server rows, checked against the window in dev and test

* fix(sidebar): settle review findings; main also publishes paired-server host facts
2026-10-17 01:13:34 -07:00
NeilandClaude 98d8257b02 fix(ssh): a password SSH host waits for you at launch instead of prompting (#30025)
* fix(ssh): a host that needs a password waits for the user at launch instead of prompting

Restores the persisted 'last sign-in needed a typed credential' flag that
the relay removal (#27214) stopped writing, and at launch marks those hosts
as awaiting sign-in so background reconnects (startup restore, managed-server
status checks and tunnels) refuse without dialing instead of opening the
password dialog. The user's own Connect lifts it and prompts. The terminal
toast shows the refusal as an SSH state, not a bug to report.

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

* test(ssh): give the raw-connected fixture the cache query; annotate the harness-shaped registration

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

* refactor(terminal): keep the sign-in branch out of the toast component

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 01:10:48 -07:00
Brennan Benson 38131048b1 feat(acp): a sign-in prompt on an ACP agent's error output ends the start or turn signed out (#28467)
* feat(acp): a sign-in prompt on an ACP agent's stderr ends the start or turn signed out

An agent that waits for a browser sign-in prints a link on stderr and blocks for
minutes. A launch spec can now name that line (signInPrompt); the connection then
closes with a typed sign-in failure through the existing process owner, so a start
reads notSignedIn and a running turn fails signed out instead of settling unknown.
The matched line, which carries the sign-in URL, never enters the retained stderr
tail, errors or diagnostics.

* Prevent sign-in URLs leaking from long stderr lines

* fix(acp): a stderr line cut at the cap that is not a recognised prompt is dropped, so its URL never reaches the kept tail
2026-10-17 00:59:44 -07:00
Jinwoo Hong fa962dd1cd Record chat tab opens and closes in main (dark, layout-owner build only) (#30019)
* Record chat tab opens, closes and host-listed chats in main (dark, layout-owner build only)

A chat tab the window opens, closes, or adds and removes from a host
listing of this machine's chats now reaches main as openAgentSessionTab,
closeTabs or setTabProps under the window's ids, so the window's save no
longer teaches main a chat tab. openAgentSessionTab takes the window's
place, creation time and new split group, and the same open again
changes nothing. Switch-off builds are unchanged.

* test(layout): a paired server's chat tabs send nothing to main (dark)

* fix(layout): keep a windowless host's chat-tab saves as they were; send synced chat opens in tab order (dark)

Only a window's openAgentSessionTab (main marks it drawn on layout.apply)
takes the new rules: a repeat changes nothing, a taken id is refused, and
a workspace with no group takes the window's. A windowless host's own
saves mint a fresh group and keep a chat whose id another tab holds, as
before. A host listing's new chats are recorded in tab order, so each
open's anchor already reached main.

* refactor(layout): one rule set for a chat tab open; the windowless caller names only a group it holds (dark)

Drops the per-sender drawn flag. The windowless save passes its
remembered active group only when the workspace still holds it, so it
mints a fresh group as before. Refusing a taken chat tab id is gated on
the build switch: shipped builds still save the duplicate id.
2026-10-17 00:57:12 -07:00
Jinwoo Hong 6c7ed18e20 feat(sidebar): main publishes desktop focus, pins, project and host order, and SSH/WSL host state, checked against the window in dev and test (#30018)
* feat(sidebar): main publishes desktop focus, pins, project and host order, and SSH/WSL host state, checked against the window in dev and test

* fix(sidebar): read pins through the host-filtered metadata helper; name pins as workspaces
2026-10-17 00:51:57 -07:00
Brennan Benson 251516de60 feat(native-chat): choose which agents open as a chat (#29071)
* 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

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

* test(chat): use checked native inventory fixtures

* Fix launch preparation lifetime and execution host startup

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

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

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

* 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.

* fix: preserve provider choice and cached native inventory
2026-10-17 00:51:43 -07:00
Neil 8f2810ab23 fix(ssh): link a folder workspace the server kept when a re-created SSH host moves again (#27285) (#30017)
* fix(ssh): link a folder workspace the server kept from an earlier move instead of conflicting (#27285)

* fix(ssh): resolve a server folder's host before linking it, and read folder lease owners

* fix(ssh): name a linked folder workspace in the connect's left-behind warning
2026-10-17 00:50:27 -07:00
Neil 824ca9545f fix(ssh): a host converted to its managed server keeps its workspaces' visit recency (#30015)
Converting an SSH host drops its relay-era rows, but recency stayed under
ssh:<target>|<id> keys, which the next startup prune discards because no row
matches them any more. Re-key that host's visits to runtime:<env>|<id> in the
same update, keeping the newer visit on a collision.
2026-10-17 00:42:34 -07:00
Brennan Benson 0f9c12db17 Chat: pasting a copied file attaches it, like dropping it (#29089)
* fix(native-chat): paste a copied file into Chat the way dropping it attaches it

Copying an image file in Finder and pasting into Chat typed the file name
and attached nothing: the paste only attached a bitmap from the clipboard.
Copied files now go through the composer's drop route, on both the
app-menu paste and the paste event.

Fixes STA-10652

* refactor(native-chat): share the paste guards with the copied-files branch

* test(native-chat): late paste results stay dropped when the image waits for the clipboard read

* Keep copied attachments pending through preparation and composition

* Update file drop fixture settlement results

* Keep pending attachment capability current within its draft scope

* fix(native-chat): reserve pending chips before checking dropped files

A local file drop checked each file before adding it, with nothing in the
composer meanwhile: Send during the check went without the file, which then
landed in the next draft; unmounting mid-check lost it; and files queued
behind an input-method composition were lost on unmount.

Drops now reserve a pending chip per file before the check, so Send waits
as it already does for server-upload drops. The prepared attachment travels
through the existing composition queue as one operation, settles into its
draft scope after unmount, releases its chips when refused or disabled, and
reads the scope's current image capability when it applies.

* test(native-chat): server-upload chip calls carry no options without paste

* refactor(native-chat): move the composer's image chip list into its own hook

Main's server-owned paste pushed the attachments hook past the file length limit.

* fix(native-chat): a screenshot picture pastes as a picture on macOS, even beside its saved file

macOS lists a screenshot tool's saved file beside its picture, so the paste read as a file copy and attached nothing once the file was gone. Only a Finder copy also names its files as text.

* fix(native-chat): a macOS right-click paste lets the clipboard tell Finder files from a screenshot

The paste event hides Finder's file names, so a path-backed picture reserves its chip at once and the native clipboard decides: named files go to the drop route, a screenshot's picture is saved from the clipboard. Both paste routes now share the clipboard-picture save.

* test(native-chat): move the paste hook's test probe into its own fixtures file

The paste tests outgrew the test file length limit.

* test(native-chat): hand the paste probe's hook result out after render

* test(native-chat): give the remote image paste probe the copied-files callback

* fix(native-chat): a pending attachment chip shows a spinner and its name, not a stacked icon

A local drop now waits as a pending chip, which drew the kind icon, the file name and a centered spinner on top of each other. Without a thumbnail the chip shows the spinner above the name; with one, the dimmed picture keeps a spinner strip along its bottom.

* test(native-chat): a pending chip shows its name and spinner, no kind icon

* fix(native-chat): a macOS file paste keeps its own chips until the clipboard decides

A macOS paste of Finder files reserved one placeholder chip, asked the native
clipboard whether it held files or a screenshot picture, and then started a
separate attach for the files. Removing the placeholder did not cancel that
attach, and leaving the chat before the answer lost the paste.

The paste now reserves one chip per copied file before asking, and captures
the drop route's intake (owner, destination, chips) at the same moment. Files
continue those exact chips through the existing local check or server upload,
so removal cancels by the original chip and an unmounted composer still
finishes into its draft. A picture keeps the first chip and drops the rest.
Ordinary drops and the picker keep their fresh-admission checks.

* test(native-chat): a macOS file paste survives removal, unmount and disable through the real intake

Drives the real pending cache, composer attachments, external intake and paste
hook with a delayed clipboard answer: removed chips never attach (before or
after the answer, through input-method composition), an unmount finishes both
the local and paired-server routes into the original draft with the original
chip IDs, and disabling the composer meanwhile does not bring the paste back.
2026-10-17 00:38:42 -07:00
Neil ee56703d51 chore(ci): ratchet importers of the deprecated runtime-owner resolvers (#30002)
* chore(ci): ratchet renderer importers of the deprecated runtime-owner resolvers

getRuntimeEnvironmentIdForWorktree and resolveRuntimeOwnerForWorktree read a
worktree two hosts publish as not-a-runtime, which callers treat as local. The
62 renderer files importing them are grandfathered; the list may only shrink.

* ci: register the deprecated runtime-owner ratchet step in the preflight phase map
2026-10-17 00:38:41 -07:00
Neil 83baa08352 fix(cli): explain why file open fails on a server with no app window (#30014)
On a headless server (orca serve, orcad) files.open and files.openDiff
answer with the bare renderer_unavailable token, which the CLI printed
as-is. Translate it in the CLI into a clear message with next steps,
keeping a stable renderer_unavailable code. The wire answer is
unchanged because the mobile app keys its preview fallback on it.

Fixes #29822
2026-10-17 00:37:16 -07:00
Neil e1a7c9725d fix(cli): stop suggesting --host to listings that reject it (#30012)
The hostScope annotation on terminal list, worktree list and worktree ps
named --host local / --host ssh:<id> as the follow-up for an omitted host,
but none of those commands accept --host. Those hosts are reached through
the runtime that answered, so no flag selects them: report a null selector
and say so in the human scope line. Paired servers keep --environment.

Fixes #29901
2026-10-17 00:27:55 -07:00
Jinwoo Hong 95580d9dd8 test(e2e): open and close structured chat tabs with an isolated Claude stand-in (#30011)
* test(e2e): open and close structured chat tabs through an isolated Claude stub

* test(e2e): satisfy changed-code lint in the structured chat stub
2026-10-17 00:25:36 -07:00
Neil 57e8c604f9 fix(renderer): resolveOwner refuses folder and project-group twins instead of guessing this computer (#30009)
* fix(renderer): resolveOwner keeps folder and project-group twins ambiguous; deprecate the runtime-owner aliases

* test(store): folder and project-group twins in the twin suite; no-host folder delete refuses

* fix review: a folder's own SSH connection names its owner despite twin project groups
2026-10-17 00:22:15 -07:00
Neil 29fa3cf9fb fix(terminal): quit no longer warns about terminals a paired Orca server keeps running (#30006)
A paired runtime (orca serve, managed SSH/WSL orcad) owns its terminals and keeps
them after this client quits, so only SSH relay terminals, which a relay kills
after its grace period, still raise the quit warning. Window close is unchanged.

Fixes #29916
2026-10-17 00:16:24 -07:00
Brennan Benson 035588e914 test(cross-version): read isStructuredTab from the shared module the newest release defines it in (#30000) 2026-10-17 00:11:43 -07:00
Jinwoo Hong 5733887967 test(layout-owner): prove main and the window agree on a worktree identity rekey (#30005)
Main's Store.migrateWorktreeIdentity re-keys every stored layout field before it sends the
rename event the window re-keys on, so the window's save only mirrors it. The new oracle checks
main's migrated layout model equals the model of the window's post-rekey save and the model's
identityRenamed transition; the first-work test pins migrate-before-notify.
2026-10-17 00:10:27 -07:00
Brennan Benson f4aa82874b fix(mobile): show the same Working line in terminal-backed chats and drop the floating 'Agent is working' pill (#29972) 2026-10-17 00:09:10 -07:00
Brennan Benson ac34ffaa81 fix(toasts): stop the notice stack flickering under a resting mouse, and fit the force-delete button to its notice (#29878)
* fix(sidebar): let kept-branch toast bodies fill the toast width

* fix(sidebar): close the force-delete menu when its toast is hidden in the stack

* fix(sidebar): keep focus out of a hidden toast after its menu auto-closes

When the toast stack hid a kept-branch or delete-failure toast, the menu
closed but Radix returned focus to the chevron inside the now invisible
toast, so Enter or ArrowDown reopened the menu floating over the app with
the "Always ..." item focused.

- Skip the close focus return while the toast is hidden.
- Refuse to open the menu while its toast is hidden.
- Treat toasts past Sonner's visible-toast limit (data-visible=false) as
  hidden too.

* fix(toasts): stop a folded toast's hidden overflow from re-expanding the stack

* fix(toasts): clip a folded toast to its card instead of disabling its content

The pointer-events rule left the unstyled star-nag card, which Sonner never
fades, visible below the stack but dead to the pointer. Clipping the folded
toast removes the overhang for every toast type, visible or not, so nothing
of a folded toast can sit outside the stack and re-expand it.
2026-10-17 00:08:26 -07:00
Brennan Benson 32a53d3ec6 feat(native-chat): image messages wait in the queue like text (#28655)
* feat(native-chat): image messages wait in the queue like text

A message with images now queues behind the running turn and the cards
ahead of it, instead of skipping the queue. Cards show an image icon and
count, an image-only card names its image, and editing a card's text keeps
its images. The host publishes no card holding an image to a client that
has not advertised agent-session.queued-image-cards.v1, since released
clients edit by copying a card's text and deleting the card.

* test(native-chat): an image follow-up queues like text

* fix(native-chat): project queued images before phone frame limits

* fix(native-chat): preserve image sends from older clients

* Clarify direct text recovery at send admission

* fix(native-chat): expose queued image names to keyboard users
2026-10-17 00:07:29 -07:00
Brennan Benson 58f1f7de1e fix(mobile): edit queued messages in place (#28646)
* fix(mobile): edit queued messages in place

* fix(mobile): use the platform input font size for queued edits

* fix(mobile): handle missing host support while probing queued edits

* Keep phone queued edits owned through save settlement

* Keep phone queued edits local to mounted chat

* fix(mobile): retire queued editor when its card disappears

* fix(mobile): retain queued edits while reconnecting

* test(mobile): await reconnect timer inside act callback

* fix(mobile): publish queued edit transport after commit

* test(mobile): await queued Edit inside act callbacks

* fix(mobile): hand a stale queued Save to the composer and reopen on the newer text

* fix(mobile): close an unchanged queued Save locally and keep stale typing in its own chat

* fix(mobile): explain disconnected queued edit refusal

* fix(mobile): combine queued session contract imports
2026-10-17 00:06:07 -07:00
Brennan Benson 8a4c031d8e fix(native-chat): a queue action still on its way greys its controls, like the phone (#29120)
* feat(native-chat): one queue header that becomes the pause row; no composer Resume or send dialog

The queued cards now sit under one header row on desktop and phone: "Queued"
and the count normally, and "Paused" with the count, Clear all and Resume
while the host holds the queue after a Stop. A chevron folds only the cards;
the choice is local to the view and resets when the queue empties.

The composer's Send no longer turns into a second Resume, and a message sent
into a paused queue no longer asks "Send message?": it sends, its accepted
turn lifts the pause, and the cards follow it. Clear all deletes every card
through the existing per-card delete. No wire or host change.

* test(mobile): find the background-tasks strip's toggle inside the strip, not the queue header's

* Make queued Clear all atomic across clients

* Show queued message failures in collapsed headers

* Use per-card delete for paused queue Clear all

* Join repeated paused queue Clear actions

* fix(native-chat): a queue action still on its way greys its controls, like the phone

The desktop kept in-flight queue actions only in refs (one per card, one for
Resume, a joined promise for Clear all), so on a slow or remote host pressing
Clear all, Delete, Steer or Send changed nothing on screen until the host
answered. One keyed in-flight set, held as a ref and as render state, now
covers cards, Resume and Clear all; Clear marks every card it deletes.

* fix(native-chat): Resume and Clear all refuse each other in the queue guard; the steer chord skips a card already on its way

Resume and Clear all now hold one shared in-flight key, so each refuses the
other in the guard rather than only in the drawn state, and both header
buttons grey on it. Cmd/Ctrl+Enter returns false when the newest card's
action (or a Clear deleting it) is still out, so the chord no longer
scrolls to the latest with nothing sent. Test controllers gain the new
fields.

* test(native-chat): move the slow Clear all test into its own file

The queued-list test file passed its line limit; the in-flight test and the
held-write render it needs now live beside it, and the list test file is
back to the base's helpers.

* fix(native-chat): Clear all waits out a card's own action instead of skipping the card

Clear all now greys while any card in the queue has its own Steer or Delete
on its way, and once that settles it claims every card. Before, it stayed
enabled and quietly left the busy cards out, so a failed Delete left a card
behind after Clear all, and with every card busy the click did nothing.

* chore(native-chat): one comment for Clear all's availability rule

* test(native-chat): update held-start queue fixture after main merge
2026-10-17 00:04:37 -07:00
Brennan Benson ed3c3cc41f Chat: dropped files hold Send until they're attached (#29173)
* fix(native-chat): reserve pending chips before checking dropped files

A local file drop checked each file before adding it, with nothing in the
composer meanwhile: Send during the check went without the file, which then
landed in the next draft; unmounting mid-check lost it; and files queued
behind an input-method composition were lost on unmount.

Drops now reserve a pending chip per file before the check, so Send waits
as it already does for server-upload drops. The prepared attachment travels
through the existing composition queue as one operation, settles into its
draft scope after unmount, releases its chips when refused or disabled, and
reads the scope's current image capability when it applies.

* test(native-chat): server-upload chip calls carry no options without paste

* refactor(native-chat): move the composer's image chip list into its own hook

Main's server-owned paste pushed the attachments hook past the file length limit.

* fix(native-chat): a pending attachment chip shows a spinner and its name, not a stacked icon

A local drop now waits as a pending chip, which drew the kind icon, the file name and a centered spinner on top of each other. Without a thumbnail the chip shows the spinner above the name; with one, the dimmed picture keeps a spinner strip along its bottom.

* test(native-chat): a pending chip shows its name and spinner, no kind icon
2026-10-17 00:01:27 -07:00
Jinjing 05086a892e Let Cmd+J open hosting-provider domains such as github.io (#29989)
Typed hosts like github.io, vercel.app, or readthedocs.io were treated as bare public suffixes and sent to search. They are registrable domains, so they now navigate directly.

- Suffix check now uses ICANN-only rules. The private-section list made `foo.github.io` a domain, but it also let the bare suffix pass, so the classifier now treats the hosting-provider suffix itself as a navigable domain.
- Website navigation now outranks a name match only when the query equals a worktree, tab, or page name in full. Partial matches such as "project.dev server" no longer block the "Go to" row.
- Browser tab creation from a URL forwards the target group as `clientTargetGroupId`, so older hosts that publish the page before the create response still place the tab in the invoking split.
2026-10-16 23:46:30 -07:00
Jinwoo Hong 77c511b316 feat(sidebar): main produces per-workspace activity facts, checked against the window in dev and test (#29980)
* feat(sidebar): main produces per-workspace activity facts in shadow mode

* perf(sidebar): agent pings keep last-read surfaces; shadow counts comparisons

* fix(sidebar): settle review findings in the activity-facts producer and shadow check

* fix(sidebar): refresh on inventory verdict clears; resync agents only when tabs move

* fix(sidebar): scope the shadow comparison by each row's owner, not the focused host

* refactor(sidebar): name the shadow scope after what it keeps

* fix(sidebar): classify sidebarInputs as host-owned in the operation ownership census
2026-10-16 23:40:35 -07:00
Brennan Benson 88f27711fd feat(native-chat): a chat whose host has no program to run shows Install in place of its composer (#29051)
* feat(native-chat): a chat whose host has no program to run shows Install in place of its composer

The chat's one input slot is chosen in one place: a final read refusal, then an owned prompt card,
then Install while the host has nothing runnable (its install included), then the composer. Until
the first status answer the slot stays empty instead of flashing either. Send and slash commands are
refused while Install holds the slot, so nothing hidden reaches the host. The draft stays in its
store and is there when the composer returns. A failed status read leaves the composer; the host
remains the final refusal.

* fix(native-chat): keep the composer slot reserved until the host's agent list says, and say nothing about sending while Install shows

- While the host's capabilities or agent list are still loading for a chat's agent, the slot stays
  quiet instead of flashing the composer.
- While the install row holds the composer's place, a failed start's notice ("Send your message
  to try again", Retry) is left out: nothing starts until the install lands.
- The chat install card's title is semibold like the other cards in that slot.

* fix(native-chat): keep the chat session view within its line limit after the install slot

* fix(native-chat): the composer stays while the host's first install answer is out, its sends held, so no agent's chat loses its input box

* fix(native-chat): the install slot holds Send through the composer's existing held-send reason instead of a flag of its own

* fix(native-chat): the install slot hands the chat its held-Send reason, keeping the chat session view under its line limit
2026-10-16 23:32:04 -07:00
Neil 7bb83b47c8 test(store): host-twin fixture and destructive/selection twin isolation suite (#29991)
Seeds local+ssh, local+runtime and folder twins (one id on two hosts) with live
shells, a dormant tab, dirty docs, drafts, sleeping-agent records and module-level
stores, then runs a hand-declared action table (remove, folder delete, purge,
kill, sleep, select) on one twin and asserts the other twin is untouched.
Today's leaks are listed cell by cell and asserted to still leak, so a fix
turns them red until the entry is removed.
2026-10-16 23:26:46 -07:00
Jinwoo Hong a349bfa6cc Agree with main on first-group ids, tab order and the preview flag (dark, layout-owner build only) (#29987)
* Agree with main on a workspace's first group and the tab order, and stop writing a preview flag main never stores (dark, layout-owner build only)

Switch-on builds only; switch-off behaviour is unchanged.

Group ids: a window save made before main's frame with a workspace's first
tab carries none of that workspace's tab-bar records, so the Loader rebuilt
the tab in the group it names for the workspace while main had minted a
random one: every workspace's first save replaced main's group. Main now
names a workspace's first group as the window's root group and the Loader
do (`workspaceRootGroupId`), or takes the sender's when the window drew the
tab in a group main lacks (the window keeps its emptied group); the
window's `ensureGroup` names it the same way. The detector reads a lone
group with no stored tree as that group's leaf, as main's own save writes
it. Empty groups: the window never saves a group it holds empty, so a save
cannot bring back one main closed (locked by a test).

Tab order: a reorder the window makes itself (an anchored CLI or agent
create, a reopened tab's old slot) is recorded in main as `moveTab` steps
at the store boundary; callers that already asked main pass
`layoutOwnerHandled`. The reopened-editor record this replaces is deleted.
A paired client's move (`ui:moveSessionTab`) goes to main first through
the tab-bar drop path.

Preview: activating a tab removed its preview flag by writing
`isPreview: false` on every tab; main stores no flag, so every activation
changed the tab in main's model. The window now drops the flag instead.

* Record a reopened tab's place in main every time, leave a move into a tab's own group alone, and keep a preserved preview untouched (dark, layout-owner build only)

Review fixes, switch-on only:
- A reopened tab's place is recorded as one moveTab whenever its old
  position is known, as the deleted editor-only record did, now for
  every kind; the reorder it may make is not recorded twice.
- A move into the tab's own group (a paired client can send one) goes
  to today's no-op instead of a moveTab that reordered the tab.
- Activation that preserves a preview returns the tab unchanged.
Tests: switch-off first group stays minted; tabOrderMoves refuses a
duplicate order.

* Send a paired client's tab reorder to main as its whole order (dark, layout-owner build only)

A client reorder names the group's full order, but it went to main as
one moveTab of the named tab, so [a,b,c] -> [b,a,c] naming c moved
nothing anywhere. It now takes the store's reorder path, whose record
turns the whole order into main's moves (tabOrderMoves), the one rule
for every window-side reorder. Moves and splits still go main-first.
2026-10-16 23:25:10 -07:00
Jinwoo Hong 5b69634c0a Agree with main on main-made terminal tabs and sleeping-agent records (dark, layout-owner build only) (#29967)
* feat(layout): main-made terminal tabs drawn with main's facts; resume records reach main record-only (dark)

In layout-owner builds only:
- A terminal tab main made itself (drag-out, CLI or host terminal, live-surface
  adoption) is drawn with main's createdAt and creation fields from the frames,
  and the window sends no create for it.
- An editor tab main records takes the time the window drew it.
- Every change of the window's agent resume records (live and quit captures,
  dropped records) reaches main through record-only sleep and wake commands,
  which stop or start nothing.
- A terminal row's remount generation is declared the window's.

* fix(layout): real wake reaches main before the record drop; drag-out keeps a frame-drawn tab (dark)

In layout-owner builds only:
- A resumed pane's wake is sent before the window drops its record, so main's
  wake still finds the pane sleeping.
- A drag-out whose new tab main's frame already drew shows the pane in that
  tab instead of throwing "Terminal tab already exists".
- A main-made tab keeps the window's shell, cwd, host-runtime and launch agent
  where main's copy names none (a runtime create records a shell only when
  its launch named one).
- No record-only wake for a tab the window closed; main dropped its records
  with the tab.
- Tests for the resume-record follower.

* fix(layout): a dragged-out chat pane's new tab opens in chat view in main (dark)

Main's movePaneToNewTab now gives the new tab viewMode 'chat' when the moved
pane is its source tab's chat pane, as the window's own drag-out does. In a
layout-owner build the window adopts the row main's frame drew, so without
this the tab opened in terminal view.

* fix(layout): a bound pane with a resume record starts as a no-op; resume records only for held tabs (dark)

- startPane checks the pane's live terminal before its resume record: a
  running agent's live or quit record does not make its pane asleep. A pane
  with no terminal and a record is still refused.
- The window sends record-only sleeps, like record-only wakes, only for tabs
  it still holds; a capture from a stale agent status no longer names a tab
  main already closed.
2026-10-16 23:23:04 -07:00
Neil 2d33cd965f refactor(identity): brand host-scoped worktree and repo keys for new code (#29990) 2026-10-16 23:19:42 -07:00