* Layout groundwork (switched off): main derives agent tab titles from its own hook status
With the layout-owner switch on, main gives agent tabs their generated titles from
the hook-server status it owns, on the desktop, headless serve and orcad: one
renameTabs per status batch, so one write and one frame. It keeps the window's
rules (setting, custom and quick-command titles, first title wins, dispatch labels).
The window stops sending generated titles for tabs main holds; a paired server's
tab, or one main has no tab-bar entry for, is still titled by the window as today.
The title rules move to src/shared (agent-row-primary-text, and the new
agent-generated-tab-title-rules) so both sides share them; switch-off behaviour
is unchanged.
* feat(layout): main applies a phone's or paired client's tab move itself, and a tab move names the tab it follows so a stale index can't misplace it (layout-owner build, L193)
* fix(layout): a move in a group showing a tab main lacks goes the window's way, a twin's agent tab keeps its window title, and orcad subscribes titles through the store attach (layout-owner build)
* chore(layout): main's tab move reaches the old-client translation through its compat entry
* chore(layout): mark the window-only-row move fallback as bridge code (ledger L280)
* test(layout): attaching the hook store follows its rows for main's tab titles once
* chore(layout): the old-client move translation is a compat entry of its own, with no cyclic re-export
* test(config): the boundary message test names the old-client folder's configured entries
* fix(wsl-host): pair again from the distro when a WSL host refuses the enrolled token
A WSL host that lost its device registry, or revoked this desktop, refused the
enrolled token forever, and an update over it read as deferred. The connect now
re-pairs through the managed repair gate SSH uses (one repair at a time, a
capped number per window, only an offer under the pinned host key), reading the
host's published offer from the distro. The incumbent's terminal count uses the
same enrolled-or-live pairing the connect does.
* fix(wsl-host): retire a bridge whose pairing was refused, and let a user's Connect retry a repair
A refused or repaired-then-refused pairing on a reused bridge left its stale
token cached, and the failure status then saved it over the repair. That bridge
now closes first. A user-initiated Connect clears a standing repair failure or a
spent cap, as an explicit SSH connect does.
* fix(wsl-host): retire a refused bridge without cancelling a queued Update or Force
* fix(wsl-host): repair only a stored pairing, dispose the repair's transport directly, and log refusals neutrally
A host still being added has no stored record for the gate to re-pair, so its
live bridge is reused as before. The WSL repair deps are typed with the WSL
transport, and the shared refusal log no longer says SSH for WSL.
* fix(ssh): point unclearable move conflicts at plain SSH, and polish the plain SSH window
Conflicts retrying can't clear now say how to keep working on the host. The
plain SSH terminal paints its theme background past the grid, and a failed
open shows only its error, not an empty terminal.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): say what plain SSH is for beside its button, not in main's refusal text
Main's refusal text also reaches clients without a plain SSH terminal, so the
next step is shown next to the button wherever it renders. Also: a translucent
theme isn't painted twice, and a failed open shrinks to its error.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): treat only opacity below 1 as a translucent plain SSH background
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Preserve deliberate terminal taps after scrolling and keep them ordered with Live input. Retain fresh keyless edits, cap coalesced sends at the existing UTF-8 request limit, and settle cancelled flush barriers without waiting for retired acknowledgements.
Adapted from @innocarpe's community PR #30514, with additional ordering, lifecycle and bounded-send regressions.
Fixes#30510
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
* Polish explorer search layout and filter disclosure
* Use compact search details disclosure and lowercase filter labels
* fix: preserve query focus when switching explorer modes
* fix: announce active explorer search filter counts
* fix(mobile): retire dismissed branch diff requests
A pending branch diff reopens a dismissed drawer and blocks replacement previews. Retire requests on dismissal and host/workspace changes, and keep spinner ownership with the current request.
* fix(mobile): retain branch preview through suspended transitions
* test(mobile): retain confirmed branch previews across connection cutover
* fix(mobile): retain quick command editor ownership during saves
A deferred Save discards newer edits or closes a replacement editor, including after host/repository changes. Bind each save to its editor and scope; preserve newer text and the persisted command id.
* fix(mobile): preserve quick command saves through suspended renders
* test(mobile): preserve quick-command edits during connection migration
Reconnect refreshes only the root and retains deleted nested entries; interrupted children can remain loading. Invalidate child reads and refresh expanded folders. Bound deep indentation to keep filenames reachable.
Fresh repository objects reset explicit setup choices, while host changes can retain stale scripts and trust. Use logical repository and runtime ownership.
Delayed hydration and out-of-order storage writes can reverse a browser destination selection. Order durable writes and preserve the confirmed value when saving fails.
With the layout-owner switch on, session.tabs takes main's markdown edit row and copies the
window's view facts onto it, but not the untitled flag, which the saved session never records.
The phone then offered to save an untitled document as an ordinary file. Carry the flag too.
The route tests now use a realistic store: it lists its host partitions and saves each open
editor's unified tab and group, as the window does.
* fix(session-tabs): a confirmed close of a pinned server tab closes it instead of snapping back
A paired desktop sent its confirmed close of a pinned terminal tab in a
server workspace over session.tabs.close, which had no force, so the
server refused it (terminal_tab_pinned) and the tab came back.
- session.tabs.close takes an optional `force: true`, honoured only with
an explicit `reason: 'user'` and passed to the server's existing forced
close (the same path worktree stop uses), in both builds.
- The server advertises `session-tabs.close-force.v1` in both builds. The
desktop sends force only after the pinned-tab confirmation and only to
a server advertising it; an older server keeps today's refusal.
- A forced close the server never received keeps its force in the held
replay record, so the replay is still the confirmed close.
- Moves the undelivered-close hold helpers into
web-session-undelivered-close.ts unchanged, to keep the lifecycle
module under the line limit.
* fix(session-tabs): a pinned server tab closes when the pin confirmation is turned off
A local pinned tab closes at once when confirmClosePinnedTab is off, so the
close itself is the user's consent. A server workspace's pinned tab now
sends that consent as force too (still only to a server advertising
session-tabs.close-force.v1). Lifecycle closes (pty-exit), rejectPinned
callers and bulk closes are unchanged.
* test(session-tabs): load the forced-close server test once; cover held and probe-failed forced closes
- session-tab-close-force.test.ts imports the runtime once and flips the
layout-owner switch per case through the mock's getter, instead of
resetting modules and re-importing the runtime for every case; drops
the 120 s timeouts.
- A forced close held while the session is inactive, and one whose
capability probe failed, keep their force for the replay.
- A pinned lifecycle (pty-exit) close never forces, even with the pin
confirmation turned off.
wsl.exe imports only the variables WSLENV names. ORCA_WORKTREE_ID crossed but ORCA_WORKSPACE_ID
did not, so a WSL shell never saw it, whether the window or main started the terminal.
* fix(ci): recover verified pnpm setup from registry metadata outages
* fix(ci): prepare native pnpm shims after fallback verification
* fix(ci): provide Node for pnpm fallback in bare containers
* chore(cloud): update relay runtimes and security scanning
* docs(cloud): record Go binary advisory reachability
* chore(cloud): rerun security checks after resolving main conflict
* Add onboarding language selection and system locale detection
- Detect and synchronize native/browser language preferences safely
- Load catalogs before rendering onboarding and support recovery
- Let users choose a supported language or request another language
* Add onboarding language experiment cohorts and detection recovery
* Stabilize onboarding language detection tests
* Test persisted onboarding language cohort behavior
* feat(ssh): open a plain SSH terminal to a host whose move to its Orca server is blocked
A refused or unserved host fails the whole connect, so the user had no way to
reach their work there. The blocked-move banner and the SSH host row now offer
an unmanaged, unpersisted ssh login in a non-modal window. Main builds the argv
from the saved target (no -T, BatchMode or Orca multiplexing; the config alias
stays bare so ssh_config applies) and spawns ssh with an argv array through the
local PTY spawn primitive.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): claim a host before awaiting, wait for ssh on quit, and share the preview terminal's keys
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): give the plain SSH terminal Orca's Unicode widths, kitty flags and spawn-helper repair
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(terminal): one standalone terminal for the agent preview and plain SSH
Extracts the preview's xterm setup (Unicode widths, links, IME bridge, kitty
flags, copy/paste and shortcut policy, user-input routing) into
openStandaloneTerminal, so the plain SSH terminal reuses it instead of a second
copy. Plain SSH output is now acked once rendered, and main pauses ssh while too
much is unrendered.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): plain SSH answers terminal queries, keeps early exits, and ends a paused pty on Windows
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): plain SSH applies theme changes live and never refits while minimized
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): a native call on a just-exited plain SSH pty never crashes main
Co-Authored-By: Claude <noreply@anthropic.com>
* test(ssh): give the plain SSH test's ResizeObserver stub unobserve, which Tooltip needs
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only)
A records frame no longer re-serializes the window's whole session and runs the
Loader. Panes' terminal bindings and live titles apply alone, to the tabs the
frame names; any other record builds only its own workspace's slices, with the
view and facts read from the store. Only a workspace the store does not show
from frames yet (startup, a reopened stream, a first visit, one sent whole)
takes the full build.
Tests install an apply check that, after every flush, compares the store with
the full build for the same cache and pre-flush store; the shared layout-apply
transport now sends main's records frames and fails a flush that differs.
* A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only)
* The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only)
* The account-restart test types its store writes, so its five old e2e type errors are gone (test only)
* A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only)
* A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only)
* The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only)
* The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only)
* wip(d3a-2): a windowless host lists a file main opens on session.tabs at once
* wip(d3a-2): the record key's fields are declared where the key is made
* wip(d3a-2): main publishes an editor tab's view, and a window rebuilds the open file it lacks from it
Main's in-memory editor view now carries the file facts an open file needs; it rides the tab row
(PublishedTab.editorView) and a frame build rebuilds a diff, conflict, preview or check-details tab.
Inert with the switch off: views are set only by layout.apply (switch builds) and read only in frame-sync.
* wip(d3a-2): a switch-on window sends editor opens, closes and promotions to main and draws the tabs from its frames
Layout-owner build only; the switch-off path is unchanged. An open proposes the tab id and sends
openEditorTab with the edit file's record or the tab's view (a reopen also its index); the open
file is written at once and the tab is selected once its frame draws it. Callers that act on the
tab (focus request, link reveal, group focus) wait for that frame through layout-owner-pending-tabs.
A close sends closeTabs; the tab goes with the frame and the file's content after it, and a
refusal keeps both. A promotion sends promotePreviewTab and the frame clears the flag. Deleted:
the drawn editor senders, their before/after captures, the editor drawn-tab holds and the
window-record keep rule (a transient tab is kept by its frame view).
* wip(d3a-2): a switch-on close keeps its selection and a reopen, a link names its group, and main stamps an open's time
Layout-owner build only. After main's frame drops a closed editor tab, the window selects the
group's previous tab and its surface through activateTab and focusGroup, after closeFile's
content cleanup rather than before it; a reopen sent while the close was in flight keeps the
file. openFileForLink's proposed view names the group the open asked for. The window's
openEditorTab sends no createdAt: main stamps the tab's time (ledger L186). Preview promotion
writes no tab itself: the window ends a preview only where a paired server owns the layout.
* wip(d3a-2): editor tests assert switch-on tabs through main's command and frame (test only)
Each store and hook test that assumed an immediate editor-tab draw now runs with main's model
through the T1 transport in apply mode and its frames (tabs-drawn-from-main.test-fixture), and
asserts the tab, its selection and its content after main's frame. The fixture fills the frame
slices a partial editor-and-tabs store lacks.
* wip(d3a-2): main takes a restored move as a command, renames a view tab's path, and keeps one tab per file per group
Layout-owner build only (the switch gates every change; shipped builds unchanged). moveTabToWorkspace no
longer skips a tab "the window's save got there first" (L160): window saves no longer carry the move, so
a repeat is tab_not_found; tab_exists and invalid_params stay. A move or rename may carry a non-plain
tab's new view (its file under the new path or owner), which replaces editorViews[tab] (L180). Main
refuses a moveTab or moveTabToWorkspace that would put a second tab for a file into a group (tab_exists),
and its loader folds a saved session's duplicate into the first, as the window's restore does
(dedupeEditorTabsWithinGroups): the window's frame dedupe hid the second, which no close could reach.
* wip(d3a-2): a switch-on restored move and disk rename are main's commands, and the frame that shows them rekeys the content
Layout-owner build only; the switch-off path is unchanged. A restored editor tab re-owned by its
file's workspace (L157, L158) sends moveTabToWorkspace (a re-own in place sends renameEditorFiles)
under the ids and group the window proposes, with each view tab's new view; a disk rename (L180)
sends renameEditorFiles per workspace and draws nothing first. The window's content rekey (open
file records, drafts, buffers, the editorBuffers echo gate, pendingLiveDiskVerification and
pendingSelfMoveEcho) waits in layout-owner-content-rekeys and the frame flush that first shows main's
change applies it, in the same store update as the frame (a flush now writes one patch). A change no
tab shows is rekeyed on main's answer; a refusal drops it and changes nothing. The move coordinator
and the restored-owner loader wait for that outcome; a refused rename rolls the disk move back.
A rename picks a runtime-owned or floating file's owned id, as main's frames restore it.
Deleted: recordRestoredEditorMoveInLayoutOwner, recordRenamedEditorFilesInLayoutOwner, the
reparent's before/after capture and the drawn-tab holds for moves and renames. Tests that assumed
the window moves or renames first now run through main's T1 transport in apply mode and its frames.
* wip(d3a-2): the static gate's casts and layout-writer ratchet hold for the switch-on move, rename and gap fixes
Layout-owner build only. The content rekey's pure store change stays in rekey-open-files-action
(its writer count drops 4 -> 2, pruned); a twin tab main routes to a paired server ends its
preview through patchTab, the existing tab writer, instead of a new one; a frame-only editor tab
takes its label through the frame build's tab-label facts, read from its view or file record; the
moved file's activation names the moved tab, writing no surface field. Test casts are typed or carry
a SAFETY note.
* wip(d3a-2): the switch-on rename takes the window's pure rekey as an argument, so the two modules do not import each other
Layout-owner build only; no behaviour change. Clears the static gate's no-cycle finding.
* wip(d3b): a switch-on window sends browser, chat and simulator opens, closes and page changes to main and draws them from its frames
Main places the chats its host lists (twin h), a local cancel keeps no tombstone (L155), and the
listing no longer writes chat tab structure where frames draw it (L153).
Co-Authored-By: Claude <noreply@anthropic.com>
* wip(d3b): a switch-on browser, chat or simulator close waits for main's frame, lands an emptied workspace as today, and its tests read main's frames
* wip(d3b): the stale-listing and cross-version chat tests check a switch-on local cancel through main, with a presence precondition and per-test resets
* wip(d3b): main places a tab whose group it lacks in the workspace's first group, not beside the window's anchor (L150)
* wip(d3b): tests for main's chat tab commands from a host's chat changes
* wip(d3b): main's windowless chat tab and chat fork tests apply a switch-on host's chat tab through main's layout
* wip(d3b): the switch-on close and open keep their layout writes in the files the writer ratchet counts them in, and a drawn browser close carries its position instead of a rebuilt layout
* Editor tabs main-first: the window sends each editor action to main and draws editor tabs only from main's frames (dark, layout-owner build only)
With ORCA_LAYOUT_OWNER_IS_MAIN on, editor open (preview replace, open to the side), reopen at
its position, close, close-all, promote, the resolved conflict placeholder, a restored-editor
move and a disk rename are layout commands the window sends before it draws anything. Window
content (open file records, drafts, buffers, the autosave gate) is written at once for an open,
and rekeyed in the same store update as the frame for a move or rename. Main publishes each
non-edit tab's view, keeps views in memory across applies and partition changes, stamps the
tab's time, folds same-file duplicates, and republishes a windowless host's session.tabs after
its own file open. The switch-off path is unchanged.
* wip(d3b): a switch-on close main answers workspace_not_found drops the tab's content as tab_not_found does, and placement reads a missing active-group map safely
* wip(d3b): switch-on browser and chat tests open and close through main's command and draw from its frame (test only)
* wip(d3b): a switch-on chat listing writes no tab structure of a workspace main's frames draw; a host chat title changes only the drawn tab's label
* wip(d3b): main's phone rows show a diff tab and a markdown preview as the window's do, with the preview's source named by the window's open-file id
* wip(d3b): the cleanup close's page-read guard counts its named linear passes, so a per-entry rescan still fails switch-on (test only)
* wip(d3b): the static gate's compat entry, focus-read ratchet and formatting hold for the switch-on test conversions and phone rows
* test(d3a-2): the buffer view index follows a rename main answers
* wip(d3b): placement reads the active-group map the real store always holds; the browser test store seeds it
* wip(d3b): a switch-on background browser open leaves the worktree's remembered browser tab unset, as with the switch off
* wip(d3b): a switch-on chat open selects the chat in its group without moving the focused group, as the window's own open does; its test counts the chats main draws
* wip(d3b): the converted simulator, launch-cancel, cleanup-close and chat-listing tests assert what the window's draws asserted, through main's command and frame (test only)
* wip(d3b): main fences a chat's create on its close even when the close fails for want of the chat host, so a cancelled launch never shows
* wip(d3b): a frame keeps the window's hidden chat launch slot after its neighbour, so a resumed chat keeps its saved place through main's frames
* wip(d3b): a declined chat's terminal is placed after the chat and pinned as it is in one main move, and shown before main closes the chat
* wip(d3b): a switch-on chat close the host refuses asks main to open the chat again, as the window's re-read shows it with the switch off
* wip(d3b): a paired server's workspace, its row naming the server, opens and closes browser and chat tabs without main's layout (test only)
* wip(d3b): a refused chat close comes back only while this machine's host still lists the chat, and a close main answers workspace_not_found removes the window-only tab
* wip(d3b): the twin chat close test names what each build stops: no host off, main's host on (test only)
* wip(d3b): the refused-close reopen takes only a chat agent it can launch
* wip(d3b): a frame's browser selection keeps an unchosen workspace's unset through the restore projection's own rule, not a second write
* wip(d3b): the simulator and paired-server tests type their mock stores without assertions (test only)
* wip(d3b): the switch-off declined-chat placement runs as before, with no wait; only the switch-on move waits for main
* chore(d3b): drop the frame-binding helper the main merge left without a caller
* chore(d3b): drop the import the deleted helper used
* wip(d3b): a chat this machine's window creates is placed only by the window's own open, so a reserved chat slot keeps its place (switch-on)
* wip(d3b): main reads a chat tab change against its layout when its queue reaches it, through the one rule the switch-off save uses, so a close right after an open closes it
* wip(d3b): a declined chat's terminal moves to its index among the tabs main draws, not past a hidden launch slot, so it still replaces the chat (switch-on)
* wip(d3b): main's phone-row comments say which rows the window's own row wins, and a test shows a window-listed diff keeps its dirty state (comments and test only)
* wip(d3b): a switch-on chat open returns only the tab's id and chat, the fields main's frame keeps; no caller reads more
* chore(d3b): the unit-routing census registers the browser suites' drawn-from-main store fixture as a support bridge (test only)
* chore(d3b): the chat tab rule's comment wraps and names its two callers (comment only)
* wip(d3b): main's chat tab queue asks a worktree's host only once it has a saved session, and one failed change no longer stops the ones after it (switch-on)
* wip(d3b): a chat tab batch whose changes cannot be read is logged, and the batches after it still land (switch-on)
* test(d3b): a CLI-chat resume's tab is opened once, by the window through main, after the tab before its hidden slot (test only)
* test(d3b): an SSH worktree has no host to run a chat on, so none of the chat open, close or fix paths reach it (test only)
* chore(d3b): the content tab placement reads folder workspaces through main's workspace-record selector, as the new ratchet requires
* fix(merge): put back main's f47f632 files that the merge took from a later main by mistake (no D3b change)
---------
Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ssh): keep the enrolled pairing across a managed server's update, rollback and recovery
orcad's startup offer names a pending device, which it re-mints once a client
has paired with it. Update, rollback and recovery re-paired the desktop from
that offer, so the desktop became a new device to its host after each one.
They now keep the enrolled grant, as WSL does since #29042.
* fix(orcad): count WSL incumbent terminals with the enrolled pairing, and warn on a changed host key
The WSL incumbent's terminal count authenticated with the startup offer, which
marked that pending device used, so every orcad start enrolled another device.
It now uses this desktop's enrolled pairing. After an SSH update, rollback or
recovery, a server that names a host key other than the pinned one is reported
with a warning instead of a plain success.
* fix(ssh): keep a pairing the terminal count repaired; leave the host-key warning to a follow-up
The census can repair a refused pairing before the update re-pairs, so the
enrolled grant is re-read from the store rather than taken from the copy read
first. The host-key warning goes back out: only the CLI would show it, and
surfacing it in the desktop lifecycle UI is a separate change.
* refactor: keep two touched files under the line limit after rebasing onto main
refreshPairing reads the enrolled offer in one statement, and the WSL connection
error classes move into their own file.