Commit Graph
8583 Commits
Author SHA1 Message Date
Neil 98c87a6325 refactor(terminal): one i18n namespace per component, and two notes worth keeping
The disconnected banner was renamed but kept reading five keys under the old
component's namespace while its new strings sat under the new one — a component
translating from two namespaces at once. Keys moved across all five locales;
values and behaviour unchanged.

Two comments earn their place. durablyBoundPtyIdForPane deliberately does NOT
require the tab to still exist, unlike findTerminalTabIdForLeaf which must —
adding the "missing" check there would make arbitration retire shells more
eagerly, which is the opposite of what this change is for. And the respawn after
the proof check in the direct-SSH catch is unreachable today by construction;
saying so stops it reading as forgotten code.
2026-08-12 04:17:57 -07:00
Neil 601604d17b fix(ssh): let a due park past the retired-delivery filter, and prove it
The cooldown added in 124e00e8a8 was reactive: it needed a later rejected frame
to reach recovery. But retirement is ours, not the host's — a stalled source keeps
publishing the very token we retired, and acceptPtyData drops those frames before
classification. So the wake-up could never fire in the case that actually happens,
and the pane stayed dark exactly as before.

A parked PTY past its cooldown is now let through that filter. It costs nothing:
the frame is re-classified as rejected and re-retired, so no output reaches the
terminal — it only regains the ability to ask for recovery.

The test shipped with that commit could not have caught this: it woke recovery
with a fabricated NEW delivery token. Retirement holds one key per relay PTY, so
that frame overwrote the key and un-retired the original — the assertion passed
with the wake-up path fully broken. It now reuses the original token throughout,
and reverting the guard above reddens it.
2026-08-12 04:12:22 -07:00
Neil 124e00e8a8 fix(ssh): make a parked PTY delivery expire instead of going dark for good
Exhausting the per-generation recovery budget parks one PTY's delivery rather
than dropping the shared relay channel — right, because the channel is shared and
a retry count proves nothing. But the only escape it named was "the next relay
open", and a channel that stays healthy never gives it one. That leaves a pane
with no output and no way back, which is the exact state this change exists to
prevent; before this branch, exhaustion dropped the channel and the reconnect
ladder recovered the pane (loudly, at every sibling's expense).

The park is now a cooldown rather than a verdict: the next rejected frame after
it starts a fresh budget. Recovery is rate-limited, never abandoned, and the
containment that made parking right in the first place is untouched.

Elapsed time, not a timer, so there is nothing to cancel on teardown and no late
fire after dispose.
2026-08-12 04:00:00 -07:00
Neil 60ae0713da perf(ssh): stop fsyncing the whole store from the main thread on reconnect
supersedeDuplicatePaneLeases runs at the top of every reattach pass, and when it
retires anything it flushed synchronously. flushOrThrow fsyncs a multi-MB file
from the Electron main thread — this file already documents (see flushAsync) that
on a stalled network profile mount that syscall is uninterruptible, so the app
stops repainting and no deadline can bound it, because the deadline's own timer
is stuck behind the same block.

The population that hits this is exactly the one the heal exists for: an upgraded
install carrying accumulated duplicates. It now awaits the async durable twin its
neighbours on this path already use. Still "OrThrow", because a retirement that
is not durable must not be believed — the rollback is unchanged.
2026-08-12 03:53:55 -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 2a17fd5e18 docs(terminal): do not claim a remote runtime is authoritative about its ptys
A remote runtime reaches its pty over a connection that can fail, so a failed
reattach proves no more there than it does over SSH. It keeps replacing in place
only because the card is SSH-scoped, not because its provider is authoritative.
Say that, so the limitation is deliberate rather than implied.
2026-08-12 03:31:41 -07:00
Neil 052a63a5d0 docs(terminal): put two comments back on the thing they describe
The lease-healing docblock had drifted onto durablyBoundPtyIdForPane, which
neither retires leases nor returns a count, leaving the real healing entry point
undocumented and its neighbour carrying two contradictory descriptions.

The suppression comment claimed to cover "every startup value this transport was
constructed with"; env/envToDelete are constructor values and deliberately stay
outside the set. Nothing behavioural changes here — but a comment that overstates
a boundary is how the next maintainer picks the wrong one.
2026-08-12 03:30:27 -07:00
Neil b46a1f8773 fix(terminal): stop stranding a local pane whose restore fails
The unreachable-pane card is SSH-only: a local, daemon or runtime-host pane has
no connection to be unreachable ON. Consolidating that guard into the publisher
made it silent, and four callers paired the now-conditional publish with an
unconditional `return` — so on the deferred-reattach path, which unlike the
direct-SSH one is not nested under `connectionId`, a local pane got no card, no
error and no replacement. Frozen, with a stale binding.

The diversion now reports whether it took ownership of the failure, so a caller
can only stop when something actually handled it. A pane that cannot show the
card falls through to the replace-in-place it had before, which is correct: its
provider is authoritative about its own ptys.

The two direct-SSH sites are inside `if (connectionId)`, so they are unchanged
in behaviour; the return value simply makes the pairing impossible to get wrong
at the next call site.
2026-08-12 03:29:04 -07:00
OrcaWin 09ec516ae5 fix(editor): index WSL watcher aliases per batch (#14015) 2026-08-12 03:24:31 -07:00
Neil f7eb6f2665 fix(terminal): suppress the whole saved startup set, not field by field
Round 4 self-check on my own round-3 fix. `suppressSavedStartup` guarded four of
the six values `connect` falls back to — `launchAgent` and
`startupCommandDelivery` still inherited from the transport's constructor. Adding
the guard per field is precisely how those two were missed, and the guarded
expressions had become unreadable.

The saved values are now one object that `suppressSavedStartup` drops wholesale.
A field added later is covered by construction rather than by remembering.

Found by asking the question the review lens was given rather than waiting for
its answer: does the suppression cover EVERY channel, or only the ones I noticed?
It did not.
2026-08-12 03:09:03 -07:00
Neil 037ee535b8 fix(terminal): put the unreachable-pane guards in one place
Correctness round 3. The leaf-keying from round 2 came back clean, twice and
independently. But the renderer produced findings for a third consecutive round,
and they were all one shape: `publishUnreachablePane` is called from seven sites,
each needing the same guards, and a different one was missing at each.

So this stops patching sites and moves the guards into the publisher:

- `disposed` — a late rejection republished a card for a numeric pane id that had
  already been reused, giving a fresh pane a phantom card whose actions closed
  over a dead session.
- `connectionId` — the deferred catch is not SSH-only. A local/daemon pane could
  be shown an SSH ambiguity card it can never clear, and would loop: retry
  remounts, reattach rejects, card returns. Its provider is authoritative about
  its own ptys, which is exactly why the two branches in handleReattachResult
  already had this guard — and why the catch needed it too.

Two more real defects in the banner's own actions:

- "Start a new terminal" passed `null` to suppress the saved agent startup, but
  connect FALLS BACK to the startup its transport was constructed with whenever a
  per-call field is absent (pty-transport.ts). So the "fresh" shell could resume
  the same provider session — the duplicate transcript this pane exists to
  prevent, for the third time in this button. `suppressSavedStartup` makes the
  suppression explicit; `??` means passing null could never have worked.
- It also cleared both durable bindings BEFORE the spawn, and discarded the
  promise. A spawn that resolves null left the pane blank, unbound, and with no
  way back to a shell that may still be running. Bindings are now cleared only
  once a replacement actually starts, and the card returns if it does not.
- "Try again" voided its boolean; a declined remount left no card and no shell,
  strictly worse than the toast it replaced. It now republishes.

The strengthened clause is the point: the old one asserted the ABSENCE of
per-call startup fields on a mocked transport, which passes whether or not the
production fallback fires. It now asserts the explicit suppression, and reddens
when the flag is dropped.

Four fixtures were under-specified — they drive SSH reattach scenarios but never
seeded an SSH repo, so `connectionId` was null and they had been passing without
the pane being SSH at all. Seeded, not weakened.

43,256 pass; the 7 failures are pre-existing on clean main in this environment.
2026-08-12 02:52:34 -07:00
Neil 7724a950ec fix(ssh): a supersession decided from one plane only mutates that plane
Correctness round 2 confirmed finding. Arbitration ranks leases using the desktop
plane's binding, then handed its losers to a scrub that walked BOTH planes — so
the headless/CLI/mobile plane's binding was deleted for a lease it never got to
vote on. Its owner then has no durable record to reattach that shell by, and can
fall back to recency or orphan a shell that is still running.

`clearSshRemotePtyBindingsForLeases` now takes `arbitratedFrom`. A decision
reached by reading one plane may only mutate that plane. An explicit expiry or
termination is plane-agnostic — the pty is gone for everyone — so those callers
pass nothing and still scrub both, which is what the existing
`markSshRemotePtyLease` oracle pins.

I initially assessed this as not-a-defect, reasoning that local winning IS the
STA-3077 fix. That was about RANKING and did not justify DELETING the other
plane's record; the round-2 verdict was right and I was wrong.

It did also catch a comment of mine that had gone stale — a clause claimed the
other plane was "deliberately left alone" while the code cleared it. The comment
now says what the code does and why.

Mutation: letting arbitration scrub both planes again reddens the new clause.
43,256 pass; the 7 failures are pre-existing on clean main in this environment.
2026-08-12 02:40:29 -07:00
Neil 1697b3be16 fix(ssh): supersede on the leaf, so a moved pane retires its own predecessor
Correctness round 2. This is the reported cardinality growth in its surviving
form, and it is the sharpest finding of the review so far.

Supersession matched sibling leases on `(worktreeId, tabId, leafId)` and bucketed
duplicates under the same key. A lease freezes its tabId when written, and
`detachTerminalPaneToTab` moves a live pane — so after a break-out the pane's
next lease carries the NEW tab and its predecessor carries the old one. The two
never match, the predecessor is never superseded, and the live count grows on
every reconnect. Exactly the reported 2 -> 19 -> 20, for any pane that has been
moved between tabs.

Round 1 fixed the same frozen-tabId mistake at the binding LOOKUP. It did not fix
it here, at the sibling match and the bucket key, which is why the bug survived a
round. Both are now keyed on the leaf — the stable half of pane identity, per
stable-pane-id.ts: the tab half changes on break-out.

Two clauses cover it: a predecessor whose lease names the tab the pane left is
superseded, and the live count stays flat across ten reconnects that each land in
a new tab. Restoring `tabId` to either the match or the key reddens both.

E2E re-run against a real Docker relay after the change: 2 passed. Full suite
43,255 pass; the 7 failures are pre-existing on clean main in this environment.
2026-08-12 02:35:14 -07:00
github-actions[bot] ad6f8011e2 Update README downloads badge 2026-08-12 09:33:52 +00:00
Neil b92498468a fix(ssh): arbitrate on the pane, not the tab its lease was written in
Correctness review round 1 on #13326: 5 raised, 2 confirmed, both real.

1. Arbitration looked the durable binding up under the lease's FROZEN tabId.
   `detachTerminalPaneToTab` moves a live pane and its PTY, and nothing re-keys a
   lease, so after a break-out the binding lives under the new tab and the lookup
   found nothing. The bound shell then lost to recency and supersession expired
   the pane's OWN lease; the follow-on scrub could not clean up either, because
   it matches lease.tabId against the layout's tabId. Reconnect skipped the
   expired-but-bound PTY and reattached the stale winner onto the pane.

   The leaf is the stable half of pane identity, so the lookup now prefers the
   named tab and falls back to wherever the leaf actually is.

2. Arbitration read the desktop plane only while the scrub reached into
   `ssh:<target>`, so a headless-plane pane was invisible to the ranking and
   then lost its live binding to it.

   Fixed at the reader, not the scrub: local FIRST, headless plane only as a
   fallback. That is NOT the STA-3077 hedge — that bug was preferring
   `ssh:<target>`, letting a copy no live writer maintains outvote the real
   binding. A fallback consulted only when local is silent gives a headless-owned
   pane a vote without ever outranking a live desktop binding.

   Proven by mutation: swapping the two back to ssh-first reddens 4 clauses,
   including the ten-reconnect cardinality one.

Also closes two fulfilled-result routes to the duplicate agent resume that the
earlier RC2 fix missed — `handleReattachResult` respawned on a result flagged
`sessionExpired` and on a result carrying no pty id. A skeptic refuted both as
pre-existing rather than PR-caused, which is correct on causation, but they are
live routes to the defect this PR claims to fix.

Scoped to SSH panes only. The first attempt diverted every pane and broke three
daemon tests — correctly: a local provider is authoritative about its own ptys,
so "cannot reattach" there is not the ambiguous evidence it is for a relay that
may have been replaced. New clause covers both routes; disabling the diversion
reddens it.

43,253 pass; the 7 failures are pre-existing on clean main in this environment.
2026-08-12 02:26:14 -07:00
OrcaWin e5cd5bf542 fix: decouple Floating Workspace shell selection (#13995) 2026-08-12 02:09:57 -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
Brennan Benson 36d45af062 feat(browser): guide Google sign-in after cookie imports (STA-3811) (#13666) 2026-08-12 00:53:02 -07:00
Neil 9c091cf77e chore(perf): add renderer agent-status benchmark harness (#13905)
Splits run-idle-cpu-benchmark.mjs into a scale fixture, an in-page timing
probe, and process sampling, and records a measured origin/main baseline so
the agent-status batching slice has an auditable before.

The agent-status write workload is not included: it needs setAgentStatuses,
so it lands with the store slice.
2026-08-12 00:37:48 -07:00
Brennan Benson 2249330acf fix(browser): bulk clear cookies during native import (#13966)
* fix(browser): bulk clear cookies during native import

* fix(browser): separate Google cookie import warning

* fix(browser): report encrypted Google cookie exclusions

* fix(browser): skip key lookup for excluded cookies

* test(browser): cover excluded-cookie key bypass

* perf(browser): keep plaintext key scan cheap
2026-08-12 00:33:50 -07:00
NeilandOrca a90a18d43d perf(renderer): anchor spinner phase from animationstart only (#13987)
Co-authored-by: Orca <help@stably.ai>
2026-08-12 00:30:14 -07:00
Jinjing a81224614a fix(agents): detect a live OpenCode pane from its native OC | session title (#13957)
* fix(agents): detect a live OpenCode pane from its native OC | session title

OpenCode publishes `OC | <session>` as its OSC title, which carries no
agent-name token. detectAgentStatusFromTitle gates status on a whole-token
name match, so it returned null and every status consumer read a live
OpenCode pane as a plain shell: no "Send notes to" entry, no title-derived
sidebar row, and a title that the runtime's agent-presence check scored as
neutral. Identity already resolved (getAgentLabel returns OpenCode); only
activity was missing.

Treat the native marker as a live idle agent, placed after the spinner and
glyph checks so the decorated frames pinned by #8940 keep their status, and
accept it in the send-readiness gate the way Claude's U+2733 prefix is
accepted -- only a running OpenCode TUI ever publishes it.

* fix(agents): require spaced `OC | ` marker for native OpenCode detection

Unspaced pipes like `OC|Build` match other tools and would mistakenly
route non-OpenCode panes as send targets. Enforce literal ` | ` as
OpenCode emits it. Also clarify that only spinner decorations carry
working status, not keywords in the session summary.

* fix(agents): require OpenCode foreground process to validate native mark

OpenCode's native `OC | ` title marker now requires an active OpenCode process
to authorize agent sends, preventing false detection when the marker is left on
shell prompts. Extends wrapper prefix matching (ssh, tmux, etc.) and spinner
glyph support. Adds permission-prompt blocking signals for guarded writes.
2026-08-12 00:07:59 -07:00
Jinwoo Hong fd2afc16c8 Fix paired web Add Project folder browsing (#13885) 2026-08-11 23:25:50 -07:00
Neil 22b7ac0d9b refactor(ssh): delete the pane-binding fold; its premise was false
The migration moved legacy `ssh:<target>` pane bindings into `local` and cleared
them, on the theory that the ssh partition was a stale spill of the desktop
plane's state. Investigation of both hypotheses the team raised disproved that:

- NOT cross-version compat. The introducing commit says it is for CONCURRENT
  multi-host. Nothing about the partition crosses the wire (zero references under
  src/main/runtime/rpc/), and the payload that does cross the SSH boundary —
  RemoteWorkspaceSnapshot — is projected from `local`. The only downgrade-compat
  comment protects `local`, the other direction.
- NOT multi-client. PersistedState is one file on one machine; phones and CLI are
  RPC clients into that same process. Orca's real per-client state is
  `mobileClientTabSelectionsByDeviceId`, 13 lines above in the same struct, and
  it carries selections only — never a ptyId.

What it actually is: the headless/CLI/mobile plane's OWN home, written and read
deliberately across several tickets (STA-3463, STA-3465), with tests that assert
that partition by name. So the fold was not tidying a spill — it was erasing
another plane's live state. Measured symptom: a split SSH tab would disappear
from mobile while its shell kept running.

The desktop plane's fix never needed it. Supersession multiplied panes because
`durablyBoundPtyIdForPane` hedged into the other plane's copy; reading `local`
alone is the fix, and it stands without any migration.

Deleting rather than redesigning, because the redesign had no target: five review
rounds each found a defect in that function, three of them inside the previous
round's fix, and every one was a cell of a merge matrix that only existed to
serve a premise that was false.

The `boundPtyIdsAcrossPartitions` clause is INVERTED, not dropped. It required
the two partitions to AGREE after load — which encoded the false premise, and is
what made a migration look necessary. It now asserts the narrower, stronger
property: what the desktop plane resolves follows `local` alone, whatever the
other plane holds.

Also proven, and the reason unification was NOT attempted: worktreeId is
`<repoId>::<path>` where repoId is a randomUUID minted client-side at repo add
(orca-runtime.ts:18722), so two servers cannot collide on one. Host scoping was
not protecting against that here — but the two planes' opposite choices are each
deliberate and each test-pinned, so choosing a winner is an architecture call,
not a cleanup.

Net production: -75 lines.
2026-08-11 23:23:42 -07:00
Jinwoo Hong c45c806220 fix(orchestration): close federated-read benchmark socket (#13939) 2026-08-11 23:19:07 -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
Brennan BensonandOrcaWin 4b9fb46646 fix(agent-hooks): stop rewriting typed Prime commands (#13906)
* fix(agent-hooks): preserve typed Prime commands

* test(wsl): enforce Prime bridge exclusion

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-08-11 22:34:54 -07:00
Neil 9e18e1456b fix(workspace-emoji): keep the ':' shortcode suggestions scrolled to the highlight (#13969) 2026-08-11 22:18:26 -07:00
Neil 3d4c968e9b perf(renderer): drop redundant worktree card cache-TTL subscription (#13903)
promptCacheTtlMs was a third store subscription for a field the card
already reads via foundation.settings; derive it instead.

Does NOT bundle the 26 foundation selectors as originally planned.
Measured on this store with React (150 cards, 400 unrelated writes,
26 keys): 26 separate useAppStore hooks 100.7ms; one useShallow bundle
402.5ms (4.0x worse); one bundle with a non-allocating keyed compare
141.5ms (1.4x worse). zustand v5 shallow() rebuilds two Maps from
Object.entries per compare, and a bundle must still build the whole
projection every notification, so collapsing cheap selectors only
trades away useSyncExternalStore dispatches that are cheaper than the
object it allocates. Listener count would have improved 26 -> 1 while
the hot path got slower.
2026-08-11 22:13:39 -07:00
NeilandOrca 971d9548b5 docs(agent-status): make design references self-contained (#13902)
docs/design/agent-status-over-ssh.md was cited from ~10 source files but
does not exist in the repo. Replace each pointer with the invariant the
code actually relies on so the knowledge survives without the doc.

Renderer-side citations (useIpcEvents.ts, agent-status-types.ts) are left
for the concurrent batching change that owns those files.

Co-authored-by: Orca <help@stably.ai>
2026-08-11 22:13:35 -07:00
NeilandOrca 0079623f26 Fix reveal active workspace needing multiple clicks (#13926)
Co-authored-by: Orca <help@stably.ai>
2026-08-11 22:10:44 -07:00
Jinjing 5bee7b5ce9 P1 STA 3887 design Preview Kitty IME (#13940)
* fix(terminal): carry kitty flags through Preview snapshots and pair rele

Preview was omitting the live kitty mirror from the IME bridge and dropping kitty flags from snapshots, so every commit was evaluated at flags 0. A TUI that negotiated bit-3 (report_all_keys_as_escape_codes) would receive the legacy raw text it declined.

Now the snapshot carries proven kitty flags beside their sequence boundary, the forwarder reads flags once per commit, and bit-1 (report_event_types) commits are paired with exactly one release regardless of keyup/insertText ordering. Snapshot authorities expose only the active screen's proven flags, so an old host's absent field stays unknown rather than downgraded to a manufactured zero.

* fix(terminal): sync kitty flags and IME releases across snapshots

* trim wordinesss

* fix(terminal): settle owed IME release before fresh same-key press

When a keyup is lost and the same key is pressed again, settle the stale
record's owed release instead of discarding it — this maintains correct
IME state during recovery. Also refine Kitty flag propagation to only
carry proven baselines across snapshots, and tighten related comments.

* fix(terminal): gate kitty flags on sequence boundaries

- Remote snapshots only include flags when seq is present
- Daemon uses parsed flags value when defined
- Ensures correct flag ordering in snapshot replay
2026-08-11 22:00:44 -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
Brennan Benson 137e724119 fix(agent-title): treat Claude Code quarter-circle spinners as working (#13889) (#13925)
* fix(agent-title): treat Claude Code quarter-circle spinners as working

Claude Code 2.1.228 swapped its busy OSC title spinner from braille
(U+2800-U+28FF) to quarter circles (U+25D0/U+25D1). Orca recognized a busy
Claude title only by braille codepoints, so the new frames matched nothing.

The summary-bearing busy frame ("<glyph> Say hi in one word") carries no
"claude" name token, so it resolved to no-status. The tracker's "idle or
permission followed by no-status means the agent exited" rule then fired
mid-turn, confirmPtyAgentExit confirmed it, and the chat surface routed
exitChat -- kicking the tab to the terminal view on every message.

Widen the accepted glyph set via a shared containsAgentSpinnerGlyph helper.
Agent-specific braille frame shapes (Grok, Pi, synthetic Cursor) stay pinned
to their own glyph set.

Fixes #13889

* fix(agent-title): satisfy static analysis and trim scope
2026-08-11 21:13:27 -07:00
948d85cabc fix(terminal): rank cold-park recency by activation order, not random UUID (#13881)
* fix(terminal): rank cold-park recency by activation order, not random UUID

The keep-warm exemption (#8262) broke equal hiddenSinceMs with
id.localeCompare over UUIDv4 tab ids, so which tab stays warm was a coin
flip. Ties are routine, not exotic: use-terminal-tab-cold-parking takes one
Date.now() per effect pass (#7214) and stamps every tab first seen hidden in
that pass with it, so switching away from a worktree ties all of them. The
tab that wins the flip is skipped by selectIdsBeyondHotRetain and never parks.

Ranks by an explicit activation sequence recorded on the hidden->visible edge.
Unit test covers the tie; red before, green after.

Does NOT fix terminal-hidden-view-parking.spec.ts:467, which still fails with
the same 'did not park' error on CI. Shipping this on its own merit.

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

* fix(terminal): preserve focused tab across parking ties

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: E2E Test <e2e@test.local>
2026-08-11 21:07:57 -07:00
Neil e5971365e4 Update pull_request_template.md 2026-08-11 20:22:17 -07:00
Jinwoo HongandE2E Test 7a58f72814 Fix remote-runtime terminal pane split authority (#13867)
* fix(runtime): preserve remote terminal pane splits

* test(runtime): tighten split authority guards

* fix(runtime): revalidate transient split sources

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-08-11 20:09:13 -07:00
Jinwoo Hong b51ef400ec fix(quick-commands): flatten the settings list and give the command editor room (#13922) 2026-08-11 19:53:22 -07:00
NeilandOrca 64aec94cb2 fix(sidebar): give draft reviews their own glyph instead of a red PR icon (#13919)
* fix(sidebar): give draft reviews their own glyph instead of a red PR icon

The worktree card review icon used one PR glyph for every state and
tinted it by check status, so a draft with failing checks rendered as a
red PR icon that read as closed. Shape now carries review state
(draft/closed/merged), and check tone applies only to open reviews so
the glyph agrees with its tooltip. Same fix for the source-control
header icon, where draft and closed differed only by a muted tone.

Closes #13088

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

* test(sidebar): pin stateless and GitLab-closed review icon behaviour

Documents why a stateless row keeps its check tone (folder cards render
one while a linked review loads or its details fail) and covers the
closed-MR glyph override, which previously read as already-merged.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-11 19:26:53 -07:00
Neil 444638d96b Revise pull request template for clarity and updates
Updated the pull request template to simplify language and clarify sections. Added new sections for AI disclosure and testing instructions.
2026-08-11 18:40:27 -07:00
Neil ebcb2b6a60 docs: expand PR template with ELI5, before/after screenshots, and X handle
Make the pull request template clearer for contributors: plain-language
ELI5, what/why, mandatory before/after UI proof, testing checklist, AI
disclosure, and an Author X field. Align CONTRIBUTING with the template.
2026-08-11 18:29:01 -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
JinjingandOrca 4c2a10d157 Fix smart sort ranking of done agents by completion time (#13899)
* Fix smart sort ranking of done agents by completion time

Completed entries stayed in the Done sort class indefinitely when
same-state writes refreshed updatedAt without moving stateStartedAt.
Introduce agentEntryCompletionAt() to use actual completion time for
both age display and sort eligibility, ensuring consistent aging
regardless of hook updates.

* Fix smart sort ranking of done agents by completion time

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-11 18:11:34 -07:00
Jinjing 96c17b44d5 Resolve threads as primary acknowledgement, not replies (#13894)
Collapsed threads visibly close on the host — this is the
acknowledgement. An extra reply only adds noise. Snapshot the
resolution target at launch to prevent silent drops if the panel
navigates before agent delivery. Only reply for comments without
resolution endpoints.
2026-08-11 17:38:15 -07:00
Jinjing 675400e318 fix(automations): give list table more breathing room (#13898)
Increase row height and the gap above the table so the Automations list feels less dense.
2026-08-11 17:33:42 -07:00
Neil 0824351fe0 fix(bitbucket): commit credentials atomically and stop blaming the token for network faults (#13887)
* fix(bitbucket): commit credentials atomically and stop blaming the token for network faults

STA-3941 (P0): a credential edit could destroy the last working pair.
`writeFileSync` replaces in place, so a failed or interrupted write truncated
the previous secret, and the secret and metadata files were published
independently — a crash between them left a new secret paired with the old
email, unusable on restart.

- Write credential files through a temp + fsync + rename, so a reader sees
  either the old bytes or the complete new ones, never a truncated file.
- Carry authMode/email/baseUrl inside the encrypted envelope and treat the
  plaintext metadata as display-only. A torn write can now only leave a stale
  displayed account, never an unusable credential. Envelopes written before
  this change still resolve through the metadata fallback.

STA-3944 (P2): timeouts, DNS failures, 5xx and unparseable bodies collapsed to
the same miss as a 401, so Orca told users their credentials were invalid when
the host was simply unreachable — sending them to regenerate a working token.
`/user` now reports rejected vs unreachable, connect explains which happened,
and an unreachable host no longer renders as "Auth failed".

Adds failure-injection coverage for a partial secret write, an interrupt
between publishing the secret and the metadata, legacy envelopes, and the
transport-vs-auth split.

* fix(bitbucket): loop short writes and keep eligibility out of the keychain

Review of the atomic writer found it ignored writeSync's return value. write(2)
may return a short count, so a partial buffer could be fsynced and renamed into
place — the same truncation the change exists to prevent, moved one step later.
Write now loops to completion, with a test that fails without it.

Also stops isBitbucketReviewCreationAuthenticated force-decrypting the stored
secret. Create-PR eligibility is evaluated proactively for the sidebar, so that
popped an OS keychain prompt just from opening a worktree. Presence is enough;
creation itself still fails closed on an unusable credential.
2026-08-11 17:29:22 -07:00
JinjingandOrca 0c13e51097 feat(pr-comments): sort grouped PR comment sections newest-first (#13893)
Grouped/triage sections now read newest-first so recent discussion surfaces
at the top, while Timeline keeps its oldest-first history order.

- rename sortPRCommentGroupsForTimeline to sortPRCommentGroupsByRecency and
  add an order parameter
- rank threads by their latest activity under newest-first so a fresh reply
  refreshes an old thread
- break timestamp ties by numeric comment id in the sort direction, so a
  GitHub review batch sharing one timestamp orders correctly
- sink groups with an unparseable createdAt to the bottom in both orders

Co-authored-by: Orca <help@stably.ai>
2026-08-11 17:26:48 -07:00