Commit Graph
15790 Commits
Author SHA1 Message Date
Kelvin Amoaba 752f531b0a fix(chat): send pi and acp messages after a model-only change (#32004) 2026-10-22 21:10:01 +00:00
Jinjing e61d062ef1 Revert HTML preview by default for the release (#31575) (#31893)
* Revert "Preview HTML files by default" (#31575)

Restore source/diff opening for general HTML file-opening routes for the release.

This reverts commit 524fa0039b, retaining the
published files.readDocPreview capability for mixed-version clients and removing
an unnecessary restored test assertion to satisfy the current lint gate.

Existing explicit previews and terminal HTML links retain their previous behavior;
the underlying local-file preview exposure remains tracked in STA-11670.

* Preserve file ownership and symlink guards during HTML preview rollback

Keep the existing owner checks after file-drop uploads and tab-entry stats so
an ownership change cannot reopen a file through a stale host. Preserve local
preview source checks so symlinks outside the workspace keep user-file access.

The race and symlink tests reproduce failures on the mechanical revert before
these guards are retained. Cover HTML source opening for folder workspaces,
Explorer, and Source Control without restoring preview-by-default.

* fix: split tab entry browser requests to satisfy lint limit
2026-10-22 13:45:36 -07:00
Neil 59497680d8 refactor: colocate Jira issue summary generation (#32099) 2026-10-22 13:42:03 -07:00
Jinjing e5df1522ce Gate draft image ownership checks on relevant local attachments (#32098) 2026-10-22 13:29:27 -07:00
Jinwoo Hong 7b70a290e9 feat(agent-write): opt-in launch cancel, approval wait and process fence on the shared agent writer (#32092)
* feat(agent-write): opt-in launch cancel, approval wait and binding fence on the shared writer

executeAgentLaunch takes an optional signal; sendTerminalAgentPrompt (and
sendAgentTurn) take optional onPermission: 'wait' and expectedBinding. No
existing caller sets them, so every current path is unchanged.

* fix(agent-write): keep the default writer path main's; fence cancel and real submit boundary

Only opted-in writes take the submit layer, so default timing and the leaf
re-read are unchanged. Cancellation is rechecked before a structured-refusal
terminal fallback, and an opted-in launch learns Enter from an onSubmitWritten
callback at the write itself, OMP's combined frame included.

* fix(agent-launch): a cancel after a failed startup spawn recovers the created workspace
2026-10-22 16:25:03 -04:00
OrcaWinandOrcaWin b77d0eb76d perf(terminal): keep forward cursors for PR URL schemes (#31949)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-22 13:24:10 -07:00
OrcaWinandOrcaWin ac9dc0bc0d perf(ai-vault): bound scope cache write batch enumeration (#31950)
* perf(ai-vault): bound scope cache write batch enumeration

* test(ai-vault): type scope cache stamp fixture

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-22 13:20:23 -07:00
OrcaWinandOrcaWin c1709f8f81 perf(mobile): index diff review notes by file path (#31943)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-22 13:20:16 -07:00
OrcaWinandOrcaWin 2a7942096f perf(ai-vault): reuse parsed Qoder transcript records (#31938)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-22 13:20:10 -07:00
OrcaWinandOrcaWin c8059ee3c0 perf(chat): avoid repeated skill catalog scans while building the picker (#31936)
* perf(chat): remove repeated scans and array copies from the skill picker

* perf(chat): keep picker grouping within the module line limit

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-22 13:20:06 -07:00
OrcaWinandOrcaWin 5dbd23364b perf(layout): index records once while patching session lists (#31935)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-22 13:20:02 -07:00
OrcaWinandOrcaWin 1a932d62aa perf(markdown): scan wiki-link delimiters without revisiting the document tail (#31934)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-22 13:19:58 -07:00
Jinwoo Hong 24f5c57030 A chat started in a server workspace drawn from the server's layout stays where you opened it (switch-on builds only) (#32003)
* fix(L273-core): tabs this desktop created in a paired server's frame-drawn workspace survive the server's frames (switch-on)

A tab the server's layout never lists (a client-hosted browser tab today; any kind recorded the
same way) used to vanish on the server's next frame rebuild. This desktop now records such tabs by
server and workspace, in its localStorage only (never a session field nor sent), and the server's
frame builds keep them as drawn, inside the server's own groups:
- A held tab drawn in a group the server lacks moves to the nearest server group; it shows there
  only if the user was on it, and the user's group follows it. The group tree stays the server's.
- The record forgets an id once the server's frame lists it, on close, and on worktree removal,
  and prunes ids the restored session no longer has before the first frame after a reload.
- A held tab's drag moves it here only (no session.tabs.move); it offers no new split, and a
  server tab sharing its group counts only the server's tabs for split no-ops.
- Client-hosted staging records its tab (both ids) in layout-owner builds; restage takes the
  worktree from its caller instead of scanning the store.

* refactor(L273-core): the desktop-created record is inert with the switch off; clearer names

- With the switch off the record is never read, kept or written (a single gate in the record).
- The frame-drawn check is isDesktopCreatedTabInFrameDrawnServerWorkspace in
  frame-drawn-desktop-created-tabs.ts: "held" stays the frame hold's word.
- The held-tab placement drops its unread activeGroupId; the group follow derives it.

* wip(L270): a host advertising agentSession.create.tab-placement.v1 opens a created chat's tab where the creating client asked, in its own group and tab ids

* test(L270): a switch-off windowless host saves a chat opened with no client place at the end of the remembered active group, as before (test only)

* wip(L270): a desktop's chat in a frame-drawn server workspace stays put: the frames hold its provisional tab until the server lists it, and the create names its group and the server tab before it (layout-owner build only)

* test(L270): a switch-on serve saves a client's chat after the tab it named; the server-layout read moves to one e2e helper (test only)

* refactor(L270): the create's tab place keeps its own name, and the layout rule's comment names the client's place first

* refactor(L270): the create's tab fields live in their own schema file and the windowless save passes its chat through, so no file passes its line limit; the placement test types its mocks without assertions

* refactor(L270): the chat's place names server tabs through H5c's route, which also skips tabs the server's frames do not hold

* fix(L270): a chat opened while a reconnected server stream awaits its snapshot still names its group; the layout-owner e2e list runs the placement spec

* fix(L270): only a create in a frame-drawn server workspace awaits its tab place, so every other create keeps its timing (the eager History joiner after an early terminal close opens again)

* fix(native-chat): read a frame-drawn chat tab's place without a tick, so an eager History joiner's create is still sent

* test(native-chat): type the create's params so an async placement cannot compile; name the probe that fills the cache

* test: type the placement mock's original through a type import
2026-10-22 16:16:43 -04:00
Jinwoo Hong e710e89832 A paired desktop hands its editor tabs to the server before drawing a server workspace from the server's layout (switch-on builds only) (#32088)
* wip(h5c-1): server workspace layout intents go to the server's main; positions per stream

* wip(h5c-1): e2e without a cast

* fix(h5c-1): a routed move never moves here; routed callers read the server's stream; a restarted main starts its positions over

* fix(h5c-1): layout-apply-send back under its line limit

* test(h5c-1): a routed answer reaches the handler with the server's stream key

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

* fix(terminal): a CLI or phone close through main keeps a pinned tab, as the window-era close did

* feat(terminal): main applies phone closes, renames, pane titles, sleeps and agent-launch tabs itself, with or without a window (layout-owner build)

* fix(terminal): a tab close through main also ends the tab's dormant SSH relay session; agent-launch tabs main writes keep the window's focus rule, prompt and outcome (layout-owner build)

* feat(terminal): main starts a new tab's first terminal under the window's ids, and the pane attaches to it (layout-owner build)

* test(terminal): tests that counted the window's create calls or publications see main's start of a new tab

* fix(terminal): an account-switch restart binds only into a pane main's layout holds, with no window tree (layout-owner build, L197)

* test(terminal): the new-tab start tests hold no type assertions; the start's ledger row is L252

* fix(terminal): a new tab's pane attaches to any terminal main binds it to, and a failed start keeps the tab's startup

* test(terminal): a Claude account-switch restart's tests draw the pane main holds

* test(d3a-2): the buffer view index follows a rename main answers

* fix(terminal): a launch pane main takes back still tells the window why, which drops its hold and prompt; a rename through main answers the name it stored (layout-owner build)

* fix(terminal): a new tab another owner's terminal won attaches to it, and a failed start's startup never runs twice

* fix(terminal): main's pane close reads its stop result as a type only, so no runtime module loads before its base class

* fix(terminal): a phone's close through main drops the closed rows from what phones list at once, not at the window's next publish (layout-owner build)

* chore(terminal): mark the phone-close row drop interim (ledger L260)

* test(worktree-create): a new local tab's startup counts as run when main starts it (layout-owner build)

* test(worktree-create): record main's starts without a type assertion

* fix(terminal): a tab closed before main declines its start gets no startup back (layout-owner build)

* feat(h5d): hand desktop-only editor tabs to a layout-intents server before its frames draw the workspace

A paired desktop dropped an editor tab it opened in a server workspace (with its open file and
unsaved draft) on the server's next records frame once that workspace became frame-drawn, and
editor opens made after gating hit the same loss.

- openEditorTab is now on SERVER_ROUTED_LAYOUT_COMMANDS: the proposed tab id and group are kept,
  the file id becomes the server's own, and the record loses the desktop's runtimeEnvironmentId.
- Before a workspace gates, server-layout-frames hands its desktop-only editor tabs to the
  server's main (sendToServerMain), recomputes them on each answer, and gates only when all
  answered ok or tab_exists. Any other refusal or a failed call keeps it on the mirror for that
  connection; the next one retries. No timer, no hold store.

* fix(h5d): only an applied open counts as handed over; tab_exists keeps the tab on the mirror

* fix(terminal): the window's new tab starts in main when its own window asks over IPC, which labels itself a client (layout-owner build)

* feat(h5c-2): a paired desktop's writes to a server workspace its frames draw land in that server's main (layout-owner build)

- Routes setTabProps, setChatPane and non-terminal renameTab to the server's main as layout.apply;
  closes, starts, sleeps and terminal names keep the RPCs that main now applies (session.tabs.close,
  terminal.close, terminal.split, session.tabs.createTerminal, terminal.rename). A window-style
  closeTabs is never routed: main takes it as already stopped and stops nothing.
- A close the server refuses (a pinned tab) redraws the workspace from the server's frames, so the
  window's optimistic drop never outlives the answer; same for a refused pane close.
- A frame-drawn workspace keeps none of the mirror's ledgers (L240): no tab-id mappings, close or
  focus intents; they stay only for servers without layout.intents.v1 until the Phase 2 cutoff.
  Its pane-geometry push is skipped (it goes as intents); pane renames still push.
- e2e: paired-desktop-server-layout-writes covers close (layout, session.tabs, terminal stop),
  rename, split and new tab on a windowless switch-on serve.

* fix(h5c-2): an undelivered close of a frame-drawn tab is held for replay; a launch-agent clear stays off the server route

* refactor(h5c-2): the undelivered-close test moves to its own module, keeping the lifecycle file under its line cap

* fix(h5d): a tab closed while its hand-over was in flight is closed on the server before the workspace gates

* fix(h5d): an editor tab closed in a workspace a server's frames draw closes on that server by session.tabs.close; a tab kept on the mirror closes here only

* fix(h5d): a handed-over tab closed here before the gate is closed on the server first; a server-opened tab's close in a frame-drawn workspace is covered

* chore(h5d): a closed stream also closes the hand-over its hand-back started

* refactor(h5d): move the frame stream's routing inputs out of server-layout-frames to keep it under the line cap

Main's #31869 grew server-layout-frames past 300 lines once merged with H5d; the two helpers move unchanged.
2026-10-22 15:47:43 -04:00
Jinjing 64b734774b Set minimum width for native chat table cells (#31848)
* Set minimum width for native chat table cells

* Allow markdown table cells to wrap long content

* Set minimum width for native chat table cells
2026-10-22 12:46:49 -07:00
Neil 2e6963d707 refactor: colocate Jira query construction (#32089) 2026-10-22 12:28:18 -07:00
Jinjing 94bea877ea Stabilize remote host status bar during retries (#32083)
* Stabilize remote host status bar layout during retries

* Add connecting status translation for remote hosts
2026-10-22 12:27:14 -07:00
Jinwoo Hong db6ab720bc Install shared skills for 11 more agents and stop Qoder listing skills twice (#31657)
* Install shared skills for 11 more agents

Add skill install provider entries for OpenClaude (own .openclaude folders,
OPENCLAUDE_CONFIG_DIR), and OpenCode 2, MiMo Code, OMP, Prime Agent, Qoder CN,
Codebuff, Freebuff, Ante, Jcode and DeepSeek Harness, which read the shared
.agents/skills folder at both scopes. A coverage test now requires every Orca
agent to have an entry or a recorded reason it has none.

* Use the shared skills folder for Qoder

Qoder's CLI loads .agents/skills at both scopes by default, so its own
.qoder/skills placement made every skill appear twice.

* Keep scanning Qoder's own skill folders

Qoder still reads .qoder/skills, so discovery keeps both folders as
discovery-only roots now that installs go to the canonical root.
2026-10-22 15:23:40 -04:00
Jinjing 8fc1698ebb Add Resource Manager PTY inventory and checked terminal termination (#31593)
* feat(resources): end local terminals with checked identity checks

- Add a PTYs view to the Resource Manager that lists local terminals and can end one. Before stopping, the host confirms the terminal's incarnation, agent ownership, and attachments. This prevents ending a same-ID replacement that started after the list was read.
- Bump the daemon protocol to v50 and gate the new fenced kill on it. Older daemons ignore the extra fields, so the client refuses End instead of sending an unchecked kill.
- Admit diagnostic reads (PTY listing and memory snapshot) only while the Resources view is visible, so a hidden window does no background scanning.
- Report incarnation and host on spawn and exit events so the renderer can match them to the terminal it showed.
- Replace the popover's terminal-session list with the new inventory and move memory polling into the Resources view.

* Show unrouted daemon PTYs in the Resource Manager

* Simplify PTY controls and clarify safe termination

* Format local build compatibility protocol lists

* Add PTY resource management localization strings

* Harden PTY resource manager test fixtures

* Use typed IPC event in PTY kill test fixture

* Refine local PTY resource view model typing

* Refine local PTY resource view model typing

* Include local host identity in PTY lifecycle event tests
2026-10-22 12:20:01 -07:00
Brennan Benson 78bfc4d956 fix(acp): OMP chats send images; an ACP agent's own startup answer decides (#30910)
* fix(acp): OMP chats send images; an ACP agent's own startup answer decides

Images reached an ACP agent only when it advertised them and Orca also listed it, and OMP was never listed. Trust the advertised capability alone, offer attachments to every ACP agent until its live report narrows them, and send an image's @path reference to an agent that takes none instead of refusing the message.

Tracked in STA-11152.

* fix(acp): preserve image file references when native content cannot fit
2026-10-22 12:14:00 -07:00
Jinjing f1189be850 Offer Resolve with AI for paused merge, rebase, and cherry-pick (#28569)
* Offer Resolve with AI for paused merge, rebase, and cherry-pick

A paused operation can need recovery after every conflict is staged, but the Resolve with AI action only appeared when unmerged paths remained. The operation banner now offers it whenever AI actions are visible and an operation is in progress.

Starting an AI conflict-resolution agent is also blocked while a Git operation is running. The composer, the banner and card buttons, and the agent launch itself check the running-operation state, so a launch that began during agent detection cannot start while Git is still running.

* Block paused-operation recovery only on Orca's own Git work

The previous gate scanned the host's process table and refused "Resolve with AI" whenever it could not prove no Git process was running. That refused recovery on older paired hosts, on Windows whenever any Git process ran anywhere on the machine, and while a paged `git reflog` or `git grep` was open in a terminal.

- The host tracks Orca's own mutating Git commands per worktree (including ones still queued) and reports them through `git.mutationInFlight`. Reads and fetch/push/ls-remote are not tracked.
- Recovery is refused only when the host reports that work. Older hosts without the method keep today's behavior.
- The gate applies only to launches from the paused-operation banner and conflict card, not every resolveConflicts launch, so the Checks panel's PR-conflict flow is unchanged.
- The agent prompt now tells the agent to stop if index.lock exists or another git command is running, which covers Git started outside Orca on every host. With no unmerged files, the prompt asks the agent to find why Git paused instead of listing zero conflicts.

* Preserve Git mutation state until terminated commands exit

- Cancel stale recovery checks and agent launches when their dialog or scope closes
- Guide rebase recovery agents to amend fixes after failed exec steps

* Cover failed rebase exec recovery with real Git

* Recognize global Git options in worktree mutation tracking
2026-10-22 12:08:38 -07:00
Brennan Benson 814bb59ef6 fix(runtime): release remote streams when the renderer reloads (#29104)
* fix(runtime): release remote streams when the renderer reloads

* ci: re-run code scanning (no source change)
2026-10-22 11:42:13 -07:00
Jinwoo Hong 43118aeb7c fix(worktree-create): a workspace created on a paired server shows one set of default tabs when the same path exists on another host (#32080)
* fix(worktree-create): a workspace created on a paired server seeds against that server, so a same-path twin on another host can't duplicate its default tabs (L277)

* fix(worktree-create): name the create's host once, keeping the flow under its line cap; type-only test import
2026-10-22 14:29:56 -04:00
Neil a1448f9f58 refactor: colocate Jira image and stream contracts (#32079) 2026-10-22 11:10:10 -07:00
Jinwoo Hong 4cfa3977e8 A server-hosted browser tab opened in a server workspace drawn from frames survives other changes to that workspace (switch-on builds only) (#31920)
* fix(L273-core): tabs this desktop created in a paired server's frame-drawn workspace survive the server's frames (switch-on)

A tab the server's layout never lists (a client-hosted browser tab today; any kind recorded the
same way) used to vanish on the server's next frame rebuild. This desktop now records such tabs by
server and workspace, in its localStorage only (never a session field nor sent), and the server's
frame builds keep them as drawn, inside the server's own groups:
- A held tab drawn in a group the server lacks moves to the nearest server group; it shows there
  only if the user was on it, and the user's group follows it. The group tree stays the server's.
- The record forgets an id once the server's frame lists it, on close, and on worktree removal,
  and prunes ids the restored session no longer has before the first frame after a reload.
- A held tab's drag moves it here only (no session.tabs.move); it offers no new split, and a
  server tab sharing its group counts only the server's tabs for split no-ops.
- Client-hosted staging records its tab (both ids) in layout-owner builds; restage takes the
  worktree from its caller instead of scanning the store.

* refactor(L273-core): the desktop-created record is inert with the switch off; clearer names

- With the switch off the record is never read, kept or written (a single gate in the record).
- The frame-drawn check is isDesktopCreatedTabInFrameDrawnServerWorkspace in
  frame-drawn-desktop-created-tabs.ts: "held" stays the frame hold's word.
- The held-tab placement drops its unread activeGroupId; the group follow derives it.

* fix(L282): a client-hosted browser tab this desktop creates in a frame-drawn server workspace completes once the server's snapshot lists it, instead of undoing itself after 8 s (switch-on)

* fix(L284): a server-hosted browser tab this desktop creates in a frame-drawn server workspace completes when the server's frames take it over, instead of reading as the user's close (switch-on)

* chore(L282/L284): adopt only staged client-hosted handles, and name the frame build's handover note (review nits)

* fix(L284): a staged tab the server's frames took over completes the create though its pane, unmounted, has no handle yet (layout-owner build only)

* fix(browser): a switch-on host answers a paired client's browser create with the new page's id, not its workspace's, before main's frame draws it

* fix(browser): the reply names the page id the create asked for first, then the new record's own page (L291)

* fix(L284): the frame build scans dropped pages for a handover only when a staged create's page is drawn, so a large close stays one pass

* fix(L285): a staged server-hosted browser tab stays through the server's frames until the server lists its page, then hands over to the server's own tab (layout-owner build only)

* fix(L285): release a held browser tab by the server listing its page, staged or adopted, in one scan per frame (review)
2026-10-22 14:01:59 -04:00
Neil a45bd06eac refactor: colocate the stored-agent contract (#32067) 2026-10-22 10:22:51 -07:00
Brennan Benson 726c572da7 Attach a long paste as a text file, and say a message is too large before it is sent (#30398)
* fix(native-chat): attach a long paste as a text file, and say a message is too large before it is sent

A paste of 32 KB or more (or one that would push the message past the 256 KB
send limit) into a structured chat is saved as a file the agent reads, through
the same pending-chip and @path route a dropped file takes: Orca's paste folder
here, or the chat's attachment store on its paired server. Claude gets the paste
folder as an allowed directory. A message still over the limit is handed back
before it is sent, on desktop and the phone, with words naming the limit instead
of the generic 'not sent, send it again'.

* test: Claude launch parity snapshot grants the paste folder

* fix(native-chat): a long paste replaces the selection at once and settles in place

A long paste now puts an inline Pasted text pill where it went, replacing any
selected text immediately, and the saved file (or inline text, or the original
selection on failure) replaces that same pill when the save finishes, in its own
chat's draft. Nothing reads the caret later, so order and position hold. The
paste never joins the image row, so image numbers are unchanged. The size check
measures the real message body with the selection replaced and images included.
A server that keeps no files takes the paste inline when the message still fits.
A pending paste a restart lost shows Paste not saved, and Send says so.

* fix(native-chat): type the pasted-text parser and the composer's missing draft scope

* fix(native-chat): admit paste references before goals and commands

* fix(native-chat): resolve staged paste paths without editing the draft

* fix(native-chat): consolidate paste imports for code quality checks

* fix(native-chat): retain checked string types for saved paste paths

* fix(native-chat): retain completed paste facts through history and delimit paths

* fix(native-chat): keep structured question rendering within file limit

* fix(native-chat): pasted or inserted text that looks like a paste token stays literal

* fix(web): keep the browser's UI bridge under the file length limit

* style: format the files #29982 merged unformatted

* fix(native-chat): retain oversized drafts before send admission

* fix(native-chat): keep the composer handle's merge resolution to main's destructure
2026-10-22 10:16:32 -07:00
Jinwoo Hong f8aa6854c7 A phone's new terminal lands after the tab it names, and its split selects the moved tab as the window does (dark, layout-owner build only) (#31924)
* fix(session-tabs): a tab main's groups lack keeps the place its writer listed it, so a headless phone lists a chat first (layout-owner build, L283)

On a host with no window, a rebuild writes its rows under a group id main's layout lacks. The row
writer's reorder treated every row of that group main does not hold (a chat, a browser page) as a
stray and appended it to the active group, so a chat the user moved first came back last, in both
the groups and the tab list. Strays now go after the id they follow in the writer's order (else
first), the same rule a group main does hold already used; the window publication path shares it.

* fix(session-tabs): a phone's split move and its create after a split tab go through main's layout on a windowless host (layout-owner build, L288, L289)

On a host with no window, a phone's split move only rewrote the runtime's own rows, and its new
terminal anchored after a split tab reached main's layout with no anchor. Every write of the rows
takes main's groups and order, so the split collapsed back into one group and the new tab landed
after the trailing tab. The split is now main's splitGroup command (the moved tab selected in its
new group, as a desktop drop), and the create passes the anchor's tab to main's createTerminalTab,
as a window's create does. An older phone sends no anchor and still gets the end.

* test(session-tabs): type the split move's async reply in the dispatch stub, and read an optional group map (L289)

* fix(session-tabs): a phone's split goes to main's layout only for a tab main holds, as the window sends it (layout-owner build, L289 review)

With the Store's write notices, today's runtime-only split already reached main's groups through
the session write; what it lost on a windowless host was the selection: the moved tab and its new
group did not become the active ones, as they do after a window's drop. The split stays main's
splitGroup for a tab main's groups hold, with the window's checks: a drop main's groups make a
no-op sends nothing (the window's pane-column rule, now in shared), and a refusal is dropped
silently. A row main does not hold (a chat or browser tab) splits as today, with no error. The
create anchor and its layout-owner hand-off move to their own module. The surface test fixture's
session writes now notify listeners, as the Store's do.

* chore(session-tabs): log when main refuses a phone's split, as the window logs its own refusals

* fix(session-tabs): a phone's terminal created after a chat or browser tab main holds lands after it, in its group (layout-owner build, L288 follow-up)

Since browser and chat tabs went main-first, main's layout holds them, but a windowless create
handed main the row id (agent-session:<id>), which is not main's tab id, so main put the new tab
at the end of its first group. The anchor is now resolved to main's own tab, as a window's create
names it; main's read of a workspace's layout is one method the tab-move path shares.

* fix(session-tabs): one move path: main's tab move selects a split tab in its new group and skips the window's no-op split (layout-owner build, L289)

A phone's split now goes only through main's tab move (#31812); this branch's own split routing is
gone. For a split only, main's tab move now does what the window's drop does: it sends nothing for
a drop that changes nothing (the window's pane-column check, reused), and after main applies the
split it selects the moved tab and makes its new group active. Refusals keep that path's codes.

* fix(session-tabs): resolve a create's anchor to main's tab inside the tab-move module, so only the old-client boundary's listed importers read old-client rows (L288 follow-up)

* perf(session-tabs): read main's layout for a create's anchor only when main places it, so switch-off and appending phones skip that load

* fix(session-tabs): restore the row-to-tab lookup the split selection and create anchor use, after #31925 dropped its import (layout-owner build, L288, L289)

#31925 removed the L280 bridge in main's tab move, and with it the move's last use of
resolveSessionTabRow, so it dropped the import; this branch's split selection and create-anchor
lookup still resolve a row to main's tab through it. A row main's layout does not hold is now
refused by main's move (tab_not_found) as #31925 rules; the phone test asserts that.

* test(session-tabs): the L283 phone-order test opens its chat as the host does and uses a store that announces its writes (layout-owner build)

Since #31925, main moves every tab of a workspace it owns and refuses a row its layout does not
hold, and since D3b a chat the host opens is saved in main's layout. The test now opens the chat
through the host's publish path and gives the runtime a profile-like store (partitions listed,
writes announced), so the reorder reaches main's layout and its commit reaches the rows.

* test(session-tabs): type the L283 test's profile-like store as the Store contract declares it

* revert(session-tabs): drop the stray-placement change; main's layout holds chats and moves every row now (L283)

Since D3b a chat the host opens is saved in main's layout, and since #31925 a phone cannot move a
row main does not hold, so the rebuilt-group stray this placed no longer reaches a phone; the
remaining strays are brief, and appending them gives the same order. withStrayIn and its callers
return to main's behaviour, with its unit test. The phone-order spec stays as a guard that a
windowless chat moved first stays first after a rebuild.
2026-10-22 13:12:56 -04:00
Brennan Benson 9613563f5c refactor(pty): one commit step for both terminal start lanes (#31596)
* refactor(pty): one commit step for both terminal start lanes

The window's pty:spawn commit and the host runtime controller's commit were two
copies of the same job (ownership, binding save, registration, size, hidden
mark, publication, reservation settle), so every fix had to land twice.

Both lanes now call one commitPtySpawn and supply what differs as data: the
binding each lane builds, its registration facts, pane key and aliases, the
Codex launch env and memory session id. Three inputs still select behavior:
hiddenDelivery (the renderer-scoped mark is refreshed here; the runtime-owned
mark stays at the host runner's await boundary), earlyExit (the window drains an
exit during start, the host throws) and joinPayload (what reservation joiners
and the caller receive).

One publication order for both lanes: note commit, Codex pane account, renderer
hidden mark, handle, size set with reflow, old-id size cleanup, serializer
readiness, restore records, command detector, telemetry, pane key and aliases,
memory registry, resume identity, inventory, Codex lifecycle, settle. Every
moved step was audited for read/write independence. Stable-owner adoption and
the adopted-claim publication set are unchanged.

Also pins that an agent launch whose record cannot open takes its tab back and
spawns nothing, and re-labels three TEMPORARY comments to their current owners.

* fix(pty): preserve adopted claim return scheduling

* fix(pty): keep shared commit reply types precise
2026-10-22 10:03:07 -07:00
Brennan Benson 199fc7b2c1 A Pi chat shows the turns made outside it (#31618)
* feat(native-chat): a Pi chat shows the turns made outside it

When a Pi chat's agent opens its conversation, Orca asks Pi for the entries
saved after the point the chat last accounted for (get_entries since that
entry id) and adds the terminal's turns once, in Pi's order, under the
"Continued outside this chat." divider. That point is saved on the
conversation handle after each chat turn and at each open. When the chat's
own unsaved work and the terminal's share a suffix that can't be split
(Orca died mid-turn), that suffix is skipped rather than duplicated, and
later terminal turns show normally. A Pi reply over the 8 MiB line limit now
fails only the optional read that asked for it instead of ending the chat.

* refactor(native-chat): share the catch-up open runner across agents

The Codex catch-up's agent-neutral parts move to one module: the open-time
envelope (its own deadline and cancellation, never gating the open), the
bounded staged batch, the single settlement commit that re-checks the
journal before writing, and the "Continued outside this chat." divider.
Codex keeps only how it reads its saved turns and what the chat already
holds. No behavior change; the other agents' catch-up builds on this
instead of copying it.

* refactor(pi): catch up through the shared catch-up runner instead of a copy

* chore: remove unrelated pnpm executable metadata

* refactor(native-chat): the catch-up runner reads the chat's view only when the agent asks

* refactor(pi): read the chat's view through the runner's view()

* chore: remove regenerated pnpm executable metadata

* fix(native-chat): the catch-up deadline also bounds waiting for its commit to be written

A journal write barrier that never settled after the import was admitted held
the provider's open past the catch-up deadline. The wait now ends with the
deadline or the open's cancellation, like every other step.

* test(pi): check Pi's session entry ids and get_entries since against the pinned real Pi

* test: type the history catch-up sink fixture without assertions

* fix(native-chat): bound and revoke history preparation

* test: the runner's journal fake answers the cursor Pi's view reads

* feat(native-chat): validate expected saved items during history catch-up

* fix(pi): preserve outside history and optional read recovery

* test(pi): fail RPC smoke promptly when the child stops
2026-10-22 10:00:11 -07:00
Jinwoo Hong c9410991ba A paired desktop's pane drag-out, handle-less split and default-tabs mark reach a server's own layout (dark, layout-owner build only; stacked on #31714) (#31869)
* wip(h5c-1): server workspace layout intents go to the server's main; positions per stream

* wip(h5c-1): e2e without a cast

* fix(h5c-1): a routed move never moves here; routed callers read the server's stream; a restarted main starts its positions over

* fix(h5c-1): layout-apply-send back under its line limit

* test(h5c-1): a routed answer reaches the handler with the server's stream key

* fix(terminal): a CLI or phone close through main keeps a pinned tab, as the window-era close did

* feat(terminal): main applies phone closes, renames, pane titles, sleeps and agent-launch tabs itself, with or without a window (layout-owner build)

* fix(terminal): a tab close through main also ends the tab's dormant SSH relay session; agent-launch tabs main writes keep the window's focus rule, prompt and outcome (layout-owner build)

* feat(terminal): main starts a new tab's first terminal under the window's ids, and the pane attaches to it (layout-owner build)

* test(terminal): tests that counted the window's create calls or publications see main's start of a new tab

* fix(terminal): an account-switch restart binds only into a pane main's layout holds, with no window tree (layout-owner build, L197)

* test(terminal): the new-tab start tests hold no type assertions; the start's ledger row is L252

* fix(terminal): a new tab's pane attaches to any terminal main binds it to, and a failed start keeps the tab's startup

* test(terminal): a Claude account-switch restart's tests draw the pane main holds

* fix(terminal): a launch pane main takes back still tells the window why, which drops its hold and prompt; a rename through main answers the name it stored (layout-owner build)

* fix(terminal): a new tab another owner's terminal won attaches to it, and a failed start's startup never runs twice

* fix(terminal): main's pane close reads its stop result as a type only, so no runtime module loads before its base class

* fix(terminal): a phone's close through main drops the closed rows from what phones list at once, not at the window's next publish (layout-owner build)

* chore(terminal): mark the phone-close row drop interim (ledger L260)

* test(worktree-create): a new local tab's startup counts as run when main starts it (layout-owner build)

* test(worktree-create): record main's starts without a type assertion

* fix(terminal): a tab closed before main declines its start gets no startup back (layout-owner build)

* fix(terminal): the window's new tab starts in main when its own window asks over IPC, which labels itself a client (layout-owner build)

* feat(h5c-2): a paired desktop's writes to a server workspace its frames draw land in that server's main (layout-owner build)

- Routes setTabProps, setChatPane and non-terminal renameTab to the server's main as layout.apply;
  closes, starts, sleeps and terminal names keep the RPCs that main now applies (session.tabs.close,
  terminal.close, terminal.split, session.tabs.createTerminal, terminal.rename). A window-style
  closeTabs is never routed: main takes it as already stopped and stops nothing.
- A close the server refuses (a pinned tab) redraws the workspace from the server's frames, so the
  window's optimistic drop never outlives the answer; same for a refused pane close.
- A frame-drawn workspace keeps none of the mirror's ledgers (L240): no tab-id mappings, close or
  focus intents; they stay only for servers without layout.intents.v1 until the Phase 2 cutoff.
  Its pane-geometry push is skipped (it goes as intents); pane renames still push.
- e2e: paired-desktop-server-layout-writes covers close (layout, session.tabs, terminal stop),
  rename, split and new tab on a windowless switch-on serve.

* fix(h5c-2): an undelivered close of a frame-drawn tab is held for replay; a launch-agent clear stays off the server route

* refactor(h5c-2): the undelivered-close test moves to its own module, keeping the lifecycle file under its line cap

* fix(layout): a paired desktop's pane detach, handle-less split and default-tabs mark on a server workspace go to the server's main (layout-owner build, L272/L276/L275)

* fix(layout): a pane a paired server's main detached takes the terminal the server's frame drew, not the desktop's possibly stale one (layout-owner build, L272)

* test(layout): the detach test resets the drawn layout after each case

* test(layout): drop the in-test reset the afterEach now covers
2026-10-22 12:58:23 -04:00
Neil 2366072dc8 refactor: colocate browser cookie validation (#32062) 2026-10-22 09:43:42 -07:00
Neil de2700d0fb refactor: colocate native-chat command envelopes (#32055) 2026-10-22 08:55:53 -07:00
Brennan Benson 97d4518d5f Show per-agent Session History reading progress (#30954)
* fix(session-history): freeze scan workload before checking candidates

* Show per-agent Session History reading progress

* Keep reduced-motion behavior in progress primitive

* test(session-history): handle native fixture path separators

* test(session-history): control bucket fixture enumeration order

* test: cover finished empty history progress rows

* test(session-history): preserve scoped fixture timestamps

* fix(session-history): repair integrated test and locale catalog

* ci: refresh history checks against current main

* ci: refresh history checks after catalog repair
2026-10-22 08:36:59 -07:00
Neil 6af21633e9 refactor: colocate native-chat command language rules (#32041) 2026-10-22 07:57:50 -07:00
Neil ec5eaf96c1 refactor: colocate provider retry transcript rules (#32024) 2026-10-22 07:02:39 -07:00
Neil d053ff97ab refactor: colocate mobile web route and bundle tooling (#32008) 2026-10-22 06:04:04 -07:00
Jinwoo Hong 4dd97f02d5 A paired desktop's tab moves, splits, resizes, props and renames in a server workspace go to the server's layout (switch-on builds only) (#31714)
* wip(h5c-1): server workspace layout intents go to the server's main; positions per stream

* wip(h5c-1): e2e without a cast

* fix(h5c-1): a routed move never moves here; routed callers read the server's stream; a restarted main starts its positions over

* fix(h5c-1): layout-apply-send back under its line limit

* test(h5c-1): a routed answer reaches the handler with the server's stream key

* fix(terminal): a CLI or phone close through main keeps a pinned tab, as the window-era close did

* feat(terminal): main applies phone closes, renames, pane titles, sleeps and agent-launch tabs itself, with or without a window (layout-owner build)

* fix(terminal): a tab close through main also ends the tab's dormant SSH relay session; agent-launch tabs main writes keep the window's focus rule, prompt and outcome (layout-owner build)

* feat(terminal): main starts a new tab's first terminal under the window's ids, and the pane attaches to it (layout-owner build)

* test(terminal): tests that counted the window's create calls or publications see main's start of a new tab

* fix(terminal): an account-switch restart binds only into a pane main's layout holds, with no window tree (layout-owner build, L197)

* test(terminal): the new-tab start tests hold no type assertions; the start's ledger row is L252

* fix(terminal): a new tab's pane attaches to any terminal main binds it to, and a failed start keeps the tab's startup

* test(terminal): a Claude account-switch restart's tests draw the pane main holds

* fix(terminal): a launch pane main takes back still tells the window why, which drops its hold and prompt; a rename through main answers the name it stored (layout-owner build)

* fix(terminal): a new tab another owner's terminal won attaches to it, and a failed start's startup never runs twice

* fix(terminal): main's pane close reads its stop result as a type only, so no runtime module loads before its base class

* fix(terminal): a phone's close through main drops the closed rows from what phones list at once, not at the window's next publish (layout-owner build)

* chore(terminal): mark the phone-close row drop interim (ledger L260)

* test(worktree-create): a new local tab's startup counts as run when main starts it (layout-owner build)

* test(worktree-create): record main's starts without a type assertion

* fix(terminal): a tab closed before main declines its start gets no startup back (layout-owner build)

* fix(terminal): the window's new tab starts in main when its own window asks over IPC, which labels itself a client (layout-owner build)

* feat(h5c-2): a paired desktop's writes to a server workspace its frames draw land in that server's main (layout-owner build)

- Routes setTabProps, setChatPane and non-terminal renameTab to the server's main as layout.apply;
  closes, starts, sleeps and terminal names keep the RPCs that main now applies (session.tabs.close,
  terminal.close, terminal.split, session.tabs.createTerminal, terminal.rename). A window-style
  closeTabs is never routed: main takes it as already stopped and stops nothing.
- A close the server refuses (a pinned tab) redraws the workspace from the server's frames, so the
  window's optimistic drop never outlives the answer; same for a refused pane close.
- A frame-drawn workspace keeps none of the mirror's ledgers (L240): no tab-id mappings, close or
  focus intents; they stay only for servers without layout.intents.v1 until the Phase 2 cutoff.
  Its pane-geometry push is skipped (it goes as intents); pane renames still push.
- e2e: paired-desktop-server-layout-writes covers close (layout, session.tabs, terminal stop),
  rename, split and new tab on a windowless switch-on serve.

* fix(h5c-2): an undelivered close of a frame-drawn tab is held for replay; a launch-agent clear stays off the server route

* refactor(h5c-2): the undelivered-close test moves to its own module, keeping the lifecycle file under its line cap

* fix(h5c): a routed tab move anchors on the server's tabs only, and never sends an anchor the server does not hold (layout-owner build)

* fix(h5c): every routed tab move is placed among the server's tabs at the routing point, skipping desktop-only tabs (layout-owner build)
2026-10-22 08:00:09 -04:00
Neil c30fa6252a refactor(tooling): group mobile web preview and evidence tools (#31976) 2026-10-22 04:57:01 -07:00
Neil 32370a5e05 fix(terminal): settle a restored remote pane whose host session already exited (#31968)
* fix(terminal): settle a restored remote pane whose host session already exited

A stale-handle or no_connected_pty refusal kept retrying forever because nothing
read the host's own exit record. On those refusals (and the restore attach's
stale branch) ask terminal.show once; settle the pane as exited only when the
host's verdict is exited, or an older host records an exit cause for a
disconnected terminal. Anything else keeps today's retry.

Fixes #31539

* fix(terminal): ignore an unverified stop and keep the host's exit code

* fix(terminal): settle on a host exit only when the host holds no replacement pane

* fix(terminal): settle only while the host pane still holds the exited process

* fix(terminal): leave a stopped host surface to materialize instead of settling it
2026-10-22 04:47:58 -07:00
NeilandClaude 8556d119bb Conflict review shows the file's live unsaved text from any workspace (read-only) (#31990)
* 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>

* Schedule editor autosave per buffer record, and add the join rules the sharing switch uses

First of the stacked sharing-switch PRs (G1). Inert while every record has
one view, which is all main produces today; tests drive the shared shape
through hand-built view indexes.

- Autosave timers are decided per record by its first view that may
  autosave, so a read-only or preview view of a record no longer cancels
  the record's timer, and a timer survives the view that scheduled it
  closing (it resolves a live view of the record when it fires).
- A view of a detached record gets its own model owner key, so divergent
  dirty drafts never share text with the file's other record.
- Pure join and rejoin rules: rekey / attach / adopt (dirty wins) / merge
  (identical dirty) / detach (divergent dirty); a detached pair rejoins
  only once one side is clean.
- Mirrored host tabs: a record's dirty flag is the OR of its host-owned
  views, since an older host publishes it per tab; the dev invariant
  tolerates exactly that disagreement.

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

* Keep pauses and pending text out of buffer merges, and pick a writable view for autosave

- A merge needs equal drafts, baselines, conflict and owner-migration state;
  anything else detaches, so a paused record never resumes autosave by joining.
  A side whose text is still pending (no draft) never counts as identical.
- A clean-flagged view that still holds a draft counts as dirty in joins.
- The invariant tolerates a clean host-owned view on a dirty record only when
  another host-owned view of that record reads dirty.
- Autosave and restart skip CSV-preview and read-only views when picking the
  view a record writes through.

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

* feat(editor): views of one file share one buffer record

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

* fix(editor): review fixes for shared buffer records

A dirty view with no draft no longer pushes disk text into the shared model; merge and
deleted-banner joins mirror the record; web-session sync honours detached views.

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

* feat(editor): shared records take every writer, rejoin, outlive no-prompt closes, restore merges

- Rich Markdown, notebook and CSV commits also edit the record's models no editor shows, as one
  undoable edit; a save moves every loaded view's panel text to the written disk text, so a stale
  dirty check can't mark the record clean over unsaved edits.
- A detached pair rejoins once one side is clean (save, Discard, reload) or the partner closes:
  the clean side attaches, a dirty side wins with its own baseline, so its next save is refused
  and the banner shows on every view. Rejoin runs on the next microtask, after the caller has
  finished settling the record; doing it mid-settle let the settle clear the adopted draft.
- A dirty record whose last view a no-prompt removal closed is kept with its draft and model;
  reopening the file joins it, it never autosaves, and dismissing the removal notice drops it.
- Restored identical drafts merge; divergent ones detach with their own models and timers.

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

* test(editor): cover shared buffer records end to end, and restart-save a record through its writable view

Adds save-queue and store tests for rename and owner-resolution re-keys carrying a conflict, a
read-only sibling joining a dirty record, retired-runtime quarantine, runtime-in-SSH identity, the
reconnect writer, restart once per record, and a 200-tab index.

Fix: restart picked a record's first dirty view, so a read-only view listed first refused the
whole restart; it now picks the record's first view that may autosave.

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

* fix(editor): G3 review fixes for kept records, rejoin, undo steps and save events

- A no-prompt removal's keep request rides in the write that closes the tabs
  (editorBufferKeepRequest, consumed by the record middleware), so a removal
  closing tabs over several writes keeps every dirty record it closes.
- Kept records carry their removal notice's token; dismissing a notice, or
  failing to show it, releases only that notice's records.
- A divergent dirty view reopening a kept record stays detached on its own
  model; a rename onto a kept record's path drops the kept record.
- Each rich/notebook/CSV commit into a retained model is its own undo step.
- The saved event names the record's views, so the panel updates them even
  when the saving tab closed mid-save.
- A queued save compares the discard epoch of the record it was queued under.
- Typing no longer runs the rejoin check; only clearing a side's draft does.

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

* feat(editor): conflict-review overview shows the file's shared record, read-only and live

The overview of a conflict review now reads each file through any open view of its
shared buffer record (this workspace's first), so it shows the record's current unsaved
text and model and follows typing in another workspace. It stays read-only and is never
a view itself: it loads as that view would and never pushes text. With no view open it
still shows disk text.

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

* fix(editor): conflict-review overview follows its borrowed view and never acts as it

- The overview's load reruns when the view it reads through opens, closes, re-keys,
  detaches or rejoins in any workspace, so it no longer sticks on "Loading...".
- Retry reloads through the borrowed view's own workspace, and the editor resolves
  through that workspace, so this workspace never re-owns the other's tab.
- Overview editors are passive mirrors: they don't move the borrowed tab's cursor
  line or take a reveal meant for it.
- This workspace's own tab (even a detached one with its own edits) wins over
  another workspace's view.
- The panel's draft selector re-resolves when tabs or record views change, not only
  when drafts do; overview lookups are memoized per tabs/views/routes, and the
  stand-in cache evicts least recently used entries instead of clearing.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 04:42:00 -07:00
Jinwoo Hong 2583e461b2 A browser tab opened in a server workspace drawn from the server's layout stays open (switch-on builds only) (#31898)
* fix(L273-core): tabs this desktop created in a paired server's frame-drawn workspace survive the server's frames (switch-on)

A tab the server's layout never lists (a client-hosted browser tab today; any kind recorded the
same way) used to vanish on the server's next frame rebuild. This desktop now records such tabs by
server and workspace, in its localStorage only (never a session field nor sent), and the server's
frame builds keep them as drawn, inside the server's own groups:
- A held tab drawn in a group the server lacks moves to the nearest server group; it shows there
  only if the user was on it, and the user's group follows it. The group tree stays the server's.
- The record forgets an id once the server's frame lists it, on close, and on worktree removal,
  and prunes ids the restored session no longer has before the first frame after a reload.
- A held tab's drag moves it here only (no session.tabs.move); it offers no new split, and a
  server tab sharing its group counts only the server's tabs for split no-ops.
- Client-hosted staging records its tab (both ids) in layout-owner builds; restage takes the
  worktree from its caller instead of scanning the store.

* refactor(L273-core): the desktop-created record is inert with the switch off; clearer names

- With the switch off the record is never read, kept or written (a single gate in the record).
- The frame-drawn check is isDesktopCreatedTabInFrameDrawnServerWorkspace in
  frame-drawn-desktop-created-tabs.ts: "held" stays the frame hold's word.
- The held-tab placement drops its unread activeGroupId; the group follow derives it.

* fix(L282): a client-hosted browser tab this desktop creates in a frame-drawn server workspace completes once the server's snapshot lists it, instead of undoing itself after 8 s (switch-on)

* fix(L284): a server-hosted browser tab this desktop creates in a frame-drawn server workspace completes when the server's frames take it over, instead of reading as the user's close (switch-on)

* chore(L282/L284): adopt only staged client-hosted handles, and name the frame build's handover note (review nits)

* fix(L284): a staged tab the server's frames took over completes the create though its pane, unmounted, has no handle yet (layout-owner build only)

* fix(browser): a switch-on host answers a paired client's browser create with the new page's id, not its workspace's, before main's frame draws it

* fix(browser): the reply names the page id the create asked for first, then the new record's own page (L291)

* fix(L284): the frame build scans dropped pages for a handover only when a staged create's page is drawn, so a large close stays one pass
2026-10-22 07:36:01 -04:00
NeilandClaude 885cd6ff44 Tabs of the same file share one record of unsaved edits (#31974)
* 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>

* Schedule editor autosave per buffer record, and add the join rules the sharing switch uses

First of the stacked sharing-switch PRs (G1). Inert while every record has
one view, which is all main produces today; tests drive the shared shape
through hand-built view indexes.

- Autosave timers are decided per record by its first view that may
  autosave, so a read-only or preview view of a record no longer cancels
  the record's timer, and a timer survives the view that scheduled it
  closing (it resolves a live view of the record when it fires).
- A view of a detached record gets its own model owner key, so divergent
  dirty drafts never share text with the file's other record.
- Pure join and rejoin rules: rekey / attach / adopt (dirty wins) / merge
  (identical dirty) / detach (divergent dirty); a detached pair rejoins
  only once one side is clean.
- Mirrored host tabs: a record's dirty flag is the OR of its host-owned
  views, since an older host publishes it per tab; the dev invariant
  tolerates exactly that disagreement.

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

* Keep pauses and pending text out of buffer merges, and pick a writable view for autosave

- A merge needs equal drafts, baselines, conflict and owner-migration state;
  anything else detaches, so a paused record never resumes autosave by joining.
  A side whose text is still pending (no draft) never counts as identical.
- A clean-flagged view that still holds a draft counts as dirty in joins.
- The invariant tolerates a clean host-owned view on a dirty record only when
  another host-owned view of that record reads dirty.
- Autosave and restart skip CSV-preview and read-only views when picking the
  view a record writes through.

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

* feat(editor): views of one file share one buffer record

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

* fix(editor): review fixes for shared buffer records

A dirty view with no draft no longer pushes disk text into the shared model; merge and
deleted-banner joins mirror the record; web-session sync honours detached views.

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

* feat(editor): shared records take every writer, rejoin, outlive no-prompt closes, restore merges

- Rich Markdown, notebook and CSV commits also edit the record's models no editor shows, as one
  undoable edit; a save moves every loaded view's panel text to the written disk text, so a stale
  dirty check can't mark the record clean over unsaved edits.
- A detached pair rejoins once one side is clean (save, Discard, reload) or the partner closes:
  the clean side attaches, a dirty side wins with its own baseline, so its next save is refused
  and the banner shows on every view. Rejoin runs on the next microtask, after the caller has
  finished settling the record; doing it mid-settle let the settle clear the adopted draft.
- A dirty record whose last view a no-prompt removal closed is kept with its draft and model;
  reopening the file joins it, it never autosaves, and dismissing the removal notice drops it.
- Restored identical drafts merge; divergent ones detach with their own models and timers.

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

* test(editor): cover shared buffer records end to end, and restart-save a record through its writable view

Adds save-queue and store tests for rename and owner-resolution re-keys carrying a conflict, a
read-only sibling joining a dirty record, retired-runtime quarantine, runtime-in-SSH identity, the
reconnect writer, restart once per record, and a 200-tab index.

Fix: restart picked a record's first dirty view, so a read-only view listed first refused the
whole restart; it now picks the record's first view that may autosave.

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

* fix(editor): G3 review fixes for kept records, rejoin, undo steps and save events

- A no-prompt removal's keep request rides in the write that closes the tabs
  (editorBufferKeepRequest, consumed by the record middleware), so a removal
  closing tabs over several writes keeps every dirty record it closes.
- Kept records carry their removal notice's token; dismissing a notice, or
  failing to show it, releases only that notice's records.
- A divergent dirty view reopening a kept record stays detached on its own
  model; a rename onto a kept record's path drops the kept record.
- Each rich/notebook/CSV commit into a retained model is its own undo step.
- The saved event names the record's views, so the panel updates them even
  when the saving tab closed mid-save.
- A queued save compares the discard epoch of the record it was queued under.
- Typing no longer runs the rejoin check; only clearing a side's draft does.

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

* fix(editor): keep open-file mirror writes out of the layout writer ratchet

The record middleware now applies reconcile's per-view mirror fields as a
membership-preserving map over openFiles, and the mirrored-dirty adoption
returns its views for the session-sync patch to write, as it did before.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 04:21:45 -07:00
Jinwoo Hong 15d663fc3f Test runner uses the repo's pinned Bun, and refuses an older one instead of silently skipping tests (#31981)
* chore(test): run-vitest launches the pinned Bun, and refuses a Bun older than config/.bun-version

An older Bun silently runs no tests in files that reach newer node builtins, so a local
comparison could skip them on both sides.

Claude-Session: W5b

* chore(test): an older Bun ends run-vitest by exit code, so its message is not cut off

Claude-Session: W5b
2026-10-22 07:17:35 -04:00
Neil 48660ae717 fix(packaging): keep ssh2's Windows-only Pageant files out of macOS and Linux packages (#31946)
* fix(packaging): keep ssh2's Windows-only Pageant files out of macOS and Linux packages

* test: explain the win32 ssh2 filter fixture and format

* test: read the excluded ssh2 file names from the exclude table
2026-10-22 03:58:38 -07:00
NeilandClaude 54acda1d12 Editor buffers ready for sharing: autosave per record, separate models for detached tabs (no behaviour change) (#31970)
* 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>

* Schedule editor autosave per buffer record, and add the join rules the sharing switch uses

First of the stacked sharing-switch PRs (G1). Inert while every record has
one view, which is all main produces today; tests drive the shared shape
through hand-built view indexes.

- Autosave timers are decided per record by its first view that may
  autosave, so a read-only or preview view of a record no longer cancels
  the record's timer, and a timer survives the view that scheduled it
  closing (it resolves a live view of the record when it fires).
- A view of a detached record gets its own model owner key, so divergent
  dirty drafts never share text with the file's other record.
- Pure join and rejoin rules: rekey / attach / adopt (dirty wins) / merge
  (identical dirty) / detach (divergent dirty); a detached pair rejoins
  only once one side is clean.
- Mirrored host tabs: a record's dirty flag is the OR of its host-owned
  views, since an older host publishes it per tab; the dev invariant
  tolerates exactly that disagreement.

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

* Keep pauses and pending text out of buffer merges, and pick a writable view for autosave

- A merge needs equal drafts, baselines, conflict and owner-migration state;
  anything else detaches, so a paused record never resumes autosave by joining.
  A side whose text is still pending (no draft) never counts as identical.
- A clean-flagged view that still holds a draft counts as dirty in joins.
- The invariant tolerates a clean host-owned view on a dirty record only when
  another host-owned view of that record reads dirty.
- Autosave and restart skip CSV-preview and read-only views when picking the
  view a record writes through.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 03:41:20 -07:00
NeilandFrank-zhu2000 92c8f3cbdc fix(ssh): stop floating terminal metadata from blocking SSH access links (#31963)
* fix(ssh): stop local-only workspace metadata from blocking SSH access links

Unstamped floating and setup terminal metadata rows were projected onto every
execution host, so the dependent-state census refused SSH access links and
empty-host claims for every SSH target with orcad_migration_dependent_state.

- Local-only workspace ids project only onto the local host, are stamped local
  on load, and their metadata writes always land on the local host.
- The census counts an unstamped legacy row only when its repo is not
  registered solely on other hosts.
- The SSH access error now names the blocking state.

Builds on the approach in #31862 by Frank-zhu2000.

Fixes #31498

Co-authored-by: Frank-zhu2000 <Frank-zhu2000@users.noreply.github.com>

* fix(ssh): rehome remote-stamped local-only metadata, skip orphan rows, and word the SSH access error for a link

* fix(persistence): keep a local or unstamped floating row over a remote copy when rehoming

* fix(ssh): make the SSH access blocker copy switch exhaustive

---------

Co-authored-by: Frank-zhu2000 <Frank-zhu2000@users.noreply.github.com>
2026-10-22 03:39:54 -07:00
Jinwoo HongandClaude 2c937546dd A paired desktop keeps the tabs it created in a server workspace drawn from the server's layout (switch-on builds only) (#31853)
* fix(L273-core): tabs this desktop created in a paired server's frame-drawn workspace survive the server's frames (switch-on)

A tab the server's layout never lists (a client-hosted browser tab today; any kind recorded the
same way) used to vanish on the server's next frame rebuild. This desktop now records such tabs by
server and workspace, in its localStorage only (never a session field nor sent), and the server's
frame builds keep them as drawn, inside the server's own groups:
- A held tab drawn in a group the server lacks moves to the nearest server group; it shows there
  only if the user was on it, and the user's group follows it. The group tree stays the server's.
- The record forgets an id once the server's frame lists it, on close, and on worktree removal,
  and prunes ids the restored session no longer has before the first frame after a reload.
- A held tab's drag moves it here only (no session.tabs.move); it offers no new split, and a
  server tab sharing its group counts only the server's tabs for split no-ops.
- Client-hosted staging records its tab (both ids) in layout-owner builds; restage takes the
  worktree from its caller instead of scanning the store.

* refactor(L273-core): the desktop-created record is inert with the switch off; clearer names

- With the switch off the record is never read, kept or written (a single gate in the record).
- The frame-drawn check is isDesktopCreatedTabInFrameDrawnServerWorkspace in
  frame-drawn-desktop-created-tabs.ts: "held" stays the frame hold's word.
- The held-tab placement drops its unread activeGroupId; the group follow derives it.

* fix(L273-core): a simulator tab opened in a paired server's frame-drawn workspace survives its frames (switch-on)

The server never has this desktop's emulator tab, so its frame rebuilds dropped it. The simulator
open now records the tab as created here when a server's frames draw the workspace, and opens it
in the server group it would have split beside, since that workspace has no group of this
desktop's own. L290 (the host runs and streams the simulator, #6701) removes the note.

* test(L273-core): the mirrored-workspace simulator test draws the tab and can fail; the side-open test starts from another group

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

* fix(L273-core): list the created-here tab record in the WSL-move storage census

It is keyed by a paired server's environment; a local or WSL project never has one, so a move has nothing to carry.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 06:37:13 -04:00
Neil a3fa44ad07 fix(push): honor "only when desktop is away" on headless orca serve (#31956)
* fix(push): decide orca serve's desktop-away from attached desktops (#31570)

* fix(push): report away on sleep and lock, guard late reports and prune stale presence

* chore: justify the presence-reporter state slot widening

* fix(push): hold away from suspend or lock until resume or unlock

* fix(push): key presence sockets by connection instance so a replaced connection reports at once
2026-10-22 03:36:10 -07:00
Neil abc0c743a2 fix(push): bound claim transactions before background admission (#31955) 2026-10-22 03:34:52 -07:00