- isTwinWorkspace replaces four inline resolveOwner(...).kind === 'ambiguous' checks
- someWorktreeCopy shared by isUnprovenServerSshRoute and hasHostEvidence
- file-explorer owner: drop the unreachable second unresolved guard and the stale comment
- native chat owner: drop a null ternary the helper already handles
- tests: delete stale getRuntimeEnvironmentIdForWorktree mocks (29 suites), one shared
local+ssh twin fixture, file:// open-target tests next to their module, reset mocks.twin
* fix(ssh): preserve activation commands through SSH gateways
* fix(ssh): fall back when a host cannot decode a long command, and read claims and snapshot verdicts through gateway noise
On top of the gateway fix (#30503):
- A host without base64 or gzip gets a long command plain: the compressed wrapper
checks for its decoders before reading anything, exits 125 with a marker, and
execCommand resends once with compressCommand: false and remembers that host.
`base64 -D` covers macOS before 13, whose base64 refuses `-d`.
- Snapshot capture, restore, presence and compare answers read the last line that is
a verdict, so shell noise a stderr-merging gateway appends after it is ignored.
- The machine claim reads the claim line by line, and every failed step now prints
CLAIM_FAILED <step> <error> with the df line, so "orcad could not claim this host's
.orca-remote" says what failed and how to fix it instead of exiting 65 with the raw
command or failing with no reason. A failed write no longer leaves a partial file.
Refs #30491
* test(ssh): size the simulated gateway cut from the command, not a fixed 10 KiB
The snapshot command repeats its temp path ~40 times, so on CI's short /tmp it fell under 10 KiB
and the truncation precondition failed.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: zzzpyyy <167217540+zzzpyyy@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(editor): opening a quoted lettered list with an unquoted continuation no longer crashes
Marked's blockquote re-reads a lazily continued list with its built-in list(),
which rejects Tiptap's extended markers (a., iii)) and returns undefined; the
caller then reads .raw and the rich editor hits its error boundary. A per-lexer
tokenizer falls back to the registered ordered-list tokenizer, cut where
Marked's own list would end at an outer quote line.
* test(editor): guard extended-marker blockquote fallback against dependency drift
Source re-review against Marked 17.0.6/18 and Tiptap 3.31.3: pin the override
to Marked's lazy-list call site, build the quote-break regex once per item,
and cover more item starts Tiptap accepts so a bump can't reopen the crash.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* perf(watcher): flush file changes on a steady cadence instead of waiting for quiet
Watcher batches used a 150 ms trailing debounce capped at 500 ms, so a worktree
with agents writing continuously always waited the full cap. The git status
nudge had no cap and could starve until the 60 s safety poll. Batches now flush
25 ms after the first change following a quiet period, then at most every 100 ms;
new events join an armed flush instead of pushing it back.
* fix(watcher): keep atomic saves in one batch and survive clock jumps
Raise the first-flush delay to 75 ms so the native watcher's split burst lands
in one batch, cap the flush delay at one interval so a backwards clock jump
can't stall delivery, and widen the editor delete debounce to cover a split
across one flush interval.
* test: wait past the widened editor delete debounce
* fix(watcher): keep markdown list refreshing under sustained writes and widen delete grace
Markdown document refresh used a resetting 125 ms debounce that never fired
once watch batches arrive every 100 ms. Delete tombstones now wait two batch
intervals so slow stat calls (WSL, AV scans) can't split a save into a flash.
* fix(watcher): bound watcher-driven reads and scans under sustained writes
One watcher-triggered editor read and one markdown scan per target in flight,
with a single follow-up; markdown scans honour the latest change time; the
headless Windows watcher no longer resets its refresh timer on every event.
* fix(watcher): make the faster remote cadence opt-in and harden watcher-driven reloads
files.watch takes an optional batchCadence; hosts keep legacy trailing batching
unless the client opts in. Coalesced editor follow-ups re-check the tab and
replace a hung read only once; Markdown mounts skip pre-change scans; the
Windows fallback refreshes the whole tree at most every 500 ms.
* fix(watcher): stop restarting the remaining watch-driven timers
Combined-diff section reloads and per-file editor reload dispatch no longer
push an armed timer back; a queued editor follow-up for a tab that left the
screen marks it stale instead of dropping the change.
* fix(watcher): let combined-diff and worktree notifications land under sustained writes
A combined-diff section discards an invalidated in-flight load at most once in a
row; worktree base-directory notifications no longer restart their timer.
* fix(watcher): never overwrite an edited diff section and unstick hung reloads
A combined-diff load never commits over a section with unsaved edits; a reload
queued behind a hung read starts after the stale limit without needing another
change event.
* fix(watcher): recover from hung markdown scans and keep evidence for edited tabs
A Markdown scan that never settles is abandoned after the join window so later
changes scan again; a queued editor follow-up for a tab edited meanwhile marks
it changed-on-disk and stale instead of dropping the change.
* chore(watcher): use a numeric markdown scan id
* fix(watcher): don't let later events join a stale echo-verification read
Payloads from the same burst still share the in-flight read; a payload from a
later task gets exactly one fresh read issued after the current one settles.
* fix(watcher): stop unwatched windows from being re-subscribed to local watchers
Two pre-existing races left a window subscribed after it unwatched:
- a re-watch waiting out a cancelled install was not tracked, so its unwatch
could not withdraw it and the retry subscribed it anyway
- restoring after a failed worktree removal dropped the suspended owners up
front, so an owner that unwatched mid-restore was still re-subscribed
Adds a randomized lifecycle model test plus regressions for both sequences.
* fix(watcher): keep a retried removal's owners when it overlaps the previous restore
The restore now leaves its snapshot in place while it works, so a retried
removal can add owners to the same entry. Stop restoring once the snapshot is
forgotten or replaced, delete it only when empty, and merge failed owners into
the current entry instead of overwriting it. Apply the same per-owner restore
to SSH watchers, which dropped the saved list before re-subscribing.
The lifecycle model now drives removals through the real coordinator and lets
them overlap restores; adds local and SSH restore regressions.
* fix(watcher): drop SSH re-watches unwatched while waiting out a cancelled install
Same leak as the local fix: a watch that could not join an aborted SSH install
waited untracked, so its unwatch had nothing to remove and the retry installed
it anyway. Record those waiters, clear them on unwatch, and retry only if the
sender is still recorded.
* fix(watcher): keep reloaded windows and cancelled-install waiters across removals
- An older restore checked a window's lifetime only after taking its entry, so
a window reloaded under the same id lost its place in a newer removal's
snapshot. Check the lifetime first (local and SSH).
- Removal now suspends senders waiting out a cancelled install and drops their
retry; previously the retry installed mid-removal and the close tore it down,
losing the window. Waiters keep their WebContents for this.
- Waiter maps live in watcher state: cleared on shutdown and per sender on
cleanup.
- Model: windows can reload (same id, new lifetime) and subscribes can be
wedged (ignore abort).
OrcaRuntimeWithDeliverPendingMessages.deliverPendingMessages was only
reachable from itself and settlePendingMessageDelivery, which was only
reachable from it. Live terminal mail delivery goes through
OrchestrationMailboxPointerDelivery (stage/submit), so this removes the
dead mixin link, its settle method, its state fields (flights, parked
redeliveries, probe dedupe, pointer watermarks), and the now-unused
isCursorAgentOrchestrationTarget and RuntimeMessageWaiters.typeHasLiveWaiter.
No behaviour change.
* fix(ssh): follow the managed server's port after activation and match quoted Windows forward argv
An update, rollback or recovery that launches a managed server now rebuilds the
tunnel so it follows the port the new server bound. Windows orphaned-forward
recovery now parses the process table's quoted command line instead of
comparing it to a space-joined argv, so a key path with spaces still matches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ssh): keep a successful activation when the tunnel rebuild fails; strip one LogLevel pair
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(ssh): soften the tunnel rebuild failure comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ssh): tokenize posix expected argv like the process table before normalizing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(runtime): project removal and settings writes keep to the resolved execution host
- Removing a project now closes its native Claude/Codex chats on this host and retires their tabs.
- A terminal spawn after removal is admitted only by a surviving row on the spawn's own host.
- repo.update and repo.setBaseRef write the resolved row's host, not a same-id sibling.
* fix(runtime): close project chats alongside the terminal stop, with their own floor
- A failed or budget-spent terminal stop no longer skips the chat closes.
- Spawn admission still refuses on any host once no row with the removed id survives.
* refactor(shell): one shell-argument-quoting module for the duplicated quote copies
Each former copy now calls the canonical function with its exact spelling; a table test pins
byte-identical output for every copy over hostile inputs. Distinct spellings stay distinct
functions: '\'' vs '"'"', the two plain-word sets, and the cmd.exe doubled quote.
Co-Authored-By: Claude <noreply@anthropic.com>
* style: keep the quoting import at the top of two import-less files
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ssh): rehome a converted workspace after a cold-start scan misses it, and notify replayed background-turn completions
A cold-start conversion now lists the server's worktrees itself when startup's scan finishes
without the destination row (the scan predated the server's projects or failed for them).
A reconnect replay of a working row carrying a new turnCompletedAt is now observed instead of
seeded; the completion observer already dedupes the stamps it handled.
* test(ssh): drive the cold-start rehome retry through the server RPC
* refactor(renderer): fold browser creation, file actions and doc-preview callers off the deprecated owner resolver
The creation-action policy and browser creation (#3, #48) fold together, so the provider the
policy offers is the one creation uses. Browser panes, page lifecycle and conversion, tab
duplication, the explorer tree pane, doc-preview actions and editor path moves (#21, #43, #45-#47,
#49, #50) read runtimeEnvironmentIdWithLegacyFallbackUnlessTwin. File-explorer writes on a folder
(#51) take the owner from the rows alone, never from the focused server. Deprecated importers
16 -> 6.
* refactor(renderer): fold file preview off the deprecated owner resolver; Open in Orca Browser refuses a twin
A project two hosts publish reads as no server for the doc-preview plan, but the file:// path now
refuses it with its own message instead of opening this machine's copy (#7). Deprecated importers
6 -> 5.
* fix(renderer): a twin's file preview and open-externally refuse instead of opening this machine's copy
Codex P2 on the F7 folds: a folder twin read as no server, so the preview plan built a local
file:// tab and open-externally handed the path to this machine's OS. Both now check the owner
for ambiguity first. Also fix two test fixtures that broke the renderer typecheck.
* refactor(renderer): move the file:// open target out of file-preview.ts
file-preview.ts crossed its line limit after main grew it; the file:// target, its type and its two
refusal messages move to workspace-file-browser-open-target.ts, which they never depended on
file-preview for.
* test: mock the replacement resolver in the quick-action availability suite (Codex P2)
* fix(terminal): client pane names reach a desktop host's window, and open panes follow the host's
- A host with its own desktop window ignored client pane-layout writes, so a
client's pane rename never reached the window. updateMobileSessionPaneLayout now
passes the client's pane names to the window one pane at a time through the
set-pane-title path, and still leaves the window's geometry and the tab's name
alone. A single pane named after its own tab is dropped, not forwarded.
- Open panes on a client only read host pane names when they mounted. They now
follow the host's published titlesByLeafId live. A local rename is held until
the host echoes it, so a snapshot taken before the host saw it cannot undo it.
No wire change: hosts already publish titlesByLeafId, and old peers keep their
current behaviour.
* fix(terminal): hold a client's pane rename over host snapshots until echoed, and fail a dropped window delivery
* test(e2e): read the created host tab id without a type assertion
* fix(terminal): only an explicit client rename reaches a desktop host's window, and local rename holds are bounded
- A client push now names the panes it renamed (optional `renamedLeafIds`), and
the host forwards only those. A resent title map, which older clients and every
divider drag send, can no longer revert or clear a name set on the host.
Older hosts strip the field. Older clients send none, so their renames stay
local as they do today.
- A held local rename now gives way to a newer host name, a failed push, or a
15s expiry, and expired holds are pruned. A host that never echoes (an older
desktop host, or a single-pane name dropped as the tab's own) can no longer
hide host titles forever.
* fix(terminal): a failed rename push retries with its rename, and an earlier echo cannot undo a newer rename
* fix(terminal): a pane rename bypasses the unchanged-layout push dedupe
* fix(terminal): reattach a restored terminal while an older daemon cannot answer (#30486)
A window save erased the PTY incarnation main persisted for each pane, so
after a restart every reattach lacked the generation proof the owner
resolver needs. With any older daemon generation that could not answer,
the inventory stayed incomplete and every restored terminal was refused as
unverifiable, on every Retry.
- keep each pane's incarnation across a window save that still binds it
- accept an exact incarnation match from a reachable daemon even when
another daemon's inventory fails (incarnation ids are unique per spawn)
- drop a provably exited older daemon from the reattach inventory
- offer Open new terminal beside Retry on the unverifiable toast
* test(daemon): build the unanswering daemon endpoint with the platform socket path
* fix(session): keep each pane's incarnation when a save carries only some of them
* fix(daemon): judge a replacement legacy daemon's exit by its own pid
* test(daemon): router fixture reports a pid that exists, so exit proof never sees a dead daemon
* test(daemon): the wedged older daemon names a live pid instead of the shared fixture's arbitrary one
* fix(daemon): a spawn treats an older daemon as exited only when its socket and pid are both gone
* test(browser): expose native popup admission blockers with faithful app events
* test(browser): probe Electron popup identity and inherited sandbox API limits
* test(browser): handle requests without associated contents in popup probe
* docs(browser): record blocked Electron popup compatibility gates and test routing
* docs(browser): keep popup evidence in PR
* fix(sidebar): the host raises the shared unread dot regardless of Filters (dark)
* fix(sidebar): tighten the shared-dot comment and tests after review
* fix(layout): two owners' files at one path keep their own host, load report entry and phone retire (layout-owner build only)
The bare-path tab's file lookup took the first record of its path, the load report keyed file
records by path alone, and a headless host's phone retire found and removed records by path.
* fix(layout): a path-id preview tab's flag stays on its own owner's record, and a stale phone id retires nothing (layout-owner build only)
* test(layout): an e2e ends the window's layout stream mid-session and checks the reopen and a relaunch (dark, layout-owner build only)
* test(layout): the stream-loss e2e covers main's change during the gap deterministically and reuses the shared pane-tree and frame readers (dark, layout-owner build only)
* test(layout): the stream-loss e2e names how long main's create took when it misses the reopen window (dark, layout-owner build only)
This reverts commit fc925c3e9b until the real fix for STA-11160 lands.
With it, `orca orchestration worker-start --agent claude` shuts a new Claude
chat worker down right after Claude acknowledges its task: between the echo
and the accepted row the send reads as no longer held, settlement reports it
unknown, and worker-start tears the session down.
Conflicts with #29150 were formatting only; resolved to the pre-#30134 code.
Builds on the program-status core:
- A determinate progress ring replaces the working spinner when a program
reports progress (rows and tabs; percent only in tooltip/aria), in the
attention tone while blocked. The working color becomes the
--agent-working token.
- Non-agent programs (brew, terraform, …) get their first letter as icon.
- The program's own one-line message shows as the row preview, a failed
program reads "Failed: <message>", and a block such as sign-in that the
row's state does not show raises attention.
- A finished program's result outlives its exit until the next keypress or
command in that pane, as the protocol asks; real agents are unchanged.
* refactor(notifications): one host read of Notify About Hidden Workspaces, used by phone pushes and the host unread flag
* fix(notifications): folder chat phone pushes read the folder's name; skip the hidden check with no Filters switch on
* fix(sidebar): run previous and next worktree shortcuts from a text field
The worktree list swallowed every key while a text field was focused, so the bound shortcut did nothing in chat, markdown, and the code editor.
* fix(sidebar): keep the default worktree chord with text fields
Previous/next worktree now carries a text-field rule in its keybinding definition. In a text field the default Cmd/Ctrl+Shift+Up/Down keeps its select-to-start/end meaning, while a chord the user bound still switches worktrees from chat, markdown and the code editor. Option and AltGr chords that type a character stay with the field.
* fix(sidebar): let user-bound Option arrow chords leave text fields
On Mac only Option chords that type a character or dead key stay with a focused text field, so a bound Option+Down now switches worktrees from chat, markdown and the code editor.
* fix(worktrees): skip Git sweeps for missing repo folders
* fix(worktrees): skip missing roots in headless scans
* fix(worktrees): check missing roots at Git listing boundary
* test(git): treat branch removal fixture as existing repo
* test(git): keep mocked worktree roots present for Git cases
* test(git): await host preflight in scan sharing fixtures
* revert(worktrees): drop the listing pre-check for missing repo folders
Reverts 82ccf05a4d, 13854fd3dc, c19604012a, 3f1b7ef64c, a868225608
and 962d9218c2. The reader-level stat ran in the wrong WSL distro, added
a wsl.exe spawn per listing, and only covered git worktree list. The next
commits replace it with a gate at the shared Git runner.
* fix(git): stop spawning Git in deleted native project folders
A registered project whose folder was deleted spawned a failing Git
process (spawn git ENOENT) on every sweep: git worktree list and the
git remote probes. The shared async Git runner now remembers, in memory,
a native cwd once a spawn ENOENT is followed by a definitive lstat
absence. Later calls for that cwd re-check with lstat before admission
and, while it is still absent, reject with the same spawn ENOENT shape
without starting a process. The entry is dropped as soon as the folder
exists again. WSL routes and UNC paths are never recorded, so their
behavior is unchanged.
The fenced-output readers move unchanged to captured-git-output.ts so
git-exec-file.ts stays under the file-length limit.
* fix(github): skip queued PR refreshes for removed projects
A PR-status refresh queued before its project was removed still ran
and probed Git (git remote) against the removed path. The drainer now
re-checks registration for each entry before any Git or GitHub call,
using the same predicate the refresh IPC entry points use, so every
removal path (sidebar, CLI, phone, closed window) and the rate-limit
re-queue are covered. The skip mirrors the existing not-visible skip,
so there is no new skip reason on the wire.
* fix(git,github): skip missing folders without a span; pure refresh registration
Round-2 review fixes.
- The known-missing-folder check now runs before withGitSpan in both
async runners, so a skipped call starts no process and writes no
git.exec span. The command is resolved lazily, only for a cwd already
recorded, and the git.known_missing_cwd attribute is gone.
- The gate uses stat, which follows symlinks the way spawn's chdir does,
so a symlinked project root whose target is gone is recorded too.
- The Set is re-checked after the stat, so concurrent first failures
record and warn once.
- The PR-refresh drain check is now a pure predicate: is a project still
registered at the candidate's path on its execution host. It reads the
store each time and no longer resolves runtime Git options, which could
throw inside the drain loop. Matching by path and host, not repo id,
keeps a re-added project's coalesced refresh from being dropped.
* refactor(git,github): one wrapper per Git runner; drainer holds its check
Tripwire re-plan cuts, no behavior change except one Windows edge.
- The missing-folder gate is a single wrapper around each exported async
runner (gitExecFileAsync outside the fetch-head lock, and
gitExecFileAsyncBuffer). It decides "native" from the call's inputs
(cwd set, not UNC, and no WSL distro on Windows) for both recording and
checking, so the early command resolution and the second filter are
gone. The span bodies are back to main's code.
- A Windows WSL project's linked worktree that runs host Git on a drive
path is no longer recorded; before, it was recorded but never gated.
- The synthesized error is built from constants (spawn git ENOENT, empty
stdout and stderr). Nothing reads spawnargs, and the buffer runner's
only caller reads code and message.
- The drainer holds the registration check the way the event publisher
holds its outcome observer, and the registration skip shares the
existing not-visible skip.
* fix(sidebar): show the project name on ungrouped compact cards
Compact and new-card layouts kept the project in an icon chip, with the
name only in a tooltip. The chip now shows the project name when the
sidebar is not grouped by project.
* fix(sidebar): show the project name on pinned chips too, muted and lowercase
* docs(sidebar): describe repo identity placement, not the chip's contents
* fix(sidebar): keep emoji project icons inside the chip
Programs can tell Orca what they are doing through the Program Status
Protocol (OSC 7501); Claude Code, Codex, Grok, pi and oh-my-pi already do.
Orca answers the protocol's feature-detection probe (visible xterm and the
hidden-pane emulator), parses reports from PTY output on the execution host
(src/shared/program-status/), and keeps two lanes:
- Lifecycle state keeps one owner per pane. Hooks win; OSC 7501 drives the
row when hooks are off, missing, or silent on a state the program has
left (server/program-status/hook-precedence).
- The program's own report (progress, its one-line message, blocks such as
sign-in) is a live-only `programStatus` facet kept per pane
(server/program-status/facet-store), stitched onto rows at publish time
and carried to the renderer store, mobile, paired clients and
`worktree ps`. Facet-only changes patch just the facet.
Title-driven readers (idle edges, delivery, tui-idle, exit inference)
classify by the program's OSC 7501 activity while it reports one and fall
back to title glyphs only for agents that report nothing explicit, so they
keep working when Claude stops painting its title glyph after the probe.
* refactor(agent-status): one live agent transition detector in main, read by the sort stamps
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(agent-status): isolate transition readers from each other; doc touch-ups
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: rewrap the stamps producer doc; restore the console spy in a finally
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sort-stamps): name the title-to-hook handover gap the flip must close
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(agent-status): forward every hook observation so the stamps hand title panes to hooks as before
The stamps producer applied only the detector's transitions, so a pane its title
showed as Working kept that tier when its first hook row was a finish, a stop, or
had no workspace. The detector now reports each live look at a pane's hook rows;
the producer applies those exactly as it applied store reads. One workspace type
and comparison. Startup-test fakes gain the runtime's new members.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sort-stamps): pin the title-pane preconditions and a no-workspace row at attach; doc wording
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(runtime): move the agent transition wiring into its own runtime layer; one workspace type
orca-runtime.ts was over the max-lines limit. The detector and attachAgentStatusStore
now live in OrcaRuntimeWithAgentTransitions, and the sidebar layer overrides the
attach to subscribe its producers first. Sort-stamp records use AgentPaneWorkspace.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(runtime): name the new base in sidebarFactsRuntime; accurate attach comment
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Give Session History's rows one decision for open and resume
A session row decided "can ▶ resume this" twice: once for the button's
disabled state and again inside its click handler, and the chat/CLI and
switch-to-pane offers were each gated inline. A second caller of those
offers would have had to copy the same conditions again.
- One projection, resolveAiVaultSessionRowChatActions, now decides every
open/resume offer a row makes; ▶'s greyed-out state and its target come
from the same value.
- The panel's host-scope menu and per-session resume lookups move into
their own hooks unchanged, and the session-list request reads the
panel's workspace through one selector.
The sidebar behaves exactly as before.
* Let Session History host a chat-bound panel with keyboard selection
A second Session History panel (the /resume dialog that follows) needs
to describe a chat's own workspace rather than the active one, and to
let ↑/↓/Enter pick a row. Mounting it beside the sidebar also exposed
a place where two panels stepped on each other.
- AiVaultPanel takes an optional workspace, an initial query, a header
action slot, a small search handle, and onSessionOpened, which also
turns on row keys. A thin sidebar wrapper now owns Settings' "focus
the search" request, so only the sidebar widens to All.
- Keyboard rows store the chosen row, its session and the search text;
a session listed twice no longer traps the arrows, and editing the
search starts again at the top match. Enter takes the row's best
offer: switch to it if open, else open it as a chat, else the row's
own ▶, else show details. A clicked row moves focus to the panel, so the
keys keep working, and a row scrolled into view stops below the
pinned group header.
- Switching to an open session reports whether it found something to switch to.
- Mounted panels share one copy of the saved view options, so a filter
changed in one is not undone by the other.
* Make /resume in a structured native chat open Agent Session History
Resuming a past session was only reachable from the Agents sidebar, so
people in a chat had no way to find it. In a structured native chat,
/resume [search] now opens Agent Session History for the chat's own
workspace in a dialog, with the search filled in.
- The dialog lives in the app's modal slot: a dialog rendered inside a
chat would bubble its key and pointer events through the chat's own
capture handlers. It closes once the tab on screen changes or a
switch to an open session succeeds; a failed action leaves it open.
Focus goes back to the chat unless the user was taken elsewhere.
- /resume is an ordinary row in the structured chat's / menu, in place
of an agent command of that name, or last when there is none. A skill
named resume keeps its own row and a picked skill still goes to the
agent. Typing it in any case opens the dialog; attachments stay.
- Terminal-backed chats (SSH, WSL runtimes, custom start folders,
reused terminals, older hosts) are unchanged: /resume reaches the
agent as before. The phone is unchanged too.
* Start /resume's Session History on the chat's own agent
The dialog's agent filter starts on the agent recorded for the chat, the
same one its tab icon shows, and stays local to the dialog: the sidebar's
saved filter is never read or written by it. Sort, grouping and limit stay
shared. An agent the filter doesn't list leaves every agent shown.
* Start an OpenCode chat's Session History on both OpenCode names
A structured OpenCode chat records its provider as opencode, but the
scanner lists current OpenCode sessions as opencode2, so filtering on the
recorded name alone hid every OpenCode row, the chat's own included.
* feat(native-chat): /resume rows resume the conversation in place of the chat
In the /resume dialog a row is now only a "resume here" target: clicking it
or pressing Enter opens the picked conversation as a chat right after the
chat /resume was typed in, and closes that chat once the new one is
confirmed and the old one is idle. A conversation already open elsewhere is
switched to, the chat's own conversation just closes the dialog, a closed
chat of this workspace and agent is reopened in its place, and anything else
only expands its details. A failed resume closes its own tab and leaves the
old chat as it was.
Dialog rows drop every other action (overflow and context menus, title drag,
hover buttons, detail and subagent buttons); the chevron still shows the
details. The dialog widens from 480px to 560px. The sidebar's Session
History panel is unchanged.
* test(native-chat): type the launch-plan mock in the /resume flow test
* fix(native-chat): find the reopened chat by its tab, typed test settlement
* fix(native-chat): /resume picker review fixes (R5)
- Subagent lines in a picker row offer no View Log either (resume null switches them off).
- An unsaved row in the picker drops the "open the log" hint, since the picker has no log button.
- The sidebar title drag handler is restored, so a row whose resume is disabled still blocks text drags.
- Only a failed resume closes its new tab; an unknown outcome keeps both tabs (the new one has its own badge and Retry).
- The replaced chat closes only when its last turn finished, its composer draft is empty, and it is not pinned.
- One pick at a time: a pick is ignored while an earlier one is still settling.
* fix(native-chat): a new, empty chat is idle for /resume replacement
A chat with no status entry closes when its launch has finished and its host,
observed live, reports no message and no provider still starting. Still-starting
and unobservable chats stay open.
* fix(native-chat): /resume knows a stale row's open chat; no Copy in picker details
- A row listed before its chat took the conversation over has no ownership link.
The pick now also reads each open chat's own record (its host status feed's
provider session, else the conversation its launch resumes): the chat /resume
was typed in just closes the dialog, and another open chat is switched to,
instead of trying the takeover again and failing.
- The first prompt's Copy button is hidden in picker rows (same switch).
* fix(native-chat): act on the host's ownership verdict for stale /resume rows
Drop the renderer's own "which chat holds this conversation" lookup: it keyed on
the record's head handle, which differs from the host's ownership key (Pi rows,
older chain links). Instead, when a resume-here launch is refused because the
conversation is held elsewhere, the refusal's owner decides: this chat closes the
picker, another chat is switched to, the temporary tab closes, and no failure
toast is shown. Rows with an ownership link keep the existing fast path.
* refactor(native-chat): share the composer editor setters with the /resume hook
Keeps NativeChatComposer under its line cap once current main is merged in.
* feat(native-chat): /resume replaces its chat, asking first only when the agent is busy
Picking a row now always closes the chat /resume was typed in once the picked
conversation's chat is confirmed, pinned or not; the unsent-draft and
finished-turn checks are gone. When that chat's agent is replying, waiting on an
approval or question, or still starting, Session History closes and a small
"Replace this chat?" prompt asks first; Cancel or Esc changes nothing.
The prompt reuses the shared confirmation dialog with a new compact look
matching the close prompts.
* fix(ui): compact confirmation look through dialog title/description sizes
The primitives own their typography, so the short close-prompt look is a size
variant on DialogTitle and DialogDescription rather than class overrides.
* fix(native-chat): command picker takes the full structured transport
* fix(ui): compact confirmation width through a DialogContent size
The /resume replace prompt matches the close prompts width (max-w-sm) via a
compact size on the primitive; callers that pass no size render as before.
* fix(editor): pan-zoom dialog props keep their own image size
DialogContent now has a size prop; the pan-zoom dialog s size means the image, so
it is omitted from the inherited props.
* fix(native-chat): /resume asks when the chat's state is unknown; prompt ignores the opening gesture
- R8-1: a chat whose host isn't observed live (old host without the status feed,
lost contact, no host stamped on the tab) counts as busy, so replacing it asks.
- R8-2: the confirmation dialog takes ignoresOpeningGesture: an outside press
doesn't dismiss it, a repeated Enter doesn't confirm, and a double-click's second
click doesn't press Replace. Other callers are unchanged.
* Point the resume-command test at the moved picker-items module
* Reopen a closed owner in place of the chat on a refused /resume
A row listed before its conversation was adopted has no structured link, so /resume launches a new chat and the host refuses it, naming the closed chat that holds the conversation. That branch reopened the owner but never closed the source chat. Both the reveal path and the refusal path now share one reveal-in-place step; an owner open in another tab is still switched to and the source kept.
* Put a reopened chat in the replaced chat's slot
Reveal appends the reopened tab to the end of its group, so a /resume that reopened a closed chat left it after unrelated tabs. Move it right after the source with the existing anchor-insertion helper before the source closes, the way a new chat lands.
* Keep the typed search when the /resume row is picked
With the caret right after /resume and a search typed past it, picking the menu row opened Session History with an empty search. Read the search from the draft, as a typed send does.
* fix(native-chat): rewind works at any chat length
Rewind copied the kept part of the chat into its saved record and, for
Codex, re-read and re-translated the whole provider history before and
after the revert. Both were bounded (10,000 rows / 2 MB host, 1,024
entries / 2 MB Codex), so long chats were refused with "too long to go
back in".
The record now names the cut; the kept rows are derived from the
journal, which keeps the old epoch until the rewind completes. Codex
reads only the turns it undoes and trusts thread/revert; recovery
proves an applied revert from the newest turn alone and never
truncates otherwise.
* test(codex): type the rewind fixture's page params instead of asserting them
* fix(native-chat): rewind recovery reads the provider handle only from a record that exists
* test: run the rewind cut test in the SQLite runtime project
* fix(native-chat): omit rejected submissions from rewound history
* Confirm bulk tab closes before closing tabs
Close Others, Close Tabs to the Left/Right, and Close All Editor Tabs now
show a confirmation dialog first, so one misclick or shortcut no longer
closes a group of tabs at once. Pinned tabs are still skipped.
Tabs are selected again after the user confirms. If a tab was pinned, moved,
or removed while the dialog was open, it is left alone. Tabs created after
the request are not closed.
The shared confirmation lives in use-bulk-tab-close-confirmation.ts and is
used by both the tab bar and the tab group. The tab group hook now reads
the store directly instead of taking group and groupTabs as props.
* Confirm bulk tab closes before closing tabs
Close Others, Close Tabs to the Left/Right, and Close All Editor Tabs now
show a confirmation dialog first, so one misclick or shortcut no longer
closes a group of tabs at once. Pinned tabs are still skipped.
Tabs are selected again after the user confirms. If a tab was pinned, moved,
or removed while the dialog was open, it is left alone. Tabs created after
the request are not closed.
The shared confirmation lives in use-bulk-tab-close-confirmation.ts and is
used by both the tab bar and the tab group. The tab group hook now reads
the store directly instead of taking group and groupTabs as props.
* Add telemetry for directional tab close requests
Measure how often people use Close Tabs To The Left and Right, without recording tab titles, paths, or workspace identifiers.
- Sample a stable 10% of installs by hashing the install ID, so membership survives restarts. The app-open event carries the sampled flag so the group can be analysed.
- Cap each direction to one event per app session, tracked in main, so repeated clicks or extra windows cannot inflate volume.
- Count the request before the close flow runs, so cancelled save prompts still count.
- Route both directions through one helper that counts eligible unpinned tabs in visible order.
* Count settings opt-in sessions in tab-close sampling denominator
Enabling telemetry from settings now fires `app_opened` for that session, not only when the opt-in came from the pending banner. Before, settings opt-ins never emitted `app_opened`, so those sessions were missing from the adoption denominator for directional tab close events.
- `trackAppOpenedOnce` now marks the session as tracked only when `track()` accepts the event. An opt-out or environment consent override no longer uses up the session's denominator.
- Tests cover sampling for settings opt-in and the `DO_NOT_TRACK` override.
* feat(accounts): sign Antigravity chats in from the Accounts page
Adds an 'Antigravity chats' row to the Antigravity section of Accounts
(behind the native chat setting, this device only): Sign in starts the
host's chat sign-in, follows it with the Link poll, opens the Google page
once, offers Copy link and Cancel, and shows one status line per phase.
The saved line reflects the sign-in file only, never that it works.
* Keep Antigravity chat sign-in bound to the current view
* feat(settings): put Antigravity chat sign-in under its install row in Chat > Providers and Accounts
The chats' sign-in row now sits directly below the Antigravity install row,
in Settings > Chat > Providers and under the agy section of Accounts, with copy
that says it is separate from the agy account. Its button reads Sign in, Change
account or Retry sign-in by state, and stays visible but disabled while the host
has no runnable server; the install row above says why.
* fix(settings): both Antigravity chat sign-in rows share one sign-in per host and pick up one already running
Settings shows the sign-in row under Chat > Providers and under Accounts at once; each kept its own
flow, so starting one silently cancelled the other and a sign-in nobody watched could not be seen or
cancelled. One entry per host now holds the flow and the saved sign-in for every row, follows the
host's Link poll while a row shows it, and on mount adopts the running flow the host names. Sign in
also waits while an install or Remove runs, read from the same install status.
* fix(settings): a failed sign-in poll re-reads the host so a running sign-in stays cancellable, and only a Remove (not an Update) holds Sign in
* fix(settings): the sign-in rows follow the host's sign-in status through the same loop as install, so an idle page sees another device's sign-in
* refactor(settings): install and sign-in rows share one subscription store; Install and Remove wait for a running sign-in, and Sign in waits for the host's answer
Both stores now follow the host through one generic subscription store
(snapshot, then a frame per newer revision, resubscribe with backoff on loss),
replacing the sign-in store's long-poll of a method the host no longer has.
While a sign-in runs, Install, Update and Remove are disabled (Cancel stays).
The sign-in row keeps Sign in disabled until the host answers, says when the
status could not be read instead of showing signed out, and sends one start or
cancel at a time.
* feat(settings): paste the last sign-in page's address to finish an Antigravity chat sign-in
While a chat sign-in waits on the browser, its row shows a field for the address of the last page after signing in, for when that page doesn't load because the browser is on another computer than the host. Continue sends it to the host, which passes it on to Antigravity; the host's refusal shows under the field. With that, remote hosts offer chat sign-in on the Accounts page too, and the row reads "Finishing sign-in…" while the host completes it.
* test(settings): give each host its own fake sign-in status and stub the new subscription export
The sign-in row tests shared one fake host across every owner, so the first test's finished sign-in made the next row show "Change account" instead of "Sign in", and rows left mounted after that failure doubled the later tests' matches. Each owner now has its own fake host behind the subscription, and every test cleans up its render. The Antigravity accounts section test's client mock also lacked the sign-in exports the status store now reads at import.
* feat(settings): the Antigravity chat sign-in row follows the host's sign-in answer
The host no longer reports whether a sign-in file exists; it reports what the
server last answered (signed in, signed out, or not known yet), and a running
sign-in started by another client arrives without its id.
The row now reads that answer:
- "Signed in on {host}." or "Not signed in on {host}."; no line until the server
has answered, with Sign in still enabled. Change account shows only when the
server said signed in.
- A sign-in another client is running reads "Sign-in is in progress in another
client.", with Sign in disabled and no Cancel, copy link or paste field, since
only the client that started it can act on it.
- "Finishing sign-in..." also follows a browser sign-in while the host checks it.
- Cancel and paste send only this client's own sign-in; the host's refusals
show under the row.
* fix(chat): a signed-out Antigravity chat says to sign in under Settings, not what the agent printed
The notice read "Sign in to Antigravity. The agent asked to sign in in a
browser": the second sentence was Orca's own log text for the sign-in prompt the
agent printed, shown as if it were the agent's words, and the notice never said
where to sign in.
The connection now closes with a sign-in prompt error whose message stays in the
logs, so the chat shows only Orca's sign-in sentence. Antigravity gets its own
sentence, in the shape Claude's already has: "Antigravity isn't signed in. Sign
in under Settings → Accounts → Antigravity." Other agents' sign-in words, and
the agent's own words in a sign-in answer, are unchanged.
* fix(chat): a sign-in prompt at chat start keeps Orca's words out of the signed-out notice
A signed-out Antigravity chat still read "... The agent asked to sign in in a
browser". The connection closes with a sign-in prompt error whose message is
Orca's log text, but the request wrapper and session setup re-typed every
sign-in answer by copying its message into a new error. The copy lost the
sign-in prompt type, so its message was quoted as the agent's own words.
Re-typing now keeps a sign-in prompt close as it is, at both the request
wrapper and session setup, so the notice says only where to sign in.
* fix(settings): the Antigravity sign-in row shows the host's answer once a sign-in is cancelled or timed out
After a cancelled or timed-out sign-in the row kept reading "Sign-in
cancelled." even when the host had answered that this computer is not signed
in. A finished sign-in now only stands in until the host answers; then the row
reads "Not signed in on <host>." or "Signed in on <host>.". A running sign-in
still shows its step.
* fix(settings): a wrong pasted sign-in address shows Orca's reason, not the browser's URL bubble
The paste field is a URL input, so the browser's own "Please enter a URL."
check ran before Orca could explain what was wrong with the address. The form
now skips the browser's check and shows the host's refusal sentence.
* style(settings): the Antigravity chat sign-in row reads as a plain row like the install row
The sign-in row sat in its own bordered box, the same bright outline the install
row lost when it became a single grey status line. It now has no box: a
medium-weight "Antigravity chats" title, the description and the grey status
line under it (errors inline in the destructive colour), and its action at the
row's right end. Sign in and Retry are primary buttons, so they stand out like
Install; Change account stays outline and Cancel stays ghost. The paste-the-
address form sits under the row without a box. Copy, keys and behaviour are
unchanged.
* ci: run the PR checks against main after the retarget
* fix(settings): list every sign-in phase in the status line switch
* test(chat): name Antigravity as the chat's agent in its signed-out sentence test
* feat(settings): Antigravity provider row with a ⋯ menu and its own sign-in area
The Providers row's status line now says only the install state. Update,
Reinstall and Remove move into a ⋯ menu beside the switch, shown for a copy
on disk; Remove keeps its confirmation. Sign-in moves into a quiet inset
area under the row, shown once a copy runs, with one sentence and one
action per state. Accounts keeps its sign-in row; its install row gets the
same menu.
* fix(settings): a damaged runtime offers Reinstall as its row's button
A damaged copy's repair is the next step, so it sits where Install does instead of inside the ⋯ menu, which then offers only Remove.
Pure git mv plus import and path-literal rewrites. Two files #29339 edits stay put:
orcad-migration-dormant-state-validation.ts and orcad-migration-manifest-contract.test.ts.
Co-authored-by: Claude <noreply@anthropic.com>
* test(wsl): exercise structured chat on real guest hosts
* test(wsl): keep chat contract within the module size limit
* test(wsl): use braced conditions in chat lane
* test(wsl): configure the fixture through host launch settings
* test(wsl): keep cancellation assertion at the RPC boundary
* test(wsl): avoid assuming cancelled turn publication timing
* ci(wsl): run guest chat coverage for Claude environment changes
* test(ci): match the WSL lane's merged unit-test exclusion
* ci(wsl): drop lane trigger paths that no longer exist
* ci(wsl): run the WSL lane when the shared process-host package changes
* refactor(renderer): fold launch, resume, note-send, paste and terminal-create callers off the deprecated owner resolver
Agent launch, AI-vault resume and reveal, note target and send, wake and creation seeds, native chat
owner, the shell menu and active-workspace terminal creation (#22, #31-#42, #44) read the owner
through runtimeEnvironmentIdWithLegacyFallbackUnlessTwin. Paste uploads images to the server its
PTY id names before falling back to the workspace owner. The folder and floating branch of the
terminal spawn route (#53) reads resolveOwnerWithLegacyFallback: spawn refuses a folder twin
instead of following the focused server, and teardown keeps tearing raw ids down locally.
Deprecated importers 31 -> 16.
* test: give the paste-listener and tab-bar probes the fields the folded callers read
* test: note why the folder-twin route fixture is cast
* test: type the paste-owner twin fixture
* refactor(pty): one spawn-options step for both spawn lanes, and one first-size rule
The window lane (pty:spawn) and the host lane (runtime terminal create)
each built the provider's spawn options with their own copy of the same
steps. Both now call one shared assembly for the env deletions and the
shared provider fields; each lane keeps only what is its own (pane
reservation, Antigravity prep, agent-session claims). The duplicated
provisional-size restore moves to the same module, and the window lane
drops its second, identical hidden mark.
A terminal's first size now comes from one rule: the caller's measured
pane, then the measured group, then a lone group, then 120x40. Host
creates accept the caller's measured size for the callers that move
onto them next.
* fix(pty): keep terminal-create under the file-length limit
* fix(pty): preserve hidden replacement and settings timing
The live "thinking" status was translated as bare nouns (思维, 考え, 생각, Réflexion) that read as a faculty or a thought rather than an ongoing state. Use present-progressive wording for the status, and add key-specific overrides so the status and the composer option keep distinct labels.
* Main keeps an agent's resume record from the status its hook server accepts (dark, layout-owner build only)
In a layout-owner build, the window's recorder and quit capture kept only
records of tabs the window holds, so an agent in a workspace the window
never opened lost its resume record at quit. Main now applies the window's
rule (moved into src/shared) to every hook status for a pane it holds, and
captures its own panes' agents at quit; the window's quit capture is gone.
Co-Authored-By: Claude <noreply@anthropic.com>
* Main writes a workspace sleep's resume records after the hook statuses it accepted before the sleep (dark, layout-owner build only)
* A snapshot replay looks up a pane's tab only when its status can leave a resume record
Restores the lookup order the window had before the record builder moved to shared code, and
updates the two handler tests for the quit-records option.
* Note why the status write applies its command directly
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(i18n): clarify bulk tab closing in all languages
* Update tab context menu tests for "Close All Tabs" labels
The menu labels were renamed to "Close All Tabs To The Left/Right" in the i18n change, but these two test files still expected the old "Close Tabs To The ..." text. Update the assertions so the tests match the current labels.
* Open pasted HTML file:// URLs as document previews
Pasting a file:// URL to an HTML file into the new-tab box used to fall through to the generic URL path, which does not show the document's relative assets. It now routes to the workspace document preview, so the file renders with its local scripts and styles.
- Classify HTML file URLs before the file list loads, so the "Open file" option appears immediately.
- Resolve file URLs and absolute paths only against workspaces on the same host, so an SSH target is never matched to a sibling on another host.
- Reuse an open document on any page of its workspace instead of opening a duplicate preview.
- Keep the preview in the group the user targeted, including for remote hosts.
* Open HTML file:// previews without resolving file-operation context
Opening an HTML file:// URL used to need the worktree and file-operation context before the preview could open. In remote or not-yet-loaded states that lookup can fail, so the preview never appeared. Pass the context as a lazy getter, so it is read only when a file operation actually needs it. The preview path now runs without it, and the dispatch in openTabBarEntry no longer special-cases HTML before the network and file paths.
* refactor(ssh): split ssh-connection-utils into concretely named modules
- classifiers -> ssh-reconnect-error-classification.ts
- retry/timeout constants and sleep -> connection/ssh-connection-timing.ts
- buildConnectConfig -> connection/ssh-connect-config.ts
- credential callbacks -> connection/ssh-connection-callbacks.ts
- exec options and abort error -> connection/ssh-exec-options.ts
- wrapRemoteCommandForPosixShell -> ssh-posix-command-wrapper.ts
- shellEscape and shared/wsl-login-shell-command quotePosixShell (identical) -> shared/shell-argument-quoting.ts quotePosixShell
No re-exports are left behind.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: point the WSL git benchmark and the WSL lane route at shell-argument-quoting
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
No behavior change. Moves cohesive pieces out of files at their line limit
so the OSC 7501 program-status work on top reads as additions:
- pane-agent-identity: the confirmed-shell tab-title reset
- SortableTab: its activity-status selector, into useTerminalTabActivity
- agent-status-event-applicator: the Pi provider-session partial update
- terminal-derived-agent-facts: resolving a PTY's status target panes
- server-row-ownership: notifying row-mutation listeners
* fix(terminal): a first visit resumes a sleeping agent once, in its own pane (dark, layout-owner build only)
Main retires a gone terminal's binding before the pane's fresh start, so on
main-owned layouts the activation sweep read the unbound pane as unable to
resume and launched the agent again in a new tab while the pane resumed it in
place. On main's layout the pane that holds the record owns its resume.
* fix(terminal): the sweep defers only to a pane whose own resume is in flight (dark, layout-owner build only)
Counting every unbound pane in main's layout as the record's owner also
covered a mounted pane whose agent died: it never restarts on its own, so
its agent was never resumed. The pane now marks its in-place resume while
the reattach or fresh start that carries it runs, and the sweep defers only
to that mark.
* test(terminal): the in-place resume hold reads only whether the pane is disposed
* refactor(terminal): the in-place resume hold takes the pane's liveness, not the session
* test(e2e): skip the single-resume spec on Windows, as its sibling resume specs do
* test(terminal): both starts that resume a record set the in-place mark
The e2e that would catch a dropped call needs a layout-owner build CI never
makes, so a switch-on unit test drives each start and checks the mark while it
runs and after it settles. Also corrects the mark's comment: a hidden pane's
background wake sets it too.
* feat(mobile): the phone page lists workspaces from the host's sidebar feed (dark)
* fix(mobile): settle pending option changes by latest intent; feed carries the desktop's focused host and host names (dark)
* fix(sidebar): the feed carries the desktop's pinned placement; e2e compares the phone list to the desktop sidebar (dark)
* test(sidebar): the phone list e2e also expands and unpins
* fix(sidebar): phone feed list review fixes (dark)
A collapsed or worktree.ps-failing feed list no longer shows an empty or error state; a queued worktree read uses the latest client; a superseded option change does not report a refusal; the window shadow leaves the focused host to main's test (focus-read ratchet).
* fix(sidebar): phone feed tests typecheck; e2e helper no longer pulls the native runtime types
Mobile test fixtures fill the fields the test project's types require; the feed trigger type lives with the feed reader, so the e2e helper's imports stop dragging react-native globals into the e2e program; React Doctor's two blocking findings.
* build: share one pnpm virtual store across worktrees, off under CI
Each worktree carried a full node_modules; the global virtual store links
one slot per package version instead. pnpm keeps an explicit setting on
under CI, so a pnpmfile hook turns it off there: CI packages from,
caches and verifies a project-local node_modules.
* fix(build): keep shared electron and windows-process-tree dirs untouched under GVS
Every worktree runs Electron from the one shared electron package, so the
per-repo dist cache must not swap that dist and a foreign-arch install must
not land in it. The relay addon builder now repairs and compiles a private
copy instead of the shared windows-process-tree dir.
* fix(build): accept CLI runtime imports reached through an in-artifact store link
Under the global virtual store node_modules/<pkg> realpaths into a store
outside the repo. The check still rejects anything not reached through a
node_modules link inside the artifact, such as a stray ~/node_modules.
* docs: explain the global virtual store layout and the ~/node_modules hazard
* test: compare CLI closure paths with POSIX separators so the suite passes on Windows
* build: keep Windows on the normal pnpm layout unless ORCA_PNPM_GLOBAL_VIRTUAL_STORE=1
Store links are true symlinks on Windows, and installs without admin or
Developer Mode are unverified; CI still forces the normal layout first.
* fix(build): close the remaining shared-slot gaps under the global virtual store
- verify-skills-cli-runtime resolves each import from the importer's realpath,
as Node does, so an artifact-local copy can't mask a ~/node_modules load.
- A concurrent sibling install of the shared Electron dist is kept, not swapped out.
- rebuild:node goes through ensure-native-runtime, which builds a shared
node-pty beside the slot instead of node-gyp cleaning its build/ dir.
* fix(build): keep pnpm rebuild:node a forced rebuild
It replaced pnpm rebuild node-pty, which always rebuilt; without --force it
only rebuilt a node-pty that failed to load.
* docs: note that the store switches must stay set, since pnpm run relinks
* build: move the pnpmfile under config/ to keep the repo root allowlisted
pnpm-workspace.yaml points pnpm at it; the lockfile checksum is unchanged
because the content is.
* fix(build): enable the global virtual store from the pnpmfile, not the workspace file
pnpm exec derives NODE_PATH from the file setting, so even with the store
forced off under CI every child process got the root node_modules on its
NODE_PATH; a Windows rebuild fixture then resolved the real
@orca/windows-registry it deliberately omits. The hook now sets the store
both ways, and ORCA_PNPM_GLOBAL_VIRTUAL_STORE=0/1 is the one switch.
* test(pty): keep F3's concurrent spawns on the node-pty mock
Two concurrent first import('node-pty') calls race vitest's mock registry
and the loser loads the real addon under the mocked linux platform. It
passed only where a platform-agnostic build/Release happened to exist,
so it failed on every shared-store worktree.
* ci: fail PRs whose packages need a dependency they never declared
A new dependency_isolation job installs with the global virtual store
forced on (ORCA_PNPM_FORCE_GLOBAL_VIRTUAL_STORE_IN_CI), HOME and the store
in an empty temp dir, then runs pnpm tc and the undeclared-dependency test.
Postinstall now warns when a node_modules above the shared store, such as
~/node_modules, would mask the same class of bug locally.
* ci: set the isolation job's HOME and store through GITHUB_ENV
Job-level env cannot read the runner context, which made pr.yml invalid.
* refactor(build): extract the lock-free shared tree publish from the Electron dist cache
Native builds in package dirs shared across worktrees need the same
stage, validate, rename-into-place publish; one implementation keeps the
never-replace-a-good-entry and quarantine-restore rules in one place.
* fix(build): never rebuild a native addon in a package dir other worktrees load
Under pnpm's global virtual store node_modules/node-pty is one store slot
shared by every worktree, so an in-place node-gyp or @electron/rebuild run
in one worktree replaced the pty.node another was running. A shared
package dir now gets an immutable per-ABI build/orca-napi-<platform>-<arch>
compiled in a scratch copy, load-checked in a fresh process, and published
atomically; a usable entry is reused, never rebuilt. The patched node-pty
and windows-process-tree loaders try that dir before build/Release, and
packaging renames it to build/Release so packaged layouts are unchanged.
Private (non-GVS, CI) installs keep the existing in-place path.
* fix(build): find shared native packages through the project's own node_modules link
require.resolve with paths fell back to another install under Bun, so a
test fixture's private node-pty was judged shared.
* ci: run native checks when the shared native build modules change
* fix(build): pin the shared native slot and keep sibling publishes intact
Resolve the package's physical dir once so a concurrent relink cannot publish
one slot's build into another, verify the quarantined tree is the one judged
unusable before replacing it, and keep in-flight staging/quarantine trees out
of the packaged resource copy.
* fix(build): never move a sibling's fresh native entry, and refuse off-platform builds
Re-check the entry's identity right before quarantining it, and fail instead
of publishing a host-OS binary under another platform's entry name.
* Main keeps an agent's resume record from the status its hook server accepts (dark, layout-owner build only)
In a layout-owner build, the window's recorder and quit capture kept only
records of tabs the window holds, so an agent in a workspace the window
never opened lost its resume record at quit. Main now applies the window's
rule (moved into src/shared) to every hook status for a pane it holds, and
captures its own panes' agents at quit; the window's quit capture is gone.
Co-Authored-By: Claude <noreply@anthropic.com>
* A snapshot replay looks up a pane's tab only when its status can leave a resume record
Restores the lookup order the window had before the record builder moved to shared code, and
updates the two handler tests for the quit-records option.
---------
Co-authored-by: Claude <noreply@anthropic.com>
The eight mirroring callers (#23-#30) read the owner through
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin, so a twin is still not mirrored. Sort-order
persistence (#2) leaves out ids that two hosts publish, and ids whose only server proof is a repo
row (isUnprovenServerSshRoute), instead of writing them to this desktop's or a guessed server's
meta. Deprecated importers 40 -> 31.