Commit Graph
541 Commits
Author SHA1 Message Date
Neil 2d8f09d137 Merge remote-tracking branch 'origin/main' into nwparker/sta-3077-reattach-pane-cardinality 2026-08-12 20:24:09 -07:00
Neil 0ba2bed7ee fix(ssh): harden PTY reattach reliability 2026-08-12 20:23:51 -07:00
Jinwoo Hong e8350f1990 fix(browser): route remote terminal links to owning host (#14117) 2026-08-12 16:15:39 -07:00
Jinwoo Hong 9a81577e91 fix(browser): disable unsupported FedCM (#14023) 2026-08-12 16:07:08 -07:00
Jinwoo Hong ebe5125476 Gate paired web creation actions by provider (#13909) 2026-08-12 13:42:42 -07:00
Neil 5dc1e01857 Merge remote-tracking branch 'origin/main' into nwparker/sta-3077-reattach-pane-cardinality 2026-08-12 03:48:31 -07:00
Neil e03728c2d0 Merge origin/main into STA-3077 (9 commits)
Two conflicts:
- .gitignore — both sides added doc negations; kept both.
- orchestration/db.ts — this branch moved `isEquivalentPaneKey` into
  shared/stable-pane-id (collapsing a duplicate), while main added
  `parseWorkerTerminalPriorOwnerIds` beside the old local copy. Kept the move
  and kept main's new helper, rather than resurrecting the duplicate.

43,250 pass. The 7 failures are the same environment-specific pre-existing ones
(osc8 snapshot round-trip, IME resumed-preedit) that fail on clean origin/main
in this environment.
2026-08-12 02:03:38 -07:00
Jinwoo Hong 70ad65fc94 Fix paired remote HTML browser ownership and focus (#13876)
* Fix paired remote HTML browser preview

* Register paired HTML preview reliability gate

* Scope browser worktree selection guard

* Harden paired HTML preview ordering

* Simplify paired HTML preview ownership

* Record final paired HTML preview evidence

* fix(remote): simplify browser preview ownership

* test(remote): refresh HTML preview evidence

* fix(remote): reconcile browser create ordering

* fix(remote): guard browser focus through reconcile

* fix(remote): bound delayed browser focus

* fix(remote): guard browser focus context

* test(remote): record final HTML preview evidence

* fix(remote): keep HTML previews in background

* test(remote): cover browser focus modes

* test(remote): record focused browser contracts
2026-08-12 02:02:10 -07:00
Jinwoo Hong 346e59c879 fix(renderer): keep commit tooltip lines intact (#14000) 2026-08-12 01:10:47 -07:00
Jinwoo Hong fd2afc16c8 Fix paired web Add Project folder browsing (#13885) 2026-08-11 23:25:50 -07:00
Neil 26af3d8234 Merge origin/main into STA-3077 (287 commits)
Three conflicts, all resolved keeping both sides:

- persistence.ts — both sides added a field to persistPtyBinding's options.
  Kept `mayCreate` (ours) and `expectedSourceBinding` (main's).
- ipc/pty.ts — main added an `expectedSourceBinding` CAS and a
  `terminal_split_source_not_found` throw to the exact block our `bindPaneShell`
  producer replaced. Threaded `expectedSourceBinding` THROUGH the producer rather
  than dropping it, and kept main's refusal throw on `bound === false`. Dropping
  it would have silently disarmed a CAS main just added.
- pty-connection.test.ts — main added `startupCommandDelivery: 'shell-ready'` to
  a resume assertion this branch deletes outright (the resume no longer happens).
  Took ours; main's addition is moot in an inverted clause.

Also repairs a rename/rename divergence unrelated to STA-3077: both sides deleted
a different half of a duplicated xterm-bypass fixture, so the merge deleted both.
Restored main's `xterm-bypass-event-fixture.ts` and repointed three test imports
at it, adopting main's convention rather than this branch's older move.

Verified after merge: 43,179 tests pass. The 7 failures (osc8 snapshot
round-trip, IME resumed-preedit) are PRE-EXISTING on clean origin/main — checked
against a baseline worktree, not assumed. Typecheck clean on node and web.
2026-08-11 23:11:00 -07:00
Jinwoo Hong 7319d59a10 Make worker completion and cleanup authoritative (#13927)
* fix: require authoritative worker completion verdicts

* Harden federated settlement replay

* fix(orchestration): reconcile dead retained workers

* docs: record SSH worker release coverage

* test(e2e): exercise worker settlement and release CLI

* docs: register combined orchestration CLI oracle

* test(orchestration): pin pre-ack attachment state
2026-08-11 23:06:12 -07:00
NeilandOrca 5ea7df1a5b fix(terminal): make DECSET 2031 subscriptions silent (#13904)
fish arms `CSI ?2031h` before painting each prompt and withdraws it when it
hands the tty to a child — a ~1ms window. Orca answered that subscribe with
`CSI ?997;Nn` across a 1-3ms renderer hop, so the reply landed after the
withdrawal and was read as stdin by the next child, corrupting `brew`/`npx`
`[y/N]` prompts.

The reply is not stale by Orca's own view when written (measured
staleReplies: 0), so no suppress-the-stale-reply scheme can close this — the
information needed to suppress does not exist yet. Nothing asked for the reply
either. The Contour spec says a terminal "should only send out the DSR when the
palette has been updated"; Ghostty (Termio.zig:729 — force=true reachable only
from the ?996n DSR), iTerm2 (VT100Terminal.m:995 — flag only) and xterm.js
(InputHandler.ts:2035 — flag only) all emit nothing on the DECSET. So stop
entering the race: record the subscription, answer nothing.

Of 17 real programs measured under a pty, only fish, tmux, claude and opencode
subscribe; none block on a reply, and answering produces one redundant palette
re-query and zero rendering difference. tmux is the only one that sends `?996n`,
which Orca still answers.

- Subscribes are record-only at all four emitters (live scan, hidden-gate fact,
  parked byte watcher, parked responder — the last is deleted, it only replied).
- `?996n` answers, the subscription registry, and the theme-flip push are
  unchanged. `paneLastThemeMode` is still seeded at subscribe so the next
  appearance re-apply is not read as a flip.
- Replay grammar carries `?2031l` alongside `?2031h`, so a late-attaching remote
  client no longer registers a subscription the TUI already retired.

Also closes fish-integration gaps found alongside: `unset` (which fish lacks)
becomes `set -e` on paths parsed by the client's login shell, `config.fish` is
parsed for agent-home detection, and bracketed-paste startup delivery is made
consistent across local/daemon/relay.

Regression test drives real fish 4.7.1 under node-pty and asserts on what the
child process reads; it fails against pre-fix code with the exact payload from
the issue. CI installs fish 4 and fails loudly rather than skipping.

Closes #9993

Co-authored-by: Orca <help@stably.ai>
2026-08-11 21:16:36 -07:00
Neil 991a3fe963 chore(lint): update oxlint to 1.77 and enable no-op cleanup rules (#13901)
Enable eleven oxlint rules that simplify code without changing behavior, and fix
every existing violation. Each candidate was gated on measured cost rather than
assumption, so rules that regressed runtime performance or type checking were
dropped instead of suppressed.

typescript/no-redundant-type-constituents is the largest addition: 113 sites, no
autofix. Dead constituents are deleted. Where the redundant literal existed to
document intent (`string | 'all'`), it is preserved as `(string & {})`, which
keeps the autocomplete hint the original code was reaching for instead of
flattening it away. The rule also caught a broken import —
remote-shared-control-retirement-probe.ts pulled RuntimeStatus from
src/shared/types, which does not export it, so the type silently degraded to
`any`; no tsconfig covers that file, so tsc never saw it.

oxlint stays at 1.77.0 rather than 1.78.0 because .npmrc sets
minimum-release-age=4320 and 1.78.0 is younger than that window.

Rules evaluated and rejected, with what disqualified each:
- prefer-string-raw: String.raw is a runtime call, not a literal (184x slower)
- prefer-string-replace-all: 26% slower
- text-encoding-identifier-case: ~5% slower, reproducible
- prefer-spread: [...str] is 110% slower than split('') and differs on surrogates
- no-implicit-coercion: `!!x` narrows types and `Boolean(x)` does not (22 tsc errors)
- prefer-arrow-callback: arrows are not constructible, breaking `new` on mocks
- object-shorthand: rewrites source text asserted by a tracked reliability gate
- switch-case-braces: pushes ten files past max-lines, which cannot be suppressed
- no-useless-switch-case: drops `case undefined:` that switch-exhaustiveness-check needs
- arrow-body-style: 115 violations have no fix, and it breaks max-lines
- newline-after-import: false-positives on the leading-semicolon ASI idiom

electron-vite-output-contract asserted on the literal
Object.prototype.hasOwnProperty.call text; retarget it to Object.hasOwn, which
rejects inherited keys identically.
2026-08-11 18:19:43 -07:00
Brennan Benson f19ff5be68 fix(browser): keep one identity across hosts during Google sign-in (STA-3811) (#13667)
While the auth document is on screen the WebContents UA is Firefox, so its
cross-host subresource/XHR requests (gstatic, play.google.com, the sign-in
challenge endpoints) reached the header layer carrying the Firefox UA yet still
bearing Chromium client hints, which the else-branch rewrote to Chrome. That
paired a Firefox UA with Chrome client hints on every non-auth Google host — a
sharper cross-host identity tell than either signal alone, and a plausible
cause of the password-submit challenge greying out and stalling.

Strip client hints on any request already carrying the Firefox auth UA so the
UA and hint surfaces tell one Firefox story for the whole flow. Gated on the
same googleAuthOverride flag as the auth-host switch, so imported-native
profiles are unaffected and the clean-Chrome default for non-Google sites
(Cloudflare) is untouched.

Extends tests/tools/google-signin-ua-probe.cjs with app-current/app-fixed
modes that mirror the shipped code and log per-request identity; on the real
accounts.google.com load they show 18 firefox-ua-with-chrome-hints cross-host
mismatches before and 0 after.
2026-08-11 16:37:53 -07:00
Jinwoo Hong 6acd69c516 fix(terminal): prevent link action clicks reaching child (#13797) 2026-08-11 12:27:54 -07:00
Neil 1f2e18acc1 fix(e2e): repair seven specs whose assertions drifted from shipped behavior (#13785)
* fix(e2e): repair seven specs whose assertions drifted from shipped behavior

The E2E suite could not collect at all until #13758, so these seven had been
failing unobserved. Each is a stale test, not a product defect — verified
individually against src/ rather than by making the assertion pass.

- worktree-jump-palette-filter: the palette placeholder gained chats and
  terminals. Hoisted to one SEARCH_PLACEHOLDER const.
- tab-create-entry-file-paths: matched the omnibox by its translated
  aria-label. Switched to aria-controls, which is structural and unlocalized.
- browser-local-https-certificate-trust: once a load settles, the toolbar
  reload button relabels itself "Retry" alongside the failure overlay's own
  Retry, so the slot-wide locator hit two elements and failed strict mode. The
  test only ever passed inside the window where the toolbar still read "Stop".
  Narrowed by visible text; the icon button has none.
- repro-7732-gitlab-checks-job-details: the activity-bar label carries a
  failure suffix ("Checks — Error"), which an exact-name match stops seeing
  precisely when the checks under test fail. Anchored regex, and bounded the
  click so the poll retries instead of hanging on a label that flips mid-action.
- floating-tab-rename: the panel's open flag is persisted, so after the restart
  the helper's blind toggle closed the panel it was about to assert on. Made it
  idempotent.
- github-created-issue-start-prefill: starting an issue now routes through the
  quick-create composer, so the launch command only forms once that is
  submitted. The spec now drives it.
- ssh-config-host-import: P6 waited on "All hosts already in Orca", a string
  that exists nowhere in src/ and never could have matched. P7 required the
  tombstoned host to be absent, but listing it behind a "Removed from Orca"
  badge is deliberate — see the rationale at ssh-config-host-picker.ts:54 and
  the unit test already pinning it. Both retargeted; P7 still proves the host
  is excluded from bulk re-adoption, which is what it was for.

Verified locally: all seven pass. github-created-issue-start-prefill cannot be
verified on a machine whose gh resolves to an Orca terminal-attribution shim,
since hydrateShellPath puts that ahead of the fixture's fake gh; CI has no shim.

Three further failures are NOT addressed here. Adversarial review found the
obvious test-side fix for each would have masked a real product bug, so they
are being fixed in the product instead.

* fix(e2e): configure the issue spec's git remote before Electron launches

The spec added origin inside the test body, after the orcaPage fixture had
already launched the app and added the repo. "New GitHub issue" is disabled
when no repo is task-eligible, and eligibility is decided by the git remote
probe alone: repos.add runs detectRepoIconAndUpstream, and a settled "no
remote" writes gitRemoteIdentity = null, which is the ineligible marker.
Background enrichment then suppresses re-probing that location for five
minutes, so a remote added afterwards can never recover the button.

It passed only when the shared seeded repo happened to already carry an origin
from an earlier spec in the same worker — github-cli-stall-repro adds one,
source-control-create-pr-intent-switch removes one — which is why it passed in
the sharded lane and failed in the changed-specs lane.

Moved to test.beforeAll, whose worker-scoped testRepoPath runs before the
test-scoped Electron fixtures. Reproduced the failure against a fresh
origin-less repo, then confirmed the fix on the same, including --repeat-each=2.

Note the earlier attribution was wrong: the local failure at this line was
never the gh attribution shim. The button's enabled state reads `git remote -v`
and never consults gh, so a shim can only bite later, at issue creation.
2026-08-11 03:07:44 -07:00
Neil c578f4e4e2 fix(e2e): restore the helpers the macOS key-remap spec imports (#13758)
#13314 landed terminal-macos-system-key-remap.spec.ts but its final commit,
"chore: drop non-mergeable IME e2e scratch files", deleted the three modules it
imports. The spec survived; terminal-ime-pane-arena, terminal-ime-cdp-composition
and terminal-ime-platform-policy did not.

Playwright resolves every spec before running any of them, so the unresolved
import is fatal for the whole run, not just that file: `playwright test --list`
on main reports "Total: 0 tests in 0 files". All ten scheduled E2E shards have
been failing at collection since, and the macOS system-key-remap coverage the PR
was for has never run once.

No typecheck project includes tests/, which is why this stayed invisible outside
the E2E lane.

Restored from the commit before the drop. pane-arena and platform-policy come
back verbatim; cdp-composition keeps only the four entry points this spec uses,
since the rest served specs that were not merged. Its doc comment is retargeted
accordingly — the composition-session drivers are the part that went.

Collection is back to 528 tests in 222 files and all six remap tests pass.
2026-08-11 00:10:40 -07:00
Neil e33863b2a9 refactor(onboarding): remove retired repository step (#13484) 2026-08-10 20:42:43 -07:00
Neil ee8b9b2cc9 refactor(profiles): remove retired switcher UI (#13502) 2026-08-10 20:25:27 -07:00
Jinwoo Hong c7850eb59c fix(terminals): prevent retired workers from cold-resuming (#13708) 2026-08-10 19:49:34 -07:00
Neil 090678a32b [Perf-SS] Keep reattach live queues linear during replay (#13478) 2026-08-10 17:33:46 -07:00
Brennan Benson a70291a918 perf(orchestration): index mutation receipt capacity (#13647) 2026-08-10 16:17:52 -07:00
bokjkandBrennan Benson 1b02cedfff fix(tabs): keep widths stable during title updates (#11476)
* fix(tabs): keep widths stable during title updates

* review(tabs): reduce the width rule to the definite width and harden its tests

Why: once the tab container has a definite width the shrink-wrapped strip never
has free space, so flex-grow and max-w-[280px] were unreachable and only
duplicated the 180/220 numbers across two properties. Measured widths, strip
scrollWidth and clientWidth are identical to the flex-based version across four
window widths, five tab counts and short/long titles.

Tests: pin the width classes literally so the guarantee cannot be edited away
via the constant, and guard the e2e check against passing vacuously on a
saturated strip.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-10 16:11:19 -07:00
Brennan Benson 6b9852af2b perf(terminal): remove output-driven WebGL atlas resets (#13657)
* perf(terminal): stop clearing glyph atlas on output

* test(terminal): gate output atlas reset removal

* test(terminal): harden atlas recovery gates
2026-08-10 16:10:47 -07:00
Brennan Benson d50adec2d2 feat(ai-vault): isolate scanning from terminal workloads (#13411)
* feat(ai-vault): isolate scanning in service processes

* fix(ai-vault): retire idle service processes

* fix(ai-vault): discard unverified cache processes

* fix(ai-vault): clear relay sidecar cancel watchdog on acknowledgement

A cancelled relay call is settled before its 2s cancel watchdog is armed, so the acknowledgement path bailed out of settle() before clearing the timer. The watchdog then faulted a healthy sidecar two seconds after every aborted scan, killing whatever request had since become active.

* fix(ai-vault): clear the pending restart before scheduling another

recordFault overwrote this.timer, stranding a restart that dispose() could no longer cancel.

* refactor(ai-vault): drop the orphaned first-prompt IPC wrapper

session-first-user-prompt-handler.ts now owns this entry point and routes through the service; the copy left in the read module had no callers.

* fix(ai-vault): retry a faulted cold start before surfacing it

A slow first start surfaced a raw 'did not become ready' error to the caller even though the supervisor was already respawning. Requeue an unsent call once onto the scheduled respawn instead.

Also stop arming the cancellation watchdog for a call the child never received: no acknowledgement is coming, so it killed a healthy service and stalled the lane.

Invalidation bookkeeping and ready-waiter construction move to the state module to stay under the max-lines cap.

* fix(ai-vault): give relay title reads their own lane

Before this branch the relay read title files directly, concurrently with scans. Routing both through one sidecar lane put title resolution behind a list scan that may run up to 130s, so SSH tab titles could lag minutes behind.

Split cache and interactive lanes in both the relay client and the sidecar entry, mirroring the desktop service.

Also: clear the ready deadline on fault, so a sidecar that dies before ready cannot fault its healthy replacement five seconds later; retry an unsent call once across a respawn; and skip the cancellation watchdog for a call the sidecar never received.

Restart/circuit bookkeeping moves to its own module, mirroring the desktop policy, to stay under the max-lines cap.

* fix(ai-vault): degrade relay title resolution on sidecar failure

listSessions already returns a host issue when the sidecar is unavailable; titles propagated the raw RPC error instead. Return no titles so callers fall back to preview text, and keep cancellation propagating.

* fix(ai-vault): scrub the service child environment

The children are forked with a 384 MiB heap cap and no loader, but both
spawn sites handed them the full parent environment, so an exported
NODE_OPTIONS silently raised the cap or --require'd code into them.

Allowlist both, following the plugin worker. The desktop child keeps the
eleven agent-root overrides it resolves its own roots from; the relay
sidecar takes remoteHome and hostPlatform from its init message and so
needs none of them. Both children share one priority module while they
share this one.

* fix(ai-vault): soft-disable relay vault when the service is missing

A missing service threw out of the constructor, so a Vault wiring bug
would abort relay startup and take every PTY on the host with it. The
unsupported-platform branch three lines above already treats a Vault
failure as a soft disable; do the same here.

Threading the service through the two handlers instead of a field also
retires the definite-assignment assertion the throw was propping up.

* fix(ai-vault): drain consumed cache invalidations

invalidatedPaths was re-applied in every request's finally and never
drained, so once N paths had been invalidated every later request paid N
evictions for the life of the process; the 4096 cap only bounded how bad
that got.

The re-apply exists to cover a read that overlapped the invalidation, so
drain once nothing is executing. Clearing unconditionally would drop the
re-apply for a request still running on the other lane.

* fix(ai-vault): keep a busy child through slow invalidation acks

invalidate() reused the 5s ready budget as its acknowledgement deadline
and killed the child on expiry, so a delete issued during a large scan
could kill a healthy process mid-scan and burn a slot toward the restart
circuit.

Fault only when nothing is executing. Fork IPC ordering already puts the
invalidation ahead of any later request, so a busy child owes no ack
here, and the 130s/15s request deadlines still catch a wedged one.

The start-retry predicate moves to the state module to stay under the
line cap, matching the shape the relay client already uses.

* fix(ai-vault): report a failed local scan as a host issue

A local-scope scan let its error escape to the renderer, which paints it
over the session list. Service supervision now produces those errors, so
"AI Vault service restart circuit is open." replaced the list.

Route local scope through the degradation the all-hosts leg and every SSH
leg already use, so it lands as a retryable host issue row instead. Same
result shape either way, so no IPC or wire contract changes.

* test(ai-vault): cover the relay restart circuit transitions

The relay policy shipped without tests. Pin both circuit edges, the
aging-out case, the forced-refresh reopen the relay has and the desktop
does not, and the backoff schedule.

* fix(ai-vault): keep the OpenCode roots in the service child env

The scrubbed allowlist dropped XDG_DATA_HOME and OPENCODE_DB, which the child
reads to locate the OpenCode store and database. The pre-PR worker thread
inherited them, so a user who sets either lost every OpenCode session.

* test(ai-vault): anchor the service spawn env assertion
2026-08-10 15:52:59 -07:00
Jinwoo HongandNeil ec7e3ea477 fix(terminal): prevent paired activity renderer starvation (#13508)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-08-10 14:32:08 -07:00
Jinjing c0c893d171 fix(pty): bound cooked reply queue (#13422)
* fix(pty): bound cooked reply queue

* fix(pty): bound cooked reply queue

Implement bounded storage for cooked-echo-safe replies: 64 pending
replies and 4096 UTF-16 code units. Shed oldest replies on overflow,
never ordinary input. Add drain failure containment with generation
fencing to prevent stale operations from clearing fresh input after
clear() reuse.

* fix(pty): report pty id in drain failures

When the async drain yields, the owner may rebind to a different PTY
before the failure surfaces. Pass the failing pty id so the transport can
ignore stale failures from a rebound owner. Also tighten the pending
reply queue size bound to prevent half-written entries.
2026-08-10 12:17:21 -07:00
Neil 016df33f00 fix(github): complete PR reactions for CodeRabbit reviews (#13456)
* feat(github): add PR comment reaction controls

* feat(github): add full PR comment reaction picker

* fix(github): cover all reactable PR comment paths

* fix(github): reconcile comment reactions with main

* fix(github): preserve focus on failed reaction removal
2026-08-09 23:14:17 -07:00
Neil 7dce0442ab Add Caffeinate controls to the status bar (#13480)
* feat: add caffeinate status controls

* refactor: compact caffeinate status

* fix: harden caffeinate readiness
2026-08-09 22:52:11 -07:00
Jinwoo HongandJinwoo-H 158212b8b3 feat(github): add PR comment reactions (#13470)
* feat(github): add PR comment reactions

* fix(github): harden comment reaction updates

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-09 22:11:54 -07:00
Jinwoo Hong d9e84d88ef fix(persistence): salvage corrupt workspace session entries (#13431) 2026-08-09 21:39:24 -07:00
Neil 4cca31f36f fix(quick-open): compact Cmd+P file rows (#13454) 2026-08-09 21:21:18 -07:00
Neil 9ccec550c3 Add emoji picker to workspace editing and search (#13429)
* Add emoji picker to workspace editing and search

* fix(sidebar): scope sticky headers by host

* fix: address emoji picker review findings
2026-08-09 20:25:15 -07:00
JinjingandOrca b075a95b06 Strip liveness gate from AI Vault session delete (#13279)
* Strip liveness gate from AI Vault session delete

Delete now requires only path validation + user confirmation — no process
roster, no liveness check, no quiescence, no ownership ledger.

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

* Remove obsolete AI Vault liveness delete reliability gate

Session delete no longer checks process liveness, so drop the
manifest entry that still referenced the deleted test files.

* minor fix

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 18:40:01 -07:00
Brennan Benson 774bbc788f fix(terminal): preserve OSC 8 links across cold parking (#13382)
* fix(terminal): preserve OSC 8 links across cold parking

* test(terminal): make OSC 8 e2e cross-platform

* test(terminal): focus OSC restore e2e on activation
2026-08-09 16:36:12 -07:00
Jinjing d23dda6d48 Move Linear issue view to localStorage (#13391)
Prevent older paired hosts from discarding the entire taskResumeState
when they encounter the strict linearIssueView field, which was causing
silent loss of github and jira queries during remote pairing. Layout,
grouping, ordering, and per-workspace filters are now device-local.
2026-08-09 16:35:24 -07:00
NeilandOrca 9aa9d45467 test(e2e): cover a macOS system key remap reaching the terminal (#11170) (#13314)
* fix(terminal): show a preedit the IME resumes without a compositionstart

Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.

The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.

Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.

The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.

* test(terminal): prove the resumed-preedit fix against a recorded Windows capture

The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.

Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.

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

* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact

The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.

Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.

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

* fix(terminal): restore the preedit visibility patch onto its own branch

The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.

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

* fix(terminal): claim printable keydowns structurally so committed text survives

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

* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal

The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.

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

* docs(terminal): record why withholding a claimed keydown needs no timer

The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.

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

* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly

Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.

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

* fix(terminal): keep the kitty key-release report for presses that reached the pty

Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.

Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.

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

* test(e2e): assert IME preedit geometry headlessly for Korean and CJK input

Both IME defects that shipped and were reverted walked through a suite of ~3000
passing assertions, because every one of them was about bytes reaching the PTY.
A preedit rendered into a hidden overlay satisfies all of them while the user
composes blind. The real-geometry coverage that would have caught it existed but
was headful, env-gated and macOS-only, so it never ran in CI.

Drives composition through CDP Input.imeSetComposition instead of a native input
source, which removes the accessibility grant and the system input source that
forced the headful gate. The suite runs in the normal headless project in about
55s serially, and asserts the composition overlay's real bounding rect — the one
property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot
produce.

Three tests are red on main and marked test.fail() so they stay visible in CI and
flip loud when their fix lands: the preedit resumed by a bare compositionupdate,
and full-width punctuation and digits committed from a keydown that still carries
the ASCII layout key.

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

* test(e2e): drop the known-broken markers now the stack closes all three

Validated on real macOS hardware: with the two fixes below this layer, all
three report "Expected to fail, but passed". Korean preedit renders at
non-zero geometry through every jamo, and an Apple pinyin source sends
ef bc 8c e3 80 82 to the pty where main sends ASCII.

Worth recording why the punctuation case looked green on main once: an input
source whose id happens to contain an allowlist term, as Sogou's does, satisfies
the old gate. Correctness there depended on which IME the user had selected.

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

* test(e2e): cover the Linux and Windows IME ownership shapes headlessly

The ten headless IME specs on this branch all decided ownership through a macOS
user-agent override, so the two platforms whose failure mode is a *dropped*
character rather than a downgraded one had no coverage at all, and the one
Windows-recorded trace already in the suite was replayed under whichever policy
the runner happened to report — macOS locally, Linux on the CI shards.

Adds four Linux specs and two Windows specs, every one of them replaying a
native capture rather than a hand-authored ordering:

- IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and
  the syllable arrives afterwards as a bare `insertText`, so reading the commit
  off `compositionend.data` — which the Windows capture rewards — drops every
  syllable on this framework.
- fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229,
  and physically wrong `code` values on the literal keys. Any ownership rule
  reading 229 or `code` fails here.
- Numeric pinyin candidate selection under both frameworks, with the ordinary
  digit kept as the negative control, so the two directions are pinned against
  each other rather than separately.
- Windows Microsoft Korean captured with real scan codes, including the two
  lines committed with Shift held.

Each asserts both sides of the boundary: the preedit's real geometry at every
frame the user would see, and the exact byte stream the native run put on the
PTY. The recorded `onData` the Windows/WSL fixture already carried is now
asserted instead of sitting unused.

Chinese moves up to first-class alongside Korean: pinyin preedit width is now
pinned the way the Japanese phrase already was, and full-width punctuation is
covered in the composition-session shape the Windows and Linux frameworks use,
not only the macOS insertText shape.

Two harness fixes fell out of the recorded traces and are why the IBus one
passes. The replay applied each event's recorded textarea state *after*
dispatch, one event too late for the handlers that read `textarea.value`; and
it left a task boundary between `compositionend` and the `input` carrying the
commit, which Chromium never inserts, letting xterm's deferred finalizer settle
against a textarea the committed text had not reached yet.

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

* test(e2e): replay the recorded macOS IME shapes instead of only synthesising them

The macOS coverage on this branch drives Chromium composition through CDP, which
is genuine but hand-ordered, so it could not assert the one property that
decides the macOS rule: a composing keydown arrives with keyCode 229 while `key`
is still the single translated character the input source produced — `ㅎ`, not
`Process` — which is indistinguishable by length from an ordinary printable key.
Three native captures were sitting unused in the evidence set.

Adds a recorded 2-Set Korean session, including the syllable boundary where one
composition closes and the next opens with no keydown between them, asserted
against its own recorded byte stream.

Adds the third failure mode, which had no coverage in any shape: an abandoned
preedit leaking to the shell. Pinyin and Cangjie both backspace a composition
away to nothing, and the assertion is not "the right bytes" but "no bytes".

Both cancellation captures continue with a literal `ordinary` typed as bare
keydowns, the recorder's own negative control. That tail carries no `input`
events because the build it was captured on produced the byte from the keydown
itself, so replaying it would measure the recorder rather than the product; the
specs cut at the `compositionend` and say so.

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

* test(e2e): promote the non-allowlist input-source punctuation spec to the suite

Qingg matches none of the terms the pre-structural build enumerated, so on that
build its bypass never installs. It is the one arm no headless spec can express,
and the only test here that the pre-structural build cannot pass.

Promoted from scratch with four changes, each forced by a measurement rather than
by taste:

- A non-attached input method is a refusal, not a negative result. macOS attaches
  per app instance and the attach can simply fail — 3 of 6 instances under
  exclusive host access, and re-selecting the source did not recover one of them
  across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun,
  not a thrown error, and the suite does not gate on a fully green session.
- Attachment is probed with a LETTER. Punctuation substitution emits no
  compositionstart and no keyCode 229 even under a fully attached source, so at
  the keydown it is indistinguishable from having no source at all. The
  punctuation arms are judged on PTY bytes alone.
- The ASCII-layout control is now part of the spec rather than a side experiment.
  Without it a build that rewrote every `.` into `。` unconditionally would pass
  the Qingg arm and be badly wrong.
- The assertion runs by default instead of behind a strict-mode flag, and gained
  a non-vacuity check: the input source must have committed something. That is
  the sharp end of the mechanism — on the old build the DOM carries only keydown
  and keyup, so nothing is committed at all and the character is destroyed before
  the source is asked.

The verdict stays an equality between two measurements, never a comparison
against a hardcoded glyph, so it holds whatever punctuation mode the operator's
input source happens to be in. It reads `beforeinput`, not `input`: the forwarder
consumes `input` in the capture phase on the pane element, so a probe on the
helper textarea never sees it and a strict run fails with correct bytes
underneath.

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

* feat(terminal): encode IME commits as CSI-u under the all-keys kitty flag

A pane that negotiates `report_all_keys_as_escape_codes` (bit 3) asked for every
printable key as a CSI-u report. The commit path wrote IME-committed text raw,
so such a pane got a legacy byte stream it had declined. That predicate has no
IME-specific condition, so it affected every macOS user in such a pane, not just
CJK users.

Encode the press that produced the commit instead, reusing xterm's own kitty
encoder rather than hand-rolling CSI-u.

`claimKeyEvent` is untouched: still unconditional, still structural, still no
kitty read on the keydown. The flag read happens once per commit.

The gate is bit 3 alone. Flags 1/2/4/16 leave printable keys as text, so panes
negotiating only those keep receiving substituted characters; gating on "kitty
active" would strip the substitution from every pane that negotiates anything.

Known limit, pinned by test: the report carries the physical key's codepoint,
not the committed glyph. Bit 3 is the app declaring it does not want text, and
bit 4 is how it asks for text back — but xterm's encoder derives that text field
from the same `key` it derives the keycode from, so carrying the committed glyph
needs an encoder change, not a wider gate.

* fix(terminal): report a held key's repeats as REPEAT under the kitty flags

The commit encoder never passed an event type, so xterm's encoder applied its
PRESS default to every auto-repeat keydown. A pane negotiating report_event_types
alongside bit 3 saw one held key as N separate strikes.

Carry the keydown's `repeat` on the claimed press and map it to the protocol's
REPEAT. The event type only reaches the wire when report_event_types is
negotiated, so this is inert for panes that asked only for bit 3.

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

* test(e2e): cover a macOS system key remap reaching the terminal (#11170)

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

* chore: drop non-mergeable IME e2e scratch files

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 16:07:29 -07:00
NeilandOrca 050ad3b32f test(e2e): normalise the pty line terminator so the IME specs can run on Windows (#13313)
* fix(terminal): show a preedit the IME resumes without a compositionstart

Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.

The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.

Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.

The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.

* test(terminal): prove the resumed-preedit fix against a recorded Windows capture

The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.

Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.

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

* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact

The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.

Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.

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

* fix(terminal): restore the preedit visibility patch onto its own branch

The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.

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

* fix(terminal): claim printable keydowns structurally so committed text survives

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

* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal

The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.

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

* docs(terminal): record why withholding a claimed keydown needs no timer

The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.

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

* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly

Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.

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

* fix(terminal): keep the kitty key-release report for presses that reached the pty

Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.

Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.

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

* test(e2e): assert IME preedit geometry headlessly for Korean and CJK input

Both IME defects that shipped and were reverted walked through a suite of ~3000
passing assertions, because every one of them was about bytes reaching the PTY.
A preedit rendered into a hidden overlay satisfies all of them while the user
composes blind. The real-geometry coverage that would have caught it existed but
was headful, env-gated and macOS-only, so it never ran in CI.

Drives composition through CDP Input.imeSetComposition instead of a native input
source, which removes the accessibility grant and the system input source that
forced the headful gate. The suite runs in the normal headless project in about
55s serially, and asserts the composition overlay's real bounding rect — the one
property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot
produce.

Three tests are red on main and marked test.fail() so they stay visible in CI and
flip loud when their fix lands: the preedit resumed by a bare compositionupdate,
and full-width punctuation and digits committed from a keydown that still carries
the ASCII layout key.

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

* test(e2e): drop the known-broken markers now the stack closes all three

Validated on real macOS hardware: with the two fixes below this layer, all
three report "Expected to fail, but passed". Korean preedit renders at
non-zero geometry through every jamo, and an Apple pinyin source sends
ef bc 8c e3 80 82 to the pty where main sends ASCII.

Worth recording why the punctuation case looked green on main once: an input
source whose id happens to contain an allowlist term, as Sogou's does, satisfies
the old gate. Correctness there depended on which IME the user had selected.

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

* test(e2e): cover the Linux and Windows IME ownership shapes headlessly

The ten headless IME specs on this branch all decided ownership through a macOS
user-agent override, so the two platforms whose failure mode is a *dropped*
character rather than a downgraded one had no coverage at all, and the one
Windows-recorded trace already in the suite was replayed under whichever policy
the runner happened to report — macOS locally, Linux on the CI shards.

Adds four Linux specs and two Windows specs, every one of them replaying a
native capture rather than a hand-authored ordering:

- IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and
  the syllable arrives afterwards as a bare `insertText`, so reading the commit
  off `compositionend.data` — which the Windows capture rewards — drops every
  syllable on this framework.
- fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229,
  and physically wrong `code` values on the literal keys. Any ownership rule
  reading 229 or `code` fails here.
- Numeric pinyin candidate selection under both frameworks, with the ordinary
  digit kept as the negative control, so the two directions are pinned against
  each other rather than separately.
- Windows Microsoft Korean captured with real scan codes, including the two
  lines committed with Shift held.

Each asserts both sides of the boundary: the preedit's real geometry at every
frame the user would see, and the exact byte stream the native run put on the
PTY. The recorded `onData` the Windows/WSL fixture already carried is now
asserted instead of sitting unused.

Chinese moves up to first-class alongside Korean: pinyin preedit width is now
pinned the way the Japanese phrase already was, and full-width punctuation is
covered in the composition-session shape the Windows and Linux frameworks use,
not only the macOS insertText shape.

Two harness fixes fell out of the recorded traces and are why the IBus one
passes. The replay applied each event's recorded textarea state *after*
dispatch, one event too late for the handlers that read `textarea.value`; and
it left a task boundary between `compositionend` and the `input` carrying the
commit, which Chromium never inserts, letting xterm's deferred finalizer settle
against a textarea the committed text had not reached yet.

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

* test(e2e): replay the recorded macOS IME shapes instead of only synthesising them

The macOS coverage on this branch drives Chromium composition through CDP, which
is genuine but hand-ordered, so it could not assert the one property that
decides the macOS rule: a composing keydown arrives with keyCode 229 while `key`
is still the single translated character the input source produced — `ㅎ`, not
`Process` — which is indistinguishable by length from an ordinary printable key.
Three native captures were sitting unused in the evidence set.

Adds a recorded 2-Set Korean session, including the syllable boundary where one
composition closes and the next opens with no keydown between them, asserted
against its own recorded byte stream.

Adds the third failure mode, which had no coverage in any shape: an abandoned
preedit leaking to the shell. Pinyin and Cangjie both backspace a composition
away to nothing, and the assertion is not "the right bytes" but "no bytes".

Both cancellation captures continue with a literal `ordinary` typed as bare
keydowns, the recorder's own negative control. That tail carries no `input`
events because the build it was captured on produced the byte from the keydown
itself, so replaying it would measure the recorder rather than the product; the
specs cut at the `compositionend` and say so.

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

* test(e2e): promote the non-allowlist input-source punctuation spec to the suite

Qingg matches none of the terms the pre-structural build enumerated, so on that
build its bypass never installs. It is the one arm no headless spec can express,
and the only test here that the pre-structural build cannot pass.

Promoted from scratch with four changes, each forced by a measurement rather than
by taste:

- A non-attached input method is a refusal, not a negative result. macOS attaches
  per app instance and the attach can simply fail — 3 of 6 instances under
  exclusive host access, and re-selecting the source did not recover one of them
  across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun,
  not a thrown error, and the suite does not gate on a fully green session.
- Attachment is probed with a LETTER. Punctuation substitution emits no
  compositionstart and no keyCode 229 even under a fully attached source, so at
  the keydown it is indistinguishable from having no source at all. The
  punctuation arms are judged on PTY bytes alone.
- The ASCII-layout control is now part of the spec rather than a side experiment.
  Without it a build that rewrote every `.` into `。` unconditionally would pass
  the Qingg arm and be badly wrong.
- The assertion runs by default instead of behind a strict-mode flag, and gained
  a non-vacuity check: the input source must have committed something. That is
  the sharp end of the mechanism — on the old build the DOM carries only keydown
  and keyup, so nothing is committed at all and the character is destroyed before
  the source is asked.

The verdict stays an equality between two measurements, never a comparison
against a hardcoded glyph, so it holds whatever punctuation mode the operator's
input source happens to be in. It reads `beforeinput`, not `input`: the forwarder
consumes `input` in the capture phase on the pane element, so a probe on the
helper textarea never sees it and a strict run fails with correct bytes
underneath.

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

* test(e2e): normalise the pty line terminator so IME specs can run on Windows

A Unix pty's line discipline turns the terminal's CR into a bare LF, but
Windows ConPTY hands the reading process CRLF. Every spec compares against a
recorded line ending in LF, so on Windows 16 of 21 failed on the terminator
alone while the IME payload bytes ahead of it were byte-for-byte correct.

Verified on real Windows: the renderer-to-pty boundary assertion passed there,
so the app writes a bare CR and the LF is added by the console downstream of
anything we control. Normalising in the reader keeps the specs asserting the
IME bytes unchanged rather than loosening them.

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

* chore: drop non-mergeable IME e2e scratch files

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 16:06:00 -07:00
Jinwoo HongandJinwoo-H 45c1cb979a fix(orchestration): release context-only dispatches (#13376)
* fix(orchestration): release context-only dispatches

Refs #13005

* test(orchestration): align PTY readiness timeout

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-09 14:49:36 -07:00
Jinwoo Hong bd9addb449 fix(tasks): restore GitHub page and scroll position on reopen (#13096) 2026-08-09 11:08:04 -07:00
Neil 22a898e2d6 test(ssh): add E2E oracles for pane cardinality and duplicate resume
Both reported failures now have end-to-end coverage against a real Docker
OpenSSH relay, gated on ORCA_E2E_SSH_DOCKER like the rest of the suite.

- ssh-reconnect-pane-cardinality-across-partitions: three real reconnect cycles;
  after each, pane ids unchanged, exactly one live lease per leaf, exactly one
  remote shell per pane key, and exactly one pty id bound per leaf ACROSS BOTH
  durable partitions. Two partitions agreeing is tolerated; two naming different
  shells is the S4 divergence and fails. PASSES.
- ssh-reattach-does-not-resume-agent-twice: the host itself records one line per
  shell launch via a .bashrc hook keyed by ORCA_PANE_KEY, so a duplicate resume
  is counted at the source rather than inferred. Fault is SIGSTOP on the
  detached relay. PASSES.
- ssh-disconnected-pane-affordance: written whole but held as test.fixme. The
  banner needs the target CONNECTED while a single pane's attach fails, and both
  host-side faults drove the target out of connected instead. Held rather than
  deleted so it runs the day a seam exists; the reason is measured, not assumed.

New helpers: docker-ssh-relay-stall (SIGSTOP/SIGCONT, reads the stop back off
/proc so a fault that did not land cannot make an oracle vacuous), and
remote-pane-launch-transcript.

Note for whoever picks these up: a stalled relay leaves two detached relay groups
on the host, and readDockerSshRelayProcessSnapshot throws on more than one, so
call it before the fault.

Writing these is what surfaced RC2 surviving S3 — fixed in 7cd7fef927.
2026-08-09 02:30:49 -07:00
NeilandOrca 970696a008 fix(sidebar): show Cursor rows and stop a stray "claude" title hijacking OpenCode (#12466)
* fix(sidebar): show Cursor rows and stop a stray "claude" title hijacking OpenCode

Two defects in the same title-resolution path.

**#10258** — Cursor's only native OSC title is the literal `cursor agent`, which both title trackers dropped unconditionally. A hookless Cursor pane therefore had neither a status entry nor any title carrying Cursor identity, so the worktree card showed nothing at all.

**#8940** — two owner-blind paths let an incidental `claude` token anywhere in an OpenCode session or task title outrank the pane's known owner, so the tab icon and sidebar row flipped to Claude Code.

#10258: let the literal through exactly once as identity, so a restored or mobile tab keeps its Cursor row instead of vanishing. #8940: require an *identity frame* — after stripping status decoration the title must PRESENT Claude, not merely mention it — before a Claude title may reclaim a pane from its prior identity, and make the sidebar row builder owner-aware.

> These two are in one PR because they share the `ownerAgentType` plumbing through `buildTitleDerivedAgentRow` — split apart, neither half compiles on its own.

Fixes #10258
Fixes #8940

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

* test(e2e): add recordable proof for sidebar-agent-row-identity

Fails on origin/main, passes on this branch.

Test: sidebar keeps a Cursor pane visible and an OpenCode pane out of Claude Code hands

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

* fix(terminal): preserve restored Cursor identity

* test(terminal): cover restored Cursor redraw suppression

* refactor(terminal): tighten Cursor identity handling and Claude frame matching

Review follow-ups on the title-resolution path:

- pty-transport dropped a native Cursor literal that main emits whenever a
  non-Cursor title preceded it, re-introducing the #10258 blank row in the
  renderer path. The pre-filter now projects the predecessor the drain will
  actually see, and defers to the drain gate while facts are still queued.
- applyTrackedPtyTitle threaded the cursor flag through 12 sites, including
  ptyRecordChanged bookkeeping the sole caller ignores. Force the status null
  once, and the activity-gated effects fall out unchanged.
- isClaudeIdentityFrameTitle missed a multiplexer-wrapped Claude title
  ("zsh | Claude Code"), costing a genuine Claude pane its identity. Reuse
  the ' | ' segment split that agent-title-owner already had inline.
- Keep title normalization on launchAgent: it only rewrites within an
  identity group (OMP wraps Pi), so a split does not make it wrong, and
  the hook-row path normalizes the same way.
- Drop the tab.ptyId tracker fallback, which read a pty that the pane
  identity check had just rejected.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 01:32:34 -07:00
Neil ea8881a3c7 fix: keep cross-project dialog actions in bounds (#13317) 2026-08-09 01:16:09 -07:00
NeilandOrca 6b6a3b666a test(ssh): add MaxSessions, lazy-discovery and paired-skew oracles
Three journeys attempted; none promoted, and the reasons are recorded in
the ledger rather than rounded up.

MaxSessions=1 against real OpenSSH, with the cap read back from `sshd -T`
rather than assumed, and remote pids read on the container two
independent ways that must agree, each carrying its kernel start time.
Two disjoint mutations discriminate — one reddens only the reconnect
clause, the other only the two restart clauses. But the disconnect clause
is a forward guard: four separate guard removals left it green, so
nothing shipped is load-bearing for it.

Lazy discovery samples sshd's own accept log and live session census
across a 22s window with the in-use host as a positive control. No
mutation reddens its third clause alone — the real cross-host lease
scoping is load-bearing, but removing it breaks the sibling host during
setup, so the failure carries no clause information.

The paired-runtime skew spec pairs two real processes at different
versions and refuses to run rather than degrade into a same-version
pairing that would look green and prove nothing.

No production code changes.

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:11:54 -07:00
NeilandOrca e4033f9994 test(terminal): add discriminating oracles for the daemon, skew and multi-host journeys
Daemon: replaces a spec that modelled only a client restart and never
crossed the daemon boundary, whose successor generation owned nothing so
"the live successor is neither killed nor replaced" was vacuous. The PTY
leader is now a real login shell reporting `$$` back through the
production write path, resolved to a kernel start time. Two mutations
each redden exactly one of the three clauses, on macOS and Linux:
reverting three-valued `hasPty` reddens only the unknown-not-dead
clause; widening the sole-provider fallback reddens only the stale
generation clause.

Skew: reverting the restore-required publication to expiry reddens 4 of
5 new tests while the legacy control stays green — the regression this
branch fixed is now caught if reintroduced.

Multi-host: restoring `mux.dispose('connection_lost')` reddens sibling
isolation on one host. It does NOT redden across hosts, and that is
recorded rather than glossed: a mux belongs to one relay session per
target, so its dispose cannot cross a host boundary. Journey 4's
cross-host clause rests on isolation-by-construction, not on a mutation.

No production code changes.

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:11:54 -07:00
NeilandOrca 56beffe5d4 test(terminal): make the restart identity spec run on Windows too
Both probes were POSIX-only and unconditional: `echo ...=\$\$` for the
shell's own pid, and `ps -o lstart=` for its start time. Running the spec
on a real Windows host proved it dies before reaching either guard, so
Journey 1's Windows half was unprovable rather than merely unproven.

PowerShell exposes the same two facts as `$PID` and `Get-Process`
StartTime. The start time still matters on both platforms for the same
reason: a PID alone cannot separate a survivor from a reused number.

Still green on macOS. The Windows path is written from the host probe and
has not itself been executed end to end — that is the next thing to run
there, not a claim being made here.

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:11:54 -07:00
NeilandOrca 482ca59282 test(terminal): poll the pane binding read so a slower host cannot flake it
`readPaneBinding` took a single unpolled read of a DOM dataset attribute
immediately after a renderer reload, while its sibling helper polls the
same data for 15s. On a native Linux host both tests failed every run
with 'No bound terminal pane is mounted' while the app was demonstrably
healthy — the screenshot showed the terminal restored with a live prompt
and the boot PID echoed.

The assertion is unchanged; it is only awaited. Nothing is weakened.

Found by running this spec on native Linux rather than assuming macOS
behaviour generalises.

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:11:54 -07:00
NeilandOrca 0fe29dc89b test(ssh): induce the reattach graft deterministically instead of racing a kill
The previous induction closed a pane while the transport was severed and
relied on `pty:kill` FAILING so the lease outlived the pane record. It
does not fail: with the provider already torn down, `pty:kill` takes its
tombstone branch and marks the lease terminated, and `reattachKnownPtys`
filters terminated leases out of the fan-out — so the reconnect never
visited the PTY the test was about. It passed on both trees.

Seed the precondition instead. Spawn a real remote PTY on a leaf that
never becomes a pane, then roll the host partition back to its pre-spawn
snapshot, leaving a live lease and a live remote shell that no durable
pane owns. No failure races a success.

Adds a vacuity guard that is independent of the tree under test: the
lease's own `lastAttachedAt` must advance, proving the fan-out actually
visited this lease before the pane census is trusted.

Verified on this machine under an isolated TMPDIR, since the e2e
harness keys its seeded-repo pointer on a machine-global tmpdir path:
guard present passes, guard removed fails with the phantom leaf grafted
into the local partition, guard restored passes.

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:11:54 -07:00
NeilandOrca 3c7ed558e3 test(terminal): add discriminating oracles for restart, daemon, skew and namespaces
Six parallel streams, each required to fail with its guard removed rather
than merely pass.

Local restart proves the OS process itself survives, by reading
`ps -o lstart=` for the shell's own pid. That matters: with the quit path
made destructive, the tab, leaf and pty ids all came back byte-identical
while the shell underneath was a new process — every existing restart
spec would have stayed green. Two separate guards were removed to redden
it, and the second reddens only the stale-operation case.

Daemon restart discriminates by reverting three-valued `hasPty`; version
skew now covers publication semantics and confirms the new
`SSH_SOURCE_RESTORE_REQUIRED` token mutates nothing on an old client;
two-host isolation censuses both containers.

Deletes `src/relay/pty-source-replay-index.ts` — 201 production lines
with no importer outside its own test, verified against an
entrypoint-rooted import graph rather than a name grep.

Five namespace tests are skipped, not passing: they reproduce a defect
still live on main where folder-workspace ids compare equal with the
instance suffix stripped. PR #12474 fixes it; they are its oracle.

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:11:54 -07:00