Skip to content

Add Shift+Enter to insert newline in terminal - #3

Merged
nwparker merged 1 commit into
mainfrom
feat/shift-enter-newline
Mar 20, 2026
Merged

nwparker merged 1 commit into
mainfrom
feat/shift-enter-newline

Conversation

@mathnogueira

Copy link
Copy Markdown
Contributor

Summary

  • Intercept Shift+Enter in the terminal pane and send \x16\x0a (quoted-insert + LF) to the PTY
  • This inserts a literal newline in the shell command line without executing, enabling multiline command editing
  • Works with both bash (readline) and zsh (zle) which both support Ctrl+V quoted-insert by default

Test plan

  • Type a command, press Shift+Enter, verify a newline is inserted and the command is not executed
  • Continue typing on the new line, then press Enter to execute the full multiline command
  • Verify regular Enter still executes commands as normal
  • Verify Cmd+Shift+Enter still toggles pane expand/collapse

🤖 Generated with Claude Code

Intercept Shift+Enter and send Ctrl+V + LF (\x16\x0a) to the PTY,
which triggers quoted-insert in both bash (readline) and zsh (zle)
to insert a literal newline without executing the command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nwparker
nwparker force-pushed the feat/shift-enter-newline branch from cf63fe6 to 352e7ba Compare March 20, 2026 00:37
@nwparker
nwparker merged commit 27da9dd into main Mar 20, 2026
brennanb2025 added a commit that referenced this pull request May 11, 2026
Instantiates the relay-side hook server in `relay.ts` and forwards every
parsed payload to Orca via `dispatcher.notify('agent.hook', envelope)`. Adds:

- An env-augmenter mechanism on `PtyHandler` so every relay-spawned PTY gets
  ORCA_AGENT_HOOK_PORT/TOKEN/ENV/VERSION/ENDPOINT in its env. Server-side
  injection avoids ferrying the relay's bearer token across the SSH wire and
  keeps the relay the sole source of truth for the live coords (see
  docs/design/agent-status-over-ssh.md §3).
- An exit-listener hook on `PtyHandler` so the hook server can evict a
  paneKey's cached entry when the backing PTY ends — symmetric with Orca's
  local clearPaneState on PTY teardown, prevents ghost replay events.
- Request handler for `agent_hook.requestReplay` — issues replay
  notifications BEFORE returning the response so Orca observes every cached
  entry strictly before the request resolves.
- Stub handler for `agent_hook.installPlugins` (real impl in commit #7).
- `hookServer.stop()` in shutdown().

Adds an integration test that POSTs a Claude UserPromptSubmit to the relay's
loopback hook server, asserts it reaches Orca's `agentHookServer.ingestRemote`
through the in-memory mux pipe with the right shape and a stamped
connectionId, and round-trips the request-driven replay.

See docs/design/agent-status-over-ssh.md §8 (commit #3, #4).

Co-authored-by: Orca <help@stably.ai>
brennanb2025 added a commit that referenced this pull request May 14, 2026
Instantiates the relay-side hook server in `relay.ts` and forwards every
parsed payload to Orca via `dispatcher.notify('agent.hook', envelope)`. Adds:

- An env-augmenter mechanism on `PtyHandler` so every relay-spawned PTY gets
  ORCA_AGENT_HOOK_PORT/TOKEN/ENV/VERSION/ENDPOINT in its env. Server-side
  injection avoids ferrying the relay's bearer token across the SSH wire and
  keeps the relay the sole source of truth for the live coords (see
  docs/design/agent-status-over-ssh.md §3).
- An exit-listener hook on `PtyHandler` so the hook server can evict a
  paneKey's cached entry when the backing PTY ends — symmetric with Orca's
  local clearPaneState on PTY teardown, prevents ghost replay events.
- Request handler for `agent_hook.requestReplay` — issues replay
  notifications BEFORE returning the response so Orca observes every cached
  entry strictly before the request resolves.
- Stub handler for `agent_hook.installPlugins` (real impl in commit #7).
- `hookServer.stop()` in shutdown().

Adds an integration test that POSTs a Claude UserPromptSubmit to the relay's
loopback hook server, asserts it reaches Orca's `agentHookServer.ingestRemote`
through the in-memory mux pipe with the right shape and a stamped
connectionId, and round-trips the request-driven replay.

See docs/design/agent-status-over-ssh.md §8 (commit #3, #4).

Co-authored-by: Orca <help@stably.ai>
thomaszdxsn pushed a commit to thomaszdxsn/orca that referenced this pull request Jun 15, 2026
Add Shift+Enter to insert newline in terminal
LesleyMurfin referenced this pull request in LesleyMurfin/orca Jun 20, 2026
…e-runtime e2e

Standalone unit test locking in that the runtimeHostIsNonWindows suppression does NOT fire for a win32 host runtime (local Windows-WSL project runtime keeps its shell menu), plus a Playwright e2e for the serve-runtime menu suppression. Per peer-review item #3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ppw-stack referenced this pull request in ppw-stack/orca Jun 29, 2026
Address review finding #3 from /review on PR stablyai#6752:

> "Custom submit labels still bypass i18n. This only localizes the
> default 'Save' path. A supplied caller in
> mobile/app/h/[hostId]/session/[worktreeId].tsx still passes
> submitLabel='Open', so that modal stays English in non-English UI.
> Either require callers to pass translated labels or extend this API
> so custom actions can be localized too."

Two-part fix:

1. mobile/src/components/TextInputModal.tsx — drop the
   submitLabel === 'Save' special case. The previous logic silently
   translated the magic string 'Save' but left every other literal
   English label untranslated. That distinction was a foot-gun:
   callers passing 'Save' got translation, callers passing 'Open'
   (or anything else) did not — there's no way to know which is
   which from the call site. Replaced with a clear contract:
   "submitLabel: caller passes a final string. Pass t('mobile.*', 'English')
   if you want it translated, or a plain English string if the call
   site owns the layout." Added a comment at the prop default
   explaining the rule so future callers don't trip on it.

2. mobile/app/h/[hostId]/session/[worktreeId].tsx:5615 — the 'New
   Browser' modal had three untranslated strings (title, message,
   submitLabel). Wired them through t() with three new keys:
     - mobile.session.newBrowserTitle
     - mobile.session.newBrowserMessage
     - mobile.session.open
   Added to all 5 locales; 打开 (zh) / Abrir (es) / 開く (ja) /
   열기 (ko).

The other TextInputModal call sites (pr-sidebar) already passed
t('mobile.prComment.*', ...) so they continue to work unchanged.

Verification:
- pnpm test: 147/147 files, 979 tests pass
- pnpm exec tsc --noEmit: No errors
- pnpm exec oxlint: clean
- pnpm run verify:localization-catalog: 5 locales × 10904 keys
- pnpm run verify:localization-coverage: 208 allowlisted
Jinwoo-H added a commit that referenced this pull request Jun 29, 2026
Five native-chat improvements:

- Per-leaf toggle eligibility (CodeRabbit #3 follow-up): a split pane can
  mix a supported agent (Claude/Codex) in one leaf with an unsupported one
  (Grok) in another. The header toggle now gates on the active leaf and the
  context-menu toggle on the menu's target leaf, each keyed by
  makePaneKey(tabId, leafId). A leaf's live agent is authoritative; tab-wide
  launch/title hints only fill in pre-hook so they can't enable the toggle on
  a sibling running an unsupported agent. Scoped + shallow-compared store
  selector avoids re-rendering on unrelated tabs' status ticks.

- Backspace/Delete focuses the composer when the pane (not the field) is
  focused, mirroring the printable-typing redirect — focus only, no insert.

- Composer textarea uses scrollbar-sleek so the overflow gutter stops showing
  the heavy native scrollbar once the draft exceeds max-height.

- Cmd/Ctrl+V pastes a clipboard image as an attachment (TUI parity) even when
  the chat pane — not the textarea — holds focus, via a new
  pasteFromClipboard handle that tries image first then falls back to text.
  Routes the context-menu and app-menu paste through the same path.

- Draft text now survives a TUI->GUI->TUI toggle: extracted useNativeChatDraft
  backs the draft by a module-level scope cache (same key as attachments).

Co-authored-by: Orca <help@stably.ai>
AmethystLiang added a commit that referenced this pull request Jul 19, 2026
Applies the code-review findings for the reworked pairing UI:

- Clear a displayed Relay QR on sign-out in both MobilePage and MobilePane
  (the rework dropped the wasSignedInRef watcher, leaving a stale Relay QR
  next to a "sign in required" prompt). Anywhere stays selected; the QR
  re-mints as local-only. [#1, #3]
- Extract useMobilePairingConnectionMode so both panes resolve the saved
  preference identically instead of duplicating the state + resync effect. [#6]
- Delete ~24 i18n keys the rework left unreferenced; sync locale catalogs
  to parity. [#4]
- Give the connection-path radiogroup roving tabindex + arrow-key nav and
  document why it diverges from SettingsSegmentedControl. [#7]
- Add MobilePane.test.tsx (previously untested safety logic) and a
  MobilePage saved-local-only restore test. [#2, #5]

Verified: 22 targeted tests pass, typecheck + oxlint clean, localization
catalog/coverage and max-lines ratchet green.
gaberudy added a commit to goldenhelix/vsagent that referenced this pull request Jul 19, 2026
VSWarehouse packaging feedback stablyai#3 (their top UX gap):

- `project setup-existing-folder` no longer requires --project: when omitted,
  the identity derived from the imported folder is used, making it an
  idempotent "open this folder" upsert (addRepo already dedupes by path+host).
  --kind is auto-detected from the presence of .git when not passed. Applies
  to the runtime RPC, desktop IPC, and CLI paths.

- vsagent-serve honors VSAGENT_OPEN_FOLDER=<path>[:<path>...]: once the
  server prints its ready URL, the launcher runs the CLI upsert for each
  folder in the background, so a container can pass the user-picked folder
  and it appears as a project in every paired browser.

Verified live: booted with a git repo + plain folder, both auto-imported with
correct kinds and derived identities; rerunning the upsert does not duplicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuXM3YjGg6Wot5X2C7YWMu
AmethystLiang added a commit that referenced this pull request Jul 19, 2026
* Restructure mobile pairing setup into a clear stepped layout

* Rework mobile pairing UI: radio-style path selector with sign-in gate fo

* fix(mobile): resolve mobile-pairing review findings

Applies the code-review findings for the reworked pairing UI:

- Clear a displayed Relay QR on sign-out in both MobilePage and MobilePane
  (the rework dropped the wasSignedInRef watcher, leaving a stale Relay QR
  next to a "sign in required" prompt). Anywhere stays selected; the QR
  re-mints as local-only. [#1, #3]
- Extract useMobilePairingConnectionMode so both panes resolve the saved
  preference identically instead of duplicating the state + resync effect. [#6]
- Delete ~24 i18n keys the rework left unreferenced; sync locale catalogs
  to parity. [#4]
- Give the connection-path radiogroup roving tabindex + arrow-key nav and
  document why it diverges from SettingsSegmentedControl. [#7]
- Add MobilePane.test.tsx (previously untested safety logic) and a
  MobilePage saved-local-only restore test. [#2, #5]

Verified: 22 targeted tests pass, typecheck + oxlint clean, localization
catalog/coverage and max-lines ratchet green.

* fix(mobile): resolve adversarial-review findings for pairing UI

Address accepted findings from the mobile pairing UI rework:

- MobilePane: add a request-generation epoch so a late getPairingQR
  response can't paint a stale Relay QR after sign-out, a mode switch,
  or an address change; arm rotation when discarding a pending mint.
- MobilePage: on sign-in, upgrade a signed-out local-only fallback QR to
  Relay (invalidate + rotate-regenerate); handle null->connected too.
- Extract useMobilePairingQrInvalidation so both sign-in/out edges and
  cross-window preference syncs invalidate/re-mint the QR consistently.
- MobilePane: clear + rotate the QR when the selected address changes
  (manual pick or refresh-driven) so it can't encode the old endpoint.
- MobilePairingConnectionOptions: guard the Sign in CTA on configured;
  show an Unavailable panel on unconfigured builds instead of dead CTA.
- Drop the duplicate sign-in helper from MobilePairingSetupSection.
- Remove dead i18n keys (title, recommended, signInToGenerate) and add
  relayUnavailable across all locale catalogs.
- Add tests: deferred sign-out/mode-switch races, sign-in upgrade,
  cross-window sync, unconfigured build, arrow-key radiogroup.

TODO left for the relay-label-honesty finding: getPairingQR does not
expose the actually-encoded mode when an automatic offer degrades to
local-only, so the mismatch can't be surfaced without a new return field.

* fix(mobile): resolve relay-pairing deep-review regressions

- MobilePane: invalidatePairing now clears loading so a superseded
  mid-flight generate can't wedge Generate disabled forever
- MobilePage: stop auto-minting a local-only QR under the Relay label
  when signed out with Anywhere; gate Step 2 auto-generate and the
  Generate button on a shared canMintMobilePairingOffer helper, align
  with Settings, clear QR + loading on sign-out, mint Relay on sign-in
- qr-invalidation: clear pairQrDataUrl (and loading) on every
  invalidation path so a stale QR can't stay scannable during rotation
- Strengthen MobilePane/MobilePage tests for the aligned behavior and
  stuck-loading coverage
- Translate relayUnavailable in es/ja/ko/zh

Co-authored-by: Orca <help@stably.ai>

* fix(mobile): refuse to mint QR when signed-out with Anywhere selected

Replace silent degradation of Anywhere mode to local-only QR with explicit
refusal. Add canMintMobilePairingOffer guard across all mint paths (address
change, network invalidation, connection mode switch). This ensures the UI
honestly encodes the selected pairing path. Improve keyboard focus visibility
on the path selector by adding a persistent focus ring.

* fix(mobile): detect and flag Relay provisioning degradation

When Relay provisioning fails during automatic (Anywhere) pairing offer creation, the offer silently degrades to local-only. This confuses users who selected Anywhere expecting cellular capability.

Add connectionMode field to the pairing offer result to expose what the offer actually encodes. The UI now flags degradation when the offered mode mismatches the user's selection.

Also move credential rotation logic to the main process: rotate when requested mode differs from the pending token's encoded mode. This ensures QR codes displayed under an old policy can't pair under a new one, and windows reminting after preference sync converge on one token.

Rename MobileRelayBetaAvailability → MobileRelayBetaNotice.

* test(mobile): verify pairing codes don't flash during Relay mint

Assert that the pairing QR and URL remain hidden while the Relay mint is
pending, preventing confusing intermediate states when signing in unlocks
Relay.

---------

Co-authored-by: Orca <help@stably.ai>
nwparker added a commit that referenced this pull request Jul 21, 2026
- Coordinator now propagates the rekey result: a collision/stale AFTER a
  successful on-disk rename triggers an inverse rename + throws, instead of
  reporting success with the source tab stranded at a vanished path (#1).
- Cross-worktree: affected set spans all worktrees at the source path, sub-ops
  scoped per (worktree, owner), and the rekey partitions tab/group/tab-bar
  migration by each file's own worktree (was applied under one scope) (#2).
- Diff tabs: single-file unstaged diff tabs are now retargeted on a directory
  move (rebuild the diff id + relative path) instead of stranding (#3).
- Mirror close-notify moved to AFTER a successful rename so a failed rename
  can't desync the host by closing its authoritative tab (#4).

Adds tests: collision->inverse-rename, diff-tab retarget. 6698 tests green.

Co-authored-by: Orca <help@stably.ai>
nwparker added a commit that referenced this pull request Jul 22, 2026
)

* fix(editor): don't flag editor-initiated moves as changed-on-disk

An in-app move/rename (explorer drag-drop, inline rename, tab rename)
re-homes the open tab to the new path and carries its unsaved draft
forward. The move also physically relocates the file, which the worktree
watcher reports as delete(old)+create(new) a few ms later. Because the
tab already lives at the new path by then, that create echo was treated
as an external write landing on a dirty tab and raised a spurious
"changed on disk" banner.

Add a short-lived self-move registry (the move analog of the existing
self-write registry) stamped at the single remap choke point, and have
the external-watch handler recognize the move's own watcher echo:
suppress the changed-on-disk mark on the re-homed dirty tab and the
tombstone on the source path. Genuine external edits are unaffected.

Covered by unit tests for the registry, the remap recorder, and the
watch-hook suppression (plus a no-over-suppression guard).

Co-authored-by: Orca <help@stably.ai>

* refactor(editor): harden move-echo suppression per adversarial review

Addresses review findings on the self-move suppression:

- Stamp the self-move at the call sites BEFORE the on-disk rename
  (recordSelfMoveForOpenTabs), not after the tab re-home, so the
  watcher echo can't win the race. This makes the source-side delete
  guard actually effective and removes a possible one-frame flash.
- Suppress only the move's own create echo, not update events, so a
  genuine external write to the moved path within the TTL still raises
  the changed-on-disk banner (closes an over-suppression gap).
- Track source/target roles independently per path so an immediate
  undo can't clobber the original move's still-in-flight stamp.
- Raise the registry cap above realistic bulk-move sizes so a large
  directory move never self-evicts its own not-yet-echoed stamps.

Updated + added tests: registry undo/role + cap, the new call-site
helper (incl. directory move), and a real-update-still-marks case.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): make move-echo suppression watcher- and TTL-robust

Round-3 hardening after adversarial review:

- Suppress the move's own watcher echo regardless of event kind. The
  main-process watcher coalesces a create+attr-change burst into a lone
  update, so a create-only gate let the echo through on some hosts and
  re-exposed the false banner. Suppression is now bounded by the
  self-move TTL; a genuine write to the exact path within that short
  window is the documented trade-off (draft is preserved regardless).

- Bracket the on-disk rename with the self-move stamp via a single
  renameOpenTabsPathOnDisk wrapper: stamp before (to beat the watcher),
  re-stamp on success (a slow SSH/runtime rename can outlive the TTL, so
  the fresh window must start when the file actually moved), and clear on
  failure (a rename that never happened must not suppress real events).
  All move entry points (explorer move, inline/tab rename incl.
  undo/redo, untitled rename) route through it.

- Treat a tab as remote for TTL purposes when it has a runtime owner OR
  an SSH worktree connection (an SSH tab can carry a null runtime owner).

- Registry tracks source/target roles independently per path so an
  immediate undo can't clobber the original move's in-flight stamp; cap
  raised above realistic bulk-move sizes.

Tests: registry role/clear/cap, the rename wrapper (success re-stamp +
failure clear), the call-site helper (dir move + clear), and watch-hook
coalescing-robust suppression + post-TTL surfacing.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): refcount self-move roles so a failed move can't clear a live one

Two concurrent moves onto the same destination both stamp that path as a
target; if the second rename fails and clears, it must not erase the
first (successful) move's still-live target stamp. Reference count each
role's registrations and clear only the failed move's own contribution.

Adds a regression test for the shared-destination case.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): give self-move stamps per-registration expiries + retract tokens

The refcount model used a single shared expiry scalar per role that only
grew via max() and reset at refs=0, so releasing the max-contributing
registration left survivors inheriting an over-extended window (and an
expired registration could be resurrected by a later stamp on a key the
opposite role kept resident). Both over-suppress genuine changes.

Model each stamp as an independent registration carrying its own expiry
(a list per role). recordSelfMove returns a ticket; clearSelfMove
retracts exactly that registration. A role is live while any of its
registrations is unexpired, so concurrent stamps, failed-move clears, and
expiry are all precise. Wrapper/helper thread the tickets through.

Adds regressions for the over-extension and resurrection cases.

Co-authored-by: Orca <help@stably.ai>

* test(editor): cover source-side self-move guard in the pre-remap ordering

Adds the case where the watcher's delete(old) arrives while the tab is
still at the old path (before remap re-homes it): with a live self-move
source stamp the tombstone must be suppressed. Pairs with the existing
naked-delete control (no stamp → deleted) to pin the guard's behavior.

Co-authored-by: Orca <help@stably.ai>

* docs(editor): document the failed-move suppression window as a bounded trade-off

A self-move stamp is placed before the rename and retracted if it fails,
so an event consumed during the pre-failure window is swallowed. Note
that this only matters for the rare unrelated-dirty-tab-at-destination
case and the recoverable missed-source-tombstone case, and that a move
has no bytes to echo-verify the way self-writes do.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): decide move echo vs external write by content identity

Replaces the time-bounded self-move suppression heuristic with a
correct-by-construction identity check, so a genuine external write to a
just-moved path is never swallowed and the move's own echo is never a
false conflict — regardless of watcher event-kind coalescing or timing.

- Remap now carries the edit-session identity (lastKnownDiskSignature,
  externalMutation, pendingDiskBaselineVerification) onto the re-homed
  tab. Previously the close+reopen dropped it, so a moved tab lost its
  disk baseline (and any pre-existing changed-on-disk conflict silently
  vanished on move).
- On a live self-move-target dirty event the watch hook now reads the
  destination and compares getDiskBaselineSignature(disk) to the tab's
  carried baseline: equal => move echo (suppress), differ/binary =>
  genuine write (banner). Autosave is suspended synchronously before the
  read so a write landing mid-read can't be overwritten; a generation
  token makes overlapping reads safe. Fails CLOSED (marks changed) on a
  missing baseline or read error — never blind-suppresses.
- The self-move registry now only scopes WHEN to verify. The source-side
  delete still can't be content-verified (nothing to read), so it stays a
  bounded, documented suppression.
- Trim the verbose Why-comments across these files to 1-2 lines.

Adds content-identity verification tests (echo/differ/no-baseline/read-
error/binary/autosave-gate) and a remap test for the carried identity;
splits the watch-hook suite to stay under the max-lines limit.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): give live move-verification its own autosave gate

The live self-move echo verification reused pendingDiskBaselineVerification
as its autosave gate, but that field is also the always-mounted restored-tab
conflict scanner's work queue: the scanner scans any pending dirty tab,
launches its own read, and clears the flag without checking the live
generation — so it could lift the gate mid-read and let autosave overwrite a
genuine external write. Give live verification a dedicated
pendingLiveDiskVerification field (both suspend autosave; each cleared only by
its owner). Transient, not persisted, not carried across a re-home.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): atomic rekeyOpenFilesForPathChange store action (move restructure stage 1)

Foundation for treating an Orca-owned move as an in-place retarget of the
open edit session (not close+reopen), per the locked design. One commit-only
store update migrates every path-derived id + all id-keyed state
(openFiles full-spread, the 6 file-id maps, activeFileId(+byWorktree),
tabBarOrder, unified tabs/groups via the now editor-family-widened
migrateHydratedEditorTabsAndGroups, pendingEditorReveal, untitled consume).
Preflight fails closed on collision (never merges two live sessions) or stale
with zero mutation. Not yet wired to a coordinator (stage 4).

Stage 1 of 5; behind the shipped content-identity fix.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): op-scoped in-flight move registry + source integration (stage 2)

editor-path-move-inflight.ts tracks Orca-owned moves for the exact duration of
the rename+rekey (no TTL): source paths suppress the delete tombstone, target
paths latch a destination event seen before the rekey (never suppress). Wired
into the watcher's delete filter alongside the old TTL registry (OR fallback)
so suppression keeps working until the stage-4 coordinator drives every move
through beginEditorPathMove. Stage 2 of 5.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): move-echo provenance + autosave gate on OpenFile (stage 3 store)

Adds pendingSelfMoveEcho {operationId,targetPath} to OpenFile and has the
rekey action install it + pendingLiveDiskVerification on dirty autosave-capable
destinations (moveOperationId arg), atomically in the same commit that re-homes
the tab — so the verify gate survives the rekey and its op-id token supersedes a
stale in-flight verification. Replaces the module-scoped generation map (which
broke under rekey). Verification-reader wiring + coordinator follow.

Co-authored-by: Orca <help@stably.ai>

* refactor(editor): remap moves via atomic in-place rekey, not close+reopen (stage 4a)

remapOpenEditorTabsForPathChange now builds an owner-aware rekey plan (plain-path
id to the first owner, owner-qualified to the rest; previews resolve their source
to the moved edit's new id) and applies it via rekeyOpenFilesForPathChange in one
commit — preserving the full OpenFile + cursor/view/group/MRU state and closing
the close/reopen watcher-race window. Passes moveOperationId through so dirty
destinations get the content-verify gate. 4545 tests green.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): move coordinator drives rename/drag/undo/redo/untitled (stage 4b)

executeOpenEditorPathMove is the single transaction for every in-app move:
quiesce affected saves -> op-scoped begin (per runtime owner) -> on-disk rename
-> atomic in-place rekey (installs the content-verify gate/provenance) -> settle
-> re-verify any destination echo latched before the rekey. On failure the store
is untouched. Verification now triggers off the on-OpenFile provenance (consumed
on resolve) and the watcher latches pre-rekey destination events. Wired into all
five call sites; old renameOpenTabsPathOnDisk + separate remap removed from them.
2751 tests green. (TTL self-move registry now dead; removed next.)

Co-authored-by: Orca <help@stably.ai>

* refactor(editor): remove the dead TTL self-move registry (stage 4c)

The coordinator + op-scoped in-flight suppression + on-OpenFile provenance fully
replace the time-bounded self-move registry, so delete it and its two helper
modules (record-self-move-for-open-tabs, rename-open-editor-tabs-path). The
watcher source filter now uses only isActiveMoveSourcePath and the verification
trigger only the tab's pendingSelfMoveEcho. Rewrote the self-move test suite onto
the new primitives. 7225 tests green.

Co-authored-by: Orca <help@stably.ai>

* test(editor): end-to-end coordinator move test (stage 4 done)

executeOpenEditorPathMove renames on disk, retargets the session in place with
draft/dirty/baseline preserved + gate/provenance installed, settles the in-flight
transaction; on rename failure the store is byte-identical and the transaction is
released.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): mirror-safe move — detach moved mirrored tab + close-notify host (stage 5)

The atomic rekey changes a tab's id, so a moved mirrored tab would be culled by
the host snapshot (losing its draft) or resurrect the old path. Ship the safe
minimum: the rekey detaches a moved tab from the host mirror
(mirroredFromRuntimeSession cleared) and the coordinator close-notifies the
host's old-path tab (close intent suppresses re-mirroring). Prevents the
data-loss/resurrection; the moved tab becomes companion-local. The full
host-rekey path-change protocol (preserving mirror ownership) is a documented
follow-up.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): address review round 1 (4 majors)

- Coordinator now propagates the rekey result: a collision/stale AFTER a
  successful on-disk rename triggers an inverse rename + throws, instead of
  reporting success with the source tab stranded at a vanished path (#1).
- Cross-worktree: affected set spans all worktrees at the source path, sub-ops
  scoped per (worktree, owner), and the rekey partitions tab/group/tab-bar
  migration by each file's own worktree (was applied under one scope) (#2).
- Diff tabs: single-file unstaged diff tabs are now retargeted on a directory
  move (rebuild the diff id + relative path) instead of stranding (#3).
- Mirror close-notify moved to AFTER a successful rename so a failed rename
  can't desync the host by closing its authoritative tab (#4).

Adds tests: collision->inverse-rename, diff-tab retarget. 6698 tests green.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): review round 2 (mirror ordering, rollback error, diff sources)

- Close the host mirror tab only AFTER the rekey commits (capture pre-rekey
  resolution first): a rekey collision after a successful rename no longer
  desyncs the host by closing its authoritative tab (high).
- Surface a failed inverse rename instead of swallowing it: the thrown error
  now states the on-disk move may remain at the new path (high).
- Restrict diff-tab retargeting to staged/unstaged (purely path-derived ids);
  branch/commit diffs carry compare metadata and combined 'Changes' is
  worktree-rooted, so rebuilding them from path would produce a wrong id (med).

Co-authored-by: Orca <help@stably.ai>

* fix(editor): resolve the move verify gate proactively (review round 3)

The rekey gates every dirty moved tab pending a destination content check,
but verification only ran when a watcher event arrived for that path. If the
watcher was down, throttled, or the event coalesced away, the gate never
cleared and autosave stayed suspended for the tab.

The coordinator now drives verification for every tab it gated once the
rename has committed, so the gate resolves on its own. That makes the
destination-side event latch redundant, so the in-flight tracker is
source-only again.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): review round 4 (gate strand, cross-worktree verify path, leak)

- Don't install the move-echo verify gate on a tab already showing the
  changed-on-disk banner: it's autosave-suspended via externalMutation and
  verification skips a 'changed' tab, so the gate would strand forever.
- Content-verify reads each moved tab's own filePath. A cross-worktree/
  floating tab's relativePath is relative to its own root ('../…') and must
  not be joined onto the initiating worktree path (would read the wrong file
  and raise a false conflict banner on unsaved work).
- Coordinator settles the in-flight source suppression in a finally so a
  throw between rename and commit can't leak it, and only after the rollback
  rename so a late forward-rename delete stays suppressed.
- Migrate pendingEditorReveal.fileId across the rekey (matcher prefers it).

Co-authored-by: Orca <help@stably.ai>

* fix(editor): don't consume move-echo provenance in the safety-net verify (round 5)

The round-3 proactive post-commit verify ran resolveLiveMoveVerification,
which consumed pendingSelfMoveEcho. On FSEvents/SSH the real destination
watcher event reliably lands AFTER the fast local read, so it then found no
provenance, took the immediate changed-on-disk mark (no baseline check when
there is no recent self-write), and raised a false conflict banner on the
just-moved dirty tab — the exact data-loss this change removes.

The proactive verify is a safety net: it now releases the autosave gate but
leaves the provenance, so a later destination event is still recognized as
the move's own echo and content-verified. Only a real watcher event consumes
the provenance. Keeping it is safe — every watcher consumer verifies by
content, which is strictly safer than the immediate mark.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): scope move rekey to the initiating execution host (round 6)

Co-authored-by: Orca <help@stably.ai>

* fix(editor): prefix-suppress the move root so late tabs under a dir move aren't flagged (round 8)

Co-authored-by: Orca <help@stably.ai>

* chore(editor): trim move-fix comments to the why; drop redundant rename quiesce

Co-authored-by: Orca <help@stably.ai>

* fix(editor): record mirrored-close intent synchronously to close the ghost-tab window

Co-authored-by: Orca <help@stably.ai>

* fix(editor): use flavor-aware path containment for move selection (Windows/UNC case)

Co-authored-by: Orca <help@stably.ai>

* fix(editor): reconstruct moved path by segment count (WSL alias / duplicate-separator safe)

Co-authored-by: Orca <help@stably.ai>

* fix(editor): infer moved-path flavor by syntax, preserving legal POSIX backslashes

Co-authored-by: Orca <help@stably.ai>

* test(editor): lock POSIX ancestor-backslash destination flavor

Co-authored-by: Orca <help@stably.ai>

* fix(editor): flavor-aware trailing-separator strip; preserve POSIX literal backslashes

Co-authored-by: Orca <help@stably.ai>

* fix(editor): flavor-aware separator folding in relative-path recompute (POSIX backslash)

Co-authored-by: Orca <help@stably.ai>

* perf(editor): keep the fs-watcher delete path O(deletes) when no move is in flight

Co-authored-by: Orca <help@stably.ai>

* docs(editor): tighten move-fix comments to one-line why-only

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
AmethystLiang added a commit that referenced this pull request Jul 24, 2026
…rvability

- move the SSH status==='connected' liveness gate and canonical host-id derivation into the
  shared buildFileMutationOwnership so web fails closed on a reconnecting connection the same
  way mobile does; reduce buildMobileFileMutationOwnership to a thin delegate (removes the
  undocumented override/divergence) [#1]
- log a cause tag at every mobile fence rejection via runtimeChangedError() so a repeated
  'Orca server changed' toast is diagnosable, mirroring the transport '[net]' convention [#2]
- add tests for the post-create files.open failure, replacement-runtime reply, and
  transport-cutover-before-open branches [#3]
- log ownership route mismatches in the server gate (assertRuntimeFileMutationExpectation),
  matching the file's other logged failure paths [#4]
- add a web regression test for the non-connected SSH fail-closed path

Co-authored-by: Orca <help@stably.ai>
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
* Restructure mobile pairing setup into a clear stepped layout

* Rework mobile pairing UI: radio-style path selector with sign-in gate fo

* fix(mobile): resolve mobile-pairing review findings

Applies the code-review findings for the reworked pairing UI:

- Clear a displayed Relay QR on sign-out in both MobilePage and MobilePane
  (the rework dropped the wasSignedInRef watcher, leaving a stale Relay QR
  next to a "sign in required" prompt). Anywhere stays selected; the QR
  re-mints as local-only. [stablyai#1, stablyai#3]
- Extract useMobilePairingConnectionMode so both panes resolve the saved
  preference identically instead of duplicating the state + resync effect. [stablyai#6]
- Delete ~24 i18n keys the rework left unreferenced; sync locale catalogs
  to parity. [stablyai#4]
- Give the connection-path radiogroup roving tabindex + arrow-key nav and
  document why it diverges from SettingsSegmentedControl. [stablyai#7]
- Add MobilePane.test.tsx (previously untested safety logic) and a
  MobilePage saved-local-only restore test. [stablyai#2, stablyai#5]

Verified: 22 targeted tests pass, typecheck + oxlint clean, localization
catalog/coverage and max-lines ratchet green.

* fix(mobile): resolve adversarial-review findings for pairing UI

Address accepted findings from the mobile pairing UI rework:

- MobilePane: add a request-generation epoch so a late getPairingQR
  response can't paint a stale Relay QR after sign-out, a mode switch,
  or an address change; arm rotation when discarding a pending mint.
- MobilePage: on sign-in, upgrade a signed-out local-only fallback QR to
  Relay (invalidate + rotate-regenerate); handle null->connected too.
- Extract useMobilePairingQrInvalidation so both sign-in/out edges and
  cross-window preference syncs invalidate/re-mint the QR consistently.
- MobilePane: clear + rotate the QR when the selected address changes
  (manual pick or refresh-driven) so it can't encode the old endpoint.
- MobilePairingConnectionOptions: guard the Sign in CTA on configured;
  show an Unavailable panel on unconfigured builds instead of dead CTA.
- Drop the duplicate sign-in helper from MobilePairingSetupSection.
- Remove dead i18n keys (title, recommended, signInToGenerate) and add
  relayUnavailable across all locale catalogs.
- Add tests: deferred sign-out/mode-switch races, sign-in upgrade,
  cross-window sync, unconfigured build, arrow-key radiogroup.

TODO left for the relay-label-honesty finding: getPairingQR does not
expose the actually-encoded mode when an automatic offer degrades to
local-only, so the mismatch can't be surfaced without a new return field.

* fix(mobile): resolve relay-pairing deep-review regressions

- MobilePane: invalidatePairing now clears loading so a superseded
  mid-flight generate can't wedge Generate disabled forever
- MobilePage: stop auto-minting a local-only QR under the Relay label
  when signed out with Anywhere; gate Step 2 auto-generate and the
  Generate button on a shared canMintMobilePairingOffer helper, align
  with Settings, clear QR + loading on sign-out, mint Relay on sign-in
- qr-invalidation: clear pairQrDataUrl (and loading) on every
  invalidation path so a stale QR can't stay scannable during rotation
- Strengthen MobilePane/MobilePage tests for the aligned behavior and
  stuck-loading coverage
- Translate relayUnavailable in es/ja/ko/zh

Co-authored-by: Orca <help@stably.ai>

* fix(mobile): refuse to mint QR when signed-out with Anywhere selected

Replace silent degradation of Anywhere mode to local-only QR with explicit
refusal. Add canMintMobilePairingOffer guard across all mint paths (address
change, network invalidation, connection mode switch). This ensures the UI
honestly encodes the selected pairing path. Improve keyboard focus visibility
on the path selector by adding a persistent focus ring.

* fix(mobile): detect and flag Relay provisioning degradation

When Relay provisioning fails during automatic (Anywhere) pairing offer creation, the offer silently degrades to local-only. This confuses users who selected Anywhere expecting cellular capability.

Add connectionMode field to the pairing offer result to expose what the offer actually encodes. The UI now flags degradation when the offered mode mismatches the user's selection.

Also move credential rotation logic to the main process: rotate when requested mode differs from the pending token's encoded mode. This ensures QR codes displayed under an old policy can't pair under a new one, and windows reminting after preference sync converge on one token.

Rename MobileRelayBetaAvailability → MobileRelayBetaNotice.

* test(mobile): verify pairing codes don't flash during Relay mint

Assert that the pairing QR and URL remain hidden while the Relay mint is
pending, preventing confusing intermediate states when signing in unlocks
Relay.

---------

Co-authored-by: Orca <help@stably.ai>
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…ablyai#9506)

* fix(editor): don't flag editor-initiated moves as changed-on-disk

An in-app move/rename (explorer drag-drop, inline rename, tab rename)
re-homes the open tab to the new path and carries its unsaved draft
forward. The move also physically relocates the file, which the worktree
watcher reports as delete(old)+create(new) a few ms later. Because the
tab already lives at the new path by then, that create echo was treated
as an external write landing on a dirty tab and raised a spurious
"changed on disk" banner.

Add a short-lived self-move registry (the move analog of the existing
self-write registry) stamped at the single remap choke point, and have
the external-watch handler recognize the move's own watcher echo:
suppress the changed-on-disk mark on the re-homed dirty tab and the
tombstone on the source path. Genuine external edits are unaffected.

Covered by unit tests for the registry, the remap recorder, and the
watch-hook suppression (plus a no-over-suppression guard).

Co-authored-by: Orca <help@stably.ai>

* refactor(editor): harden move-echo suppression per adversarial review

Addresses review findings on the self-move suppression:

- Stamp the self-move at the call sites BEFORE the on-disk rename
  (recordSelfMoveForOpenTabs), not after the tab re-home, so the
  watcher echo can't win the race. This makes the source-side delete
  guard actually effective and removes a possible one-frame flash.
- Suppress only the move's own create echo, not update events, so a
  genuine external write to the moved path within the TTL still raises
  the changed-on-disk banner (closes an over-suppression gap).
- Track source/target roles independently per path so an immediate
  undo can't clobber the original move's still-in-flight stamp.
- Raise the registry cap above realistic bulk-move sizes so a large
  directory move never self-evicts its own not-yet-echoed stamps.

Updated + added tests: registry undo/role + cap, the new call-site
helper (incl. directory move), and a real-update-still-marks case.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): make move-echo suppression watcher- and TTL-robust

Round-3 hardening after adversarial review:

- Suppress the move's own watcher echo regardless of event kind. The
  main-process watcher coalesces a create+attr-change burst into a lone
  update, so a create-only gate let the echo through on some hosts and
  re-exposed the false banner. Suppression is now bounded by the
  self-move TTL; a genuine write to the exact path within that short
  window is the documented trade-off (draft is preserved regardless).

- Bracket the on-disk rename with the self-move stamp via a single
  renameOpenTabsPathOnDisk wrapper: stamp before (to beat the watcher),
  re-stamp on success (a slow SSH/runtime rename can outlive the TTL, so
  the fresh window must start when the file actually moved), and clear on
  failure (a rename that never happened must not suppress real events).
  All move entry points (explorer move, inline/tab rename incl.
  undo/redo, untitled rename) route through it.

- Treat a tab as remote for TTL purposes when it has a runtime owner OR
  an SSH worktree connection (an SSH tab can carry a null runtime owner).

- Registry tracks source/target roles independently per path so an
  immediate undo can't clobber the original move's in-flight stamp; cap
  raised above realistic bulk-move sizes.

Tests: registry role/clear/cap, the rename wrapper (success re-stamp +
failure clear), the call-site helper (dir move + clear), and watch-hook
coalescing-robust suppression + post-TTL surfacing.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): refcount self-move roles so a failed move can't clear a live one

Two concurrent moves onto the same destination both stamp that path as a
target; if the second rename fails and clears, it must not erase the
first (successful) move's still-live target stamp. Reference count each
role's registrations and clear only the failed move's own contribution.

Adds a regression test for the shared-destination case.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): give self-move stamps per-registration expiries + retract tokens

The refcount model used a single shared expiry scalar per role that only
grew via max() and reset at refs=0, so releasing the max-contributing
registration left survivors inheriting an over-extended window (and an
expired registration could be resurrected by a later stamp on a key the
opposite role kept resident). Both over-suppress genuine changes.

Model each stamp as an independent registration carrying its own expiry
(a list per role). recordSelfMove returns a ticket; clearSelfMove
retracts exactly that registration. A role is live while any of its
registrations is unexpired, so concurrent stamps, failed-move clears, and
expiry are all precise. Wrapper/helper thread the tickets through.

Adds regressions for the over-extension and resurrection cases.

Co-authored-by: Orca <help@stably.ai>

* test(editor): cover source-side self-move guard in the pre-remap ordering

Adds the case where the watcher's delete(old) arrives while the tab is
still at the old path (before remap re-homes it): with a live self-move
source stamp the tombstone must be suppressed. Pairs with the existing
naked-delete control (no stamp → deleted) to pin the guard's behavior.

Co-authored-by: Orca <help@stably.ai>

* docs(editor): document the failed-move suppression window as a bounded trade-off

A self-move stamp is placed before the rename and retracted if it fails,
so an event consumed during the pre-failure window is swallowed. Note
that this only matters for the rare unrelated-dirty-tab-at-destination
case and the recoverable missed-source-tombstone case, and that a move
has no bytes to echo-verify the way self-writes do.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): decide move echo vs external write by content identity

Replaces the time-bounded self-move suppression heuristic with a
correct-by-construction identity check, so a genuine external write to a
just-moved path is never swallowed and the move's own echo is never a
false conflict — regardless of watcher event-kind coalescing or timing.

- Remap now carries the edit-session identity (lastKnownDiskSignature,
  externalMutation, pendingDiskBaselineVerification) onto the re-homed
  tab. Previously the close+reopen dropped it, so a moved tab lost its
  disk baseline (and any pre-existing changed-on-disk conflict silently
  vanished on move).
- On a live self-move-target dirty event the watch hook now reads the
  destination and compares getDiskBaselineSignature(disk) to the tab's
  carried baseline: equal => move echo (suppress), differ/binary =>
  genuine write (banner). Autosave is suspended synchronously before the
  read so a write landing mid-read can't be overwritten; a generation
  token makes overlapping reads safe. Fails CLOSED (marks changed) on a
  missing baseline or read error — never blind-suppresses.
- The self-move registry now only scopes WHEN to verify. The source-side
  delete still can't be content-verified (nothing to read), so it stays a
  bounded, documented suppression.
- Trim the verbose Why-comments across these files to 1-2 lines.

Adds content-identity verification tests (echo/differ/no-baseline/read-
error/binary/autosave-gate) and a remap test for the carried identity;
splits the watch-hook suite to stay under the max-lines limit.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): give live move-verification its own autosave gate

The live self-move echo verification reused pendingDiskBaselineVerification
as its autosave gate, but that field is also the always-mounted restored-tab
conflict scanner's work queue: the scanner scans any pending dirty tab,
launches its own read, and clears the flag without checking the live
generation — so it could lift the gate mid-read and let autosave overwrite a
genuine external write. Give live verification a dedicated
pendingLiveDiskVerification field (both suspend autosave; each cleared only by
its owner). Transient, not persisted, not carried across a re-home.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): atomic rekeyOpenFilesForPathChange store action (move restructure stage 1)

Foundation for treating an Orca-owned move as an in-place retarget of the
open edit session (not close+reopen), per the locked design. One commit-only
store update migrates every path-derived id + all id-keyed state
(openFiles full-spread, the 6 file-id maps, activeFileId(+byWorktree),
tabBarOrder, unified tabs/groups via the now editor-family-widened
migrateHydratedEditorTabsAndGroups, pendingEditorReveal, untitled consume).
Preflight fails closed on collision (never merges two live sessions) or stale
with zero mutation. Not yet wired to a coordinator (stage 4).

Stage 1 of 5; behind the shipped content-identity fix.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): op-scoped in-flight move registry + source integration (stage 2)

editor-path-move-inflight.ts tracks Orca-owned moves for the exact duration of
the rename+rekey (no TTL): source paths suppress the delete tombstone, target
paths latch a destination event seen before the rekey (never suppress). Wired
into the watcher's delete filter alongside the old TTL registry (OR fallback)
so suppression keeps working until the stage-4 coordinator drives every move
through beginEditorPathMove. Stage 2 of 5.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): move-echo provenance + autosave gate on OpenFile (stage 3 store)

Adds pendingSelfMoveEcho {operationId,targetPath} to OpenFile and has the
rekey action install it + pendingLiveDiskVerification on dirty autosave-capable
destinations (moveOperationId arg), atomically in the same commit that re-homes
the tab — so the verify gate survives the rekey and its op-id token supersedes a
stale in-flight verification. Replaces the module-scoped generation map (which
broke under rekey). Verification-reader wiring + coordinator follow.

Co-authored-by: Orca <help@stably.ai>

* refactor(editor): remap moves via atomic in-place rekey, not close+reopen (stage 4a)

remapOpenEditorTabsForPathChange now builds an owner-aware rekey plan (plain-path
id to the first owner, owner-qualified to the rest; previews resolve their source
to the moved edit's new id) and applies it via rekeyOpenFilesForPathChange in one
commit — preserving the full OpenFile + cursor/view/group/MRU state and closing
the close/reopen watcher-race window. Passes moveOperationId through so dirty
destinations get the content-verify gate. 4545 tests green.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): move coordinator drives rename/drag/undo/redo/untitled (stage 4b)

executeOpenEditorPathMove is the single transaction for every in-app move:
quiesce affected saves -> op-scoped begin (per runtime owner) -> on-disk rename
-> atomic in-place rekey (installs the content-verify gate/provenance) -> settle
-> re-verify any destination echo latched before the rekey. On failure the store
is untouched. Verification now triggers off the on-OpenFile provenance (consumed
on resolve) and the watcher latches pre-rekey destination events. Wired into all
five call sites; old renameOpenTabsPathOnDisk + separate remap removed from them.
2751 tests green. (TTL self-move registry now dead; removed next.)

Co-authored-by: Orca <help@stably.ai>

* refactor(editor): remove the dead TTL self-move registry (stage 4c)

The coordinator + op-scoped in-flight suppression + on-OpenFile provenance fully
replace the time-bounded self-move registry, so delete it and its two helper
modules (record-self-move-for-open-tabs, rename-open-editor-tabs-path). The
watcher source filter now uses only isActiveMoveSourcePath and the verification
trigger only the tab's pendingSelfMoveEcho. Rewrote the self-move test suite onto
the new primitives. 7225 tests green.

Co-authored-by: Orca <help@stably.ai>

* test(editor): end-to-end coordinator move test (stage 4 done)

executeOpenEditorPathMove renames on disk, retargets the session in place with
draft/dirty/baseline preserved + gate/provenance installed, settles the in-flight
transaction; on rename failure the store is byte-identical and the transaction is
released.

Co-authored-by: Orca <help@stably.ai>

* feat(editor): mirror-safe move — detach moved mirrored tab + close-notify host (stage 5)

The atomic rekey changes a tab's id, so a moved mirrored tab would be culled by
the host snapshot (losing its draft) or resurrect the old path. Ship the safe
minimum: the rekey detaches a moved tab from the host mirror
(mirroredFromRuntimeSession cleared) and the coordinator close-notifies the
host's old-path tab (close intent suppresses re-mirroring). Prevents the
data-loss/resurrection; the moved tab becomes companion-local. The full
host-rekey path-change protocol (preserving mirror ownership) is a documented
follow-up.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): address review round 1 (4 majors)

- Coordinator now propagates the rekey result: a collision/stale AFTER a
  successful on-disk rename triggers an inverse rename + throws, instead of
  reporting success with the source tab stranded at a vanished path (stablyai#1).
- Cross-worktree: affected set spans all worktrees at the source path, sub-ops
  scoped per (worktree, owner), and the rekey partitions tab/group/tab-bar
  migration by each file's own worktree (was applied under one scope) (stablyai#2).
- Diff tabs: single-file unstaged diff tabs are now retargeted on a directory
  move (rebuild the diff id + relative path) instead of stranding (stablyai#3).
- Mirror close-notify moved to AFTER a successful rename so a failed rename
  can't desync the host by closing its authoritative tab (stablyai#4).

Adds tests: collision->inverse-rename, diff-tab retarget. 6698 tests green.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): review round 2 (mirror ordering, rollback error, diff sources)

- Close the host mirror tab only AFTER the rekey commits (capture pre-rekey
  resolution first): a rekey collision after a successful rename no longer
  desyncs the host by closing its authoritative tab (high).
- Surface a failed inverse rename instead of swallowing it: the thrown error
  now states the on-disk move may remain at the new path (high).
- Restrict diff-tab retargeting to staged/unstaged (purely path-derived ids);
  branch/commit diffs carry compare metadata and combined 'Changes' is
  worktree-rooted, so rebuilding them from path would produce a wrong id (med).

Co-authored-by: Orca <help@stably.ai>

* fix(editor): resolve the move verify gate proactively (review round 3)

The rekey gates every dirty moved tab pending a destination content check,
but verification only ran when a watcher event arrived for that path. If the
watcher was down, throttled, or the event coalesced away, the gate never
cleared and autosave stayed suspended for the tab.

The coordinator now drives verification for every tab it gated once the
rename has committed, so the gate resolves on its own. That makes the
destination-side event latch redundant, so the in-flight tracker is
source-only again.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): review round 4 (gate strand, cross-worktree verify path, leak)

- Don't install the move-echo verify gate on a tab already showing the
  changed-on-disk banner: it's autosave-suspended via externalMutation and
  verification skips a 'changed' tab, so the gate would strand forever.
- Content-verify reads each moved tab's own filePath. A cross-worktree/
  floating tab's relativePath is relative to its own root ('../…') and must
  not be joined onto the initiating worktree path (would read the wrong file
  and raise a false conflict banner on unsaved work).
- Coordinator settles the in-flight source suppression in a finally so a
  throw between rename and commit can't leak it, and only after the rollback
  rename so a late forward-rename delete stays suppressed.
- Migrate pendingEditorReveal.fileId across the rekey (matcher prefers it).

Co-authored-by: Orca <help@stably.ai>

* fix(editor): don't consume move-echo provenance in the safety-net verify (round 5)

The round-3 proactive post-commit verify ran resolveLiveMoveVerification,
which consumed pendingSelfMoveEcho. On FSEvents/SSH the real destination
watcher event reliably lands AFTER the fast local read, so it then found no
provenance, took the immediate changed-on-disk mark (no baseline check when
there is no recent self-write), and raised a false conflict banner on the
just-moved dirty tab — the exact data-loss this change removes.

The proactive verify is a safety net: it now releases the autosave gate but
leaves the provenance, so a later destination event is still recognized as
the move's own echo and content-verified. Only a real watcher event consumes
the provenance. Keeping it is safe — every watcher consumer verifies by
content, which is strictly safer than the immediate mark.

Co-authored-by: Orca <help@stably.ai>

* fix(editor): scope move rekey to the initiating execution host (round 6)

Co-authored-by: Orca <help@stably.ai>

* fix(editor): prefix-suppress the move root so late tabs under a dir move aren't flagged (round 8)

Co-authored-by: Orca <help@stably.ai>

* chore(editor): trim move-fix comments to the why; drop redundant rename quiesce

Co-authored-by: Orca <help@stably.ai>

* fix(editor): record mirrored-close intent synchronously to close the ghost-tab window

Co-authored-by: Orca <help@stably.ai>

* fix(editor): use flavor-aware path containment for move selection (Windows/UNC case)

Co-authored-by: Orca <help@stably.ai>

* fix(editor): reconstruct moved path by segment count (WSL alias / duplicate-separator safe)

Co-authored-by: Orca <help@stably.ai>

* fix(editor): infer moved-path flavor by syntax, preserving legal POSIX backslashes

Co-authored-by: Orca <help@stably.ai>

* test(editor): lock POSIX ancestor-backslash destination flavor

Co-authored-by: Orca <help@stably.ai>

* fix(editor): flavor-aware trailing-separator strip; preserve POSIX literal backslashes

Co-authored-by: Orca <help@stably.ai>

* fix(editor): flavor-aware separator folding in relative-path recompute (POSIX backslash)

Co-authored-by: Orca <help@stably.ai>

* perf(editor): keep the fs-watcher delete path O(deletes) when no move is in flight

Co-authored-by: Orca <help@stably.ai>

* docs(editor): tighten move-fix comments to one-line why-only

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
* fix(runtime): surface desktop RPC startup failures

* fix(runtime): isolate RPC failure telemetry

* fix(runtime): satisfy the changed-code quality gate and kill vacuous dialog tests

The `no-floating-promises` label span covers the whole `app.whenReady().then()`
callback, so adding lines inside it made a long-standing finding overlap changed
code. `void` is the linter's own suppression; no `.catch()` on purpose.

The startup-failure tests were vacuous: mutation runs showed the wait-for-show
deferral, the destroyed-window guard, the `closed` companion event, listener
cleanup, the cause walk, the cycle guard, and the truncation bound could all be
deleted with every test still green. The "not called yet" assertion ran before
any microtask, so it passed either way.

* test(runtime): de-brittle the desktop RPC-failure source assertions

Anchoring the slice on the full destructure and matching the whole dialog
call expression made an innocuous rename break the test with a cryptic
'expected -1'. Match the shape that is actually the contract instead.

* test(runtime): repair the silently-unbounded desktop startup slice

The desktopEnd anchor comment lost a word in 624232e, so indexOf returned
-1 and slice(start, -1) covered index.ts to EOF. Moving the dialog call to a
path that never runs at startup still passed. Anchor on code instead, and
assert both bounds so a future reword fails loudly.

* test(runtime): bound the attach anchors in the startup ordering slice

Round 3 bounded the desktop pair but left attachStart/attachEnd unguarded in
the same test: deleting the PTY startup barrier from attachMainWindowServices()
and breaking the rateLimits.attach(window) end anchor still left the case green.

* test(startup): bound the last two unguarded slice anchors in this file

Rounds 3 and 4 fixed the desktop and attach pairs; two instances of the same
class survived in the same file, both proven vacuous by mutation:

- it stablyai#3 never bounded readyEnd. Renaming the `pairing:` payload key makes it
  -1, widening readyPayload from 372B to ~52KB. Moving the reconciliation
  status out of the serve-ready payload (its whole point) but leaving it later
  in index.ts then kept all 6 cases green.
- it stablyai#2 bounded desktopWindowStart against reconciliationStart rather than
  serveEnd. An earlier `Promise.resolve(openMainWindow())` steals the anchor,
  collapsing desktopStartup to '' while every existing guard still passes, so
  its only assertion — a negative — succeeds against an empty string.

Both mutants now fail. `src/main/ipc/pty-startup-barrier-ordering.test.ts:11`
has the same latent shape; left alone as out of scope for this PR.

* fix(runtime): keep walking the cause chain past an unmapped code

getErrorCode returned the first code it found, so an outer wrapper carrying
an unrecognised code masked a nested EACCES/ENOSPC and classified it unknown.
Only a mapped code ends the walk now; every other input classifies as before.

Unreachable today (writeSecureFile rethrows raw fs errors with .code intact),
but the classifier's job is surviving whatever error shape reaches it.

* fix(runtime): tell the user what to fix, not just to restart

The dialog's only advice was "Restart Orca to try again", which is true for
address_in_use and wrong for the rest: permissions, a full or read-only disk,
and a missing data folder all survive a relaunch, so the user restarted, hit
the same failure and had no next step.

Route the error class we already compute into the copy so each cause names the
thing the user has to change. Guidance and telemetry now derive from the same
classifier, so they cannot drift apart.

* fix(runtime): guide users through long RPC paths

* fix(runtime): avoid false window listener warning

* fix(runtime): guard destroyed window before web contents
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…(C1) (stablyai#10625)

* fix(terminal): park SSH worktrees like local ones (C1 retention, slice A)

SSH ptys were blanket-excluded from hidden-view parking, so a hidden SSH
worktree retained every pane forever (C1: renderer heap climbs to the V8
ceiling). SSH bytes transit local main — fact-mode watchers already cover
them, and main keeps a headless model served over pty:getMainBufferSnapshot
that the SSH reattach path never consulted.

- isParkRestorableTerminalPty: snapshot-backed OR (SSH + policy); threaded
  through both park verdicts, both selectors, watcher coverage, and the
  watcher start guard. Remote-runtime/fail-open/foreign/null unchanged.
- Parked-SSH reveal paints from main's headless model (dimension-matched,
  ~5k rows) and degrades to the relay 100KiB replay unless the snapshot is a
  non-empty source==='headless' payload — never a blank/stale paint.
- Kill switch: settings.terminalSshViewParking (default on).

DESIGN.md records the approved plan and the H1 magnitude non-claim.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): bound hidden-worktree retention with a force-park budget (C1, slice B)

Un-parkable worktrees (remote-runtime ptys, uncoverable tabs, SSH with the
slice-A switch off) had unlimited retention: the parking cap/TTL only ever
saw eligibility-passing worktrees, so one bad tab pinned a whole worktree's
panes forever. Retention is now memory-bounded, not eligibility-bounded.

- terminal-hidden-worktree-retention.ts: retention budget (12 hidden / 45min
  TTL, sized from the measured 2.5-19MB per-pane V8 cost, DESIGN.md §2) over
  hidden worktrees ordinary parking can never evict; reuses the hot-retain
  ranking so last-active exemption, deterministic ties, and deadline-driven
  rechecks hold. Fail-open/foreign-pty tabs are eviction-exempt (a remount
  would fresh-spawn and orphan the live shell).
- Terminal.tsx: force-parked ids join the parked set AFTER the coverage veto
  (darkness for uncoverable tabs is the accepted cost); buffers captured via
  the sleep-flow registry before the unmount render; retention TTL added to
  the recheck deadlines for budget candidates only.
- Verdict stays out of its own effect deps; policy test asserts idempotence
  and time-monotone membership (flip-loop dwell regression).
- Kill switch: settings.terminalHiddenWorktreeRetentionBudget (default on).

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): demote hidden scrollback for eviction-exempt worktrees (C1, slice C)

The retention budget (slice B) must exempt worktrees holding fail-open or
foreign-worktree ptys — a remount would fresh-spawn and orphan the live
shell — which would leave that class unbounded again. Instead, past the same
45min retention TTL their hidden panes drop to the minimum scrollback tier
(measured: ~19MB -> ~1.3MB V8 heap per 50k-row pane; trimmed history is
gone by design, reveal restores the configured cap for future output).

- terminal-hidden-scrollback-demotion.ts: module-state verdict registry
  (parked-watcher pattern) with content-equality notify damping; applied in
  the existing scrollback-rows effect in use-terminal-pane-lifecycle.
- selectScrollbackDemotedTerminalWorktrees: pure, TTL-gated, time-monotone.
- Retention TTL wakeups now also cover exempt worktrees so demotion fires.
- Kill switch: settings.terminalHiddenScrollbackDemotion (default on).

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): paint the SSH model snapshot inline, not via nested coordinator (C1 slice A fix)

applyMainBufferSnapshot runs its own structuralReplayCoordinator.run; calling
it from applyReattachPayload (already inside the coordinator when a relay
replay exists) deadlocks on the coordinator's tail chain. The model paint now
mirrors the daemon-snapshot branch inline (folded scrollback + rehydrate +
screen, dimension-matched, escape tail last) and arms the restored-snapshot
seq baseline so deferred/live chunks the snapshot covers dedupe instead of
double-painting. Also falls through (no early return) so reattachPayloadApplied
still latches. Adds the folder-workspace id parity unit case.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): SSH park+reveal e2e round-trip + as-built design notes (C1)

Docker-gated (ORCA_E2E_SSH_DOCKER=1) spec: SSH tab parks behind a decoy and
reveal restores marker content at multi-viewport scrollback depth. DESIGN.md
records the as-built deltas (inline paint, force-park shape, last-active
floor) and the residuals so follow-ups aren't lost.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): paint SSH reveal from main's model even when the relay replay is empty (C1 review stablyai#1)

A relay restart empties the replay buffer; the reveal previously painted
nothing even when main's headless model held the session. The reattach now
prefetches the model snapshot when no structural replay exists (SSH-shaped
ptys only) and paints it inside the coordinator; emptiness is judged on the
composed payload (scrollbackAnsi + data + pendingEscapeTailAnsi) so an
alt-screen snapshot with an empty screen frame still paints.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): decouple scrollback demotion (slice C) from the retention-budget switch (C1 review stablyai#2)

Per the approved contract each slice reverts behind its own switch: slice C
now requires only the master terminalHiddenViewParking plus its own
terminalHiddenScrollbackDemotion flag. The TTL wakeup timer fires for
demotion candidates even with the budget switch off. No DEFAULT_SETTINGS
entries exist for sibling flags (defaults are the '!== false' optional
pattern), so no explicit defaults are added.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): scope eviction exemption to the tab, not the worktree (C1 review stablyai#3)

One eviction-exempt tab (fail-open/foreign pty) previously vetoed force-park
for its whole worktree, pinning co-located remote-runtime tabs forever. The
worktree now force-parks while exempt tabs keep their mounted panes via a
per-tab exclusion mirroring the Activity-portal pattern (legacy watcher sync,
legacy render, and the overlay cold-parking hook). Ordinary parking is
untouched — a worktree with an exempt tab still cannot ordinary-park.
Slice C now also demotes exempt tabs' panes as soon as their worktree
force-parks under the count budget (they are the only panes left mounted).

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): demote un-parkable worktrees the force-park lever spared (C1 review stablyai#4)

The last-active exemption means a single hidden un-parkable worktree never
force-parks — and slice C previously only targeted exempt-tab worktrees, so
its panes held full scrollback forever. Demotion now also covers un-parkable
non-exempt worktrees past the retention TTL that are absent from the
force-parked set (last-active spared, or slice B switched off). Membership
stays time-monotone for fixed inputs; covered by new idempotence/monotone
selector tests.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): keep the hidden clock running through transient background-measure windows (C1 review stablyai#5)

Whole-worktree background mounts (browser-automation bootstrap lease, mobile
mounts, agent wakes) open a ~3s self-clearing measure window that previously
deleted hiddenSince — every remount restarted the 30s hysteresis and the
45min retention TTL, so a periodically re-mounted force-parked worktree
never re-parked. The measure window still pauses parking/eviction verdicts
(all selectors skip measuring candidates); only the clock survives, so the
prior verdict resumes as soon as the window closes. Visible and
portal-holding worktrees still reset the clock.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): make the SSH park+reveal depth assertion prove the model paint (C1 review #6a)

Pad the session with ~180KB of output after the numbered markers so the
earliest marker falls outside the relay's 100KiB rolling replay buffer while
staying inside main's ~5k-row headless model; asserting marker_1 after
reveal now proves the headless-model paint rather than passing under the
relay fallback.

Co-authored-by: Orca <help@stably.ai>

* docs(terminal): rewrite DESIGN.md as the single as-built C1 contract (review stablyai#7)

One contract matching the code: status IMPLEMENTED around force-park (not
the unmount proposal), real kill-switch names with coupling + revert
matrices, the true retention-floor formula with measured per-pane and
demotion numbers, an explicit when-OOM-is-still-possible paragraph naming
the H2 pendingSideEffects residual, the applyMainBufferSnapshot deadlock
constraint inside the slice-A section, stable-signal phrasing instead of a
capability latch, fail-open AND foreign-worktree exemption class, verified
cites, and a planned/landed/follow-up test matrix.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): resolve the eviction exemption per pane, not per tab (C1 review stablyai#8)

isEvictionExemptTerminalTab read only tab.ptyId — the FIRST leaf's pty —
while the coverage veto that makes a worktree a retention candidate walks
every pane. A split tab whose second leaf held an unrestorable pty therefore
failed coverage (→ force-park target) yet looked exempt-free, so force-park
unmounted it and orphaned the live shell. The exemption now resolves panes
through the same resolveParkedTerminalPaneCandidates, keeping tab.ptyId in
the union for the no-layout/no-capture case.

Also from the same review round:
- force-park's capture passes includeLocalBuffers:false like every other
  shutdownBufferCaptures caller; it was serializing up to 512KB/pane of
  scrollback into the store inside a fix meant to bound renderer heap.
- Terminal.tsx unmount resets the scrollback-demotion registry — module
  state with no reset path, read by a pane effect that runs before the host
  effect that would clear it, so a stale verdict trimmed restore replays.
- memoize watcher coverage per tab within the parking pass; the retention
  candidates re-asked it for every mounted worktree, not just the parked few.

* docs(terminal): drop DESIGN.md — the as-built C1 contract moves to the PR body

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): cap the deferred PTY side-effect queue (C1 residual H2)

pendingSideEffects grew without bound under background timer throttling
(~64 drained/s vs hundreds queued/s overnight). Cap at 512 entries with
oldest-first eviction: titles drop (last-wins), a pending bell latches
onto the next survivor, agent-status payloads collapse onto the survivor
keeping the newest 16 (last-wins store state, KB-scale strings).

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): carry command-lifecycle facts through parked watchers (C1 follow-up)

Parked fact-mode watchers omitted onCommandFinished/onCommandCode*, so
OSC 133;D and Command Code scrape signals went dark while parked. New
parked-terminal-command-status.ts ports the store-level subset: git-UI
nudge on every command finish, same-turn status-row drop for SSH PTYs
(exact mounted-path parity — the foreground tracker refuses SSH ids),
and the Command Code working seed / 1500ms done settle. Byte mode scans
the same shared parsers for authority-off parity. Local-PTY status drops
stay with the mounted pane: they need pty-connection's process-confirm
ladder to tell a leaked nested-shell 133;D from a real agent exit.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): retention-budget force-park e2e with a retentionLimit override (C1 6b)

ORCA_E2E_TERMINAL_RETENTION_LIMIT flows preload → e2e-config →
getTerminalParkingPolicyOverrides (exposeStore-gated, positive-integer
only) so a spec can shrink the force-park budget to 1. The Docker-gated
spec opens two remote worktrees on one relay target (second pre-seeded
remote repo), disables terminalSshViewParking to make both un-parkable,
hides both behind the local context, and proves the older one force-parks
while the last-active exemption spares the newest; re-activating the
evicted worktree restores the marker tail via relay replay.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): retention-budget e2e via same-repo remote worktrees (passes docker lane)

The first draft added a second remote repo mid-session, whose pane pty
spawn misroutes to the local daemon with the remote cwd (pre-existing
multi-repo issue, reproducible without any retention override — a seeded
local repo plus one remote repo shows the same misroute). The spec now
budgets across three worktrees of the ONE connected repo, created through
the product createWorktree path (an external git-worktree-add only lands
as a detected worktree needing adoption) and polled through the relay's
transient post-connect reconnect window. Verified green on the local
Docker lane in 20.8s.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): prevent remount thrashing during post-measure cool-down (

Implements the C1 retention contract: preserve worktree `hiddenSinceMs` through a
background-measure window (so TTL/ranking stay honest), but re-park waits for a
full `coldParkDelayMs` cool-down after the measure ends. Without the cool-down,
every ~3s measure lease on a past-deadline worktree thrashes remount/reattach.

Core changes:
- Terminal.tsx: add measure clock (measuringTerminalWorktreeIdsRef) and post-measure
  cool-down tracking (terminalWorktreeParkCooldownUntilRef); gate parking candidates
  until cool-down expires.
- Extract snapshot replay choreography to shared terminal-snapshot-replay-paint.ts
  (used by SSH reattach + daemon restore paths).
- Add SSH model snapshot timeout (750ms) with fallback to relay replay.
- Move cold-park recheck deadline logic to terminal-cold-park-recheck-deadlines.ts;
  add cool-down deadline to scheduling.
- useTerminalTabColdParking: implement matching measure-clock contract with per-tab
  cool-down gate to keep tab deadlines synced with worktree retention clock.
- Add resolveTerminalMountScrollbackRows() to demote new xterms under demoted
  worktrees (pane births during demotion must take the demoted tier at create).
- Add kill switches: terminalSshViewParking, terminalHiddenWorktreeRetentionBudget,
  terminalHiddenScrollbackDemotion.

* fix(terminal): detect Command Code completion in parked mid-turn panes

Seed the byte watcher with in-flight turn state from agent status: the
watcher is recreated per park cycle with no startup command to arm it,
and the banner scrolled away before parking. Also memoize
eviction-exempt checks and use SSH PTY ID builder in tests.

* fix(terminal): flush pending command-code settles on reveal remount

When a parked pane reveals mid-Command Code turn, the new detector
cannot re-observe the already-passed idle composer. Cancelling the settle
leaves the row stranded at 'working', so dispose now flushes the pending
settle instead.

Extract readInFlightCommandCodeTurn to shared space and seed detectors
with in-flight turns so remounts complete mid-flight commands. Also
memoize SSH model probes to prevent double timeouts on reattach.

* fix(terminal): remove scrollback demotion (C1 slice C)

The scrollback demotion feature for eviction-exempt hidden worktrees is no longer needed. Retention budget limits are now sufficient without this additional bound. Remove the terminal-hidden-scrollback-demotion module, the selectScrollbackDemotedTerminalWorktrees function, and related per-pane demotion logic.

* test(terminal): assert bounded probe during stalled reveal

Add assertion to verify that a stalled reveal operation makes exactly one
`getMainBufferSnapshot` call, ensuring retry logic doesn't introduce
redundant probes that would extend the timeout window before relay fallback.

* fix(terminal): implement C1 retention budget for hidden parked worktrees

Addresses OOM regressions in hidden parked terminals by force-evicting
worktrees past a retention budget: at most 12 mounted while hidden, none
past 45 minutes (absolute, not exempted by last-active). Eviction is
least-recently-hidden-first. Exempt tabs (unrestorable local PTYs) keep
their panes to avoid orphaning shells; worktrees are force-parked even
if they contain exempts, and their buffers released elsewhere. SSH/remote
worktrees serialize buffers pre-eviction for reveal; local worktrees keep
daemon snapshots. Command Code's done-settle window is transferred across
park/reveal boundaries so the row cannot strand at 'working'. Model probe
on SSH reattach is scoped to park-reveal only, not ordinary reconnects.
Includes new E2E suite proving the budget actually releases memory.

* memoize eviction-exempt terminal tabs to avoid redundant store reads

Each tab's exemption check re-reads the store and walks the layout tree.
Introduce selectEvictionExemptTerminalTabIds() to resolve all exempt tabs
for a worktree in a single pass, then memoize the result in Terminal.tsx
and useTerminalTabColdParking. This prevents O(n) store reads when checking
exemptions across multiple tabs and ensures the set remains stable across
unrelated re-renders.

* refactor: reformat hidden-worktree retention comments

Reflow to 80-character lines and remove internal ticket references
(C1, C1 slice C).

* fix(lint): split overlay slot and eviction-exempt tabs under max-lines

Static analysis failed because TerminalPaneOverlayLayer (401) and
terminal-parked-tab-watchers (304) exceeded oxlint max-lines. Extract the
slot component and eviction-exempt helpers into dedicated modules.

* test(terminal): stabilize retention budget e2e control arm

Stage un-parkable remote pty ids only after both worktrees are hidden, and
keep re-staging during the control-arm poll so a late updateTabPtyId cannot
flip the decoy back to park-restorable and ordinary-park it before budget
engages.

* test(terminal): pin retention e2e decoy to a mounted pane snapshot

Use the active pane-identity snapshot for the decoy tab instead of all
worktree tabs, and re-assert un-parkable ids after the control-arm hold so
a deferred/empty tab id cannot fail the budget-off mounted-count check.

* fix: memoize terminal eviction exemptions on layout leaf PTYs

Splits add leaf panes to the layout store without changing the tabs
array. A memo keyed only on tabs misses this change, leaving new panes
unexempted for unmount. Include layout leaf PTYs in the exemption memo
key so it recalculates when splits occur or PTYs are re-minted.

---------

Co-authored-by: Orca <help@stably.ai>
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
… tab layout (stablyai#18409)

* perf(runtime): stop the expired-SSH-lease sweep from rescanning every tab layout

The `runtime:syncWindowGraph` IPC handler is the most expensive thing the main
process does: measured on a real session it costs 20.7 ms per call at 0.71
calls/sec, which is 1.47% of wall and ~17% of all main-thread JS. 76% of that
sits in one subtree: `getHydrationTargets` -> `hasRuntimeOwnedPtyCandidate` ->
`getRecentExpiredSshLease` -> `findTerminalTabIdForLeaf`.

Three pieces of pure waste, none of which change an answer:

1. `getRecentExpiredSshLease` evaluated its cheapest and most selective filter
   LAST. `SSH_PANE_RECOVERY_GRACE_MS` is 30 s, so nearly every stored expired
   lease fails it — but only after the predicate had already resolved the
   lease's leaf to its current tab, which is the expensive part. The freshness
   and reattach-eligibility gates now run first; the predicate is otherwise
   identical and side-effect free, so the selected lease is unchanged.

2. The sweep ran once per tab. `workspaceSessionWorktreeHasRuntimeOwnedPtyCandidate`
   asked "does a recent expired lease name THIS tab" for every tab in a
   worktree, and each ask re-read and re-filtered the whole lease list. It now
   resolves the worktree's recoverable tab ids once, lazily, so a worktree whose
   first tab already owns a serve/SSH pty still never sweeps.

3. `findTerminalTabIdForLeaf` allocated a `Set` and walked a whole pane tree per
   tab to answer one leaf lookup. It now reads a leafId -> tabId index built
   once per layouts record and reused until a layout object is replaced, which
   keeps first-tab-wins ordering identical.

Measured by replaying a real 414-worktree / 801-tab / 137-lease session:
2.51 ms -> 0.27 ms per publish for this subtree, a 9.3x cut.

No user-facing trade-off: same leases selected, same tabs reported recoverable,
same SSH pane recovery affordance.

* fix(runtime): revalidate the leaf membership index on root identity

persistPtyBinding grafts a leaf by assigning `layout.root` on the SAME
layout object inside the SAME layouts record, so the layout-identity
revalidation kept serving an index blind to the grafted leaf and
findTerminalTabIdForLeaf answered `undefined` where the pre-index linear
scan answered the tab. That fed the SSH reattach fence
(restoreReattachedPtyRuntime) and the expired-lease pane recovery
resolver, both of which then fall back to the frozen lease tabId.

Membership is a pure function of the root tree and no writer mutates a
node in place, so root identity is the exact revalidation key — same
O(tabs) pointer compare, no new cap, cadence or staleness window.

* perf(runtime): resolve a leaf's tab by scan instead of a cached membership index

Fix stablyai#3 of this PR cached a leafId -> tabId map per layouts record and revalidated
it by comparing every root reference on every read. It was the only mutable
cross-call state in the change, the only piece carrying a staleness invariant,
and it had already needed one follow-up fix (1c23c54) after a layout-identity
key turned out to be blind to `persistPtyBinding`'s in-place `layout.root` graft.

The index was never what produced the measured win. After fix stablyai#1 moves the
freshness gate first, the reporter's replay never calls `findTerminalTabIdForLeaf`
at all — every stored expired lease is older than the 30 s recovery grace, so the
entire 24.9 ms -> 0.9 ms comes from fixes stablyai#1 and stablyai#2, both of which are unchanged.

`findTerminalTabIdForLeaf` is now an allocation-free scan over the existing
`layoutContainsLeafId`, which short-circuits on the first matching leaf instead of
materialising a Set per tab. Same answers, same first-tab-in-record-order
semantics, no revalidation key, nothing for a writer to invalidate.

Re-measured on the same 414-worktree / 801-tab / 137-lease replay
(process.cpuUsage deltas, median of 3; wall clock is useless on this box):

  scenario                     main     index     scan
  all leases stale (replay)   24.86     0.87     0.88 ms/publish
  one lease inside the grace  24.31     1.08     1.04 ms/publish
  all 137 inside the grace    18.04     3.71     4.31 ms/publish

The measured win is unchanged. Only the synthetic worst case — every one of 137
leases expiring inside the same 30 s window — pays for the cache's absence, and
even there the two ranges overlap because the index's own revalidation is O(tabs)
per lookup.

Removes 208 net lines. `terminal-leaf-tab-resolution.test.ts` keeps the parity
cases and adds the guard the cache needed: a subtree replaced in place after an
earlier read must be visible to the next one. That test fails against the index.

* docs(runtime): say why the leaf scan keeps Object.keys

'Allocation-free' overstated it — Object.keys does allocate one key array.
A guarded for...in trades that for a hasOwn call per tab and measures slower,
so record the reason the next reader does not re-litigate it.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…stablyai#18356)

* fix(crash-reporting): sample system memory before the renderer dies

* fix(crash-reporting): make the pre-gone host sample decisive, not just present

Round-1 review said the shipped field set could not decide G4-oom. Fixed.

Decisive field (blocking stablyai#1). The investigation's own win-lowspec repro
falsified "low available commit kills": at a 127 MB commit floor Windows grew
the pagefile to 2029 MB and nothing died, and it named the missing datum —
pagefile-growth headroom / system-drive free space. `getSystemMemoryInfo()`
gives neither. Added `swap-volume-free-space.ts`: one `fs.statfs` on the volume
backing the pagefile (SystemRoot on Windows, the root fs elsewhere, resolved
via `path.parse().root`), published as `systemMemoryPreGoneSwapVolumeFreeMB`.
Together with the already-emitted commit limit that separates "commit was low"
from "commit was refused". Pagefile *max* size needs a registry read; skipped
deliberately — per-operation interpreter spawning is exactly what
docs/reference/windows-edr-posture.md says not to add for telemetry.

Darwin honesty (blocking stablyai#2). Every reading now carries
`systemMemoryPressureSignal`: `available-commit` on Windows (swapFree is
ullAvailPageFile), `mem-available` on Linux when MemAvailable is present,
`none` otherwise — which is always on darwin. A future analyst cannot now table
`freeMB: 272` from a healthy Mac as evidence of exhaustion, because the same
record says the platform gave no pressure verdict. Partial rebuttal on the
suggested reuse: `host-memory.ts:86` was considered and rejected as a periodic
source. It spawns `/usr/bin/memory_pressure` per call, and the sampler this PR
needs runs every 10 s for the app's lifetime; a subprocess at that cadence is
worse than the gap it closes. The reviewer conceded this tradeoff is arguable —
what was not acceptable was shipping the darwin gap silently, so it is now in
the data, not only in a comment.

Staleness (blocking stablyai#3). Confirmed the measurement: four of five G4 reports
carried a ~37 s-old sample (4872/36796/37332/38017/39715 ms). Host memory no
longer rides the 60 s process-metrics sweep; `pre-gone-host-memory.ts` samples
it on its own 10 s timer with its own `systemMemoryPreGoneSampleAgeMs`. One
GlobalMemoryStatusEx-class call plus one statfs is cheap enough at that rate. A
refusal shorter than the interval stays invisible and the module comment says
so — no polling cadence fixes that.

Non-blocking, all taken: renamed `gone-time-system-memory.ts` ->
`system-memory-details.ts` with the now-false "reads AFTER the crash" framing
scoped to the gone-time caller; pre-gone host keys moved out of the
`processMetrics` namespace to `systemMemoryPreGone*`, so the string-surgery
`preGoneDetailKey` helper is gone and a `systemMemory` prefix scan sees both
reads; the bare catch no longer spans both halves of the sample, and a test
pins that a throwing host read leaves the process-metric sample intact; the
inert second test is replaced by three that go red without this change
(verified: swap-volume, pressure-signal and cadence assertions all fail when
the production hunks are reverted).

Rebuttal, non-blocking stablyai#5 (duplicated electron mock across two test files):
declined. `vi.mock` is hoisted per file, so the mock cannot be shared without a
setup module, and this directory already has 26 focused test files that each
re-declare it. Splitting by concern is the local convention.

`startPreGoneProcessMetricsSampling` is renamed `startPreGoneCrashSampling`
since it now starts two samplers.

* fix(crash-reporting): test the arming, gate the swap volume, unblock the host read

Round-2 review blocked on four items. All four addressed.

WHAT THIS BRANCH ACTUALLY DOES, AT HEAD (blocking stablyai#4). The commit-1 message
("13 lines, 1 production file, no new module, new optional numeric fields
only", `processMetricsPreGoneSystemMemory*` keys, a `preGoneDetailKey` helper,
a `pre-gone-system-memory.test.ts`) describes a superseded revision; every one
of those claims is false now, so it must not be used as the PR description.
The change against origin/main is: 3 new production modules
(`pre-gone-host-memory.ts`, `system-memory-details.ts`,
`swap-volume-free-space.ts`), 1 deleted (`gone-time-system-memory.ts`), plus
edits to `process-gone-diagnostics.ts` and `main-process-ready-runtime.ts` and
2 test files. It adds a second main-process interval timer that runs for the
life of the app: every 10 s one synchronous GlobalMemoryStatusEx-class read,
and on win32/darwin one `fs.statfs` on the swap-backing volume. Details are
`systemMemoryPreGone*`, and two of them are STRINGS, not numbers:
`systemMemoryPreGonePressureSignal` (enum) and `systemMemoryPreGoneSwapVolume`
(a drive label, separator-trimmed so it is not a path). Both are assigned after
`sanitizeCrashReportDetails`; neither carries user content.

Arming is now tested (blocking stablyai#1). The reviewer deleted
`startPreGoneSystemMemorySampling(...)` from `startPreGoneCrashSampling` and
all 264 tests stayed green — confirmed and fixed. `pre-gone-host-memory.test.ts`
now calls `startPreGoneCrashSampling()` with production defaults and asserts
both `setInterval` calls, their literal periods `[60_000, 10_000]`, that both
timers are unref'd, and that advancing 10 s takes a fresh host sample that
reaches `buildProcessGoneCrashDetails` with `SampleAgeMs: 0`. Verified red on
revert: deleting the arming line -> 1 failure; changing the interval constant
to 30_000 -> 1 failure (the old assertion compared the constant to itself and
caught neither). The tautological `10_000 < 60_000 / 2` test is gone,
superseded by this one.

Swap volume is win32/darwin only (blocking stablyai#2). On Linux swap is a fixed
partition, a fixed-size swapfile, or zram; none grow into root-fs free space,
so `SwapVolumeFreeMB: 380000` beside `SwapFreeMB: 0` would have invited exactly
the wrong verdict on the two Linux cluster members. `swapVolumeAnchor` returns
undefined off win32/darwin, so no field and no statfs at all. The comment
claiming "elsewhere swap is on the root fs" was wrong and is gone. The Windows
anchor is still the DEFAULT pagefile volume, so the measured volume now ships
with the number (`systemMemoryPreGoneSwapVolume: 'C:'`) instead of being
implied. The honesty label covers it: win32 reads `available-commit` only when
the volume datum is present, and `available-commit-unqualified` otherwise —
which also fixes non-blocking stablyai#5, where the synchronous gone-time read claimed
a verdict its own fields could not support.

Host read no longer waits on statfs (blocking stablyai#3). `samplePreGoneSystemMemory`
now commits the synchronous memory reading first and merges volume free space
in afterwards, so the cadence is 10 s regardless of disk-metadata latency and a
hung volume can no longer stop host sampling — precisely the paging-storm case
this exists for. The in-flight latch now guards only the statfs. Verified red
on revert to the serialized shape (2 failures). A stale-but-slow-moving volume
value merging into a newer memory sample is deliberate and commented.

Non-blocking stablyai#3 (reset does not invalidate an in-flight sample): fixed with a
generation counter bumped by `resetPreGoneSystemMemorySamplingForTest`, so a
late statfs cannot repopulate a reset sample. Separately, the volume read now
only runs after a host sample committed, which removes the real `statfs('/')`
side effect from `process-gone-diagnostics.test.ts` entirely.

REBUTTAL, darwin `memory_pressure` reuse (non-blocking stablyai#2): declined, with
evidence. `readDarwinAvailableMemory` at src/main/memory/host-memory.ts:87 is
reached only via `collectHostMemory` <- `runSnapshot` <- `collectMemorySnapshot`,
whose only callers are the `memory:getSnapshot` IPC handler and
orca-runtime-pty-foreground-process-reads.ts:170 — both on demand. There is no
periodic snapshot, so there is no cached reading to reuse for free; adopting it
means spawning `/usr/bin/memory_pressure` on a main-process timer for the life
of the app, and its module-global `darwinAvailabilitySupported` latch is shared
with the memory UI. The gap is not hidden: darwin ships
`PressureSignal: 'none'` in the data, and the module comment now cites the
existing reader and why it is not used here rather than claiming Orca lacks
one.

Verified: `vitest src/main/crash-reporting src/main/startup src/main/memory` =
769 passed / 6 skipped (crash-reporting re-run 5x, no flake);
`tsc --noEmit -p config/tsconfig.node.json` 0; `oxlint` 0; `oxfmt --check` 0.

* fix(crash-reporting): stop a stale statfs qualifying the commit verdict

Round-3 adversarial review, 2 blocking. Both fixed with mutation-verified
tests.

1. `mergeSwapVolumeFreeSpace` merged the volume reading into whatever sample
   was current at RESOLUTION time, and `pressureSignal` then upgraded win32
   from `available-commit-unqualified` to the decisive `available-commit` on
   the strength of it. The `swapVolumeReadInFlight` latch makes every
   intervening tick skip the merge, so the lag is as old as the last STARTED
   statfs, not the last tick — and no age field exposed it, because
   `systemMemoryPreGoneSampleAgeMs` describes only the synchronous memory read.

   Reviewer's executed scenario: a statfs issued at t=0 on a healthy host
   (40 GB free) resolving at t=20 s of commit pressure emitted
   `SwapFreeMB: 200` beside `SwapVolumeFreeMB: 40000`, labelled
   `available-commit`, with `SampleAgeMs: 0`. That reads as "the pagefile had
   room, so this was not a commit refusal" — the opposite conclusion, wearing
   the branch's highest-confidence label, on exactly the win32 G4-oom reports
   this exists to decide.

   The datum still ships (it is the only pagefile-expandability signal there
   is), but now:
   - the sample carries `swapVolumeSampledAtMs` — the tick that ISSUED the
     statfs, never the one it resolved on — surfaced as
     `systemMemoryPreGoneSwapVolumeAgeMs`;
   - only a statfs that answers on its own tick may qualify the verdict.
     `withSwapVolumeFreeSpace` takes `coTimed`; false keeps
     `available-commit-unqualified`.
   The next tick issues a fresh statfs, so the verdict recovers on its own.

2. The branch's sole production entry point — `startPreGoneCrashSampling()` at
   main-process-ready-runtime.ts:128 — was untested. Deleting it left 691
   tests across crash-reporting/ and startup/ green, while a comment in the
   new test file claimed that gap was why the test was written. This is pure
   instrumentation, so that one line is the whole of its value in the shipped
   app. Added a source-level wiring test (the pattern this repo already uses
   for arm-once ready-phase lines) that pins the import, exactly one call, the
   call at statement indent, and that `main-process-ready.ts` awaits the
   function it lives in. The misleading comment is gone.

Mutation-verified — each goes red alone:
  coTimed -> always true                     1 failed (verdict)
  drop swapVolumeSampledAtMs age             1 failed (verdict test)
  delete startPreGoneCrashSampling()         1 failed (wiring)
  wrap it in `if (!is.dev) { ... }`          1 failed (wiring)

Verified: tsc -p config/tsconfig.node.json exit 0; oxlint
src/main/crash-reporting src/main/startup exit 0; 268 tests in
crash-reporting/ pass. Across crash-reporting/ + startup/ + memory/: 769
passed, 2 failed — both environment-dependent and failing identically on the
unmodified tree (Xvfb rebind, and a whole-repo glob census that times out).

* fix(crash-reporting): stop free disk standing in for pagefile growability

The win32 reading was promoted to the decisive `available-commit` whenever a
co-timed volume number merely existed, which the data cannot support: a fixed
or disabled pagefile grows into no amount of empty disk, its maximum is
unreadable here, and the measured volume is only the DEFAULT pagefile drive. A
host with 180 MB of available commit, a commit limit at RAM and 812 GB free
read as "the pagefile had room" — the opposite conclusion, under the branch's
most confident label.

The volume datum is now named for what it is (`available-commit-volume-cotimed`,
context beside the commit number), and the one decisive win32 case — a commit
limit at or below RAM, i.e. no pagefile behind it — gets its own label.

Also: carry the last volume reading onto the sample that replaces it, aged and
non-qualifying, so a statfs slower than one tick no longer makes the field
vanish from the reports it exists for; don't commit a reading whose every
memory field failed, which shipped an age and a disk-free number with no host
memory beside them; and move the startup wiring test beside the file it pins,
scoped to the ready-phase entry's own body so the call cannot satisfy it from
a sibling export nothing calls.

* fix(crash-reporting): co-time the statfs by tick, not sample identity

A tick whose host read fails leaves the pre-gone sample object in place, so
the identity check still read a 25 s-late statfs as co-timed.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…tablyai#18336)

* fix(agents): detect agent CLIs installed outside a version manager

The install-dir fallback that answers "is this agent installed?" when the
login-shell PATH probe fails listed only version-manager bin dirs, so
codex/opencode/cursor-agent installed by Homebrew, npm's default global
prefix, snap, nix, or the CLI's own installer read as not installed.

Where this is decisive, corrected from the previous message: the `orca` CLI,
whose detectSkillsCliAgentKeys (src/cli/handlers/skills.ts) calls
detectCommandsInInstallDirs directly and whose entry point seeds no PATH, plus
unpackaged/dev runs. NOT the packaged desktop app: patchPackagedProcessPath
(configure-process.ts:114) appends /opt/homebrew/bin, /usr/local/bin,
~/.opencode/bin and the Linux/nix prefixes onto process.env.PATH at
main-process-preflight.ts:147, before any detection, and mergePathSegments
preserves them, so the PATH scan reaches those dirs first and this fallback
never fires for them. That means this does NOT explain the packaged macOS
v1.4.194 report of codex/cursor-agent/opencode all undetected -- that report
stays open and uninvestigated.

Second correction: the fallback now carries the prefixes Homebrew actually
uses on Linux (/home/linuxbrew/.linuxbrew/bin), plus /snap/bin and the two nix
profile dirs, matching what patchPackagedProcessPath already seeds. The WSL
guest prelude gains the same entries. Leaving them out closed the native/WSL
asymmetry on darwin only, on the platforms where the fallback is decisive.

Appended last so a version-manager install still wins, and kept out of
getVersionManagerBinPaths, whose result is PREPENDED to PATH (stablyai#18234). Lives in
its own module so node-cli-command-resolution.ts stays under max-lines.

The test stages every path through `join` and asserts via
detectCommandsInInstallDirs as well as resolveCliCommands, so it holds on a
Windows dev machine and pins the "absolute path means installed" contract.

* fix(agents): align system install-dir order across the three PATH lists

Round-2 review remediation. The blocking finding was about the handoff
artifact, not the code: the summary handed to review described a 2-file/+38
change with 3 new macOS dirs, while HEAD is 4 files/+227 with 6 lookup dirs
plus 5 new WSL-guest prelude entries, and the quoted failing test names never
existed. Restated against HEAD in the handoff; no rebuttal, the reviewer was
right.

Justification for the entries the summary never described: the fallback exists
to close the native/WSL asymmetry for a CLI no version manager installed, and
patchPackagedProcessPath already seeds Linuxbrew, /snap/bin and both nix
profile dirs (configure-process.ts:141-158). Shipping only the darwin subset
would have left a Linux or WSL user with a snap/nix install still reading as
not installed while the packaged macOS user did not -- the asymmetry the change
is for.

Code changes, all from the non-blocking list:

- The three lists disagreed on order while claiming to be kept in step, so a
  CLI in both /usr/local/bin and /snap/bin could resolve to a different binary
  than the seeded PATH scan or the WSL guest probe found. All three now use the
  seed's relative order, pinned by a new duplicate-install test and an offset
  assertion on the prelude. The prelude's system block also moved after the nvm
  glob so a version manager still wins in the guest, as it does natively.
- The parity docstring asserted "the same set patchPackagedProcessPath appends,
  minus the sbin dirs and the generic ~/bin", which was false: it also omits
  ~/.vite-plus/bin (seeded by configure-process.ts:156, but no probed agent
  command maps to it) and /opt/homebrew off darwin. All three gaps are now
  named as deliberate.
- win32 returns [] and stays that way, but the branch now says why:
  %USERPROFILE%\.opencode\bin has never had install-dir coverage in either
  list, and the seed's system block is POSIX-only too. Pre-existing, unchanged.
- The detectCommandsInInstallDirs case read the ambient process.env.PATH, so on
  a box with /usr/local/bin on PATH only the opencode assertion exercised the
  fallback. It now stubs the GUI-launch PATH, so both do.

Unchanged and restated: this does NOT explain the packaged macOS v1.4.194
report of codex/cursor-agent/opencode all undetected, and must not close G6
report stablyai#3. patchPackagedProcessPath returns early unless app.isPackaged and
seeds all six dirs before any detection, and mergePathSegments never deletes
them, so the packaged PATH scan reaches them first and this fallback never
fires there. Decisive only for the `orca` CLI's detectSkillsCliAgentKeys and
unpackaged/dev runs.

Verification: the suite is red without the production hunks (5 failed/3 passed)
and green with them (8 passed); 11 related suites pass (197 tests); tc:node,
tc:cli, oxlint, oxfmt and the max-lines ratchet are clean.

* fix(agents): correct the ordering-parity claims and widen the seed-leak guard

Round-3 review found two docstring claims that are false as written and one
guard that only asserted a third of its list.

- system-cli-install-dirs.ts claimed a CLI in two of these dirs resolves the
  same here as in the packaged PATH seed. True inside the system block, false
  across it: `claude` in both ~/.local/bin and /opt/homebrew/bin resolves to
  ~/.local/bin via the fallback (getBaseVersionManagerDirectories leads) and to
  /opt/homebrew/bin via the seed, which appends ~/.local/bin last. Scope the
  claim to the block and name the gap instead of asserting it away. No behavior
  change: closing it would hoist a system dir over a version-manager one
  (stablyai#18234).
- posix-version-manager-bin-dirs.ts justified moving "/usr/local/bin" after the
  nvm glob with "a version manager still wins in the guest, as it does
  natively". The glob expands lexicographically; native orders nvm dirs
  default-alias-first (stablyai#10932), so that is not parity. Record the move as the
  one behavior change in the file and bound it: entries are appended behind a
  resolved login PATH, and both consumers only test presence.
- The stablyai#18234 seed-leak guard asserted only /opt/homebrew/bin and /usr/local/bin
  were absent from getVersionManagerBinPaths, leaving the four other new dirs
  unpinned. It now spells out all seven across darwin and linux -- spelled out
  rather than derived from getSystemCliInstallDirectories, which would pass
  vacuously against exactly the refactor it guards.

Tests 8 passed (8); 5 failed / 3 passed with the production hunks reverted to
origin/main. tsc node + cli clean, oxlint clean.

* fix(agents): find Pi's own installer dir in the CLI install-dir fallback

The fallback added `~/.opencode/bin` but skipped `~/.vite-plus/bin` on the
claim that no probed agent command maps to it. False: `pi` is a probed detect
command on every runtime (`tui-agent-config.ts`, no `detectUnsupportedRuntimes`)
and `~/.vite-plus/bin` is the Pi installer's default — the two dirs stablyai#829 named
and `patchPackagedProcessPath` seeds together. Added to both the native
fallback and the WSL guest prelude, so a Pi installed by its own script is
found by the `orca` CLI and in WSL, not just on a seeded packaged PATH.

Also, all narrower:
- `/snap/bin` + Linuxbrew now gate on `linux` like the seed does, instead of
  every non-darwin posix.
- Docstring: `/opt/homebrew` off darwin is the one remaining seed gap and says
  why; the "lookup-only" paragraph names the `withCliRuntimeOnPath` exception.
- New seed-order test derives the expected order from
  `getSystemCliInstallDirectories`, so reordering either list fails.

The PR body's claim that SSH hosts benefit is dropped: they answer
`preflight.detectAgents` from `src/relay/preflight-handler.ts` via
`isCommandOnPathForRelay`, a separate bundle that never reaches this module.

* fix(agents): build the CLI install-dir fallback order once and pin it on both resolvers

resolveCliCommand (every spawn site) and resolveCliCommands (detection) each
spelled the nvm -> version-manager -> system-dir order by hand, which is how
the native and WSL lists drifted apart before. One getCliInstallDirectories
now feeds both, and the test pins system dirs LAST on both resolvers, on
darwin and linux, plus a derived check that the WSL guest prelude keeps every
native version-manager dir ahead of the native system block.
nwparker added a commit that referenced this pull request Oct 19, 2026
…ew 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.
nwparker added a commit that referenced this pull request Oct 19, 2026
…iew off the deprecated owner resolver (#30562)

* 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants