Commit Graph
15665 Commits
Author SHA1 Message Date
Neil 307f0cdaee Move Relay deployment configuration into private cloud operations (#31792)
* refactor(cloud): move deployment configuration into private operations repo

* docs(cloud): clarify private operator ownership

* fix(ci): install pnpm through npm-compatible bootstrap

* fix(ci): avoid native downloader metadata timeout

* test(push): move deployment preflight contract into private repo

* fix(ci): bootstrap Node before installing pnpm in minimal containers

* fix(ci): pass Windows boundary test paths through the Node runner

* fix(ci): run Windows boundary Vitest with installed Node
2026-10-21 21:01:25 -07:00
Jinwoo Hong 4c949cd111 fix(session-tabs): a confirmed close of a pinned tab in a server workspace closes it instead of snapping back (#31791)
* 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.
2026-10-22 00:00:51 -04:00
Jinwoo Hong 0947c493b7 fix(wsl): forward ORCA_WORKSPACE_ID into WSL shells like the other pane identity (#31668)
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.
2026-10-21 23:49:51 -04:00
Neil 8ba1c9ba09 fix(ci): restore cloud and main checks during pnpm registry outages (#31793)
* 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
2026-10-21 20:25:36 -07:00
Neil 5c5f9a2362 Update cloud relay runtimes, providers and security checks (#31753)
* chore(cloud): update relay runtimes and security scanning

* docs(cloud): record Go binary advisory reachability

* chore(cloud): rerun security checks after resolving main conflict
2026-10-21 19:43:26 -07:00
Jinjing a53c2a2a72 Add onboarding language selection (#31784)
* 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
2026-10-21 19:43:22 -07:00
Neil 95d9682b0c fix(ci): prepare historical VM rollback snapshots (#31775) 2026-10-21 19:40:42 -07:00
NeilandClaude 56143a9a5e feat(ssh): open a plain SSH terminal to a host whose move to its Orca server is blocked (#31716)
* 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>
2026-10-21 19:35:32 -07:00
99d18f7a2f Browser, chat and simulator tabs main-first: the window asks main for each change and draws only main's update (dark, layout-owner build only) (#31516)
* 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>
2026-10-21 22:32:07 -04:00
Neil 5793b05f39 fix(ssh): keep the enrolled pairing across a managed server's update, rollback and recovery (#31684)
* 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.
2026-10-21 19:28:12 -07:00
NeilandClaude 7a912a3a24 Autosave relies on the guarded save instead of pause flags; identical saves don't rewrite; non-UTF-8 files aren't autosaved (#31768)
* Autosave relies on the guarded write instead of pausing restored or moved tabs

Delete the pendingDiskBaselineVerification and pendingLiveDiskVerification
pause flags. Every editor save is already a guarded write; a refusal raises
the changed-on-disk banner, which is what stops autosave until an explicit
save.

A guarded write now succeeds when disk changed but already holds exactly the
text being saved (local/host compare is byte-exact; client-side fallbacks
compare decoded text and refuse when it holds U+FFFD), so a restored draft
that matches disk goes clean instead of showing a banner.

The restore scan only raises the banner early for tabs that arrive dirty; a
watch resync no longer re-checks dirty tabs.

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

* fix(editor): skip guarded saves when disk is already identical

Return a compatible optional no-op result from hosts and client fallbacks, clean the tab, and clear its provisional self-write stamp without rewriting identical bytes.

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

* fix(editor): refuse lossy automatic saves, keep identical-skip on older hosts, flag dirty tabs on resync

- Autosave and pre-restart saves refuse when disk text can't be decoded losslessly, so a
  baseline that decoded to U+FFFD can't authorize overwriting different bytes.
- New files.write.identical-noop.v1 capability; for hosts with only the v1 guarded write the
  client compares first, skips identical saves, then delegates to the host's guarded write.
- Watch resync compares dirty tabs against disk and shows the changed-on-disk banner.
- Local guarded writes check for identical content before the write-only regular-file probe.

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

* fix(editor): tell non-UTF-8 files apart from disk changes, and quiet false resync banners

- Local guarded writes judge losslessness by bytes, so a valid UTF-8 file containing U+FFFD
  autosaves again. A file that really isn't UTF-8 is refused with its own reason, 'not-utf8'
  (older clients read it as 'changed'), which pauses autosave for that file with one notice
  until an explicit save converts it.
- A watch resync whose disk read fails leaves dirty tabs alone; the guarded save still catches
  a real conflict.
- Resync and restored-tab checks treat disk holding the tab's own just-written save as an echo.
- On hosts with only the v1 guarded write, only automatic saves read the file first; explicit
  saves go straight to the host's guard.

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

* fix(editor): lift the non-UTF-8 autosave pause when its edits end, and report disk changes first

- The pause is released once no dirty tab of the file remains (close, reload from disk,
  discard), so a reopened or reloaded file's next autosave asks the host again and notifies.
- Client-side precondition checks compare the signature before looking for U+FFFD, so an
  external rewrite shows the changed-on-disk banner rather than the non-UTF-8 notice.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 19:20:04 -07:00
Neil 75e1f1c0f4 fix(wsl-host): prove an unrecorded orcad by kernel identity before adopting it (#31685)
* fix(wsl-host): prove an unrecorded orcad by kernel identity before adopting it

A WSL connect adopted the orcad named by a leftover instance lock whenever a
bare kill -0 found its PID. After a distro restart PIDs are reused, and a stale
lock plus a stale readiness file that name the same PID passed every other
check, so the record was repointed at a dead build. The holder is now live only
when the lock's boot id and start ticks match the PID's; an unanswered guest or
an older lock without them is unverifiable and is never adopted.

* fix(wsl-host): compare a lock's boot id case-insensitively
2026-10-21 19:14:43 -07:00
Jinwoo Hong ed13ca1e6f Layout groundwork (switched off): main starts and restores every pane, and the window only attaches and paints the host's history (#31567)
* feat(terminal): the window's store shows main's pane start status (layout-owner build)

A status-only frame changes one store entry and no pane tree, so the pane's view stays mounted.

* refactor(terminal): keep the frame sync within its line budget

* feat(terminal): every pane main starts shows 'starting' from before it is written (layout-owner build)

* feat(terminal): a pane main is starting waits and attaches; a failed start shows why (layout-owner build)

Fixes owner-visible row 15: a failed agent or setup start no longer opens a plain shell.

* test(terminal): iterate store listeners directly

* fix(terminal): an exit that reports no code keeps its pane, as lost contact (layout-owner build)

* test(terminal): a pane main started attaches to main's terminal at its own grid

* test(terminal): detach census tests assert main's move and frame with the switch on

* fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)

* fix(terminal): main releases a painted cold restore only for the pane's current terminal (layout-owner build only)

* test(terminal): typed ack args in the painted cold-restore test

* test(terminal): exited-pane tests assert main's close with the switch on

These 8 have failed switch-on since #31130 moved the exit close to main.

* feat(layout): main starts the panes a client's startPane or wake names, through its one restored-pane starter (layout-owner build only)

The model's start and stop effects reach the runtime once the command is written, never the
reply. startPane starts a pane whose binding has a start status, and a slept pane from its record.
A wake keeps a pane's resume record until its start binds; a failed start keeps it and shows why.
With the switch on, main owns a running agent's wake even with a window open.

* fix(layout): a window's wake of a pane it resumed itself is record-only, so main does not start it again (layout-owner build only)

* test(layout): a client's startPane starts the pane before any other trigger

* Revert "fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)"

This reverts commit a646d89b26.

* test(terminal): the cold-restore repaint ack names its pane in the layout-owner build

* fix(layout): a wake keeps the pane's resume record in the pane-owned form, so a host crash resumes the agent again (layout-owner build only)

* test(layout): a failing start in the pane-start runner test throws

* feat(terminal): main writes and starts every split of a pane it holds; the window attaches (layout-owner build)

The window's split, a startup split with its command, and a CLI split all go through main's
terminal.split; main's frame mounts the pane, with or without a window intent. Deletes the
window's split wait and withdraw, the split appear intents, and the queued-split skip and cancel.
The e2e helper uses the real split. A startup split leaves focus on its source pane.

* fix(terminal): the attach-only wait leaves sleeping, resuming and held panes to their restore path

* fix(terminal): a split pane attaches to the terminal main bound it to; a taken pane id is refused

Also restores the proof that a split closed while it starts gets one answer and leaves no
pane or start status behind.

* Reapply "fix(terminal): a restored pane paints history only from its terminal's host, never the window's copy (layout-owner build only)"

This reverts commit 420e605ad0.

* fix(layout): a trigger between a restored pane's start and its bind write joins that start, not a second one (layout-owner build only)

* fix(terminal): a window split main cannot start still shows its pane, failed, with why

* fix(terminal): keep the switch import the D1b-1 merge dropped from the layout restore

* feat(layout): main's starter shows a restored pane starting before any await, and a failed start with its reason; startPane starts a pane whose bound terminal ended (layout-owner build only)

* feat(terminal): a window's attach-only view of a pane attaches to its terminal or starts nothing, keeping the binding for main's start (layout-owner build only)

* feat(terminal): a restored pane with no live terminal asks main to start it and attaches once main binds it; the window's wake, hibernated wake, restart-on-show and gone-terminal fallbacks route through main (layout-owner build only)

* fix(terminal): check a taken pane id first, keep the start's own error, clear a gone pane's status

Three test stores gain the pane-start field the window's store always has.

* test(terminal): the admission stub answers whether its pane is still written; a closed pane's failed start clears

* test(terminal): with the switch on, the window's restores, wakes and restart replacements ask main to start the pane and attach to the terminal main binds, starting and resuming nothing themselves

* refactor(terminal): the rebound workspaces' bindings show through one call in the bindings module

* fix(terminal): a held pane connects as today so main's refusal holds it; main's start follows output; a capacity death after main's resume keeps the resume (layout-owner build only)

* fix(layout): main passes its start options only for commands that start panes; a windowed open resumes the agent under main (layout-owner build only)

* refactor(terminal): break the pane start module cycle; typed test fixtures

* test(terminal): a pane main wrote and the user closed mid-start drops its failed status

* fix(terminal): every waiter on main's start runs, a failed or refused start settles and is asked again on the next view, a hidden gone pane waits for its view, a twin main hands back stays on today's path, and lost contact never starts a pane (layout-owner build only)

* test(terminal): iterate the store listeners directly

* test(terminal): a cold-restore paint applies no window resume with the switch on

* fix(terminal): a split main starts is listed to phones and session.tabs without a window's report

* test(terminal): name the main pane start test helper as a fixture

* fix(terminal): a reconnect shows its pane starting and why it failed; a lost owner race shows no failure; a split no one asked to see keeps the phones' tab

* fix(terminal): a kept create another owner won shows no failure; the window attaches to the winner

* fix(terminal): a pane main starts at relaunch keeps its full history and starts before the window's graph lists its tab (layout-owner build only)

* feat(terminal): main replaces the empty shell a hibernated agent left and resumes the agent when a view asks; a start main could not attach shows main's own terminal_not_started (layout-owner build only)

* test(terminal): main seeds its copy of a restored pane even when a window declared one, with the switch on

* fix(terminal): a pane main started hidden paints once when its view attaches (switch-on builds only)

The view's attach snapshot, taken after main unmarks the hidden gate, already holds the bytes main
dropped, so the reveal no longer asks a second restore from main's buffer.

* refactor(terminal): main's pane start reads the pane from its own layout, not the client projection (switch-on builds only)

* fix(terminal): a pane restart stops its terminal before writing the pane unbound, so a stop that fails keeps the pane (switch-on builds only)

* test(terminal): a start main could not bind shows terminal_not_started through the window's wait, from one copy site (switch-on builds only)

* fix(terminal): main's pane start resumes an agent recorded under the pane's number, as the window did; stale switch-on tests show main's start and the known banner loss (switch-on builds only)

* fix(terminal): review round: a visible view alone retires main's hidden-start drops, by episode; main reads a split pane's numbered record by its number; the window keeps a shell main kept (switch-on builds only)

* fix(terminal): main's start of a pane a window view is attaching to leaves it visible, so the view paints once (switch-on builds only)

* fix(terminal): a window view that attaches while main starts its pane takes main's start, with no second provider attach (switch-on builds only)

* fix(terminal): review round: a view takes main's start only when it carries a payload and still owns its terminal; attaching views are counted, keep main's pre-spawn mark off, and a failed attach hides only what it kept visible (switch-on builds only)

* test(terminal): checked test doubles in the view-join and numbered-record tests; reattach options fit the file budget

* test(e2e): with the switch on, the mount-authority spec keeps the window's bindings, which only main's frames write

Claude-Session: W5b

* test(e2e): the mount-authority spec fits its line budget; the layout-owner build check is one helper

Claude-Session: W5b

* test(layout): an unbound pane holding a resume record starts and keeps the record, since main's start resumes from it

Claude-Session: W5b

* fix(terminal): a pane main resumed holds a running agent's record, so it reads awake, not sleeping (switch-on builds only)

Claude-Session: W5b

* fix(terminal): in a split where no pane owns a numbered record, main resumes the tab's one conversation in pane 1 only (switch-on builds only)

Claude-Session: W5b

* fix(terminal): main marks only the record a resume started from live, never a sleep captured after it (switch-on builds only)

Claude-Session: W5b

* test(terminal): with the switch on, a windowless resume leaves the pane's record a running agent's

Claude-Session: W5b

* test(terminal): the resumed-record test double typechecks

Claude-Session: W5b

* refactor(terminal): with the switch off, main's pane start reads no resume record for the live mark

Claude-Session: W5b

* refactor(terminal): the merged files fit their line budget: the folder-fallback notice and a hidden pane's wait for its first view are helpers, and the answering-transport lookup moves to its one test caller

Claude-Session: W5b

* refactor(runtime): the window's layout apply fits the runtime's line budget after the main merge

Claude-Session: W5b
2026-10-21 22:13:57 -04:00
ea10994820 fix(terminal): link a framed multi-row URL when an overlay shows through (#21816)
* fix(terminal): link a framed multi-row URL when an overlay shows through

A TUI that draws a box-framed modal over existing output leaves the screen
underneath visible beside the frame, so the text left of the URL differs from
row to row. The hard-wrap detector required that text to be byte-identical
across rows, so the join stopped at the first row whose gutter differed and
the candidate collapsed back to a single row, opening a truncated URL.

Anchor continuation rows on the frame's cell column and the run between the
frame and the URL instead, which is the part that is actually shared.

* refactor(terminal): drop the redundant scheme column guard

The start row's fragment start column is re-derived per row, so guard that
value where it is used instead of asserting it non-null.

* fix(terminal): reject shared-table URL joins

* test(terminal): isolate shifted URL frame guard

* fix(terminal): keep joining framed URLs whose row prefix is unchanged

Rows whose text left of the URL matches the start row are joined as
before; only a changed prefix uses the overlay frame anchor and its
table-cell guard. Restores nested-frame and overlay-over-frame wraps.

* Verify framed overlay URL hover and cold clicks with real terminal buffers

* Remove retired Portuguese agent-version translation

* Preserve translation exports in queued-message editor tests

* Run CI after resolving the upstream merge conflict

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
2026-10-21 19:13:12 -07:00
Jinwoo Hong bf0f7f58cc feat(connectors): add an off-by-default experimentalConnectors setting (no behaviour change) (#31757)
* feat(connectors): off-by-default experimentalConnectors flag and seam lifecycle (B2a)

Adds the experimentalConnectors setting (no UI, telemetry whitelist) and a
runtime-owned reconciler that builds the connector seam while the flag is
on and disposes it with connectors_disabled when it is turned off. The
seam is empty; later connector PRs land behind it.

* fix(connectors): partial runtime stores read as connectors off

* refactor(connectors): B2a is the setting only; the lifecycle lands with its first consumer
2026-10-21 22:07:53 -04:00
NeilandClaude 84cafff059 Stale delayed edits in notebook and rich Markdown views no longer overwrite newer text (#31760)
* fix(editor): reject stale delayed rich/notebook view edits instead of overwriting

A rich Markdown or notebook view commits its typing into the tab's shared text
after a short delay. If another pane of the same tab (or a disk reload) replaced
that text in the meantime, the delayed commit either overwrote the newer text
(notebook) or was silently dropped (rich Markdown). Each pending commit now
carries the text it was based on; a commit onto newer text is refused, the view
re-syncs, and a toast says the last edit in that view couldn't be applied.
Notebooks also commit pending drafts when focus leaves them, so ordinary
alternation between panes never goes stale. CSV already refuses stale cell edits.

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

* fix(editor): keep unrelated notebook drafts and silence discards when rejecting stale view edits

- Notebook drafts record each cell's source when typing began; a flush merges
  drafts whose cell is unchanged onto the latest notebook and drops only drafts
  whose cell changed or no longer resolves (position keys also require the same
  cell layout). The toast fires only when typing was actually lost.
- An acknowledgement that empties the draft map clears the flush timer and bases.
- Rich markdown flushes its pending edit when focus leaves the view; the
  debounce/flush logic moves into useRichMarkdownPendingSerialization.
- Dropped or cancelled edits republish the shared text so the tab's dirty flag
  is recomputed.
- Discard bumps its epoch before flushing, so pending edits it cancels never toast.
- Clearer toast copy in every locale.

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

* fix(editor): keep stale view flushes from overwriting newer text or saving rejected edits

- A notebook flush republishes only after its own typing, a dropped draft, or
  a real change, so stale text never lands over another view's newer text.
- The rich flush reports whether it rejected the pending edit; Cmd/Ctrl+S skips
  the save when it did.
- The rich discard epoch is captured where the serialize timer starts, replacing
  the dirty-hint wrapper; the redundant shared-text comparison is removed.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 19:07:07 -07:00
Neil a8dff64cb8 chore: remove unused shadcn generator dependency (#31737) 2026-10-21 18:59:05 -07:00
Neil 01456c14ec refactor(browser): group Chromium cookie import stages (#31730) 2026-10-21 18:55:15 -07:00
Neil bc60f69517 ci: fix protected-main downloads badge updates (#31747)
* ci: propose downloads badge updates through pull requests

* fix(i18n): align Portuguese CLI version message with current catalog

* test: provide locale in queued editor translation mock
2026-10-21 18:54:07 -07:00
NeilandClaude 924970836a Offer back unsaved edits when a workspace is removed without a prompt (#31738)
* fix(editor): list unsaved editor files before a workspace removal closes them

Removing a workspace closed its editor tabs and threw away unsaved text
without a word, including with "Don't ask again" on.

Removal confirms now list the files whose unsaved edits the removal
discards: the tabs the purge itself would close (host twins included),
kept only when no view sharing their model or record stays open. Editable
unstaged diff tabs count; delayed rich Markdown, CSV and notebook text is
flushed first.
- Delete Workspace dialog: the list sits with the git-change warnings.
  While it is non-empty the dialog no longer starts focus on Delete, and
  "Don't ask again" no longer skips the dialog.
- The same list in Remove Project, SSH workspace forget/reconnect-and-
  delete, SSH host removal with its workspaces, nested-worktree deletion
  (rows matched by path containment under the parent) and project-group
  deletion with its contained projects.
- Removals with no dialog whose files stay on disk refuse instead, with
  "Save or close N unsaved files in <name> first": folder-workspace
  delete (card action, context menu, hovered shortcut, through one shared
  helper) and Settings > Remove Project.

Force retries with no list of their own (the failure toast's Force
Delete / Delete Anyway, Space's Force button) re-check at that moment and
refuse the same way. A folder-workspace delete that leaves a same-id
folder on another host closes no tabs, so it is not refused. Removal
confirms touched here render on the security surface.

Removals with no prompt (CLI, automations, paired clients) behave as
before; a follow-up offers their text back.

Known limitation: the list is decided by the tabs a removal closes, not
by paths; a symlink into a deleted folder is not resolved on the host.

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

* fix(editor): offer back unsaved text a removal with no prompt closed

A workspace removed with no dialog (CLI, automations, paired clients,
removal outside Orca, listing and runtime-host purges) still closed its
editor tabs and dropped their unsaved text.

Those removals now flush delayed rich Markdown, CSV and notebook text,
then show one toast, "Closed N files with unsaved edits from <workspace>",
whose Reopen action reopens each file as an unsaved tab: in its own
workspace if it is still open, else the active one, only when that
resolves to the same host. The next save is guarded against the disk
version the edits came from. Dismissing the toast drops the text and
logs it. The captured entries belong to that removal alone.

Removals confirmed in a dialog that listed their unsaved files pass
unsavedEditsConfirmed through removeWorktree, deleteFolderWorkspace,
removeProject, project-group deletion and the purge builders, so they
discard without a toast. Folder-workspace and Settings project removals
refuse while files are unsaved, so they never reach the toast.

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

* fix(editor): keep reopened drafts editable when their file is missing

Preserve recovered text through a missing-file read, retaining previous content and rename state. Cover empty drafts and keep unrelated failures on the existing error path.

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

* fix(editor): recover removed workspace edits into new files

Create collision-safe recovered files in the active workspace using the existing untitled flow. Capture delayed edits across host twins as one removal, match stale-host flushes to purge selection, honor host-qualified confirmations during announcements, and release retained toast text.

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

* fix(editor): snapshot confirmed discards and simplify removed-workspace recovery

A removal dialog now records the exact text it listed when the user
confirms. Every purge path (worktrees:changed, announcements, removal
teardown, runtime host retirement, repo removal, nested children) skips a
closed file only while its text still equals that snapshot, so edits made
after confirming are offered back instead of dropped.

- One flushAllPendingEditorBuffers() before each purge replaces per-id
  flushing and the getState threading; idle CSV cells stay open.
- Captures merge into one toast per microtask, deduped by file id; the
  per-call options, scope ref-counting and announcement wrapper are gone.
- Capture reads a shared model's live text, else any sharing tab's draft.
- Recovery writes through a guarded write that only fills the empty file
  it created, moves to the next name on conflict, and never deletes it.
- Reopen resolves the destination path from the provenance owner host.

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

* fix(editor): scope removal discards to the confirmed teardown and flush only closing panes

- No lingering allowlist: a dialog's confirm returns a snapshot that rides
  the confirmed removal (removeWorktree, removeProject, project groups, SSH
  host clear) to its own teardown capture. Every other purge path offers
  unsaved text back, even if it races a confirmed delete.
- The pre-purge flush covers only the closing files and views sharing their
  records, and flushes every pane registered for them, so split rich and
  notebook panes all commit while other workspaces' CSV cells keep editing.
- Listing announcements flush the files they will remove before the purge.
- Capture reads a model's text only while it is attached to an editor;
  otherwise it prefers the drafts.

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

* fix(editor): keep a CSV cell that can't commit as draft text when its tab closes

A pending cell over the table size limit fails commit(), so the closing
flush now falls back to the view's preserve hook, which writes the cell
through the same limit-free serialization the unmount path uses. Recovery
then captures the typed text instead of an older draft.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 18:52:00 -07:00
ea42a203a0 fix(mobile): distinguish delayed dial deadlines from unreachable hosts
Classify substantially delayed connect/handshake deadlines as interrupted
instead of blaming endpoint reachability, retaining the existing close/retry
path and ordinary timeout errors. Limit sleep-clock compensation to observed
background segments and let fresh failures replace delayed evidence.

Remove the stale host-version fallback and discard captured versions when
report hydration crosses an observed reconnect. Independently reviewed;
128 focused tests, adjacent transport contracts and changed quality pass.

Adapted from community PR #24425.
Fixes #18829.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 18:49:14 -07:00
Neil ec546de75a fix(sidebar): remove redundant host badges in every grouping (#31744)
* fix(sidebar): omit host badges beneath host section headers

* fix(mobile): keep host health without repeating section labels

* test: align host section goldens and preserve chat translation exports

* fix(i18n): retire obsolete Portuguese Codex version key
2026-10-21 18:34:23 -07:00
Brennan Benson 9738dc6c08 Bring main back under the line limits (main lint is red) (#31729)
* fix(browser): bring the client-hosted page pane back under the 400-line limit

Move the client-hosted guest's six event-listener add/remove calls into
bindBrowserClientPageGuestEvents beside the guest load-failure handler,
so ClientHostedBrowserPagePane.tsx drops from 404 to 395 counted lines.
No behavior change.

* test(browser): pin the client page guest event binder's bind and unbind contract

* fix(native-chat): bring the question module back under the 300-line limit

Move answer encoding (pasted answer text and the Claude and Codex selector
keystrokes) into native-chat-ask-answer.ts and re-export it, so importers are
unchanged.
2026-10-21 18:01:39 -07:00
2a45fdc4b6 fix(mobile): capture Android terminal keyboard controls (#31666)
Capture Android terminal keyboard controls in the OTA live input before native web input edits or moves focus. Preserve composition handling, send ordering, and stale-target protection.

Fixes #25496

Co-authored-by: Sangrak Choi <kars@kargn.as>
Co-authored-by: zenghaolun02 <zenghaolun02@baidu.com>
2026-10-21 17:59:21 -07:00
Brennan Benson 349e3d19b9 Agents need only a minimum version for chat; newer releases and prereleases keep working (#30393)
* fix(native-chat): agents need only a minimum version for chat; newer releases and prereleases run

OpenCode chats accepted only the 1.x and 2.x lines and Pi only 0.x and 1.x,
every prerelease was refused, and a slow or failed --version run counted as
unsupported. A new major would open as a terminal and existing chats said
"Orca can't run this agent in a chat here."

Each agent now lists ascending minimum versions; a version must reach the
latest one on or below its own major line, compared by semver. The check
answers ready, too old, unknown (slow, failed or wordless run) or missing
(did not start). Only too old refuses a launch, as Codex already did, with
"<Agent> X is too old for chats. Update to Y or newer." carried in a new
optional refusal fact agentInstallation; Codex keeps sending
codexInstallation for older clients. Codex's reader and error fold into the
same check.

* refactor(native-chat): a launch check declares its minimum versions; create support and the agent health snapshot run the one shared check

* fix(native-chat): read an agent's version from stderr too, so Pi 0.73 is known too old instead of unreadable
2026-10-21 17:57:10 -07:00
Neil 0834610eeb fix(mobile): reset agent history when switching desktops (#31695)
Switching desktops retains another host history and can scan its filesystem path on the new host. Reset content by host while preserving same-host reconnect identity.
2026-10-21 17:50:09 -07:00
Neil 03672738fd Fix terminal URLs split across padded output rows (#31713)
* fix(terminal): resolve URLs across padded hard-wrapped rows

* Reject oversized wrapped URL prefixes and isolate reconstruction

* Handle preceding links and uppercase schemes in wrapped URLs

* Restore full lint by extracting props and ask dismissal identity

* Keep wrapped URL clicks consistent and require padding evidence

* Preserve soft-wrapped sibling URLs in cold click fallback
2026-10-21 17:45:30 -07:00
Jinjing ad0bf9a393 fix(native-chat): open inline images directly in preview dialog (#31620)
* fix(native-chat): open inline images directly in preview dialog

* fix(i18n): add inline image preview translation keys
2026-10-21 17:35:48 -07:00
Neil 7a8974b701 fix(mobile): reconcile concurrent legacy terminal closes with current state (#31711)
Concurrent close replies filter captured terminal arrays and resurrect an already closed terminal. Reconcile the current collection synchronously and respect newer selection and metadata.
2026-10-21 17:25:17 -07:00
Neil 250f81ae9f fix(mobile): own commit generation by workspace and admit once (#31709)
* fix(mobile): own commit generation by workspace and admit once

AI commit generation crosses workspace transitions and admits duplicate calls before a render. Retire the active run on scope changes and use synchronous admission.

* fix(mobile): retire commit generations only after scope commits
2026-10-21 17:24:58 -07:00
Neilandwelldoingk 50da78aa85 fix(mobile): detect New Workspace agents in the selected repo runtime
Pass the selected repository to New Workspace agent detection through the host-authoritative runtime resolver, including WSL paths and Windows project preferences. Preserve old-client request shape, SSH routing, and strict ownership/refusal behavior.

This merge credits @welldoingk’s original WSL agent-detection investigation and community PR #20101, adapted to the current host-resolved contract.

Co-authored-by: welldoingk <thkwon96@gmail.com>

Fixes #20060
2026-10-21 17:24:31 -07:00
Jinjing 718fb9fdc4 Extract shared image clipboard copy utility (#31600)
* Extract image clipboard copying into shared utility

* Await image reads before reporting clipboard copy success
2026-10-21 17:24:04 -07:00
Neil a3c9958973 fix(mobile): show commit comparison failures with retry (#31706)
* fix(mobile): show commit comparison failures with retry

Comparison refusals, transport errors and malformed payloads appear as No file changes. Decode accepted data, preserve loaded files on failed refresh, and offer Retry.

* test(mobile): record commit comparison errors and retry affordance

* refactor(browser): simplify address bar callbacks within lint limit

* style(browser): keep client-hosted pane within line limit

* fix(i18n): retire obsolete Portuguese default zoom entries
2026-10-21 17:21:16 -07:00
Neil aad1fe0a82 fix(mobile): invalidate chat file search on authority migration (#31702)
* fix(mobile): invalidate chat file search on authority migration

Modern search caches and late replies survive authority migration on the same client object. Own cache, capability decision and inventory by logical generation.

* test(mobile): record retired file-search requests after reset

* refactor(browser): simplify address bar callbacks within lint limit

* style(browser): keep client-hosted pane within line limit

* fix(i18n): retire obsolete Portuguese default zoom entries
2026-10-21 17:20:05 -07:00
Kelvin Amoaba b0fcdb493c fix(omp): show OMP's plan review as a plan card and answer it from chat (#31635) 2026-10-22 00:10:02 +00:00
Kelvin Amoaba b17477614d fix(agent-status): questions over 16 KB show their card with every option (#31632) 2026-10-22 00:08:35 +00:00
Neil eb9d2f9a06 fix(security): update Node runtimes for October security releases (#31687)
* fix(security): update Node runtimes for October security releases

* fix(ci): compare normalized web file inventory without ordering artifacts
2026-10-21 17:06:28 -07:00
Jinwoo Hong f47f632bd7 feat(mobile): Home's New Workspace opens the desktop's create drawer over Home (native part ships with the next app build) (#31305)
* fix(mobile): the home screen's New Workspace opens the create sheet again

The native host-list route dropped the home screen's action=newWorktree when it built
the page's route, so the page opened as a plain list. The route now forwards it, and
the page names it back once applied so a reload does not reopen the sheet.

Reaches phones only with the next native build.

* test(mobile): drop the New Workspace tests that only re-check types and a null guard

* fix(mobile): Home's New Workspace opens the create sheet over Home

Replaces the action=newWorktree route hand-off. The quick action picks a
host (as Tasks does), then mounts the host's create sheet on Home; a
create opens the new workspace's session marked as just created.

The sheet controller becomes a plain visible/onClose component, and the
host screen's route-action state, the action route param and its builder
are deleted.

* fix(mobile): Home's create sheet seeds only proven worktree rows

Home's cold-start seed is a pathless resume row that the name suggester
cannot read, so the sheet now seeds from proven rows and is keyed by host.
The host screen gets one setter for the sheet's visibility and its ref.

* test(mobile): name the Home new-workspace test after the screen it renders

* proto(mobile): Home's New Workspace opens a native drawer hosting the page's create form

Home pushes /new-workspace/<host>, a react-native-screens formSheet over Home whose content is
MobileWebShellScreen for the page route /h/<host>/new-workspace. The page renders the shared
Create Workspace form with its drawers inline, so the form and its pickers fill the native
drawer. The shell's loading, download and failure states render inside the drawer. Prototype only.

* proto(mobile): the New Workspace drawer is the host screen's own sheet over a transparent modal

The native route is a transparentModal hosting the shell over Home; the page route renders the
host screen's NewWorktreeModal on a transparent page, so the drawer is the same component. While
the page loads, the shell shows a placeholder of that sheet (scrim, grabber, spinner or download
bar), and the page's sheet opens in place without its slide. A host that cannot serve the page
(old desktop, blocked, failed) opens the host list instead of a wall in a drawer.

* feat(mobile): Home's New Workspace opens the page's create drawer over a dimmed Home

A transparent native overlay route hosts the host picker and then the page's
/h/[hostId]/new-workspace route, which renders the same NewWorktreeModal as the
host screen's +. The native scrim matches the drawer backdrop and the page's
first drawer opens already dimmed, so Home stays dimmed throughout. Old desktops
without the route fall back to the host list.

* fix(mobile): one loading layer for Home's New Workspace overlay

The overlay route owns a single tappable loading layer (spinner, or the
download on a small card) that stays mounted from the pick until the page
paints, so the fade and spinner no longer restart per shell state. The shell
reports its load and shows the host list for anything it cannot open; the
picker hands the pick over at once and keeps sliding away. The page's first
drawer takes the dimmed backdrop once, and a gate's overlay pending view no
longer stacks a second scrim over a mounted stack. Native /h/[hostId]/new-workspace
redirects to the overlay. Drops the unused session-route params, the
clipboard grant, and pass-through tests.

* feat(mobile): New Workspace loads in a short drawer over an undimmed Home

While the page loads, the overlay shows one short native drawer in the shared
drawer chrome with a spinner or the download, and nothing dims Home: a tap
above it, a drag down or Back cancels. The page's real drawer then slides up
with its own backdrop exactly as on the host screen's +. Drops the native
scrim, the loading card and the already-dimmed backdrop handoff; the overlay's
WebView stays invisible until the page paints, since a fresh one draws opaque.

* feat(mobile): the New Workspace loading drawer dims Home like any drawer

The short loading drawer opens with the shared drawer backdrop and show
duration, and a tap on that backdrop, a drag down or Back cancels. It stays
in the screen's own window (a native Modal over the loading WebView stalls
the page) and goes in the frame the page paints: the shell reports its load
from a layout effect so the WebView shows and the drawer leaves together.

* fix(mobile): the page's first paint waits for a drawer opening with it

A drawer opening as its page screen mounts holds that screen's first-paint
report until its open animation ends, through a hold on the existing report
rather than a new bridge message. The New Workspace overlay's loading drawer
now stays until the create drawer is up, so a cold open no longer shows Home
undimmed between the two.

* refactor(mobile): the New Workspace overlay owns its exits; the loading drawer fades out

The shell no longer imports the feature: an overlay is a shell with
onOverlayLoad, which reports `unavailable` for what it cannot show, and the
overlay route opens the host list for every exit. HostProtocolGate takes one
transparentWhilePending flag. The loading drawer's drag handler follows a
stable cancel, and on paint the drawer fades out eased in, as the shell's page
cover does, because the WebView takes a frame or two to show the page after it
is revealed; removing it outright left Home undimmed for those frames.

* Revert "fix(mobile): the page's first paint waits for a drawer opening with it"

This reverts commit be0913b9d5.

* fix(mobile): Home's host picker leaves when there is no desktop to pick

An unreadable host catalog, or offered desktops that are all gone, left the
picker invisible inside the transparent overlay, holding a screen over Home
with nothing to dismiss it. The catalog read now resolves to no hosts on
failure, and an empty pick list goes back to Home.

* fix(mobile): Home's create drawer names SSH hosts as the host screen does

The page drawer opened from Home passed no host labels, so a project on an SSH
host showed its run target by raw host id. Reading the SSH target summaries
and display overrides moves into one function that both the host screen's
metadata fetch and the page drawer call.
2026-10-21 20:02:11 -04:00
Jinwoo Hong 79c4b46624 fix(browser): bring the address bar back under the 400-line limit (#31727)
#31656 left BrowserAddressBar.tsx one counted line over max-lines, which fails
Lint for every PR. The six copies of clear-a-timeout-ref become one helper.
2026-10-21 20:01:34 -04:00
Neil 4ed095d328 refactor(linear): group issue-context reads under concrete owners (#31686)
* refactor(linear): group issue-context owners and use direct imports

* test(linear): mock concrete current-context owner in WSL move suite
2026-10-21 16:58:22 -07:00
Kelvin Amoaba d15677e334 fix(native-chat): Codex async questions show an answer card on desktop and phone (#31652) 2026-10-21 23:51:47 +00:00
Kelvin Amoaba e34b3709b3 feat(browser): new tab page with a workspace card and suggested sites (#31656) 2026-10-21 23:50:12 +00:00
Kelvin Amoaba 54ef452d71 feat(mobile): load earlier chat messages on scroll-up, keeping the reader's place (#31339) 2026-10-21 23:49:32 +00:00
Kelvin Amoaba 106716c700 fix(mobile): say why a terminal paste sent nothing (#31640) 2026-10-21 23:48:58 +00:00
Kelvin Amoaba c5e07bc3af feat(browser): per-site appearance and zoom, from the page menu and Cmd/Ctrl +/-/0 (#31621) 2026-10-21 23:47:12 +00:00
Kelvin Amoaba 7e795b6be6 feat(browser): show every page load, link clicks included, as a line along the address bar on desktop and mobile (#31597) 2026-10-21 23:46:33 +00:00
Kelvin Amoaba 06861d9072 feat(browser): Chrome-style new tab and address bar suggestions (#31604) 2026-10-21 23:44:40 +00:00
Brennan Benson d085427628 Keep model choices coherent across Claude updates (#30107)
* Use one model catalog per execution scope

* Accept a missing session record in host scope

* Merge duplicate launch-resolution import

* Fix catalog typecheck errors

* Align catalog tests with the one-list design

* Type the integration fixture's send without a cast

* Give the discovery test complete runtime dependencies

* Fix model catalog completion and scoped configured display

* Preserve model catalog authority across released clients

* Fix mobile first-frame catalog fixture narrowing

* Keep startup catalog persistence with one completion owner

* fix: preserve chat selections across CLI model updates

A newer CLI can omit a model that an existing chat still runs. Keep that chat's selected model and effort instead of replacing them from the available list.

Discover one model list per host, account, launch command and folder, refreshing after two minutes. Keep current values and the running model's exact effort choices with its chat. Show only the known effort when Claude supplies no matching menu.

Preserve older paired-client delivery and configured launch arguments. Make Codex use independent discovery and retain the established ACP and Pi paths.

Tracked in STA-10947.

* fix: keep the Pi options read within its file limit after main's growth

Tracked in STA-10947.

* fix: import the option snapshot's shared module once

Tracked in STA-10947.

* fix: keep Grok and Pi model menus and fast controls across restarts

Restore the established ACP and Pi catalog route: newer listed models and qualified startup defaults stay visible, and genuine retained effort menus survive later reads. Keep exact-child Fast and Speed controls. Update the phone option contracts and the remaining stale expectations to the selection rules.

Tracked in STA-10947.

* fix: share each workspace's model list and keep live Grok and Pi menus

New chats read their workspace's own model list through the one shared request for that workspace, so a second chat in the same folder starts on the model and effort the agent actually runs there, and a slow first answer can no longer overwrite a newer list. A running Grok, OpenCode or Pi chat keeps its live model menu when an older shared list arrives later. Update the tests that still expected the replaced behavior.

Tracked in STA-10947.

* fix: keep separate agent and chat model lists for Grok, OpenCode and Pi

A brand-new Claude chat now offers the selected model's effort levels before the first message, as it did before. Grok, OpenCode, omp and Pi again keep the list read from the installed agent apart from the list a running chat reports, and each newer list replaces only its own source. So a chat shows the model the agent is actually running with that model's own effort menu, and an effort change can no longer act on an older model's menu. Saved model lists on WSL are readable again without starting the agent.

Tracked in STA-10947.

* fix: keep desktop catalog storage out of the phone's type graph and show Grok's late model answer

The phone app's type check no longer pulls in desktop-only catalog storage. The model-list code no longer reaches the saved-catalog file through a type import. When a Grok model change times out and Grok later confirms the model, the chat shows the model Grok actually runs.

Tracked in STA-10947.

* Remove unrelated merge formatter and package manager churn

* Clarify independent catalog ownership and refresh contracts

* Declare picker demand callback as an effect dependency

* Complete mobile catalog surface and catalog merge types

* Complete catalog contract fixtures for current host types

* fix: preserve catalog source contracts and account fallback

* test: inject independent Codex discovery transport

* test: remove accidental client fixture assertion

* test: qualify the saved account fallback read

* fix(chat): preserve catalog evidence and current child controls

* Fix catalog CI types and mobile option readiness

* Reconcile session controls after inventory changes

* Fix catalog import and deferred test resolver types

* Keep model capability observations separate from picker views

* Use installed model choices for chat options

* Fix model catalog test fixture contracts

* Restore model option compatibility and stable settlement

* Return a catalog reply in provisional launch fixture

* Compose unreported live model defaults in the host

* Preserve explicit replacement model on recovery send

* fix(chat): use stable scoped catalogs on local desktop

* test(chat): preserve generic catalog runner type

* fix: scope catalogs to configured environment and keep saved reads passive

* fix: satisfy catalog regression and controller CI checks
2026-10-21 16:42:27 -07:00
Neil 41fb3c7a9c fix(mobile): retire host edit navigation after leaving the route (#31692)
Saving an endpoint after the user leaves can pop a second screen. Retire navigation ownership while allowing the endpoint save and client refresh to finish.
2026-10-21 16:41:13 -07:00
Neil 74d11d93d0 fix(mobile): preserve notification changes across delayed focus reads (#31690)
A focus read started before Enable can finish afterward and switch notifications back off. Fence reads against permission and preference mutations.
2026-10-21 16:41:08 -07:00