Commit Graph
649 Commits
Author SHA1 Message Date
Jinwoo Hong f572ba34bc feat(browser): address-bar convergence — previews and browser tabs convert in place (STA-5681) (#16998) 2026-08-29 22:38:59 -07:00
Jinjing 73ff003147 test(e2e): cover session upgrade and Windows terminal recovery (#17289)
* coverage report

* rm test coverage

* test(e2e): cover session upgrade and Windows terminal recovery

* fix stub
2026-08-29 22:37:05 -07:00
Neil c6641152f1 Split relay dispatcher layers (#17174)
* Split speech session lifecycle

* Split terminal output scheduler pipeline

* Split mobile browser pane modules

* Prune resolved max-lines suppressions

* Split pane tree equalization logic

* Extract mobile troubleshoot screen styles

* Split external automation manager

* Split main window service attachments

* Split hosted review creation checks

* Split automation dispatch event handling

* Split settings navigation metadata

* Split daemon initialization lifecycle

* Split GitLab item dialog

* Split relay dispatcher layers

* Fix F3-speech for #17123

* Fix F1-cycle for #17131

* Fix F4-navtest for #17157

* Fix F2-allowlist for #17161
2026-08-29 20:15:10 -07:00
Jinjing 2214d29f15 fix(browser): close guest-owned split tab (#17281)
* fix(browser): close guest-owned split tab

* fix: check sourceId before toggling floating panel on close

The empty-panel toggle is the ambient fallback only. Guest-initiated
closes (with sourceId) target the main workspace and should not toggle
the panel.

* test(browser-split-shortcuts): remove terminal-mirrors close test and un

Removes test case that verified Cmd+W closes guest-owned browser splits when
active-tab mirrors point to a terminal, along with the helper function and
unused fixture properties that only that test required.
2026-08-29 15:43:36 -07:00
Neil 2dfaa676d8 chore: update oxlint and oxfmt (#17150) 2026-08-29 14:13:35 -07:00
Neil b17f60d744 build: upgrade to pnpm 12 (#17156) 2026-08-29 14:13:26 -07:00
Brennan BensonandBrennan Benson 11d8673112 test(cross-version-wire): derive skew expectations from the baseline under test (#17178)
* test(cross-version-wire): derive skew expectations from the baseline under test

The cross-version wire job pairs current code against whichever release tag is
newest, so a hand-written "the old side does not have X" assertion expires by
itself: v1.4.192 was the first tag containing the SnapshotStart `terminalOwner`
field, and cutting it turned the new-client/old-server pairing red on unrelated
pull requests with no code change anywhere.

Read what each build publishes from that build. Each host is now paired against
a client of its own version to produce a reference, and the skewed pairings are
compared against that reference, so the expectation is whatever the release
actually shipped. The same class of assertion in the agent-session suite —
"the old build advertises no structured capability and registers no structured
method" — becomes "each build's advertisement agrees with what it registers",
and the "client too old to know this capability" is derived by removing the
capability from the baseline's own list.

The guard is unchanged in strength: a field the old host still publishes may not
be dropped, skew may not change what a host puts on the wire, and a new pairing
asserts the oracle still stalls when a peer cannot decode an opcode the other
side sends.

* test(cross-version-wire): exercise release structured methods

* test(cross-version-wire): load the registered method manifest

* test(cross-version-wire): assert execution, not registration, on both host gates

The release-shaped checkout gate accepted any reply that was not
method_not_found, so a registered-but-throwing handler passed it. The
capability gate asserted a shared host spy had been called at all, so the
second method mapped to that spy could stop reaching the host unnoticed.

* test(cross-version): make the release-shaped skew cover the whole agent-session manifest

The release-shaped checkout is the only place the "registered means usable"
claim is executable today — the baseline release registers none of these
methods — and it was exercising one of sixteen. A handler registered and
returning an execution error passed the suite.

- Declare each method's result in the manifest, so "answered" is the contract
  rather than "did not say method_not_found".
- Give each build a seam to install a host into its own module slot; a release
  checkout has its own copy, so the working tree's host was never this
  dispatcher's, and every host-backed method answered
  structured_agent_session_unsupported — the capability gate's own words.
- Run one execution contract over both skews instead of two divergent loops.
- Pair the AI Vault never-called spy with a positive control; renaming the
  runtime method it watches left it green.

---------

Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
2026-08-29 13:42:50 -07:00
Neil 92ab618a11 Repair scheduled computer-use CI (#17122)
* Repair scheduled computer-use CI

* Make Calculator E2E Windows-version neutral

* Handle classic Calculator accessibility panes

* Update Calculator E2E source contract
2026-08-29 01:50:38 -07:00
Brennan Benson fd9125ea8c feat(native-chat): Codex structured native chat restructure (#16729)
* feat(native-chat): port structured Codex sessions from restructure-recovery

Rebuilds the desktop structured native-chat implementation from
brennanb2025/native-chat-restructure-recovery (tip 4e31c08db3) on top of
current main as a single commit, scoped to the local Codex path.

Ported:
- Structured agent-session core: durable record store + single-writer lease,
  canonical journal, agent-session wire host/attach/eviction/subscribers,
  `agentSession.*` RPC surface (registered via ALL_RPC_METHODS; host-side
  mobile allowlist included for wire compat), pty write gate, transcript
  additions, and the Codex app-server adapter/launch resolution.
- Renderer: NativeChatStructuredSession view/composer stack, structured
  launch path with the single-flight guard, local structured session tabs
  sync, activation gate + structured inventory (read-only
  `agentSession.handoffStatus` probe), agent-session tabs in the tab strip,
  AI-vault structured session activation, and the settings pane with the
  parent Experimental Chat UI toggle plus the nested "Use updated structured
  native chat" toggle. New sessions require both flags, agent codex, no
  prompt, and a local non-WSL, non-Windows-host execution host
  (structured-native-chat-availability).
- Fixes 72c013cea6 (verified Codex launch recovery), 8ddbaf5e3d (defer
  native terminal view switching affordances), and 4e31c08db3 (release the
  launch gate after a visibility retry) with their regression tests,
  including the third-launch-after-retry guard case.
- Cross-version agent-session wire test + CI lane, packaging entries
  (proper-lockfile, agent-tooling asar excludes), and the wire-compat doc
  section.

Deliberately not ported: mobile/ changes, the Claude structured runtime
(only the claude-transcript-branch-proof and claude-structured-owner-identity
leaf modules remain, backing the kept TUI-recovery arms), the terminal↔chat
adoption/handoff flow (`agentSession.adoptTerminal`/`requestHandoff`, the
handoff request engine, TUI adoption machinery, orca-runtime adoption
methods), renderer switching affordances and their dead leftovers, the
hook/subagent-status refactor cluster, and unrelated branch changes. The
crash-during-acquisition recovery path (restart handoff adjudication,
restore/reverse re-acquire, lease schema handoff keys) is kept because every
plain direct launch depends on it; a trimmed handoff coordinator exposes
only status/restore/close.

Branch edits that targeted files main has since split (ipc/pty.ts,
worktrees.ts, rpc/methods/terminal.ts, useIpcEvents, pty-connection,
store/slices/terminals.ts, runtime-types, web preload) were re-applied to
the split modules, preserving main's newer logic (Windows CIM fallback,
browser tab close rework, cold-restore resume flow, dispatcher threading).

Known seam: the mobile clipboard image-provenance CONSUMER gate ships
(agentSession.send refuses unproven mobile image refs with
agent_session_image_untrusted) but the producer hunk in
rpc/methods/clipboard.ts stays with the unported mobile cluster, so mobile
image sends into structured chat fail closed until that side ports.

* fix(native-chat): trust only authenticated local image uploads

* fix(build): preserve Windows process-tree patch application

* test(windows): include process creation time in addon fixture

* fix(build): run windows-process-tree node-gyp from the physical package dir

gyp expands the node-addon-api dependency by probing node, whose cwd
resolves to the package's physical directory in the store, so the emitted
target is a store-relative ../../../../node-addon-api@... hop. gyp then
resolves that hop against the rebuild cwd; from the node_modules
symlink/junction it escapes the store and configure fails with
"node_addon_api.gyp not found" (run 32999886072).

Rebuild from realpath(package dir) so both bases agree, matching how the
package manager itself runs native install scripts. The regression test
replays gyp's expansion+resolution against the planned cwd and fails
without the fix.

* fix(native-chat): keep chat tabs visible through terminal closes and empty-worktree launches

Two proven blockers in the native Codex tab contract:

closeTerminalTab pre-empted the canonical unified close. With one terminal
left it deactivated the worktree on a terminal/editor/browser-only check,
blanking a workspace that still held a renderable agent-session tab; with
two or more it pre-picked a successor from terminal entities only,
re-stamping the group active before closeUnifiedTab's MRU/neighbor repair
could land on the chat tab. Successor choice now defers to the unified
contract whenever the terminal has a unified row, and deactivation is
gated on the unified renderable count (matching leaveWorktreeIfEmpty),
with the legacy pre-pick kept only for terminals without a unified row.

A structured session created on an empty worktree was published into the
host's headless group while preserveLocalLayout froze the local layout,
leaving the tab in store but permanently off screen. A preserveLocalLayout
owner now always takes client-owned placement — repairing a rendered
leaf whose group record is missing, or materializing a rendered group on a
truly empty worktree — and applies the client-derived layout repair while
still rejecting host-authored layout.

Regression tests drive the real store through closeTerminalTab (git
worktree and folder workspace) and the real snapshot applier for the
empty-worktree adoption states; all fail without the fixes.

* fix(native-chat): close stale turns and retry rejected sends

* fix(native-chat): retire hosted rows on structured tab activation

* fix(native-chat): preserve rpc defaults across main merge

* chore: format remote wire compatibility guide

* test(native-chat): cover retry after unconfirmed send

* fix(native-chat): reload outbox on session switch

* docs(settings): disclose structured chat platform limits

* fix(native-chat): await Codex launch-home preparation

* fix(codex): align child-process allowlist with async trust bridge

* test(identity): update inventory for tab surface refactor

* fix(windows): preserve process-tree CRLF patch sources

* fix(native-chat): anchor an unmatched chat echo where it was sent (#16117)

* fix(native-chat): anchor an unmatched chat echo where it was sent

The reported symptom was old user messages replaying below every new turn, so the
conversation read as scrambled. The cause was not that the echo failed to match a
transcript row. Claude consumes a mid-turn send through a `queued_command`
attachment and writes no `type:"user"` record for it, so some echoes can never
match, and no amount of matching will change that. The cause was WHERE an
unmatched echo rendered: buildMobileNativeChatTransientData appended every pending
item after the entire transcript, so it re-read below each turn that landed
afterwards.

Render each echo directly after the transcript row it was sent against, using the
baseline the send already captures. An unmatched echo is then at worst a duplicate
in the right position rather than a scrambled one, and it stays visible. Echoes
sharing an anchor keep send order; a send with no baseline, or one whose anchor
folding dropped, still falls back to the tail.

Deliberately NOT fixed by deleting the echo. Inferring from send ordering that an
echo can never match, then removing it, loses the user's own text for a message
the agent did receive, and it cannot fire in the common case anyway - measured
drain groups are 1,017 of size 1 against 55 larger. It also escalates an existing
gap: the count pass has no baseline-tail guard, unlike the glue pass, while
`messages` is a 40-row window that head-trims, resets on reconnect and grows at
the front on loadEarlier, so a false landing there would license deleting a
DIFFERENT outstanding message.

That count-pass gap is real and left for a separate change; anchoring makes its
worst case a duplicate in place rather than a scrambled conversation.

* fix(native-chat): preserve folded echo anchors

* fix(native-chat): preserve forward-folded echo anchors

* fix(native-chat): keep leading folded echoes in place

* fix(workspace-cleanup): show git status for every row (#16690)

* fix(native-chat): refuse structured chat on every Windows execution path

canUseStructuredNativeChat only refused win32 when a project runtime
resolved, so folder-workspace keys (and other keys with no project
runtime) failed open into structured chat on Windows. Fail closed on
win32 unconditionally after the host check, matching the settings copy:
local macOS/Linux only; Windows/WSL/SSH stay on terminal chat.

* fix(native-chat): restore runtime refusals behind the win32 gate

506d375de3 replaced the project-runtime checks with a bare platform test,
so a WSL or repair-required runtime resolution would no longer refuse
structured chat off-win32. Keep the unconditional win32 refusal and
re-run the runtime resolution after it, so the gate does not depend on
the resolver's own platform guard. Tests inject WSL and repair-required
resolutions on darwin/linux and fail against the regressed gate.

* fix structured session journal durability

* fix structured tab active pointer after restart

* fix(native-chat): await optional lease renewal callbacks

* refactor(skills): extract install error messages

* fix(agent-session): harden recovery ownership

* fix(native-chat): retain panes across tab activation

* fix(native-chat): address round-one review findings

* test(native-chat): align integration coverage after main merge

* fix(native-chat): harden round-two reliability

* fix(native-chat): harden round-three reliability

* fix(native-chat): close round-four recovery gaps

* fix(native-chat): separate bounded journal key forms

* fix(native-chat): reset outbox error in render on session switch

The switch effect adjusted error state after the sessionId prop changed,
tripping react-doctor's no-adjust-state-on-prop-change on the changed-code
gate and flashing the old session's banner for a frame. Reset it with the
render-time previous-value guard instead.

* fix(native-chat): invalidate stale outbox settlements

* test(native-chat): restore settled-error session-switch regression

a6e2379bd1 replaced this test with the in-flight settlement race test,
leaving the render-time error reset unpinned: deleting the reset block
still passed the whole native-chat suite. Keep both scenarios pinned;
they are distinct (settled error clears on switch vs stale settlement
invalidated in the commit-to-passive window).

* test(wire): make release checkouts race safe

* test(wire): pin cross-process checkout single-flight and importer specifier contract

* test(wire): harden release checkout lifecycle

* fix(build): drop CR-byte residue from windows-process-tree patch

The two trailing CR bytes on the patch's deletion lines are a proven
no-op: pnpm hashes patches CRLF-normalized (both forms hash to the
lockfile's 946ffb2b) and materializes this package without applying the
patch in either form, so the load-bearing build edits come solely from
applyWindowsProcessTreeBuildFixes() (#16947), which handles both source
EOL forms. Restore byte-identity with main and repin the contract test
to the post-#16947 reality: LF-only patch bytes plus lockfile hash sync.

* fix(native-chat): skip empty startup recovery
2026-08-28 16:45:58 -07:00
Brennan Benson 774ce22e84 fix(e2e): drop the pi-title spec's stale private connect fork (#17017)
`ssh-pi-compatible-agent-title.spec.ts` kept a private `connectDockerRemote`
that predates #11003. Commit a40183389b gave `fetchWorktrees` a host-qualified
authority gate that short-circuits to `return false` for an SSH host with no
complete `directSshAuthority`, and updated the shared
`connectDockerSshRelayTarget` helper in the same commit -- but never touched
this spec. The fork still does `fetchRepos()` -> bare `fetchWorktrees(repoId)`
-> `worktreesByRepo[repoId][0]`, so the fetch short-circuits, the listing is
empty, `[0]` is undefined, and setup throws
`No remote worktree found for /tmp/orca-docker-relay-perf-repo` before the
spec reaches a single title assertion.

Differential across 5 lane runs (~130 SSH connects): the shared helper's guard
errors fired 0 times; `No remote worktree found` fired 3 times, always via the
fork. The fork accounts for 6 of 7 failures on `e2e / ssh docker watcher
isolation`.

The spec now calls the shared helper. Every helper default matches what the
fork passed (`relayGracePeriodSeconds: 1`, `remotePath`
DOCKER_SSH_RELAY_REMOTE_REPO_PATH, `seedInitialTab` true, port `target.port`);
the helper additionally uses `target.host` rather than a hardcoded
`127.0.0.1`, which is the correct value under `ORCA_E2E_SSH_TARGET_HOST`.

No assertion changed; no retry, sleep, or timeout was added anywhere.
2026-08-28 14:16:50 -07:00
Jinjing c4b39295c1 style: format codebase (#16935)
* style: format codebase

* style: format codebase

* refactor: extract skill install dialog footer and content

Extract footer and content sections from SkillInstallDialog and
SkillInstallManagementDialog into separate components for improved
maintainability and clarity of component responsibilities.
2026-08-28 00:59:21 -07:00
Jinwoo Hong 59515beb70 fix(release): recover immutable patch validation gates (#16984)
* fix(release): recover immutable patch validation gates

* test(e2e): locate wrapped terminal file links

* test(e2e): keep sibling file links on one terminal row
2026-08-28 00:55:45 -07:00
Neil b1fe9075db fix(ssh): never claim authority over host tabs this client could not place (#16956)
* fix(ssh): a host tab row this client cannot place is unverifiable, not absent

A degraded listLineage leaves worktreesByRepo empty, so exactTargetWorktreeIds
returns nothing, every host path fails to resolve, and importRemoteWorkspaceSession
silently dropped every tab row. The apply then marked the target hydrated and
'synced', freezing that emptiness in permanently — nothing re-pulls a hydrated
target (STA-3593).

The importer now reports unplaceable rows, the apply claims authority only when
every row landed, and a bounded chain re-pulls the missing input (catalog +
lineage). On exhaustion it settles back to the pre-fix behaviour so a genuinely
unplaceable path is never left worse off than today.

* fix(ssh): keep the re-pull chain bounded, unwedgeable, and announced once

Five defects in the first cut of the chain, all found before merge:

1. the caller owned the attempt counter, so an unsolicited host push re-armed
   it at 0 and the chain never exhausted - an unbounded workspace.get loop;
2. exhaustion never cleared the counter, so after one bad connection every
   later reconnect re-exhausted instantly and the retry was silently dead;
3. the exhaustion check preceded the armed-timer guard, letting a concurrent
   report cancel the still-pending final retry;
4. a rejected host read left no timer armed and nothing rescheduled, stranding
   the target on 'pulling' and un-hydrated forever - and an un-hydrated target
   never uploads again, the exact permanent degradation this design avoids;
5. exhaustion re-announced on every later report, re-marking hydrated and
   rewriting status on each host push.

The module now owns the counter, resetTarget gives each connection a fresh
chain, the armed guard precedes exhaustion, the timer body always reschedules
so any failure walks to exhaustion, and exhaustion is announced once.

* fix(ssh): never authorise uploads from a picture known to be incomplete

Reversal of this branch's own exhaustion fallback, on evidence.

Hydration authorises uploads (use-app-session-persistence.ts), and an upload is
a workspace.patch of kind 'replace-session' (remote-workspace-relay-sync.ts:66)
which wholesale replaces the host snapshot (relay/workspace-session-handler.ts).
So marking a target hydrated on a picture we know is missing rows does not
'settle back to the old behaviour' - it uploads an empty projection that DELETES
the host tabs we failed to adopt. Suppressed uploads are recoverable; a wiped
host snapshot is not. That data loss is reachable on main today, because today
the apply marks hydrated immediately.

Exhaustion therefore reports 'error' and leaves the target un-hydrated, so
terminal authority stays 'unverifiable' and no upload can be built from it.

Also closes two chain-lifecycle gaps found in review:
- the callback dropped its timer guard before awaiting the host, leaving a gap
  in which a concurrent report armed a second overlapping chain and could trip
  exhaustion before the pending apply resolved; an in-flight guard now spans it;
- resetTarget could not cancel a callback already past its await, so a stale one
  rescheduled on top of the new connection's chain; chains are now generation
  stamped and a stale callback exits.

* fix(ssh): scope re-pull in-flight ownership to a generation

Two races found in review of the previous commit:

- schedule(target,'placed') cleared timer/count/exhaustion but did not
  invalidate an apply already in flight. When that apply later resolved
  'unplaced' its generation still matched, so it started a fresh chain from
  attempt 0; a host repeating placed pushes during each in-flight retry could
  reset the budget indefinitely. Retirement now bumps the generation too.

- the in-flight marker was a bare Set, so a superseded callback's finally
  deleted whichever marker was present - including one a newer generation had
  since taken. A later report could then arm an overlapping timer while that
  newer apply was still running. Ownership is now a target -> generation map and
  a callback releases only the marker it still owns.

resetTarget deliberately no longer drops the marker: its owner is the only party
that may release it, and clearing it there would let a new chain arm while the
superseded apply is still running.

* fix(ssh): replay an unplaced report that was blocked by a superseded apply

Regression from the previous commit. Keeping the stale in-flight marker across
resetTarget stops overlap, but it also swallows the new connection's result: the
new apply reports 'unplaced', hits the guard because the superseded apply still
owns the marker, and the superseded callback then exits on its stale generation
without scheduling. Nothing replayed the dropped report, so no chain started -
the retry silently never ran for that connection.

A blocked unplaced report is now recorded, and the marker's owner replays it on
release. Only the stale path reaches the finally still owning the marker, so the
normal path - which released early and scheduled its own outcome - cannot replay
twice.

* refactor(ssh): drop the re-pull retry chain, keep the fix

The chain produced ten defects across review - unbounded retry, dead retry,
cancelled final attempt, wedged chain, repeated exhaustion, overlapping chains,
stale-callback cleanup, a lost report - every one in code that passed the full
suite at the time. It bought only faster recovery *within* one connection:
syncAfterConnect and applyUnsolicitedSnapshot already re-pull on the next
connect or host push, so dropping it costs a retry, never the data.

What remains is the part that was correct from the first commit: the importer
reports rows it could not place, and an apply that could not place them neither
marks the target hydrated nor sets 'synced'. Because hydration is what
authorises uploads, and an upload wholesale replaces the host snapshot, that
single rule is what stops a client from deleting the host tabs it failed to
adopt.

Status is now 'error' rather than 'pulling': with no chain pending, 'pulling'
claimed a request that was not in flight.

* test(ssh): name the upload-suppression case for the chainless design

* fix(ssh): revoke stale hydration and keep authority unverifiable when unplaced

Two holes in the previous commit, both found in review.

The hydrated set is add-only (ssh.ts), so withholding hydration only protects a
target that never synced. A target that synced cleanly and then reconnected with
a degraded lineage kept its flag, and hydration is what authorises uploads - so
it would still send a replace-session patch built from the incomplete picture
and delete the host tabs it had just failed to place. Hydration is now revoked,
not merely withheld.

The status phase was 'error'. workspace-terminal-host-authority.ts treats
'offline'/'error' on an un-hydrated target as its bounded floor and resolves
them to 'none' - which authorises seeding AND sleeping-agent resume, the exact
double-resume this gate exists to prevent. 'conflict' is the phase that actually
describes the situation, is excluded from uploads by use-app-session-persistence,
and is deliberately outside that floor set, so authority stays 'unverifiable'.

Both invariants are pinned by tests verified to fail when either fix is reverted
individually; the pre-existing tests passed with both reverted.

* fix(ssh): drop the mismatched message on the unplaced conflict status

The phase drives the user-visible label ('Workspace sync conflict'); carrying an
'unavailable' message alongside it only risked contradicting that wherever the
message is surfaced.

* docs+refactor(ssh): correct the authority floor's premise, drop a dead wrapper

Two findings from the post-merge correctness sweep.

The bounded floor in workspace-terminal-host-authority.ts justified itself on
'remoteWorkspaceHydratedTargetIds is add-only, clearRemoteWorkspaceHydrated has
no production caller'. This branch adds that caller, so the premise is now false
and a future reader would have been misled by it. The comment records the real
consequence: a target that later lands on offline/error reaches the floor having
demonstrably answered, so seeding is authorised over live host terminals. Not a
regression - before revocation existed the same target was marked hydrated and
synced, reaching 'none' sooner - but the floor should learn to tell a revoked
target from one that never answered. Flagged for the SSH-v3 consolidation, where
one authoritative liveness source replaces this pair.

applyUnsolicitedSnapshot had become a pass-through to applyPreparedSnapshot,
carrying a docstring about a re-pull chain that no longer exists. The two
collapse back into one function.

* refactor(ssh): delete the DirectSshSnapshotPlacement union

Consolidation pass finding. The union was exported and threaded through two
modules, but no production consumer ever read it: remote-workspace-ipc-bridge.ts
discards the promise's value and syncAfterConnect ignored it. 'not-applied' was
not a placement at all, only 'this apply did not happen'.

That is a parallel verdict concept with no consumer - precisely what the SSH-v3
consolidation would have had to unpick. It collapses to a local
hasUnplacedTerminalTabs boolean and a void return.

The one test that asserted the return value now asserts adoption instead, which
is the observable outcome rather than a proxy for it. All five unplaced oracles
still fail when the placement decision is forced, verified individually.

* docs(ssh): compress the tombstone rationale to its load-bearing WHY

Elegance pass. Kept the two non-obvious claims - absence cannot distinguish
'never told' from 'user closed', and uuid tab ids make a tombstoned id safe to
drop - and cut the incident narrative around them. The twice-reverted history in
remote-workspace-session-merge.ts is deliberately left alone: that one is
institutional memory about regressions, not restatement of the code.

* test(ssh): pin the fixed behaviour instead of the defect it replaced

The spec was a characterization test whose own title said 'because hydration is
marked even when adoption wrote nothing', and whose comment described exactly
the defect this branch fixes: markRemoteWorkspaceHydrated ran unconditionally
after the hydrate calls, so in the same tick adoption yielded zero, authority
flipped unverifiable -> none, and Terminal.tsx seeded a phantom tab. It polled
for hydrated === true, so the fix turned it red.

It now asserts hydrated === false, phase === 'conflict', and zero tabs - the
count measured before asserting rather than assumed, confirming the phantom seed
is gone. The phase is re-read after the tabs settle and asserted a second time,
because a conflict verdict a later apply flipped back would silently re-authorise
seeding and a single poll would miss it.

The fixme stays a fixme: this branch stops the client claiming false authority
and overwriting the host, but adoption is still the open gap. Declining to seed
is a safe wait, not the destination.

Three-legged A/B against fork point c72afda498, spec byte-identical across the
first two legs:
  branch   + updated spec  -> PASS
  baseline + updated spec  -> FAIL 'never reported the unplaced snapshot as a conflict'
  baseline + original spec -> PASS (baseline actively exhibits the old behaviour)
2026-08-27 22:14:35 -07:00
Jinwoo Hong cb848647e5 fix(browser-preview): require explicit preview capabilities (STA-5758) (#16921)
* fix(browser-preview): require explicit preview capabilities (STA-5758)

Scope document reads to approved directories, confirm external links before opening them, revoke grants with tab lifecycle, and keep document-preview session state rollback-safe across mixed client/runtime versions.

* Harden document preview lifecycle and permissions

* Document preview DNS prefetch residual

* Make preview E2E guest focus explicit

* fix(browser-preview): entry-file-only authority for root-level docs, contained chip layout, re-issued gate paths (STA-5758)

A grant whose document directory is its own request base — a doc at the
workspace root, or outside any workspace — now reads nothing but the entry
file until the reader approves a directory, at both the lexical and the
canonical containment pass. The DNS-prefetch residual can only beacon what
the page can read, and a root-level document could previously read the
whole worktree silently.

The identity chip's host badge overflowed the chip's layout box under
squeeze (Linux CI): every row member can now shrink and truncate, verified
by a width sweep in isolated Chromium down to ~120px chips.

The Allow banner says what it grants: 'Allow folder', reading files in the
named directory, for the life of the preview.

The reliability-gate manifest command, testFiles entry, assertion refs and
dated evidence naming the deleted doc-preview-external-link-bridge.test.ts
are re-issued at doc-preview-external-link-confirmation.test.ts with a
fresh 189/189 run; the focus-gate assertion text follows the shipped gate.

* fix(browser-preview): hide the chip identity row below 24rem instead of clipping it, ellipsize the host badge, catalog the new i18n keys (STA-5758)

CI's preview pane leaves the chip ~40px: no truncation shows anything
there, so the Workspace-file label and host badge now hide whole below a
24rem container threshold sized so that visible implies contained. The
badge text gains an inner text box — text directly inside the flex pill
clipped both ends with no ellipsis. The e2e geometry oracle asserts
containment when the row shows and the threshold when it does not.

verify:localization-catalog: the hardening's new preview keys (and the
renamed allowDirectory) join en.json via sync:localization-catalog.

* feat(browser-preview): batch blocked folders into one access decision (STA-5758)

Sequential per-folder banners trained the allow reflex without adding
judgment — a reader cannot weigh assets/ against data/. The banner now
accumulates every folder a load surfaces, names them (three, then a
count, full list in the title), and grants exactly that set with one
Allow-N-folders click and one reload. Dismiss fences the whole named
set. The map lives behind a ref with a version tick so a dismissal
fences an offer landing in the same event batch.
2026-08-28 00:42:07 -04:00
Jinwoo Hong b19a397d3e feat(browser-preview): reland remote HTML document previews (STA-5758) (#16920)
Reapply the reverted remote HTML document preview implementation so remote workspace files render locally over the orca-preview scheme.
2026-08-28 00:27:01 -04:00
Neil 2b391652b1 fix(terminal): a close the host never heard must survive the reconnect (#16752)
An enterprise user: "Every day I open orca and it opens more tabs daily at a
linear scale." Three reports over a week, told on 08-19 that a PR had fixed it,
reported twice more after. STA-4658 (P0), GH #12447, #15136, #10342, #9585. One
install held 39 zombie tab records. The revived tab's sleeping-agent record still
holds the pre-close session id, so it boots `claude --resume <old id>` -- two
agents on one transcript.

## The chain, measured

Reproduced deterministically in `ssh-lost-kill-tab-resurrection.spec.ts`: close
an SSH tab, kill the relay daemon in the container so `pty.kill` rejects with a
transport-class error, reconnect.

    drop 2 resurrected the closed tab <id>:
      baseline=1  drop1=1  drop2=2 (closed tab returned)  drop3=1

The trigger is narrow and had to be measured rather than assumed: killed relay
daemon reproduces **6 of 6 runs**; an orderly `ssh.disconnect` **passes**. Only
an ungraceful loss -- network partition, host reboot, relay crash, a laptop
sleeping mid-session -- strands the close with the RPC rejecting on a
transport-class error. Both variants live in the spec behind one
`runResurrectionCycles` parameterized solely by the disruption, so the difference
is attributable to that single variable.

What actually carries the tab back, from the pull path
(`workspace.get` -> `getRemoteSnapshot`, `remote-workspace-relay-sync.ts:29`):

    pullSnapshot rev=3 tabs={repo:["16c4a3e1","06aba6b6"]}
    pullSnapshot rev=4 tabs={repo:["16c4a3e1","ff72768e"]}   <- ff72768e IS the resurrected tab
    pullSnapshot rev=5 tabs={repo:["16c4a3e1","ff72768e","da21b76c"]}

The client uploaded the session containing the tab; the user closed it; the kill
RPC rejected so the close never reached the host; the host's snapshot still lists
it; the client pulls it back and the merge restores it -- **correctly, by its own
rule that the host is authoritative for what it knows.** A pane then mounts,
respawns, and takes the recycled pty id.

Client-side correlation from the same run, two controls and one positive in one
run differing in exactly one variable:

| Tab | Close events observed | Resurrected? |
|---|---|---|
| `6305cc07` | `user` + `pty-exit` | No |
| `ed56f66c` | `user` + `pty-exit` | No |
| `2036e760` | `user` only | **YES** |

## The fix

`src/shared/closed-terminal-tab-tombstones.ts` (99 lines). A client-recorded
close is first-party intent and must survive until the host acknowledges it. Per
`docs/reference/ssh-execution-boundary.md` the remote verdict is `unverifiable`
-- which may not authorise declaring the process dead, but equally must not
authorise resurrecting the tab. This is SSH-v3 principle P2, "durable tombstones
with a monotonic per-scope revision", reusing the existing
`RemoteWorkspaceSnapshot.revision` rather than adding a twelfth per-tab identity
field (the codebase carries eleven, 784 refs, that SSH-v3 Phase 3 deletes).

- **Recorded** only on `closeReason === 'user'` (`terminal-tab-close.ts:69`).
- **Suppresses** a host-sourced tab only when
  `tabId in tombstones && !currentTabsById.has(tabId)` -- a live local tab always
  wins, because deleting a live pane is the one outcome the merge exists to
  avoid.
- **Retires** on positive acknowledgement:
  `!hostKnownTabIds.has(tabId) && hostRevision > observed`. Strictly newer, so a
  pull already in flight at close time cannot ack a close it predates.
- Three never-retire guards: no revision retires nothing; a worktree the snapshot
  has no row for retires nothing; the first omitting snapshot only stamps the
  watermark.
- TTL (30d) + cap (500) are **backstops** for a target the user never returns to,
  not the mechanism.
- **Client-local only** -- never crosses the wire, so there is no mixed-version
  exposure.
- Suppression is scoped to `replaceWorktreeIds`, which is what makes the
  live-tab check meaningful. A final whole-map sweep over the assembled
  `tabsByWorktree` would break that (a live tab is absent from `currentTabsById`
  outside the scope and would look suppressible); it is deliberately not there,
  and the comment at the top of the function says so.

## Evidence

The load-bearing evidence is an A/B control on one tree, not the oracle's
assertion. Flipping `isSuppressedByClose` to `false` -- one character --
reproduces the resurrection on demand:

    --repeat-each=2:
      1) drop 2 resurrected the closed tab ab0e305d-…: baseline=1 drop1=1 drop2=2
      2) drop 2 resurrected the closed tab 51533e34-…: baseline=1 drop1=1 drop2=2
      2 failed

With suppression on: **0 occurrences of "resurrected the closed tab" across five
runs plus one independent run by a second agent.** Provenance verified
positively, not by mtime: `closedTerminalTabTombstonesByTabId` appears 13x across
3 renderer chunks including `store-Do3KBvRE.js`; for every red control run
`mayCreate` appeared 0 times in `out/main/index.js`.

At the unit layer, disabling the same predicate: 3 failed | 39 passed. Restored:
42 passed; 287 across the workspace-session, terminal-store, remote-workspace,
shared-tombstone and profile suites; 24 in the four tombstone suites.

## The oracle spec: GREEN in the full lane

`ssh-lost-kill-tab-resurrection.spec.ts` passes both tests at this commit. Full
Docker-SSH lane, clean tree:

    BUILD_SHA=49bb96e0b4c   DIRTY=0
    PROVENANCE  tombstone=13  hasLocalTabsRow=2  hostAuthority=4  mayCreate=3
    14 specs / 20 tests -> 17 passed, 2 failed, 1 skipped (10.7m)

    [12/20] :178 does not resurrect tabs whose kill was lost to a killed relay
            daemon                                                      PASSED
    [13/20] :190 does not resurrect tabs closed while the host is
            disconnected                                                PASSED

    grep -c "resurrected the closed tab"  (whole lane)  -> 0

It passes WITHOUT PR 7 in the build (`mayCreate` present,
`SshPtyAbsentFromRelayError` absent), so the bug-2 fix below is not required for
it.

Test 1 fails intermittently in ISOLATED single-spec runs, where a third defect
blocks its cycle-2 setup. The resurrection assertion itself has never failed with
this fix in place -- the intermittent failure is always a setup failure, never a
resurrected tab. A reviewer running the spec alone may see it red; that is not
this fix regressing.

Three defects sit under STA-3374 and should not be conflated:

- Bug 1 -- the closed tab resurrects. Fixed here.
- Bug 2 -- `ssh-pty-session-reattach.ts:227-231` rewrites the relay's
  `PTY "pty-1" not found` into a bare `SSH_SESSION_EXPIRED`, so
  `isPtyAlreadyGoneError`'s `/PTY ".+" not found/` cannot match and
  `attachStablePaneOwner:242`'s already-correct fallback never runs. Owned by
  PR 7 (`nwparker/ssh-07-absent-from-relay`). Not required for the oracle above.
- Bug 3 -- after the daemon is killed and the client launches a replacement, the
  client's OWN SSH transport drops and does not reconnect within 60s: no
  "delay step 2/9", no handshake failure, nothing. `ssh-connection.ts:1533` only
  logs on an SSH-level close. Unfixed, its own ticket. This is what makes test 1
  intermittent in isolation.

Discriminator for bug 3, measured in the isolated runs (the lane above ran
without `ORCA_E2E_FORWARD_APP_LOGS=1`, so it was not re-confirmed there):
`[ssh-relay] Socket probe result:` reads "DEAD" on every cycle of test 1 (daemon
killed, a NEW relay must be launched) and "ALIVE" on every cycle of test 2
(daemon survived). Whenever a new daemon must be launched, the SSH transport
drops afterwards and does not recover.

An earlier reading blamed `kill.ts:82-84` for skipping `finishPtyShutdown` on a
non-already-gone error. That was eliminated by direct test: the implied fix,
`markSshRemotePtyLease(…, 'expired')` in that branch, was implemented, changed
nothing, and was reverted rather than shipped unproven. Recorded so the path is
not re-walked. The `SSH_SESSION_EXPIRED` rejection is real but fires during cycle
1 for the baseline pane, after which cycle 1 completes; the 60s silence begins
only after `Relay channel lost ..., triggering reconnect`.

The spec is claimed by the Docker-SSH lane, and that lane does not gate merges
today.

## Persistence: the tombstone must survive a relaunch

`closedTerminalTabTombstonesByTabId` is declared on `WorkspaceSessionState` but was missing from
`workspaceSessionStateSchema` (`src/shared/workspace-session-schema.ts`), which is the load boundary
for BOTH partitions -- `normalize-loaded-state-collections.ts` for `local` and
`workspace-session-partitions.ts` for `ssh:<target>`. Zod strips unknown keys and the write side does
not validate, so the map reached disk and was discarded on the next launch. Measured with the repo's
own parser:

    input : closedTerminalTabTombstonesByTabId: { 'tab-1': {...} }
    ok    = true
    tombstones after parse = undefined

That made the fix ineffective in the exact reported scenario: close an SSH tab with the transport
down, QUIT, relaunch, reconnect -- the merge runs with an empty map, the host still lists the tab,
and it resurrects. "Every day I open orca and it opens more tabs" is a claim about restarts.

Neither the green oracle nor the A/B control could see it: both run entirely inside one app process.
It also made the 30-day TTL and the 500 cap unreachable.

Fixed by adding the field with a `salvagingRecord` matching its sibling
`terminalSurfaceTombstonesByPaneKey`, so one malformed entry drops that entry rather than the map.

`workspace-session-schema.ts` was one line under its 300-line max-lines limit, so adding the field
required room rather than a suppression (the project forbids max-lines disables and per-file bumps).
Two value schemas were extracted to modules named after what they contain:
`terminal-tab-id-schema.ts` and `terminal-surface-tombstone-schema.ts`. The closed-tab tombstone's
own schema is colocated with its type in `closed-terminal-tab-tombstones.ts`, which is where it
belongs -- omitting it from the session schema is exactly the drift that caused this bug.

`workspace-session-schema-field-coverage.test.ts` is the ratchet. Two sibling tables already pin
themselves with `satisfies Record<keyof WorkspaceSessionState, ...>`; this schema had no such guard
and is the one that fell behind. The new file adds both halves -- a `satisfies` list that makes a
forgotten field a compile error, and a runtime assertion that names it -- plus a
`parseWorkspaceSession` round-trip. Without the schema entry: 3 failed. With it: 3 passed.

## A host tab the user never closed could be deleted

`tabId in closedTerminalTabTombstonesByTabId` answers true for every `Object.prototype` key even on
an EMPTY map, because the map is a plain object from `Object.fromEntries`. A host tab whose id is
`toString` was filtered from the reconciled list, blocked from the host-unknown branch, and stripped
of its layout and session id. Tab ids are validated only as non-empty and colon-free, and `createTab`
honours caller-supplied id hints, so the id is reachable rather than theoretical. This was the only
path in either direction that could delete a tab the user never closed.

Now `Object.hasOwn`, as the same file already uses elsewhere.

Suppression is also scoped structurally: `isSuppressedByClose` compares the tombstone's stored
`worktreeId`, which it already carried, so it cannot reach another workspace's tab. The two sweeps
that have no worktree in scope (`terminalLayoutsByTabId`, `remoteSessionIdsByTabId`) now consult the
set of ids this merge actually suppressed rather than re-deriving a verdict without that scope.

The scope comment at the top of the function was also wrong and is corrected. It claimed every use of
suppression sits inside `replaceWorktreeIds`; it does not -- the tabs pass walks all of
`orderedWorktreeIds` and the two sweeps cover the whole remote maps. What actually makes it safe is
that `closeTab` strips the id from every worktree row before recording the tombstone, plus the
worktree match above, plus `closeReason === 'user'` being the only writer. Real guarantee, different
from the documented one.

## Divergences from open PR #16571

#16571 implements the same concept. Three deliberate changes:

1. It never retires on acknowledgement -- TTL+cap only, so it never converges.
   Ack retirement added.
2. It crosses the wire and lets a HOST-sourced tombstone delete a LOCAL tab in a
   final whole-map sweep. After #14361 that is the wrong risk; dropped. This also
   removes the mixed-version regression its own body flags.
3. Its hydration unions rather than replaces the map -- a union resurrects every
   tombstone the merge just retired, so it never converges.

Its `activeTabId` nulling is also dropped as redundant:
`workspace-terminal-hydration.ts:99-105,126-138` already revalidates both
pointers against the tab rows it just built, and nulling twice would add a second
rule that has to stay in step with the first.

## Can a tab the user did NOT close disappear?

No, but the guarantee needs stating precisely. The only writer is
`recordClosedTerminalTabTombstone` (`terminal-tab-close.ts:69`), reachable only
on `closeReason === 'user'`; suppression additionally requires the tab not be live
locally. Reopen (`recently-closed-tabs.ts:122-166`) calls `createTab` and restores
cwd/shell/title/color/position, never the old id.

**Caveat, stated because the slogan is not literally true:** `createTab` honours a
caller-supplied id hint (`terminal-tab-creation.ts:53-65`, used by `useIpcEvents`
for host-admitted tabs), so "tab ids are uuids that never recur" does not hold in
this codebase. The guarantee rests on the `closeReason === 'user'` writer plus the
live-local-tab check, not on id uniqueness.

## Risk

Renderer-side, client-local, no wire change. The blast radius is
`mergeDirectSshRemoteWorkspaceSession` and the persisted session field. Worst case
if the ack logic were wrong in the retiring direction: a tombstone outlives its
usefulness and suppresses a host tab whose id the host re-issues -- bounded by the
live-local-tab check, the 30d TTL and the 500 cap. Worst case in the other
direction is today's behaviour. `profile-project-session-field-disposition.ts`
records the new field as `notRepoScoped` / `notTransferred` residue, bounded by
the same TTL and cap.

## Verify

    pnpm test src/shared/closed-terminal-tab-tombstones.test.ts \
      src/renderer/src/lib/workspace-session-closed-tab-tombstones.test.ts \
      src/renderer/src/store/terminals/terminal-tab-close-tombstone.test.ts \
      src/renderer/src/hooks/remote-workspace-session-merge-close-tombstones.test.ts

To reproduce the bug this fixes, set `isSuppressedByClose` to `() => false` in
`remote-workspace-session-merge.ts` and run
`pnpm test:e2e:ssh-docker -- tests/e2e/ssh-lost-kill-tab-resurrection.spec.ts --repeat-each=2`.
2026-08-27 19:47:15 -07:00
Neil e06a8667a9 fix(terminal): do not seed or resume while the execution host has not answered (#16750)
Two client behaviours read local tab rows as the verdict on what the execution
host is running. Before the host answers, "I hold no pane for this" is
`unverifiable`, not `exited` -- the collapse
`docs/reference/ssh-execution-boundary.md` forbids.

Symptom 1, seeding. `worktree-initial-terminal-seeding.ts:47,128` seeds a
terminal when `renderableTabCount === 0`. Its only bail-out (`:72-77`) covered
the paired-web-runtime flavor -- "while that session is live the host owns
terminal creation" -- with no equivalent for direct SSH. So a client that has
never held the workspace runs the predicate during the hydration gap and creates
a tab from nothing. The snapshot then arrives, the merge rightly keeps the tab it
was never told about, and the union uploads as the new host truth. Measured on a
fresh client against a host owning 3 tabs: **1 tab created from nothing, 0 of the
host's 3 adopted.** (A restart never reaches the predicate -- local state
restores the row first -- which is why restart-only repros came back flat.)

That guard was also the wrong question. It asked "am I a client of a live paired
session?", which a host desktop window answers "no" and a paired client answers
"yes", so both seeded -- #15556.

Symptom 2, sleeping-agent resume, and the data-corrupting half.
`Terminal.tsx:1554` calls `resumeSleepingAgentSessionsForWorktree` twenty lines
after the seeding call at `:1529-1534` -- same startup path, same pre-hydration
window, and not SSH-gated at all. Seeding produces a spare empty tab; the sweep
launches `claude --resume <id>` for a session still running on the remote and
still owned by a live pane. Two agent processes writing one transcript; STA-3498
observed five. STA-3500 files exactly this race. Failure is asymmetric: declining
to resume is user-recoverable, a duplicate resume corrupts a transcript
irreversibly.

`workspace-terminal-host-authority.ts` answers the one ownership question both
paths ask, in the three-verdict vocabulary the renderer already uses for host
terminal inventory (`HostLiveTerminalProbeVerdict`, aliased rather than restated
so the two cannot drift): `live` (a remote host owns creation here),
`unverifiable` (there is a remote host and it has not answered), `none` (local,
or the host answered and holds nothing). Seeding requires `none`; the sweep
declines on `unverifiable` without consuming its one-shot, so the agents are not
stranded for the session once the verdict lands.

Shape notes:
- An ownership question, not a client-liveness one -- that is what fixes #15556.
- Folder workspaces resolve to `none`: the snapshot replaces exactly
  `DirectSshTargetScope.gitWorktreeIds`, so a folder's rows are never replaced by
  the host and waiting for an answer that will never name them would leave it
  terminal-less for good.
- A `conflict` sync phase is `unverifiable`, matching the pair
  `use-app-session-persistence.ts` already gates uploads on.
- Explicit launch work (setup/issue commands) stays ungated -- that is a request
  to create a terminal now.
- `Terminal.tsx` subscribes through a retained selector rather than reading in
  the effect: the verdict flipping to `none` is what must re-run the passes, and
  resolution walks the owner catalogs, so recomputing per store write would be
  the STA-3363 render-path multiplier again.

The `unverifiable` verdict is BOUNDED, and must be. `remoteWorkspaceHydratedTargetIds` is add-only
in practice -- `markRemoteWorkspaceHydrated` has two production call sites, both on success paths,
and `clearRemoteWorkspaceHydrated` has NONE. Four paths return without marking: local-hydration
timeout (`remote-workspace-target-sync.ts:136-145`), a null `remoteWorkspace.get` (`:160-169`), a
falsy apply token (`:172-185`), and never connecting at all. Without a floor, any of them would
leave every git worktree on that target `unverifiable` for the rest of the app session: no initial
terminal, no sleeping-agent resume, escapable only by creating a tab by hand. That is strictly worse
than the behaviour it replaces -- on main the user got a terminal. So a sync that terminates in
`offline` or `error` without ever hydrating resolves `none`: declining to seed is meant to be a
wait, not a permanent refusal. `pulling` still declines, and a target that HAS hydrated stays `none`
even if a later sync errors.

Scope, stated because the doc comment previously overstated it: this gate is
first-hydration-per-target, not per-connection-generation. Since nothing clears the flag, a
disconnected target that hydrated once reads `none`. It does not cover mid-session reconnect or
sleep/resume.

The memo's input list is checked for COMPLETENESS, not just membership. `satisfies readonly
(keyof State)[]` only proves each listed key exists; a field added to the state and forgotten from
the list would type-check while making the memo return a stale verdict -- silent, and it looks like
"the gate did not fire". A conditional type now names the missing key at compile time. Deliberately
not `const x: Missing[] = []`, which passes regardless because an empty array literal is assignable
to every array type.

Known limitation, stated rather than hidden: the SEEDING half of this change has no measurable
end-to-end effect today, and the branch's own e2e spec says so.
`applyDirectSshRemoteWorkspaceSnapshot` calls `markRemoteWorkspaceHydrated` unconditionally AFTER
the hydrate calls -- including when they wrote nothing. So in the same tick adoption yields zero,
the verdict flips `unverifiable` -> `none`, `Terminal.tsx` re-runs the effect, and it seeds. The
gate cannot outlive the failure it guards against, because the same function that fails to adopt is
the one that lifts it.

`ssh-cold-hydration-gap-tab-seeding.spec.ts:218` is named for what it asserts -- one tab, adopted
none -- rather than for the behaviour we want. The fixme at `:293` pins the intended behaviour.

Making the seeding half effective needs hydration resolved PER WORKTREE (or a refusal to say `none`
when the completed apply's `replaceWorkspaceKeys` did not name this worktree) rather than a
per-target "some apply finished" flag. That is deliberately not in this commit.

The RESUME half is the valuable half and is unit-proven: it declines while the host is unanswered
and wakes the same session once the verdict lands, without consuming its one-shot. Preventing one
duplicate `claude --resume` on a live transcript is worth more than preventing one spare tab --
declining to resume is user-recoverable, a duplicate resume corrupts a transcript irreversibly.

Before: 7 failed | 3 passed. After: 10 passed; 103 across the seeding, resume,
authority and remote-workspace suites.
2026-08-27 19:44:30 -07:00
Neil 971d987c4b ci(e2e): trigger the Docker-SSH lane from SSH source and claim every gated spec (#16746)
The Docker-SSH e2e lane only ran when a PR's changed specs happened to include
`ssh-startup-exec-readiness.spec.ts` or `paired-startup-exec-readiness.spec.ts`.
Editing SSH source itself did not trigger it, and pruning either spec from a
route's list would have silently retired the whole lane. Meanwhile the sharded
lanes set no `ORCA_E2E_SSH_DOCKER`, so every Docker-gated spec skipped itself
while the shard still reported green -- the exact silent-skip shape
`docs/reference/ssh-reconnect-source-recovery.md` blames for four regressions
that reached users.

Separately, the modules that actually own direct-SSH workspace and tab restore
carry no "ssh" in their names, so the `ssh-terminal-source` route never reached
them. Measured on the real script before this change:

    printf '%s\n' src/renderer/src/hooks/remote-workspace-session-merge.ts \
      src/main/ipc/remote-workspace-snapshot-normalization.ts \
      src/renderer/src/lib/worktree-initial-terminal-seeding.ts \
      src/shared/remote-workspace-session-projection.ts \
      | node config/scripts/pr-e2e-source-routing.mjs
    => []

Three changes, all pinned by the executable gate contract:

- `hasSshSourceChange` derives an `ssh_source_changed` signal from the SSH
  routes themselves, plumbed pr.yml -> e2e.yml, so the lane triggers on source
  rather than on a spec name surviving in a list. One list, so the two cannot
  drift.
- A sibling `ssh-workspace-session-restore` route names the restore seams
  (`remote-workspace-*`, `worktree-initial-terminal-seeding`,
  `worktree-default-terminal-tabs`, `initial-terminal`) and routes them to the
  two restore specs -- a sibling rather than more paths on `ssh-terminal-source`
  so a tab-tombstone edit does not run the whole SSH terminal list.
- A new `test:e2e:ssh-docker` runner claims the remaining Docker-gated specs on
  the one VM that sets the flag, and the contract now fails by name when any
  Docker-gated spec is claimed by no runner. `ssh-docker-relay-perf` and
  `ssh-codex-display-artifacts-repro` are recorded exemptions (wall-clock
  budgets; needs a real remote codex binary) and the contract asserts each
  exemption still corresponds to a real gated spec, so a stale one cannot
  quietly excuse a gap. Lane timeout raised 35 -> 60 minutes for the added
  serial specs.

The lane's first act was to surface four latent bugs in a spec that had been
silently skipping. `ssh-docker-bulk-open-freeze-repro.spec.ts` is four call sites
out of date against `tests/e2e/helpers/terminal.ts`: `startDockerSshRelayTarget()`
is called with no argument though the helper dereferences `testInfo.workerIndex`
(a 100% failure, not a flake), `execInTerminal` gained a `ptyId` parameter, and
`splitActiveTerminalPane` gained a direction. It was invisible because it ran
nowhere and `typecheck:e2e` is red on main with 240 pre-existing errors, so four
more could not be seen.

The `testInfo` bug is fixed here -- correct on its own, and it removes one real
error from `typecheck:e2e` (240 -> 239). The other three are not, because they
are not argument plumbing: repairing them requires choosing which ptyId to
capture and which split direction to use, and both change what the repro
measures.

The spec is therefore added to the exemption list rather than repaired, for two
independent reasons recorded in the runner: it is a perf oracle, not a
correctness one (`SOFT_FREEZE_LAG_MS=2500` / `HARD_FREEZE_LAG_MS=5000` measured
under a deliberate 5-pane flood on a 420s budget -- the same rule already applied
to `ssh-docker-relay-perf.spec.ts`), and it is known-rotted. Repair is tracked in
stablyai/orca#16764. Applying an existing written rule to a sibling that plainly
meets it is consistency; inventing a new exemption to dodge a red would not be.

Three hardening fixes to the contract itself:

- Runner text is comment-stripped before the claimed-by-a-lane scan. A substring
  scan over raw text lets a spec merely *discussed* in a runner comment count as
  claimed -- the silent skip this assertion exists to catch, re-entering through
  the documentation. Not live today only because the existing comments write the
  spec names without their `tests/e2e/` prefix.
- An exempt spec must not be invoked by any runner. `unreachableSpecs`
  short-circuits the unclaimed check, so a spec could be documented as exempt
  while a runner still ran it -- an exemption that reads as coverage removal but
  changes nothing, leaving the lane red for a reason the file says it excluded.
  This is not hypothetical: adding the bulk-open exemption without removing it
  from the runner's spec list produced exactly that state, and this assertion is
  what caught it.

- The Docker-gate detector is now `/ORCA_E2E_SSH_DOCKER\s*[!=]==\s*['"]1['"]/`
  rather than one fixed string, so a double-quoted or `!==` spelling can no
  longer escape the contract.

`ssh-restart-tab-accumulation.spec.ts` is a new three-cycle restart fence
asserting tab-id set identity, not just the active pane's reclaimed ptyId as
`ssh-cold-activation-restore.spec.ts:241` did. It passes today; it was validated
by a negative control that injected one tab after cycle 1 and correctly failed.
2026-08-27 19:40:38 -07:00
Jinjing 1320a2a953 Support nested toggles as editable blocks with recursion guards
Previously nested details blocks were preserved as inert passthrough HTML.
Now, nested details that themselves meet editability criteria are opened as
editable toggle nodes. Recursive validation includes a 16-level nesting limit
to prevent stack exhaustion on pathological input. Refactors common markdown
editor test helpers into a reusable fixture module.
2026-08-27 16:59:38 -07:00
Jinjing 551fbb9ac7 Revert "feat(browser-preview): render remote HTML docs locally over an orca-preview scheme (STA-5557) (#16679)"
This reverts commit 249d93bc5d.
2026-08-27 16:32:35 -07:00
Neil 6c0de76ced Split port scanning and terminal helpers (#16765) 2026-08-27 16:29:05 -07:00
Brennan Benson 913509edeb fix(orchestration): prevent slow worker-start stalls (#16300)
* Extend orchestration agent submission timing budgets

* fix(orchestration): preserve mutation recovery identity

* fix(orchestration): preserve recovery executable identity

* fix(orchestration): keep worker starts and recovery commands safe

* test(orchestration): cover federated worker preflight

* fix(orchestration): harden mutation recovery

* fix(orchestration): redact dispatch recovery credentials

* chore: preserve upstream skill dialog formatting

* test(orchestration): stabilize agent prompt submit e2e

* fix(orchestration): validate federated start receipts

* perf(runtime): cache unchanged prompt verification tail

* fix(orchestration): reject worker-start timer overflow

* fix(orchestration): normalize worker-start timeout defaults

* fix(orchestration): normalize worker-start readiness budgets

* fix(orchestration): normalize federated readiness timeout

* test(runtime): tolerate current-main degradation exports

* chore: preserve current-main orcad formatting

* chore: drop unrelated formatting carryover
2026-08-27 15:25:30 -07:00
Jinwoo Hong 249d93bc5d feat(browser-preview): render remote HTML docs locally over an orca-preview scheme (STA-5557) (#16679) 2026-08-27 14:34:21 -07:00
Neilandinnocarpe f400f8fd5f fix(macos): opt out of press-and-hold so held keys repeat (#14746) (#15589)
* fix(macos): opt out of press-and-hold so held keys repeat (#14746)

macOS routes press-and-hold to the accent picker unless an app sets
ApplePressAndHoldEnabled=false for its own bundle, so holding j in vim
inserted one character instead of repeating. Orca never set it.

Written at most once, and never over an explicit value: `defaults read`
is domain-scoped and exits 1 when the key is absent, which is the only
way to tell "unset" from a deliberate false — Electron's
systemPreferences.getUserDefault reports false for both. A recorded
decision in userData keeps a later launch from re-clobbering a user who
deletes the key to get the accent picker back.

* docs(macos): record the revert hazard and CI's macOS test gap

Two things a reader of this module cannot otherwise know.

A revert leaves the key written in every user's domain forever. AppKit reads
the plist, not this file, so removing the code alone keeps press-and-hold
disabled for everyone who ran an affected build. The sibling period-substitution
module carries the same warning because that fix was already lost once this way.

And the real-binary test file that pins the defaults(1) exit-code semantics this
design rests on never runs in CI: the e2e workflow and both unit-test jobs are
ubuntu and windows, and the only macOS runners in the repo are build and
packaging jobs that run no tests. Those six tests plus the real-bundle e2e case
pass on a developer Mac and execute zero times in a green PR, so the comment
should not imply enforcement that is not there.

Refs #14746

* feat(macos): let users turn the accent menu back on (#14746)

Orca disables press-and-hold for its own preferences domain so held keys
repeat. That is the right default, but the way back was a `defaults write`
buried in a source comment: nothing in docs/ or the README mentioned it, and
the preference is per-application, so it silently takes the accent picker
away from the Markdown editor and every other text field too.

Terminal -> Advanced now carries a "Character Accent Menu" switch, macOS and
desktop only. A web client cannot write a macOS preference for the machine the
user is looking at, so the control and its search-index entry are both gated on
that, not on the client's platform alone.

Precedence, which is the part that is easy to get wrong: the setting is
`undefined` until the user touches it, which is what keeps a hand-run `defaults
write` in charge for everyone who never opens the toggle. Once used, Orca owns
the key and writes exactly what the switch asks for -- `ApplePressAndHoldEnabled`
*is* the accent-menu switch, so it maps straight through with no inversion. The
choice is compared against `appliedSetting` in the existing decision record
rather than against the domain, so a `defaults write` made *after* using the
toggle is still the newer choice and survives the next launch. Re-asserting the
value every launch would have reintroduced the clobbering the record exists to
prevent.

The write lands for the next launch, since AppKit reads the preference as the
process starts, so the toggle shows the same restart banner the window-blur
setting uses. That banner is now a shared component, keeping its original
translation keys.

docs/reference/macos-press-and-hold.md records the precedence rules, the
`defaults read` rationale, the revert hazard, and the fact that none of this
executes in CI: every macOS job builds or packages and runs no tests, so the
real-binary and e2e coverage here passes only on a developer Mac.

* docs(macos): stop asserting when AppKit re-reads the press-and-hold key

Five places stated "AppKit reads the preference as the process starts" as
fact. That is the reason given for requiring a relaunch, and it is not
something this change ever measured.

Evidence points the other way: terminal emulators that register this key
after their process has started get key repeat in that same launch, which a
read-once-at-startup model cannot explain.

The relaunch requirement itself still looks right, but for a different and
verifiable reason: the write goes out through a separate `defaults` process,
so this app's own cached copy need not observe it. That is what the comments
now say, with the AppKit question left open rather than answered.

Refs #14746

* docs(macos): correct the startup comment's launch-timing claim

The comment said this call site is "the last point that can still matter for
this launch", which contradicts the rest of the module: the write is assumed
to land for the next launch because it goes out through a separate `defaults`
process. Reported on the PR by @innocarpe, who also supplied the replacement
wording.

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

* refactor(macos): probe press-and-hold through the shared spawn chokepoint

`src/shared/child-process/child-process-import-boundary.test.ts` forbids a
direct `node:child_process` import outside its allowlist, and the allowlist only
shrinks — so this module moves to `runProcessSync`, which exists for callers
that genuinely cannot await. This one runs before `app.whenReady()`.

`runProcessSync` returns a non-zero exit instead of throwing it, so the
three-way read decision is re-expressed against `ProcessResult`: exit 0 is an
explicit value, exit 1 is a missing key, and a timeout, a signal kill, any other
exit, or a child that never started all stay 'unknown'. The throw path is now
inside `interpretDefaultsRead` so a spawn failure is reachable from a test
rather than hidden in an untested catch, and the write checks the exit code —
a refused `defaults write` no longer looks like success.

Both boundary-test failures were the same import: with it gone the offender
count returns to 155, so no ratchet baseline is bumped.

* Revert "feat(macos): let users turn the accent menu back on (#14746)"

This reverts commit cc5669f306.

---------

Co-authored-by: innocarpe <innocarpe@users.noreply.github.com>
2026-08-27 13:44:03 -07:00
Jinjing 07b7e9e68d Open target=_blank links and unnamed popups in new Orca tabs (#16720)
* feat(browser): open target=_blank links and unnamed popups in new Orca t

- Treat target=_blank as a new-tab request matching browser behavior
- Route unnamed, featureless window.open() calls to Orca tabs instead of native popups
- Add rate limiting to prevent page-initiated tab loops
- Inherit session profiles when opening links to maintain isolation boundaries

* fix(browser): deny new-tab window.open when renderer is destroyed

Move deny action outside conditional to ensure new-tab intents are
safely rejected even if renderer vanishes mid-open, preventing native
popup fallthrough. Add test coverage and simplify comments.

* Share page-initiated tab budget across opener popup tree

Prevent pages from bypassing the new-tab rate limit by chaining popup
windows. The page-initiated tab quota is now shared by all popups in
an opener tree (root + named children), so child windows inherit their
root's budget instead of each getting a fresh allocation.
2026-08-27 08:49:02 -07:00
Jinwoo Hong 0f522c35e5 fix(remote): gate empty session inventory on host authority (#16546) 2026-08-26 22:30:48 -07:00
Jinjing 9fb5220239 Prevent duplicate file renames when input unmounts after Enter (#16719)
When Enter is pressed to confirm a rename, the input unmounts and its onBlur
handler fires as it detaches from the DOM. Without consuming this event, a
second commitRename call would attempt to rename against the old path. Setting
the cancel flag after capturing the new name causes the trailing onBlur to
return early, preventing the duplicate operation.
2026-08-26 22:17:59 -07:00
Jinwoo Hong d60a3c900b Reset stale terminal modes after dead TUI replay (#16379) 2026-08-26 19:49:26 -07:00
Brennan BensonandSiddiqui Qamar 5a59bc5bc4 fix(grok): stop Orca's Grok hooks from costing anything outside Orca (#16666)
* fix(grok): stop Orca's Grok hooks from costing anything outside Orca

Orca registers Grok agent-status hooks in the global $GROK_HOME/hooks. Grok
loads that directory on every session, so a Grok run that Orca did not launch
still paid for the hook on every event, and Orca rewrote the file even after a
user had emptied it to opt out (#15518).

The registered POSIX command now guards on ORCA_PANE_KEY before doing anything.
That variable is part of the pane identity Orca injects into terminals it
launches, and unlike the port and token it never comes from the endpoint file,
so it is present exactly when the session belongs to Orca. A standalone session
short-circuits without spawning a shell for the managed script at all. The same
guard is applied to the remote install, because a remote host runs standalone
Grok sessions too.

PreToolUse is no longer registered. It is a blocking hook, so Orca sat on the
critical path of every tool call and doubled the per-tool spawns, for a
transition PostToolUse already reports.

Windows cannot use the guard: the command there must be a single spawnable
token, so it is a bare script path with no shell to evaluate a test. For that
case the hooks are removed when Orca quits -- locally, on WSL guests, and on
connected SSH hosts -- and reinstalled on the next launch. A config the user has
emptied is left alone on startup; turning the setting back on in Settings is an
explicit and later choice, so that path reinstalls.

Removal is careful about what it is deleting. It strips only Orca's own entries,
keeps user-authored ones, and deletes the file only when no hook entries remain
-- keying that off the whole object would leave a stray non-hook key behind, and
the emptied-config check would then read that remnant as a deliberate opt-out
and never reinstall. A config the user has symlinked into a dotfiles repo is
written through rather than unlinked, and is exempt from the emptied-config
check for the same reason: after a quit it is a file Orca emptied, not one the
user did.

Writes go through temp+rename. Grok refuses to build a sandbox profile for a
hook JSON with more than one hard link, so publishing by hard link would fail
any session that started during the write.

Install and removal on remote hosts now read the platform from the same field.
They did not, so a Windows remote whose bridge env was incomplete had hooks
installed and never removed.

Co-authored-by: Siddiqui Qamar <137684575+siddqamar@users.noreply.github.com>

* fix(grok): preserve hook state outside Orca

---------

Co-authored-by: Siddiqui Qamar <137684575+siddqamar@users.noreply.github.com>
2026-08-26 12:48:52 -07:00
Jinjing cda2280d63 Show all automations (#16532)
* Add all-host automations with scoped ownership and multi-authority suppo

Enable automations to run on multiple hosts (SSH targets and local) with
owner-fenced mutations, scoped list queries per host, and conflict
resolution. Introduces desktop and runtime authorities as distinct
automation storage owners, with per-host caching, invalidation, and
retry scheduling on the renderer. Captures registration generations for
SSH hosts to survive re-adoption. Adds CLI support for destination
selection and conflict recovery.

* Filter automation create projects by destination host

Only offer projects available on the selected destination, preventing
the mismatches that would fail at submit time. Auto-adjust the project
selection if it becomes unavailable when the destination changes.

* Add runtime storage authority support for automations

- Support both runtime and desktop as automation storage authorities
- Make owner preconditions optional for legacy-client compatibility
- Cache automation list projections to improve performance
- Add per-row repo/worktree resolution for cross-authority collisions
- Extend automation.list RPC to always include owner metadata

* Replace child_process.execFile with runProcess for external automations

- Migrate external-manager to use cross-platform runProcess wrapper per child-process safety policy
- Abstract electron app/ipcMain APIs in orca-runtime via environment accessors
- Install fake app environment in automation tests for consistent setup
- Reorganize imports to use specific module paths (ssh-target-registry, agent-detection, browser-error)
- Remove external-manager from child-process import allowlists (no longer violates direct import)

* Unify desktop automation CRUD onto the local runtime RPC surface

The desktop authority now speaks the same automation.* RPC contract as
remote runtimes, via callRuntimeRpc({kind:'local'}) -> runtime:call ->
the shared RpcDispatcher. The automations:list/listRuns/create/update/
delete/runNow IPC arms, their preload members, and every renderer
desktop-vs-runtime transport fork are retired; the runtime methods are
the single implementation of scoped lists, owner fencing, and change
publication for both transports (mobile clients already exercised them).

The desktop probe scheduler's priority lease survives the move as an
AutomationService hook the IPC registration installs and the runtime
methods take, so Orca's own automation traffic still parks queued
external-manager probes.

External-manager scope arms and dispatch-loop plumbing stay on IPC by
design; automation change events keep their existing channels (renderer
ingestion already converges them by authority).

* Remove automation ghost SSH tombstone scanning

This functionality for synthesizing tombstones for automation-referenced SSH
targets is no longer needed as part of the automation system refactoring.

* Refuse orphan automations at dispatch time, not migration time

Remove migration-time disabling of orphan automations and the `enabledDecidedBy` field. Dispatch now refuses orphans at runtime instead, simplifying state management and UI. Orphans are left unstamped and enabled; dispatch refuses to run them via `resolveAutomationRunTarget`.

* Show all automations in flat table with unified filter menu

- Replace host picker component with comprehensive Filters menu supporting status, last run, agent, and host filters
- Flatten automation list layout to single table instead of host-grouped sections
- Add Host column to display execution host for each automation
- Display active filters as removable pills below toolbar
- Delete unused AutomationHostPicker* components

* Add automation owner fencing and destination validation

- New AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY for owner preconditions; legacy clients get owner metadata snapshotted at RPC boundary for compatibility
- Editor captures and revalidates automation destination before save, preventing silent retargeting if SSH infrastructure changes mid-edit
- SSH target types now isolate renderer-authored fields; generation is server-owned and stripped by IPC handlers

* Route automation recovery actions to the origin host

When an automation action fails due to owner fencing, recovery verbs
("Update server", "Reconnect") must run on the host where the refusal
originated: the row's captured owner for row operations, or the
destination the create dialog captured, not the list's filtered host.

* Remove external manager scope limitation notices

Consolidate create destination eligibility checks with a unified predicate
and fix the bug where desktop repo IDs could be sent to runtime hosts where
they cannot resolve.

* Persist only store-derived automation contexts, not client-perspective o

Store contexts must never be based on client-provided runContext or sourceContext
values—clients speak a different perspective (e.g., 'runtime:<id>' for host IDs
they assign), and persisting those makes the store projection orphan automations
it actually owns. Derived contexts now take precedence in create and update paths,
with explicit null still honored to clear a value. Tests verify this by simulating
drift after storage and confirming that moves re-derive while toggles preserve.
2026-08-26 09:50:12 -07:00
Jinjing 4d2dc0fae5 test: pin cross-version browser placement test to explicit baseline (#16554)
* test: use explicit baseline for cross-version browser placement test

Pin to v1.4.184 to ensure consistent testing against the release
predating client placement. This avoids coupling the legacy-baseline
bump to unrelated schema refactors in newer versions.

* fix(windows): treat inaccessible processes as alive in tests

When checking process state on Windows, EPERM (permission denied) indicates
an inaccessible but live process. Only ESRCH (process not found) proves
exit. Correct isAlive() to distinguish these cases.

Also add windowsHide:true to child process spawns and use explicit SIGKILL
when force-killing the host process.
2026-08-26 00:02:12 -07:00
Jinwoo Hong 868fc39d32 fix(worktrees): refresh paired clients after external discovery (#16557) 2026-08-25 23:18:30 -07:00
Jinjing 933345d347 Clarify upstream divergence stats for rebased branches (#16358)
* Clarify upstream divergence stats for rebased branches

When a branch is rebased, it still tracks the pre-rebase upstream
while comparing against the new base. Move upstream arrows to the
head line to prevent them being confused with compare-base counts.

* Show upstream divergence stats independent of compare base

Measure HEAD against upstream regardless of compare-base state,
so divergence indicators stay visible even when comparison is
missing, loading, or failed. Also use cross-platform temp paths
in tests.

* Show commit counts against compare base, not upstream

Upstream divergence (↑/↓ against tracking branch) was confusing for
rebased branches — the counts appeared beside the base ref but measured
against the upstream branch. Show only the compare base count instead,
on the line that names it.

* Report branch divergence in both directions

Rebased branches are typically ahead AND behind their base; a single count
hides this case. Use symmetric range with --left-right --count to capture
both directions efficiently, then expose commitsBehind in the UI alongside
commitsAhead.

* Use semantic names for i18n keys and template variables

Rename hash-based translation keys to descriptive identifiers and replace generic value0/value1 placeholders with semantic variable names like `count` and `ref`. Improves code maintainability and makes translation strings self-documenting.
2026-08-25 22:19:04 -07:00
Jinjing 07b82340f3 Route terminal file links to sibling workspace tabs (#16544)
* fix: route terminal file links to sibling workspace tabs

Detect when a clicked file is already open in a sibling workspace and route
to that existing tab instead of creating a duplicate. Reorganizes workspace
activation to dispatch by both worktree id and execution host, allowing the
same worktree name across different remotes to be disambiguated and routed
correctly.

* test: validate terminal file link opens in correct sibling worktree

Enhance test to check both file path and active worktree ID, ensuring
the linked file opens in the intended sibling workspace.
2026-08-25 22:17:59 -07:00
Jinwoo Hong c8567eb16e fix(sidebar): preserve hidden rows in manual order (#16488) 2026-08-25 16:46:42 -07:00
Jinwoo HongandJinwoo-H a9781a4118 STA-4150: client-hosted remote browser (consolidated) (#15448)
Co-authored-by: Jinwoo-H <jinwoo@stably.ai>
2026-08-25 15:36:51 -07:00
Jinjing 32df073e44 fix(browser): focus unified tab on browser page palette activation (#16366)
* fix(browser): focus unified tab on browser page palette activation

When activating a browser page from the palette, find and focus the
corresponding unified tab before setting active state. Ensures the
tab group receives focus. Also increase e2e test timeouts to improve
stability on slower runners.

* test(e2e): read latest restored terminal frame

* Fail browser page activation when unified tab is missing

Without a unified tab, the workspace can't render in the pane. Reporting
success leaves the previous tab on screen. Fail the activation to prevent
this confusing state.
2026-08-25 04:00:59 -07:00
NeilandSeongho.Bak fcf55f2d68 fix(terminal): stop Orca mangling the OMP/Pi title it writes itself (#16381)
* fix(terminal): collapse identity group in the title churn signature

Replaces the ingest-time title rewrite from #16373 with a non-destructive
fix at the actual cause.

The churn suppressor `isDecorativeAgentTitleFrameChange` keyed on the
literal label, so `working:OMP` and `working:Pi` compared unequal and every
alternating frame from a wrapped harness committed a store patch. #16373
made the labels agree by rewriting the stored title to the tab's launch
owner — but `runtimePaneTitlesByTabId` is also the Windows Shift+Enter
byte-encoding input, so normalizing at ingest destroyed evidence other
consumers read (fixed separately in #16376).

Collapse the identity group inside the signature instead. Which member of
a group a frame names is decoration, exactly like the spinner glyph the
signature already strips, so frames compare equal without touching what is
stored. Suppression now changes only WHETHER a frame commits, never WHAT
it says.

Also fixes the flap under a multiplexer (#8032): the collapse runs over
wrapper segments, so "zsh | ⠋ Pi" and "zsh | ⠙ OMP" compare equal, which
the anchored owner-relabel in #16373 never matched.

Reverts the store changes from #16373 and drops the helper it added.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* fix(terminal): fold only bare identity frames into the group token

A legacy "π - <session> - <cwd>" title is Pi-compatible too, so folding
every profile match collapsed two different sessions to the same signature
and suppressed the change outright — reintroducing #16093 through the
churn signature.

Fold only exact bare identity frames, matched per wrapper segment, so
semantic session titles keep comparing on their own text.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* docs(terminal): correct the flap diagnosis in the repro header

Verified against the OMP source: it emits only π-glyph frames
(`DEFAULT_TERMINAL_TITLE = "π"`, title-generator.ts:25), and on an
Orca-hosted pane its native titler cedes to Orca's own injected extension,
which writes `⠋ π - <session> - <cwd>`.

So OMP emits neither "OMP" nor "Pi". Both flap sides are Orca's:
"OMP" from driveSyntheticTitleFromHook, "Pi" from normalizeTerminalTitle
collapsing our own extension's output to a hardcoded literal.

The prior header credited the wrapped harness for frames it never sends,
which is the same wrong narrative that produced eight fixes at eight
layers. No behavior change.

* fix(terminal): stop Orca mangling the OMP/Pi title it writes itself

Verified against the OMP source: it emits only π-branded frames
(`DEFAULT_TERMINAL_TITLE = "π"`, title-generator.ts:25), and on an
Orca-hosted pane its native titler cedes to Orca's OWN injected extension,
which writes `π - <session> - <cwd>` / `⠋ π - <session> - <cwd>` at 80ms.

So neither flapping string came from OMP. Orca made both:
  "Pi"  — normalizeTerminalTitle collapsing our extension's output to a
          hardcoded literal, discarding the session name and cwd (#16093)
  "OMP" — driveSyntheticTitleFromHook injecting over it every 80ms

Fixed at the source:
- normalizeTerminalTitle canonicalizes only the rotating braille frame and
  keeps the rest, in both spinner positions and through a multiplexer
  prefix (#8032). Status still round-trips through normalization.
- detectAgentStatusFromTitle reads the π state separator, so `π ! <label>`
  is permission instead of the blanket idle that hid a blocked agent.
- normalizeCompatibleAgentTitleForOwner swaps only the brand for the
  owner's label, so a pane still reads as its launch owner (#6689, #7633,
  #9077) without losing the session text.
- pi/omp set synthesizeWorkingTitle: false — the agent animates its own
  working title. Terminal states still synthesize; they carry the pane's
  agent identity downstream.

Reverts the ingest-time title rewrite from #16373, whose normalization of
runtimePaneTitlesByTabId also changed Windows Shift+Enter bytes (#16376).

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* fix(terminal): match the state separator only in exact profile casing

The separator check runs on every title, so `omp - deploy notes` and
`pi - refactor the parser` read as an idle agent. The owner rewrite only
ever emits the exact profile labels, so dropping case-insensitivity keeps
`OMP - tmp` classifying while ordinary prose stops matching.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* test(terminal): pin one real OMP turn to two committed patches

Drives 30 working frames as Orca's injected extension emits them plus the
idle transition, and asserts what survives the churn gate. Before the fix
every frame alternated "⠋ Pi"/"⠋ OMP" and each one committed — ~12 store
patches per second on a working tab.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* fix(terminal): carry the permission guard inside the separator reader

`-` is both a π state separator and the delimiter in the synthetic
permission label, so `OMP - action required` read as idle. It resolved
correctly only because detectAgentStatusFromTitle happens to check the
synthetic label first — and the separator fn is exported, so a direct
caller inherited the bug.

Also pins the owner rewrite's fixed-point property, which holds only
because getAgentLabel does not tokenize omp/pi, and corrects a comment
that overstated how tightly the brand swap is scoped.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* docs(terminal): name the flag the code actually sets

The suite header cited `synthesizeTerminalTitle: false`; the profiles set
`synthesizeWorkingTitle: false`. The distinction is the whole reason the
narrower flag was chosen — terminal-state frames still carry the pane's
agent identity downstream — so the wrong name buried the rationale.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

---------

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
2026-08-25 02:28:08 -07:00
Wooseong Kimandm4air 60a3fd8873 fix(i18n): localize the keep-awake corner chip (#14775)
* fix(i18n): localize the keep-awake corner chip

Route the status-bar keep-awake chip through the shared Agents copy
helpers and add missing locale entries for chip-only words.

Fixes #14490

* test(i18n): restore previous language after keep-awake locale suite

* test(i18n): render component in localization tests instead of static che

Converts the keep-awake localization test from static source-code validation to actual component rendering with React Testing Library, providing more reliable verification that the UI displays correctly across all supported languages. Improves translated descriptions for consistency and accuracy.

* test(i18n): add aria labels and descriptions to localization test

- Adds missing localization keys to test data for Spanish, Japanese, Korean, and Simplified Chinese
- Updates test assertions to verify `ariaLabel`, `onDescription`, `autoDescription`, and `offDescription` are properly translated
- Completes localization coverage for the keep-awake corner chip component

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-24 22:46:29 -07:00
Jinwoo Hong c60d2ba895 fix(agent-resume): stop ghost resume tabs after finished turns (#16308) 2026-08-24 19:32:52 -07:00
Neil c83499fc8c Keep sidebar position when deleting active worktree (#16040) 2026-08-24 18:14:12 -07:00
JinjingandBrennan Benson 7b9529da22 Add keyboard shortcut for workspace deletion (#16271)
* Add keyboard shortcut for workspace deletion

Default Mod+Shift+Backspace (⌘⇧⌫ on Mac) lets users delete the hovered
worktree or folder workspace immediately. The shortcut targets the
sidebar hover state rather than requiring focus, and avoids terminal
pane D-based split shortcuts on all platforms.

Co-authored-by: Brennan Benson <brennankbenson@gmail.com>

* Omit delete shortcut from disabled Delete Worktree for primary checkout

- Remove shortcut badge from the disabled "Delete Worktree" action when it cannot be executed
- Only show shortcut in multi-context delete actions where the command is available
- Extract host identity parsing into reusable helper function to prevent inline string manipulation
- Fix folder workspace deletion to use correct host-qualified identity comparison

* Document host extraction safety for destructive worktree ops

Unqualified identities must stay undefined rather than defaulting to
'local'. Destructive operations depend on correct host identification.
Added tests and JSDoc to clarify this safety-critical behavior.

* fix test

---------

Co-authored-by: Brennan Benson <brennankbenson@gmail.com>
2026-08-24 10:12:38 -07:00
Jinwoo Hong c618ec7393 test(reliability): protect recent P0 regression invariants (#16163) 2026-08-24 09:38:46 -07:00
Jinjing f5fd7303ab test(e2e): cover tab-bar agent launches on Windows and WSL (#16110)
* test(e2e): gate the tab-bar agent launcher on Windows shells and WSL

The `+` menu agent launcher had no golden coverage in the Windows lane, so a
Windows-only break anywhere in its chain (detection row, startup-plan build,
tab create, PTY spawn, startup-command injection) could ship unnoticed.

Adds a golden spec that launches a stub agent from the menu and asserts the
agent's own banner reached the pane — a tab that spawned a bare shell instead
is indistinguishable at the store/tab layer. Runs two agents everywhere, and
on Windows also PowerShell, cmd, Git Bash and a WSL project runtime.

* test(e2e): track WSL stub agent staging state for precise cleanup

Refactor `stageWslGoldenStubAgent` to track which artifacts it creates
during setup, then only remove those artifacts during cleanup. This
prevents the test from destructively removing pre-existing symlinks or
state from previous runs, improving test isolation and idempotency.

* test(e2e): track WSL stub agent staging state for precise cleanup

- Back up and restore pre-existing stub agents to avoid destroying them
- Simplify verbose test comments to match project style guidelines

* test(e2e): serialize WSL stub agent setup with distributed lock

- Add mkdir-based lock to prevent concurrent staging invocations
- Reclaim stale locks after 10 minutes to recover from crashes
- Track lock ownership in stage state for safe cleanup

* test(e2e): track WSL stub agent staging state for precise cleanup

Track which stubs this test helper stages by writing a marker file, then
only remove stubs during stale-lock recovery if we created them. Prevents
cleanup from removing stubs left by other processes.
2026-08-24 08:58:19 -07:00
Neil afd76a4df9 fix(terminal): preserve synchronized frames on reveal (#16026) 2026-08-24 00:01:53 -07:00
Neil 95633a7883 Fix stale task-source flashes in new workspace input (#16145)
* fix(new-workspace): prevent stale GitHub URL selection

* fix(new-workspace): guard all task URL transitions

* test(e2e): make task URL frame proof runner-safe

* fix(new-workspace): guard Enter during task URL lookup
2026-08-23 22:18:39 -07:00
Jinwoo Hong 4ee41fede2 fix(automations): reveal full prompt from detail view (#16067) 2026-08-23 19:59:30 -07:00
Jinwoo Hong 0a613d5fed test(e2e): stabilize paired Quick Open large-tree coverage (#16058) 2026-08-23 16:47:12 -07:00
Jinwoo Hong 3d74f5fe03 test(remote): preserve HTML inventory RPC failures (STA-5210) (#16056) 2026-08-23 16:09:49 -07:00
Neil 8af02d658c Preserve code editor selections across tab switches (#16132)
* Preserve editor selections across tab switches

* Defer editor selection caching to tab lifecycle
2026-08-23 14:25:03 -07:00