* refactor(editor): drop SSH endpoint, path flavor and WSL alias folding from buffer identity
SSH files are keyed by connection target; the guarded save catches a retarget.
\\wsl$ and \\wsl.localhost stay distinct, including for discard holds and the
per-file write lock: main compares WSL writes at write time.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): keep WSL alias folding in discard-hold and write-lock keys
A discard via \\wsl$ must hold autosave of a tab opened via \\wsl.localhost:
the local guarded write is not atomic against git. Buffer identity stays
unfolded; only the hold/lock key folds, on the local Windows client.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(terminal): no catalog-less fail-open to the focused runtime in terminal spawn routing
* chore(ratchet): prune the workspace-record read baseline for terminal-worktree-route
* test(terminal): pin that a state with no owner catalogs never spawns on the focused runtime
Retire queued continuation echoes when transcript rows represent the sends. Preserve literal backslashes, occurrence boundaries, and image barriers on mobile and desktop.
Closes#15288.
Adapted from #15283.
Co-authored-by: grboy6770 <2000grboy@gmail.com>
* refactor(editor): one running + one queued save per tab, settle instead of cancel
Saves now run one at a time per tab with at most one queued save behind
them; later requests join the queued save, which reads the latest text
when it runs and writes nothing if the tab is already clean. Writes to
one physical file (edit tab, unstaged diff, combined-diff section) share
one lock. Quiesce no longer cancels saves: it flushes pending text, drops
the autosave timer and waits for the running and queued saves. Autosave
and restart write through the first view whose owner is still current.
Removes save generations, bumpSaveGeneration, the exported
clearAutoSaveTimer, the model-URI queue key and the unbounded save chain.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): Don't Save drops edits before draining saves; one lock key
Don't Save now marks the tab clean and clears its draft before waiting
on saves, and a save requested while the tab was dirty writes nothing
if the tab is clean when it reaches the write (Overwrite excepted). A
queued or not-yet-writing save can no longer put discarded text on disk
or recreate a deleted file. A Cmd+S on an already-clean tab still writes,
so a tab whose file was deleted can still recreate it.
Editor saves and combined-diff section saves share one lock key and
skip the lock when the file's host is unknown.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(editor): type-safe held writes in save queue tests
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): a discard voids every save asked for before it
Don't Save bumps a per-buffer discard epoch alongside clearing the
draft. A save whose epoch is stale writes nothing, Overwrite included,
and skips the pending-edit flush that could republish the discarded
text. Dirty state alone wasn't enough: a notebook's retained cells or a
remote snapshot can re-dirty the tab after the discard.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): stop discarded bytes at the send and hold autosave through Don't Save
A save that passed the discard check could still send discarded content
after a remote host's status/capability round trip or an older host's
compare read. The writer now takes a last-moment check, run just before
the bytes are sent on every path (local, unguarded, guarded, older-host
fallback, retry); a discard in that window aborts the send.
Don't Save's drain no longer flushes pending editor text, and it holds
autosave for the file until the tab is closed, so a view that
republishes the dropped text mid-drain can't schedule a fresh autosave.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
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>
* feat(settings): the default agent can be set to open as a chat or a CLI
With Chat UI on, Settings > Agents > Default Agent now shows a Chat | CLI
choice for the selected agent. It writes the same per-agent chat switch
that Chat > Providers and the + menu already use, so no new setting is
saved and the two places can't disagree.
* test: type the store stand-in without an assertion
* chore: drop accidental lockfile churn
* fix: clarify default agent launch preference scope
* refactor(renderer): name the owner adapters for what they do: resolveOwnerOrLocal, runtimeEnvironmentIdOrLocalUnlessTwin
The Active Server focus fallback they were named for is gone (#31058, #31119); what is left reads an
id no row places as this app's (a folder's saved-session server, a web client's only server).
* docs(renderer): resolveOwnerOrLocal keeps a named host's miss; baseline note names the setup-terminal read
* Preview HTML files by default across file-opening flows
* Centralize workspace file opening and HTML preview routing
- Preserve source, diff, and host-safety behavior across all entry points
- Reuse previews within the requested editor group and support branch HTML files
* Respect working tree state when opening HTML previews
* Route HTML links through owning workspaces and editors
* Preserve HTML preview state and refresh changed local files
* Resolve HTML preview hosts from workspace ownership
* Update file-opening tests for HTML preview routing
* fix(ssh): name what refuses an SSH host's move, and offer Move anyway in the terminal pane
The refusal line said only "something on the host stops the move" and hid main's
reason (which already names the blocker and the way out) behind a Details menu,
and the terminal pane banner dropped it entirely. The line now shows that reason,
capped inline with the whole text behind Details, and the pane offers Move anyway
for a refusal saved state alone causes.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): keep a refusal's way out in the pane, and don't race a connect in flight
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(editor): remote models are keyed by an owner authority, so TSX parses and imports stay on one host
Remote editor models were keyed `file:///path#<owner>`. Monaco's TypeScript service names files by
URI, so the fragment hid the `.tsx` extension and imports resolved across hosts. They are now
`vscode-remote://<owner hash>/<host path>`: the file name stays last, and TypeScript treats
`scheme://authority/` as a root, so `..` can't climb into another owner's tree or into local
`file:` models. Monaco keeps closed-file undo for this scheme, and its default label service shows
the URI path, so peek, references and snippet variables show the host's path with no override.
Local models keep their `file:` URIs. A UNC server moves into the path.
A parity test proves exactly the same tab pairs share a model as before; model URIs stay in memory.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): key remote UNC models so the TypeScript worker finds them
A remote UNC file was keyed `vscode-remote://<owner>//server/share/a.ts`. TypeScript collapses the
doubled slash, but the worker looks models up by exact URI, so hover and definitions failed. UNC
paths now mark the owner authority (`<owner>-unc`) and keep `/server/share/a.ts` as the path, which
stays distinct from the same POSIX path. Test-only Monaco module types move out of env.d.ts.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Mobile chat composer uses desktop's placeholder
The phone composer said "Message Codex" (or "Message, @files, /commands") while desktop says "Ask anything, @ to mention files, / for commands". Both now read the one string from the shared native-chat copy table, so they can't drift again.
* Ship the shared composer placeholder in the runtime English catalog
* Keep the composer placeholder beside, not inside, the turn-status copy
The turn-status table maps one-to-one onto the status.* catalog keys; the placeholder is composer.placeholder, so it gets its own constant and its own catalog check.
* Read the composer catalog without a type assertion
* Read the composer placeholder from the typed catalog directly
* feat(native-chat): a Codex chat shows the turns made outside it
When a Codex chat's agent opens its conversation, Orca reads Codex's own saved
turns and adds the ones the chat doesn't have, after the chat's rows, behind a
quiet "Continued outside this chat." divider. Turns are matched by Codex's own
turn ids, read whole before anything is written, and never re-imported; a chat
imported from a transcript gets only turns started after the import. A failed
or partial read adds nothing and the next open tries again.
* chore: remove accidental package manager lockfile churn
* test(codex): expect restored turns with optional timestamps
* test(codex): run catch-up SQLite fixtures under Node
* Make Codex history catch-up atomic and preserve adoption origin
* Keep imported Codex history separate from send delivery
* Derive Codex history reconciliation from saved journal facts
* Validate saved Codex turn identity before planning its notice
* Align Codex catch-up fixtures with current journal contracts
* Check oversized item echo declarations by their saved identity
* test: retain Codex catch-up flow and concrete regressions
The card rebuilt its Linear source context on every workspace write, and both
card Linear effects listed that object as a dependency, so a comment, agent
status or activity update re-ran them and refetched the issue whenever the 60s
cache had expired. Hold the context while it is equal to the rebuilt one.
Tracked in STA-10988. Same approach as community PR #30090.
* feat(agents): Create worktree and continuation pickers offer chat or CLI per agent, like the "+" menu
The Create worktree, Create folder and session-continuation agent pickers now
list the same rows as the tab bar "+" menu: chat agents first, each with a
terminal button that picks the agent's CLI instead, then the agents that only
run in a terminal, labelled "(CLI)". A terminal pick shows as "Claude (CLI)"
and starts the agent in a terminal even when chats are the default.
A continuation into a subfolder shows chat as unavailable there.
* fix(agents): picker rows keep the combobox item spacing instead of restyling CommandItem
* chore: drop accidental lockfile churn
* fix(agents): type the surface picker's props and the composer's agent rows
* fix(agents): submit the picker selection captured by each form
* test(agents): include repository identity in continuation fixture
* fix(agents): settle inventory when workspace ownership is unresolved
* test: hydrate workspace owners in quick launch fixtures
* fix(terminal): start every terminal in the workspace root when its saved folder is gone, and say so
A terminal whose start folder was deleted opened at the workspace root only
when the window started it; the CLI, phone restore, orchestration and other
host starts failed with the raw "Working directory ... does not exist" error,
because the renderer had to opt in with a cwdFallback flag.
The execution host now makes that decision for every start that may create a
process (anything but a known live pane), with no caller flag. The fallback is
reported only when a process was proven started at the requested folder, so a
reattach or a crash restore in its recovered folder never claims "workspace
root", and only then does the saved start folder heal. A recovered folder that
was itself deleted yields to the request's resolved folder.
The notice reaches each caller through surfaces it already has: the window's
terminal line (now also written when a host-created tab attaches, and on a
restored reattach), the agent.launch warning (also for callers that show the
tab themselves), and a toast when a phone tap restores the tab.
* fix: recover terminal startup folders without healing unreadable paths
* fix: preserve saved start folder when revealing fallback terminals
* Reuse saved-tab lookup in cwd persistence test
* Import mobile tab activation result from its contract
* Check startup fallback response without a test cast
* Update expectation to catch up with accepted saved-folder metadata
* Preserve inconclusive WSL directory evidence when saving startup folders
* fix: project restored terminal ids for handle consumers
* test: return void from launch parity cleanup
* test: retain saved folder in shared spawn expectation
* 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)
* fix(terminal): a refused close of a pinned tab leaves its running agent launch alone (layout-owner build)
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)
The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.
- Main stops counting a closed tab's terminal once it was asked to stop
and no pane shows it; a failed or refused stop takes the request back,
and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
feed stands in while a fresh read is out; it highlights its own
last-opened workspace and a fold never scrolls; projects file under
their project group with the desktop's indent; the collapse spinner
shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.
* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises
- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.
* feat(sidebar): main publishes cached snapshots and announces its own stores; detected, view and live-content domains (inert)
* fix(sidebar): keep links confirmed only by a new git run, no rescan on metadata writes, no phone wait on detected checkouts
* fix(sidebar): rescan the workspace rows when the Windows runtime default changes
* test(sidebar): gate the 1000-workspace publish cost on its median
* test(sidebar): name the git-run token type in the identity test
* 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.
* test(d3a-2): the buffer view index follows a rename main answers
* test(e2e): the old-phone baseline records a windowless server opening the file, as main does since #31291 (test only)
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)
The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.
- Main stops counting a closed tab's terminal once it was asked to stop
and no pane shows it; a failed or refused stop takes the request back,
and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
feed stands in while a fresh read is out; it highlights its own
last-opened workspace and a fold never scrolls; projects file under
their project group with the desktop's indent; the collapse spinner
shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.
* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises
- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.
* Transfer keyboard focus when a sibling chat pane opens
* fix(native-chat): hand focus to newly opened chat composer
* fix(native-chat): keep tab bar controller within line limit
* fix(chat): use terminal pane opacity for split chat focus
* test(chat): verify split opacity follows the focused group
* fix(chat): an unfocused split chat dims exactly as much as a split terminal
A split chat was dimmed with the terminal's inner-pane opacity (0.9 by
default) while terminals and editors in an unfocused split group get the
group's 0.95, so chats looked dimmer than terminals beside them. The chat
layer now takes the same shared group dim and nothing more.
* fix(chat): terminals and chats in an unfocused split dim like editors
Editors and tab bars in an unfocused split group were dimmed to 0.95, but
terminals and chats are drawn in a layer outside the group, so they stayed
at full opacity. One shared opacity now dims the group panel and the
retained terminal and chat hosts alike, so all three look the same.
* fix(browser): a paired desktop shows a server's browser tab without opening a second page (switch-on builds only)
With the layout-owner switch on, a server browser tab drawn from the server's layout
reaches the desktop without a saved page handle. Showing it made the desktop create a
new page on the server, so the server went from one page to two.
Before creating, the desktop now asks the server for its browser pages and adopts the
one already named by the tab's page or browser tab id. It creates a page only when the
server has none, when the lookup fails, or when the server gives no list. A pane that
moved on during the lookup adopts nothing. Switch-off builds keep creating directly.
* test(browser): explain the fake runtime call's cast in the page session test
* fix(browser): adopt a server's browser page by the id its layout already names, not by listing pages (switch-on only)
This replaces the tab-list lookup. A tab list misses saved pages a restarted server has
not started, wakes the active saved page as a side effect, and can match a page another
client hosts.
The server's layout already records the page id on the browser tab. Desktop hydration
ignored it whenever the page record had not landed yet, and made up a new id on every
frame build. With the switch on, hydration now uses the tab's recorded page id, so the
desktop row is named by the server's page from the first frame and stays named by it.
A frame-drawn row still has no saved page handle: hydration has none for server-hosted
pages, and the server environment is known only to the pane. So, with the switch on, the
pane probes its row id with browser.tabShow. tabShow also starts a saved page the server
has not opened since a restart. On success the pane stores the handle and streams that
page. On not-found, or any other error, it creates a page as before and closes nothing.
A pane that moved on during the probe adopts nothing.
Why not reorder the server to save the page record before opening the tab: it would only
fix the id, not the missing handle, so the probe is needed either way. The page record
write also depends on the tab main writes asynchronously, so the order cannot be made
certain from the server alone.
* test(browser): explain the lifecycle harness's fake runtime call cast
* fix(browser): create a server browser page only when the server lacks it, and prefer the tab's active page id
The switch-on probe created a page after any tabShow failure, so a timeout or a brief
connection error could duplicate a page the server still runs. Now only a page-missing
answer creates one. Any other error is raised, as the stored-handle path already does, and
the pane shows its stopped state with Reconnect.
Hydration now names a tab's page by its recorded active page before its first page id,
which matches the page the server starts for a saved tab.
* refactor(renderer): read workspace records through typed selectors
* refactor(renderer): keep only the selectors with readers; type the host owner lookup
* feat(accounts): add Claude and Codex accounts to a remote server from a paired client
The server runs the login (Claude's pasted-code sign-in, Codex's device
code) and keeps the credentials; the client only opens the page and
relays the code. New capability-negotiated accounts.hostSignIn* methods;
older servers degrade to an update message.
Fixes#20009
* fix(accounts): settle host sign-ins by the login's outcome, pin pairing, guard stdin
Review fixes: a cancel the login refused still reports the added account;
the client pins each flow to its pairing revision; Claude's stdin EPIPE no
longer crashes the host; both sides accept only the provider's sign-in
host; pasted codes reject whitespace; a host-side supersede says so; Codex
cancels its own login handle; opt-in real-CLI test.
* fix(accounts): keep an early Cancel, retry polls through the host's window, scan Claude stdout only
A Cancel clicked before the host answers the start is applied once it does;
the client keeps polling through a short outage until the host would have
ended the flow; Claude's link is read from stdout only; a timeout after the
code was sent keeps its own message.
* test(accounts): name the stdin sender and allow its stream write in the persistence ratchet
* fix(accounts): register each Add Account attempt before its first await
One attempt per click owns the pairing pin, the cancel flag and the host's
flow, so a Cancel during the capability check or the start is never lost;
polls retry only lost answers; the stdout forwarder is removed on cleanup.
* refactor(accounts): drive the client sign-in as one explicit state machine per server and provider
checking -> starting -> following -> settled, with Cancel as a flag the run
loop delivers once the host names its flow and re-asks after any answer
until the host takes it. A second Add joins the running attempt; a Cancel
the server could not be told about is surfaced; retry policy tested.
* fix(accounts): retry a lost start answer, and report a cancelled sign-in as cancelled however it ends
* fix(accounts): Cancel reaches the server a sign-in runs on, and a typed code stays with its flow
The host picker can move while a sign-in runs: Cancel now targets the
running server sign-in (or the desktop's own login when none runs), and
the pasted-code form is keyed by server and flow.
* fix(accounts): run at most one server sign-in per provider, so Cancel has one meaning
* fix(accounts): the server sign-in prompt owns its Cancel
Cancel for a server sign-in now lives in the prompt bound to that server,
which also reappears with its Cancel when Settings reopens mid-sign-in; the
header Cancel is only for this desktop's own login.
* fix(accounts): hide the desktop-login Cancel while a server sign-in runs; narrow the prompt's status
* fix(accounts): keep the desktop-login Cancel in local scope even while a server sign-in runs
* fix(accounts): the header Cancel follows the running addition, wherever Settings now points
Each Add Account records where it is signing in. Cancel stops that
sign-in, on a server or this desktop, and is hidden only where that
server's own prompt already offers Cancel.
* fix(remote-terminal): repaint an idle program when a reconnect can't image its pane
After an SSH reconnect the pane is repainted from the host's screen
snapshot. When the host cannot give one (too large for the replay limit,
no buffer, unanswered, overflowed), the pane only printed a loss banner and
an idle full-screen program that writes diffs kept its pre-disconnect frame
until the user resized.
The two places a remote pane declares it has no host image now ask the
program to repaint through one choke point: a one-row viewport claim and
back, which every TUI honours (Node's tty ignores a same-size SIGWINCH).
It respects a phone or another desktop holding the size, skips hidden
panes, and puts the row back even if the pane unmounts mid-nudge.
Fixes#26887
* test(e2e): idle full-screen program frame after a managed SSH outage
* test(e2e): wait for the pane before typing in the idle-frame spec
* fix(remote-terminal): pick the redraw height on the host's clamped grid
* fix(remote-terminal): wait for the host's grid before putting the redraw row back
* test: give transport mocks a connection status for the redraw gate
* test(e2e): keep the orcad idle-frame proxy typed for the e2e ratchet
* fix(remote-terminal): let a new PTY's redraw supersede a swapped-out nudge
* test: keep new transport-mock lines free of type assertions
* ci(e2e): route the managed-SSH idle-frame spec to its sources
* fix(layout): a running agent's resume record is not a sleep in switch-on builds
The shared layout load put every pane resume record into leaf.sleeping, so main's
terminal index read a live or quit capture as a slept pane and terminal.list
reported running agents as disconnected and unverifiable. Records of slept panes
(isPaneSleepRecord) now load to leaf.sleeping and running agents' records to
leaf.resume; both save to the same sleepingAgentSessionsByPaneKey entry, so the
disk format is unchanged. The disk-field census names both holders, and a model
rule refuses a pane holding both.
* fix(layout): a waking start wakes only slept panes; a recordOnly wake drops either record
* fix(layout): main's shutdown reconnect facts read a running agent's resume record as awake
Adopt the nine Japanese wording corrections from community PR #29248. Clarify smartphone control, manual restoration, and mobile data connections.
Closes#29247.
Co-authored-by: sojiro-o <53679703+sojiro-o@users.noreply.github.com>
* fix(native-chat): typing fast after focus moves keeps keys in order
Typing a burst right after focus left the composer (Move Tab to Split, a
paired chat, a click on the transcript) could scramble the first letters:
"Reply exactly" came out as "Rply exactly...e".
The first key was routed into the composer programmatically: focus moved
at once, but the caret was set a frame later from a position computed
before the key. The next keys landed natively, then that late caret write
pulled the caret back. Quote "Add to chat" and picker completion had the
same deferred caret write.
Each programmatic write now puts the text and caret into the editor in one
synchronous step, read from the editor's live value, then focuses it. No
write happens while an IME composition owns the field.
* fix(native-chat): retain saved chips after synchronous composer edits
* feat(layout): main places a swept workspace's live terminals; the window sweep only reads (switch-on builds only)
The window's activation sweep (first visit, an empty or sleeping activation, a phone wake) now
asks main, through its existing terminal.list call, to place that workspace's unshown live
terminals before listing them. With the switch on the sweep writes nothing to the window store and
counts a pane main names as shown, so it no longer seeds a fresh shell beside a terminal main just
placed. Main's "still binding" check also covers a registration in flight.
* fix(layout): a sweep's placement pass holds back only its own workspace's first surface (switch-on builds only)
* test(layout): switch-on activation-gate and census-call tests read main's listing, not window binds
* fix(ssh): draw every host list through one bounded, searchable host picker
A generated ~/.ssh/config can import 13k+ SSH hosts, and several surfaces drew a
row per host: Settings > General workspace directory 'Apply to', Notifications >
Machines, the sidebar and Agents Hosts submenu, the composer run-target picker,
the Add Project host selector and remote step, the skill-install machine pickers,
Settings > Servers SSH host picker, repository 'Add to another host' and the
automation host field and filter.
They now share components/host-picker: boundHostList (pinned hosts plus a ranked,
capped match list), HostCombobox for single choice (a Command in a Popover that
mounts nothing until opened, replacing Selects that mount every item), and
useBoundedHostRows with HostListSearchField and HostListOverflowNote for
checklists. A host-list-render ratchet stops new JSX from mapping straight over a
host collection.
* fix(ssh): host lists keep room for search results, menu search is keyboard-reachable
- boundHostList gives pinned hosts and other matches separate limits, and narrows
pinned hosts by the query, so a large selection never hides what the user types.
- In a dropdown, the host search is a menu item: arrow keys reach it, focus moves
into the input, and arrows leave it again.
- The Add Project host picker hides its setup actions while searching, so Enter
picks the matching host.
* fix(ssh): re-picking the current host in a host picker is not a change
The Selects these pickers replaced suppressed same-value changes; the skill
install form clears its workspace and target on a machine change.
* chore(ci): let the shared ratchet runner pick up the host-list render ratchet
main now discovers every check-*-ratchet.mjs through run-ratchets.mjs, so the
separate workflow step would run it twice.
* fix(ssh): host lists rank pinned matches and keep input order; label and scroll fixes
- boundHostList ranks pinned and other hosts by the same match order before
cutting each to its limit, then keeps input order among equal ranks, so a
muted or checked exact match is always reachable and clicking a row never
moves it.
- The Servers SSH host picker is named by its form label again.
- The sidebar Hosts submenu scrolls its host rows instead of running off screen.
- The composer's overflow note sits outside the listbox.
* fix(shortcuts): app shortcuts run before the terminal, whatever order they mounted in
App shortcut owners and terminal panes both listened on window capture,
so React's effect order decided who saw a chord first. After a worktree
switch the new pane registered before the tab shortcut listener and
turned a user-bound Cmd+Right into Ctrl+E; clicking a tab flipped the
order back. A keyboard router installed before React renders now runs
every app owner, in registration order, ahead of any pane listener; a
key no owner claims still reaches the terminal's own handling. Held tab
chords are claimed on every repeat but switch once.
Tracked in STA-11205 (same bug as STA-4602).
* test(shortcuts): type the router mock with key event names
* fix(shortcuts): install the keyboard router in the web client before it renders
* test(shortcuts): floating panel specs read the routed key listeners from the window stub
* fix(notifications): tie a completion banner to the hook turn it announces, not a 10 s window
A title-only working->idle edge (Claude's /btw side question fires no hook) raised a
generic "Task complete" banner because the pane's hook row was older than 10 s. On a
hook-reporting pane the hook row now decides: a row not yet announced supplies the
banner content however old it is; an already-announced row means no turn ended, so
no banner. Hookless agents keep the generic title fallback.
* fix(notifications): a pane with hooks gets its completion banner from its hook turn only
The title lanes (mounted pane and parked watcher) announced a finished task on every
working->idle title edge, even on panes whose agent reports turns through hooks. A title
spin that ends no hook turn (Claude's /btw side question, Codex's /compact) raised an extra
banner that could only be filled with the workspace name and tab title.
On a pane the hook lane is tracking, with a settled Done row from the same agent, the title
lane now defers before it claims the turn, so the hook lane's banner (with the last message)
is the only one. A BEL in the same burst waits on the hook lane's pending Done. Hookless
agents, restored rows, hooks switched off and the post-restart re-arm keep the title banner.
This replaces the previous commit's announcement memory in the dispatcher.
* fix(notifications): hook-owned turn ends settle pane bookkeeping and announce re-entered Stops
- A title idle edge that defers to the hook lane now settles the pane's turn, so quitting the agent after a /btw no longer raises a generic "Task complete" banner.
- A BEL held while the hook lane's Done is pending is superseded by that Done at fire time instead of being held until the next turn start.
- A Stop re-entered after a blocking Stop hook carries its new final message in its identity, so the hook lane announces it instead of deduping it against the first Stop.
- Title lanes ask the hook lane whether it can announce the turn end (live, agent evidence, not re-arming) instead of copying its conditions, and fall back when the agent's own hooks are switched off.
- The hook lane's non-quiet Done forwards its own snapshot, so its banner content no longer depends on the stored row's age.
- The whole-flow test now drives the real pane wiring, the real hook lane and the real dispatcher.
* fix(notifications): the host marks each real turn end, so a repeated status can't raise a second banner
The renderer told a Stop re-entered after a blocking Stop hook from a
restatement by its final message. The message also changes when the
listener's cache resets (a session reset, then an idle reminder), so a
restated finished turn raised a second, empty "finished" banner.
The execution host now classifies each provider's own turn-end event by
name and stamps an optional `turnEndedAt` on the status row. Idle
reminders, session events, replays, child re-emits and result retries
restate it; a turn-end event on an already finished row is a new end only
when the agent says it re-entered (`stop_hook_active`). Local hosts and
SSH hosts (orcad) mint it in `attachStatusTiming`; the WSL relay mints it
at admission and sends it on its envelope. The field is persisted,
forwarded over IPC and the paired-server session projection, and the
renderer's done identity is `turnEndedAt ?? stateStartedAt` (after the
existing `turnCompletedAt` precedence). `stateStartedAt` and every other
clock are unchanged.
Paired servers that predate the stamp advertise no
`agent-status.turn-ended-at.v1` capability; their panes keep the
terminal-title completion fallback exactly as before.
* test(notifications): type the old-server pane setup in the completion flow test
* fix(notifications): a blocking Stop's final banner reaches the user; /compact and quitting an idle agent stay quiet
Real-agent QA on the hook-only completion design found three gaps:
- A Stop re-entered after a blocking Stop hook could be dropped twice
over: inside 1 s of the first banner by the replay guard (a new host
turn-end stamp is now a real end), and by main's 5 s per-workspace
desktop cooldown. A banner with the same notification id and changed
text now replaces the one already shown instead of being dropped;
every other cooldown rule is unchanged.
- On a mounted pane, a hook-owned idle edge (Codex /compact) no longer
claimed a completion, so the burst's BEL went out as a Bell banner. The
deferred idle edge now absorbs that BEL like a claimed completion did.
- Quitting an idle agent after a title spin raised a generic process-exit
banner. On a pane whose hook lane owns turn ends and whose last turn is
done, an exit ends no turn; a crash mid-turn still gets its banner.
* test(runtime): the projection reference keys on the host turn-end stamp too
* test(relay): restore the Copilot retry test while CI isolates a runner error
* test: key the hot-path projection reference on the turn-end stamp; relay retry restates the end
* fix(notifications): host turn identity per agent session; keep later blocking-Stop banners past main's cooldown
- The execution host (local main and the WSL relay, one shared reducer) now tracks which turn an
agent session is in: Codex by its own turn id, Command Code by its transcript prompt position,
every other agent by its prompt-opening hook. A turn end mints a new end when its turn differs
from the last ended one, so a Codex turn woken by agent mail and a text-only Command Code turn
get their banner; duplicates, replays, tool reopens and result retries restate it. The record
is saved with the status row (validated on restore) and never published.
- Main's desktop and mobile notification cooldowns are back to main's code exactly.
- A later end in the same done segment (a Stop re-entered after a blocking Stop hook) that lands
inside main's cooldown is held, latest only, and sent once after that cooldown through main's
unchanged gates; the renderer learns main's answer to anchor it.
- One pane check gates every changed pane behavior: the pane's bound PTY runs on a host that
stamps turn ends and its agent's continuation signal is verified (Claude, Codex). Every other
pane, and an unbound PTY, keeps main's behavior.
- A hook-owned idle edge absorbs its burst's BEL for main's full 1.5 s; exits and replacements
ask the owner check with the exiting agent; a title spin on a hook-owned pane opens no turn.
* test(notifications): run the whole-pane harness through main's shared desktop cooldown, not main's service module
* fix(notifications): decide hook ownership from how the host identified each turn end
The execution host now labels every classified turn end with one optional
disposition beside its end stamp, by the same rule for every agent:
identified (an unused prompt opening, a new native turn id, or a verified
re-entered Stop) mints a new end; a same-turn repeat is a proven duplicate
only when the agent supplies both a turn id and a verified continuation
flag; anything else is ambiguous and never mints. Replays and retries
restate the decided answer, and local and relay hosts carry an end only
within a done segment.
The renderer replaces the Claude/Codex pane gate with one evidence
decision: identified ends are hook-owned, ambiguous ends keep the title
lane's role, and rows without a disposition (older hosts, unclassified
agents) keep the previous behavior exactly. A title working edge on a
hook-owned pane keeps its turn bookkeeping and only the hook receipt; an
owned idle edge settles after the existing max wait, or announces through
the title path if the end turned out ambiguous. Idle and working title
edges resolve the same owner (the title's agent, else the foreground one).
The held later banner for identified continuations is unchanged.
* test(notifications): give the whole-pane harness's host event its local connection id
* fix(notifications): a settled cancellation ends the open turn, so its late Stop folds as on main
A cancel Orca inferred from Esc or Ctrl+C writes a done row with no turn-end event. The host reducer now completes the current turn key on that settled cancellation, so the same turn's late Stop is a repeat (ambiguous, no new stamp) instead of a second identified end that requested a second banner.
* fix(notifications): a settled cancellation ends the open turn on every lane, so a relay's late cancel end restates none
The desktop restates a WSL relay's end as decided. The relay never learns a
cancel the desktop inferred for a non-Claude agent, so Grok's own late
stop_cancelled reached the desktop as an identified end and gave a second
banner. One predicate now decides both lanes: after a settled cancellation
that carried no end, a restated end keeps the shown one, and a child event
carrying the cancellation also ends the open turn. The relay's own cancel
row is the main agent's, never a child's.
* fix(notifications): a relayed done segment shown without its own end keeps showing none
The restatement fold tested the previous row's cancel verdict, so the row it left behind (a late
plain stop) no longer matched and the relay's next restatement brought its end back. Fold on the
row's turn-end fact instead, which the folded row satisfies again.
* fix(sidebar): a slept chat workspace reads asleep and hides
* fix(sidebar): a wake during Sleep's teardown keeps the workspace awake
* test(sidebar): give the activation-race mock store the chat tab map Sleep reads
* feat(native-chat): the host marks the chats an explicit Sleep stopped
The fact "the user's Sleep put this chat to rest" now lives on the host
that runs the chat, on its session record, instead of in the window.
- New runtime capability agent-session.workspace-sleep-mark.v1 and an
optional markAsleep on agentSession.sleepWorkspace (strict params, so
only hosts advertising the capability are sent it).
- The Sleep close decides each chat's receipt inside the same per-session
step that stopped it; with markAsleep it writes sleptAt in that step's
record write, so a chat is marked exactly when it is in the receipt.
- The next send the host hands over clears sleptAt, whichever client sent
it; the idle sweep and plain stops never mark.
- sleptAt rides the status summary (optional field), its equality, and
the retained summary of a closed chat, so it survives restart through
the persisted tab index.
* refactor(sidebar): read a slept chat from its host's mark, not a window record
The window no longer keeps its own saved record of which chats Sleep
stopped (explicitlySleptChatsByWorkspace). That record had to mirror every
host event that should end it (sends, replaced conversations, renames,
profile and WSL moves, removal, wakes during teardown), and each missed
mirror was a bug. The host now owns the fact (previous commit).
- Delete the record, its schema, store slice, workspace-session field
registrations, moves, renames, purges, migrations and hydration, and the
renderer clearing hooks. Those files are back to main.
- Opening a workspace no longer wakes its slept chats (product ruling):
drop the chat Sleep-attempt tokens and their set-active-worktree hook. A
slept chat wakes only when a send reaches its host, from any client.
- The status bridge mirrors each chat tab's published sleptAt into a
store map keyed by the chat's pane key, dropped when its projection
unmounts and never saved; a chat whose host runs it again is not slept.
- The asleep derivation, hide filter, palette, board and context menu read
that map. Explicit Sleep sends markAsleep only to the local host and to
hosts last probed with the new capability; a project move never does.
* fix(native-chat): preserve host-owned Sleep across fallback and legacy tabs
* fix(native-chat): clear Sleep when a new send is admitted
* chore: restore incoming documentation after merge hook formatting
* fix(native-chat): let admitted sends supersede pending Sleep receipts
* fix(sidebar): wait for host Sleep state before showing restored chats
* fix(sidebar): retain host Sleep answers when chat projections remount
* fix(native-chat): clear Sleep when queued drafts become sends
* test(sidebar): cover unknown and known chat Sleep state on the board
* test(vault): expect both host status capability checks
* test(native-chat): consolidate Sleep behavior coverage
* fix(native-chat): commit Sleep wake with send admission
* test: pin visibility while chat host is unidentified
* test: consolidate workspace sleep regression flows
* test: keep status feed regression within renderer boundary
* Editor: git Discard pauses that file's autosave in every workspace; read-only markdown/CSV never pushes over edits (phase 1b-3)
- A Source Control discard holds autosave (and waits for in-flight saves) for every tab of that
file in every workspace, keyed by its model save key, until the discard finishes or fails; the
quiesce now returns the release, called in a finally. Watcher notices don't release it. A 120 s
backstop only shows the changed-on-disk banner on dirty tabs and never reopens autosave. This
closes the race where the compare fallback on older hosts reads disk just before git restores it.
- A read-only tab renders raw Monaco source whatever its type, so it counts as a code view: a clean
read-only .md/.csv tab can no longer push disk text over a writable tab's unsaved edits.
Only removes or delays writes. One autosave timer per model moves to 1b-4.
Co-Authored-By: Claude <noreply@anthropic.com>
* Editor discard hold review: release on a failed quiesce, cover unstaged diffs, re-show an overdue banner (phase 1b-3)
- A quiesce that throws (e.g. an unresolved CSV cell) releases its hold and rejects, so the discard
reports the error instead of hanging with autosave off; a bulk discard frees the holds the other
paths took.
- Holds key on the physical file (host + path), so an unstaged diff tab of the file in another
workspace is paused and drained too; save-queue keys are unchanged.
- Past the backstop, a dirty held tab whose banner was dismissed gets it back, so the pause is
never silent. Taking a hold no longer re-syncs; the save-time check already refuses.
Co-Authored-By: Claude <noreply@anthropic.com>
* Editor discard hold: key the physical file with the 1b-1 path rules, not the client's URI rules (phase 1b-3)
The hold key went through the Monaco model URI, which normalizes paths by the client platform, so
on a Windows client `/repo/a\b.ts` and `/repo/a/b.ts` on a POSIX host shared a key. It now uses
the owner key plus `editorBufferOwnerPathKey` (new, in editor-buffer-identity), which applies the
1b-1 rules: POSIX backslashes kept, separators folded only for proven-Windows paths, UNC server
case, and WSL aliases on the local Windows client. Same on the tab and the discard-path side.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ssh): the Remote Hosts menu lists a capped set of idle SSH hosts
* fix(ssh): the SSH Hosts settings list renders hosts a page at a time
* fix(ssh): window the SSH Hosts settings list and keep project hosts in the capped menu
Settings > SSH Hosts now windows its rows against the Settings scroller with the
shared VirtualizedList instead of paging 100 at a time, so every host stays
reachable by scrolling or the filter while only the rows near the viewport mount.
The Remote Hosts menu cap rises to 25 idle hosts, fills it with hosts that own
projects first, and the overflow row says to search the rest in Settings.
* fix(ssh): search the Remote Hosts menu's capped idle hosts in place
Web clients reach SSH hosts through this menu but have no SSH Hosts settings
pane, so a link to Settings stranded every host past the cap. The menu now shows
a search box whenever idle hosts are hidden; it narrows them by name, and the
overflow line says how many are left. Also gives the two existing status-bar
store mocks the repos field the menu now reads.
* fix(ssh): typing in the Remote Hosts menu goes to its host search
The search box is not a menu item, so arrow keys can't reach it; letters typed
while the menu has focus now land in the search instead of Radix typeahead.
* fix(ssh): the Remote Hosts search keeps focus and drives its results by keyboard
The host rows are not menu items, so the menu's arrow keys never reach them. The
search box is now a combobox over the connectable results: it keeps focus, arrow
keys move the active result (announced through aria-activedescendant), and Enter
connects it through the same path as the row's Connect button.
* fix(ssh): Remote Hosts search only reaches hosts that match, and keeps the active one in view
Every not-connected row now has to match the search, so Enter can't connect a
failed host the user didn't type. Arrow keys scroll the active result into view.
Also drops restyles on the search Input and fixes the singular overflow default.
* refactor(ssh): find hidden Remote Hosts in a searchable picker, not a search box inside the menu
A search box inside the dropdown kept fighting the menu over focus, Tab, Enter
repeat and IME input. The overflow row is now a menu item that opens a host
picker (Command in a Popover, as the style guide prescribes for single choice
with search). It ranks exact names first so any host is reachable by typing its
name, mounts at most 50 results, and says how many more match. The menu keeps
config order when nothing is capped.
* fix(ssh): closing the host search from the keyboard returns focus to the status button
The picker is anchored, not triggered, so Radix had nowhere to restore focus and
it fell to the page. A keyboard close now focuses the Remote Hosts button; a click
elsewhere keeps focus where the user clicked. Also drops the padding restyle on
PopoverContent.
* fix(ssh): the host search marks the keyboard-selected host with the palette highlight
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* 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
* 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(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.
* fix(terminal): a window split main cannot start still shows its pane, failed, with why
* 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
* refactor(terminal): the rebound workspaces' bindings show through one call in the bindings module
* test(terminal): a pane main wrote and the user closed mid-start drops its failed status
* fix(terminal): a split main starts is listed to phones and session.tabs without a window's report
* 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): start an agent's terminal at its first real size instead of after a 12-frame wait
The startup grid settle waited for the grid to change and then hold for 2 frames;
a grid that was right from the start always paid the 12-frame maximum (~90 ms at
120 Hz, ~200 ms at 60 Hz, seconds when frames are throttled, since it also cleared
the 250 ms fallback timer). Its only case, the setup split, was retired in #29819.
Command panes now connect like plain panes: on the first frame with a real
measurement, or the 250 ms fallback timer.
* fix(terminal): bound startup by completed fit and deadline
* test(terminal): brace startup contract control flow
* fix(terminal): release pending startup when window hides
* fix(runtime): a phone reads and saves Markdown tabs on a headless server
With no window to relay through, markdown.readTab and markdown.saveTab answered
renderer_unavailable. Main now reads the tab's file (or its saved draft) from disk and
saves with a version compare plus the disk write precondition, in the shapes the
window's editor relay returns. With a window attached the relay is unchanged.
* Headless Markdown save: keep the draft's disk baseline, honour read-only tabs, re-find the record to clear
* Use the disk baseline only for a draft
* ci: run the phone Markdown spec's orcad case in the orca serve mode-switch job
* Headless Markdown: answer from disk only; a window's saved draft stays its view state
Clearing a saved draft wrote workspace layout from main, which no layout command carries.
Main now never shows or edits a draft; the desktop's restore check reports a disk change.
* Phone Markdown: one path through main and the disk, window or not
A paired server's workspace is read and saved on that server with files.read /
files.writeIfUnchanged, so it answers from its own disk and keeps the precondition.
The window relay (ui:mobileMarkdownRequest and the renderer bridge) is removed.
* e2e: a phone saves a Markdown tab the desktop holds unsaved edits in
The phone reads the disk with the tab marked dirty; its save lands on disk, and the
desktop keeps its draft and shows the changed-on-disk banner.
* Group the Markdown disk host's imports
* Drop an unneeded store cast from the dirty-readers test
* Phone Markdown: refuse a save on a server too old to check the disk; cover the untitled tab
A server before #30605 has no files.writeIfUnchanged, so the save is refused with the
existing update-required message rather than written unchecked.
* Phone Markdown: refuse a file an SSH host holds outside the workspace; pin the untitled answer
* Phone Markdown: untitled tabs stay read-only; older servers save like the desktop editor; a closed-source preview still reads
- The desktop lists an untitled document's tab with isUntitled (optional, additive); main answers
it read-only with unsupported_untitled and refuses its save before touching the disk.
- A server without the files.write.disk-precondition capability gets the desktop editor's guarded
save: compare from here, then a plain files.write, or a conflict. Every call of a save is pinned
to the pairing it probed.
- A preview whose source tab was closed has no saved record; it reads its own file, read-only.
* e2e: read the untitled tab's answer without a cast
* Phone Markdown: pin a save's pairing before its first read
A save captures the server's pairing before it reads, and every call of the save, the read
included, is pinned to it, so a re-pair between the read and the write is refused.
* Test: with no window, a phone saves an existing untitled.md and never creates a deleted one
---------
Co-authored-by: E2E Test <e2e@test.local>
* test(layout): downgrade probe for the layout-owner session format, and the touched-records patch's guard
Downgrade probe (ledger L8): a checked-in golden of the session main writes in a layout-owner
build (layout commands through the Store plus a window save), matched byte for byte; a
switch-off Store hands it on unchanged and a switch-off window restores its tabs, panes, groups,
editor file and browser tab. Regenerate with ORCA_UPDATE_LAYOUT_OWNER_GOLDEN=1.
L104: for each writer of the touched-records patch (layout.apply, the cross-partition move, the
window save), the patch equals a full save on every partition the load leaves alone, and differs
only on records the load normalizes otherwise. Two concrete legacy cases are pinned, and a
switch-off window restores each differently after a full save, so the patch stays.
* test(layout): type the guard's partition ids and locate the golden without import.meta
* test(layout): pin every legacy fixture's patch difference, assert restored panes and group selection, keep the golden deterministic