Commit Graph
8250 Commits
Author SHA1 Message Date
Brennan Benson 5435cf7257 Join structured attach teardown through journal bind 2026-08-09 22:42:02 -07:00
Brennan Benson 8623f12957 Fix paired mobile structured session creation 2026-08-09 22:34:13 -07:00
Brennan Benson 73c7f95457 Join structured session teardown work 2026-08-09 22:05:07 -07:00
Brennan Benson cbd79a61e9 Keep mutation context updates out of render 2026-08-09 21:39:10 -07:00
Brennan Benson dcd26ef677 Close remaining structured mobile RC blockers 2026-08-09 21:35:24 -07:00
Brennan Benson ec917184c3 Satisfy the changed-line React Doctor gate 2026-08-09 20:18:43 -07:00
Brennan Benson 19df0ced4c Align mobile wire fixtures with create intents 2026-08-09 20:08:32 -07:00
Brennan Benson 8778fdedad Harden structured mobile writes before RC 2026-08-09 19:58:58 -07:00
Brennan Benson bbc6aba927 Resolve structured mobile read review blockers 2026-08-09 19:58:23 -07:00
Brennan Benson 59ad53fecf Add mobile structured session write flow 2026-08-09 19:11:12 -07:00
Brennan Benson b4bcd20caa Add mobile structured session read flow 2026-08-09 18:22:23 -07:00
Brennan Benson 76cce58478 Drive a structured Codex session end to end over agentSession.*
One test walks create, send, stream, approval, cancel, reconnect, and paged
history against the real dispatcher, the real schemas, the real capability
gate, the real record store, journal, lease, adapter, and translation. Only
the Codex child is scripted, and it answers the same JSON-RPC calls the real
app-server does.

It is the first thing in the stack that proves the parts fit: that the
provider's echo of a user message reconciles into the bubble the client
already has instead of appending a second copy, that a client reconnecting
from its cursor replays only what it missed while a takeover advances the
fence and reaps the old child, and that the whole conversation reconstructs
from the durable journal alone with no page overlapping another.
2026-08-09 16:54:49 -07:00
Brennan Benson b4ed53f7c6 Install the structured agent-session host on first use
The record store and the journals live under the profile's user-data path,
which is not final until Electron is ready, and a runtime that never serves a
structured session should not pay for a store it will never read. So the host
is built by the first `agentSession.create` or `.ensure` rather than at
startup, and the memoized slot clears on failure so the next call retries.

The capability gate runs first: building the store and spawning a child is an
observable side effect, and a client that cannot read structured sessions must
not be able to cause it.

Owner probing is scoped to this host. Checking another host's pid against this
machine's process table is exactly how a live owner gets declared dead, so a
foreign owner reports `indeterminate` and routes to manual recovery instead.
So does a reservation that named no process — freeing it needs positive proof
that nothing spawned under its token, and this host has no spawn-token process
scan yet.
2026-08-09 16:54:49 -07:00
Brennan Benson d28861f718 Resolve a Codex launch from the durable session record
Command, cwd, pinned account home, and the thread to resume all come back
out of the record the lease already wrote, never from the client-declared
identity: a caller that could name the thread could point a session at
someone else's conversation.

It refuses rather than guesses — a record pinned to another execution host or
a WSL distro is not this host's to spawn for, a record for another provider or
another account-home variable is not this adapter's to speak for, and a
workspace that no longer resolves surfaces instead of falling back to a
default cwd.
2026-08-09 16:54:49 -07:00
Brennan Benson 61cb67c7c5 Translate Codex app-server events into journal rows
The adapter now writes what Codex streams straight into the session's
journal, so nothing between the child and the durable record has to remember
state: streamed text folds through the coalescing window, lifecycle events
flush it first, and every write is a highest-revision-wins upsert on the
durable key, which is what lets a completed body land on the row its own
deltas opened.

Journal first, observers second — a tap must never see a row the journal has
not been told about. A session with no sink still runs, unjournaled, which is
how the existing adapter tests keep working unchanged.

Starting or resuming the thread moved into its own module on the way past the
adapter's line cap; the check it carries is unchanged, and still refuses a
resume that lands on a different thread, since recording that would make the
durable handle chain lie about what the session proved.
2026-08-09 16:54:49 -07:00
Brennan Benson 115ef40f72 Key a Codex journal row by thread, turn, and ordinal
Codex item ids are not stable across resume, so a journal keyed by them
cannot reconcile a recovered thread against a live one: the same message
would arrive under a new id and append a second copy of itself.

The durable key is instead the position the item holds — thread, turn, and
ordinal within that turn, counting message items only. Non-message items get
no ordinal at all rather than a number from a second counter, because a
counter a resumed history cannot reproduce is worse than no key: it looks
reconcilable and reconciles wrongly. Those rows fall back to a live-session
namespace that a recovered journal simply will not contain, which is correct,
since Codex does not persist them either.

Approvals and questions become journal rows the same way, naming the command
the item already announced so the user is not asked to approve an opaque id,
and one row per question rather than one row per request.
2026-08-09 16:49:06 -07:00
Brennan Benson 5b34f3116f Coalesce streamed agent text on a short server-side window
Providers emit one notification per token. Journaling each one writes a row
and wakes every subscriber per token, so one long answer costs thousands of
appends and thousands of stream frames on a phone.

Deltas accumulate and flush on a 60ms window, and the row carries a SNAPSHOT
of the text so far rather than the increment — which is also what makes
dropping intermediate frames safe for a reconnecting client. All streams share
one timer, so the deadline bounds latency the same way at one wakeup per
window instead of one per stream. Lifecycle bypasses the window by flushing
first, so nothing can be journaled ahead of the text that preceded it.
2026-08-09 16:49:06 -07:00
Brennan Benson 4a9004f0b8 Buffer the provider events an adapter streams before its journal exists
A provider starts streaming the moment its process exists, and that moment
is inside `adapter.acquire` — before the journal is open and before the host
has registered the session. The adapter now receives a deferred sink whose
writes queue in arrival order and drain once the journal is bound.

One sink lives for the session rather than for one acquisition, so an adapter
can hold it across a re-acquire without noticing the new journal object. It is
bound last in `onAttached` so nothing the provider streamed can interleave with
the attach result the client is about to read, and a refused attach that never
reached a journal closes it instead of leaving writes nothing will drain.

The host was one line under the 300-line cap before this, so the mutation
admission pipeline moved to its own module. That is a straight extraction:
recompute fingerprint, admit through the ledger, check the lease, run the
plan — the same route, now somewhere a new method cannot sit next to.
2026-08-09 16:49:06 -07:00
Brennan Benson 4d70b189de Harden the Codex app-server session against partial acquisition
Six findings from the review of the structured Codex session, all of them
about a session that is live in the map while the thing it names is not:

- A prompt is addressed by its own approval id, not by the tool item it is
  about. A shell bridge re-asks per command under one parent item, and the
  second ask used to evict the first, leaving that turn blocked on a request
  nobody could answer.
- A subagent runs its own thread over the same connection. Its `turn/started`
  no longer satisfies the root thread's waiter, and every event now carries the
  thread it actually came from.
- Publication is one step at the end of acquire: the process identity is in
  hand, the child is still alive, and the attempt still owns the session. A
  failure reaps only its own child, so a replacement already published keeps
  running.
- Codex talks before its thread is named. Those events wait in the acquisition
  window and are delivered in arrival order once the session is published,
  instead of being dropped.
- A broken stdin pipe ends the transport rather than one write: the child is
  reaped and the owner learns its lease is worthless.
- A death that reaches us through several listeners reports once, keeping the
  first cause rather than the generic exit that follows it.
2026-08-09 16:48:51 -07:00
Brennan Benson b6c98f8ca5 Run a Codex session under the agent-session lease
Implements the structured agent-session adapter for Codex: one long-lived
app-server child per session, started or resumed under the lease the host
already reserved, reporting the process identity the lease re-proves and the
provider handle link the journal binds to.

The thread the adapter reports is the one Codex actually opened, never the one
a client declared, and a resume that lands on a different thread is refused
rather than recorded as a resume. A turn is named from the start response when
Codex sends an id there and from the turn-started notification when it acks
first; when neither names it in time the submission settles unknown, which the
journal renders as delivery unconfirmed rather than failure. Only a refusal
Codex actually answered becomes a rejection.

Model, effort, and policy are per-turn overrides — there is no thread-settings
write — so an option set between turns is applied to the next turn/start, and
unlisted keys are refused instead of becoming arbitrary client-controlled
parameters.
2026-08-09 14:54:39 -07:00
Brennan Benson a32e11bd08 Reply to Codex approval and input requests
Codex blocks a turn until every approval and tool-input request it sent is
answered, but the answer arrives much later and through a durable journal item
id rather than the live JSON-RPC request id. This holds the live requests for a
session and turns a chosen option back into the reply Codex expects.

Three cases the journal boundary forces: one request can carry several
questions but takes a single reply, so answers are buffered until the request is
complete; a request this build does not model is refused rather than left
blocking or silently approved; and a prompt is forgotten before its reply is
written, so a second answer finds nothing instead of replying twice.
2026-08-09 14:51:36 -07:00
Brennan Benson 8b86ac374f Open a long-lived codex app-server connection
The existing app-server client is request-scoped: one deadline for the whole
session, and every inbound message that is not a pending response is dropped. A
structured chat session needs the opposite — a child that outlives any single
call, per-request deadlines, and both traffic directions, because Codex streams
a turn as notifications and asks for approvals by sending requests back.

Adds a connection that spawns the child, completes the initialize handshake,
and stays open until closed, keeping the same environment overlay and reap
semantics as the short-lived client. A refusal is now distinguishable from a
call that never settled, so callers can tell "Codex declined" from "the child
died" instead of reporting both as failure.
2026-08-09 14:51:22 -07:00
Brennan Benson 2d66ded357 Merge remote-tracking branch 'origin/brennanb2025/native-chat-1d-wire' into brennanb2025/native-chat-structured-base 2026-08-09 14:16:37 -07:00
Brennan Benson 3e4fa050a1 Harden structured agent-session wire recovery 2026-08-09 14:12:33 -07:00
Brennan Benson c82bcbbe6b Reuse the shared lease fixture in the envelope tests
The hand-written lease drifted the moment the record grew a field. Same reason
the fixture exists for the write-admission tests.
2026-08-09 13:36:08 -07:00
Brennan Benson 1a4cea1fd1 Merge remote-tracking branch 'origin/brennanb2025/native-chat-structured-base' into brennanb2025/native-chat-1d-wire 2026-08-09 13:35:34 -07:00
Brennan Benson 775fee768e Pair the agent-session wire across versions
Extends the cross-version harness, which covered the terminal stream only, with
the structured surface. The old side's advertised capabilities and registered
method names are read from the extracted release checkout rather than written by
hand: a build cannot advertise a string its own source never names, so "the old
side does not have it" stays a fact about a real release.
2026-08-09 13:35:30 -07:00
Brennan Benson 7924a339e0 Publish agentSession.* behind the structured capability
Ten methods on the runtime manifest, including a paged `agentSession.history`
over the journal cursor and a subscribe that streams cursor-qualified batches.
A declared client that did not advertise `agent-session.structured.v1` is told
the whole surface does not exist, so an old client never sees a session it
cannot render; an in-process caller negotiates nothing and is served.
2026-08-09 13:35:28 -07:00
Brennan Benson fc36d58bbb Add the structured agent-session host
One place where the lease, the journal, and the provider adapter meet: every
mutating call recomputes the fingerprint, admits through the durable operation
ledger, checks the lease, then acts, so no method can grow its own admission
rules. Journal recovery rehydrates an unreadable journal from provider history
into a fresh epoch and tells subscribers which reset they are seeing.

Attach adjudicates the leases it loaded from disk before granting any writer.
Without that step every lease stays unreconciled for the life of the process and
a restarted host refuses every persisted session forever.
2026-08-09 13:35:19 -07:00
Brennan Benson a0d829194e Define the structured agent-session wire contract
Adds the request/refusal shapes every `agentSession.*` method speaks, the
four-field idempotency envelope and its lease admission, the item-key inverse an
upsert needs, and the `agent-session.structured.v1` capability. The protocol
version stays at 3: the surface is additive, and bumping it would strand every
paired device on the current release rather than degrade one feature.
2026-08-09 13:35:08 -07:00
Brennan Benson 2cda9b8870 Merge remote-tracking branch 'origin/brennanb2025/native-chat-1b-lease-enforce' into brennanb2025/native-chat-structured-base 2026-08-09 13:19:49 -07:00
Brennan Benson 0f97a9414e Fence PTY writes across async admission gaps 2026-08-09 13:14:41 -07:00
Brennan Benson c69b12b3dd Enforce the agent-session lease at every PTY write choke point
Route every byte that can reach a PTY through the single-writer lease
adjudicator, so a session owned by one runtime cannot be typed into by
another. Nothing binds a PTY to a durable session record yet, so the gate
short-circuits and today's shells, legacy agent terminals, and unbound
sessions are untouched.

Gated paths: terminal.send RPC, the renderer IPC write paths
(pty:write / pty:writeAccepted), plugin sendTerminalText, the agent-prompt
paste, and ptyController.write as a backstop for runtime writes that never
passed a typed gate. Every yield inside a write - inter-chunk, the
text/suffix pause, the agent-prompt submit delay, deferred size
measurement - re-checks the fence, so a handoff mid-paste stops the
remaining chunks instead of interleaving two owners.

Refusals are typed and additive on the wire: an optional
agentSessionRefusal on RuntimeTerminalSend and on the existing
pty:writeUnavailable payload, refusal codes drawn only from the existing
agent-session error codes, and stream-input refusals mapped onto the
WriteUnavailable frame old clients already decode.
2026-08-09 13:00:56 -07:00
Brennan Benson a2b20acfcc Merge branch 'brennanb2025/native-chat-1c-journal' into brennanb2025/native-chat-structured-base 2026-08-09 12:29:13 -07:00
Brennan Benson ce06e5055b Merge branch 'brennanb2025/native-chat-1a-session-store' into brennanb2025/native-chat-structured-base 2026-08-09 12:29:13 -07:00
Jinjing 082cc31703 Exclude idle current tabs from Cmd+J recent; add live attention badges (#13299)
* Show attention badges on recent chats in Cmd+J palette

Keep current tabs visible only when they have a scannable badge
(working, permission, unread, done). Snapshot unread maps alongside
status maps to freeze recent-section membership on open instead of
churning with live updates. Unify badge logic across tab strip and
palette, and extract test fixtures for reuse between suites.

* Exclude idle current tabs from Cmd+J recent; add live attention badges

Current tabs no longer appear in Recent Chats when idle—only working, blocked, or
unread agents keep the current slot visible. `done` no longer admits current tabs;
the user watched it complete on screen, so that slot goes elsewhere.

Add live attention badges to recent rows (working/permission/unread/done), matching
the tab-bar ladder. Freeze recent-section membership at open-time to keep row order
and inclusion synchronized instead of changing live.

* minor fix
2026-08-09 12:18:14 -07:00
Jinwoo HongandJinwoo-H 394e4bf1c0 Polish Artifacts management UI (#13356)
* refactor(artifacts): polish artifact management UI

* fix(artifacts): address UI polish review

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-09 12:11:33 -07:00
Jinwoo Hong bd9addb449 fix(tasks): restore GitHub page and scroll position on reopen (#13096) 2026-08-09 11:08:04 -07:00
Neilandbbingz 34f2a62cda fix(pty): stop color-scheme 997 replies from painting cooked prompts (#13309)
Route cooked-echo-risk terminal replies through bounded echo-safe delivery across local, daemon, and SSH relay PTYs. Preserve repeated valid replies, bypass the daemon startup input gate, and keep ordinary input plus latency-critical replies on their existing paths.

Closes #13137

Co-authored-by: bbingz <zzb@gxsmjx.com>
2026-08-09 01:45:15 -07:00
Neil 9f7522dee9 fix(workspace): stabilize smart entry keyboard flow (#13319) 2026-08-09 01:36:32 -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
Neil c6ded160b2 Revert the Korean Won to backquote mapping (#13312)
* Revert "fix(terminal): detach the Korean input-source probe when its setting goes off (#13283)"

This reverts commit 5a84dbb564.

* Revert "fix(terminal): stop the Korean gate caching an unknown input source as negative (#13182)"

This reverts commit 434959965a.

* Revert "perf(terminal): gate the Korean input-source probe on its setting (#13181)"

This reverts commit 42fc5375e8.

* Revert "feat(terminal): Korean Won (₩) → backquote key mapping for Korean keyboards (#13104)"

This reverts commit 24003936a5.
2026-08-09 00:44:35 -07:00
Neil 78f434dd85 fix(agents): deliver grok launch drafts on its composer frame (8s → 0.7s) (#13308)
* fix(agents): deliver grok launch drafts on its composer frame

Grok has no --prefill-style flag, so a launch draft (e.g. the issue URL of
a worktree created from a GitHub issue) always goes through Orca's
paste-after-ready path. That path used the default readiness signal: DECSET
2004 plus 1.5s of PTY silence. Grok shimmers its startup logo at ~12fps
until the session opens, so the quiet window never settled and the draft
fell through to the 8s hard timeout before it appeared in the composer.

Gate grok on its own composer glyph instead, anchored on the alternate-screen
switch rather than DECSET 2004: the shell that runs the launch command emits
2004 too, and its prompt may itself be the same glyph (starship, pure), so a
Codex-style anchor could paste into the shell. Grok keeps the quiet window
armed as a fallback because it renders differentially and paints the glyph
once, so a late-attaching scanner would otherwise wait out the hard timeout.

Measured against grok 1.0.0 driving the real scanner over a zsh -> grok PTY:
draft delivery moves from 8003ms to 689ms, with the URL landing unsubmitted
in the composer exactly as before.

* fix(agents): keep grok's quiet-window floor on DECSET 2004

The composer-glyph marker is anchored on the alternate-screen switch, but grok
can render inline (`--no-alt-screen`, `--minimal`, `[ui] screen_mode =
"minimal"`), where 1049h never arrives. Anchoring the quiet-window fallback
there too left those launches with no delivery path at all: readiness never
resolved, and the main-process caller drops the draft when it resolves null —
so the issue URL vanished instead of arriving late.

Give the signal two independent anchors: the marker still waits for the
alt-screen switch (so a starship/pure shell prompt can't trip it), while the
quiet window arms off DECSET 2004 exactly as the default signal does. Inline and
legacy-Windows-console launches keep their pre-existing timing; alt-screen
launches keep the fast marker path.

Verified on grok 1.0.0 over a real zsh -> grok PTY: alt-screen delivers at 687ms
via the marker, inline at 1949ms via the quiet window (the default signal
measures 1861ms on the same launch), URL landing unsubmitted in both. Adds a
recorded inline-mode trace fixture so the no-1049h path stays covered.

* fix(agents): revoke grok's alt-screen anchor when the screen is handed back

The composer-glyph anchor latched forever: once \x1b[?1049h had been seen, any
later `❯` counted as grok's composer. Two ways that pastes the launch draft into
the user's shell instead of into grok:

  - grok enters the alternate screen and then dies before painting a composer;
    the shell prompt that follows is `❯` under starship or pure.
  - a pager or editor started from the user's shell rc enters and leaves the
    alternate screen before grok is ever launched, arming the anchor against the
    shell's own prompt.

Track the anchor in stream order instead of as a latch: \x1b[?1049l revokes it,
re-entering re-arms it, and a marker only counts inside a segment where the
anchor is actually held. The chunk is walked segment by segment so ordering
within a single PTY packet is honored, with a 7-char carry — one short of the
escape sequence — so a split sequence rejoins without re-walking scanned output
into a second transition. Signals with no `markerAnchorEnd` (codex, opencode,
the default) keep their existing latch semantics untouched.

Also makes the trace-replay test model the hard timeout: the real waiters settle
at 8s, so a marker landing after that is not a delivery time.
2026-08-09 00:06:55 -07:00
NeilandOrca e172a51649 test(terminal): assert preedit visibility across the recorded IME traces (#13286)
Two IME defects shipped past this suite because every assertion here was about
bytes reaching the PTY. A preedit written into a hidden overlay types blind and
still satisfies all of them.

Samples the composition overlay at each recorded compositionupdate and requires
it to be shown, so the existing recorded corpus now covers what the user sees
rather than only what the shell receives.

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:31:44 -07:00
NeilandOrca 5a84dbb564 fix(terminal): detach the Korean input-source probe when its setting goes off (#13283)
Gating the prefetch call site stopped the probe from ever attaching, but not
from surviving. prefetchKoreanInputSource installs global focus/keydown/keyup
listeners behind an idempotent listenerAttached guard, and the only teardown
was test-only — so disabling the setting mid-session left the listeners live
and still spawning defaults export | plutil | plutil on keyboard activity.

Syncs in the effect body rather than its cleanup: cleanup also runs on tab
switches and on any dep-identity change, so disposing there would detach and
re-probe constantly.

Also replaces the initial-probe abort guard with an epoch. It compared window
identity, which is dead code in production where window is a singleton, so a
stop/start could let a sleeping probe loop wake and race the new one.

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:20:59 -07:00
NeilandOrca 17cfc968cf Revert the terminal IME composition-ownership change (#13282)
* Revert "test(ime): restore coverage the composition-ownership change removed (#13168)"

This reverts commit 25a8c517e1.

* Revert "refactor(terminal): return IME composition ownership to xterm (#13128)"

This reverts commit 17b3dff3c4.

* test(ime): keep the architecture-neutral Korean trace coverage

The recorded IBus/fcitx5 and Windows MS-Korean traces from #13168 assert PTY
byte order, not composition ownership, so they still hold once the terminal
composition layer is restored. The mobile accessory-order test pinned the new
handleLiveInputChange signature and does not.

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

* fix(terminal): keep the macOS Backslash bypass through the revert

The restored native-text forwarder only claims keys for input sources in its
hardcoded CJK allowlist, so third-party IMEs off that list (Qingg, #10896) still
get a raw backslash. #13128 added this bypass as a partial replacement; keep it
rather than trade the open issue back.

Scoped to the bare backslash key. The rest of shouldBypassXtermForMacNativeText
bypassed all unmodified non-ASCII text, which would race the restored forwarder.

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

* fix(mobile): move the mirror-step ref write out of render

The restored hook assigned runMirrorStepRef during render, which is not
replay-safe — React can discard render work, so the mutation can leak from UI
that never commits. Its only read is inside the held-commit timer, which fires
long after commit, and the ref has a safe default, so an effect is soon enough.

Surfaced by the changed-lines React Doctor gate: the rule postdates this code,
so restoring the file re-introduced it as a new violation.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-08 19:06:50 -07:00
Jinjing 17eefef502 [Tabs] Preserve host routing and reduce search churn (#13114)
* fix tab search host routing and churn

* Fix open-tab search to resolve hosts from worktree when active host unkn

- Use worktree.hostId to resolve execution host instead of defaulting to LOCAL_EXECUTION_HOST_ID
- Correctly populate search results for remote-only worktrees when activeWorkspaceExecutionHostId is null
- Remove automatic focus of terminal tabs after search activation

* Prevent stale tab results when user keeps typing ahead of deferred searc

- useOpenTabSearch now returns {query, results} to track which query the results describe
- Gate tab results on query match so stale results don't appear on user's screen
- Add live region (role=status) for accessibility of tab switch error messages
- Distinguish missing-worktree from missing-page errors in browser page activation
- Improve host resolution to prefer active host when worktree and repo don't specify one

* Re-pin entry to deferred tab results that rank higher

Track whether selection auto-follows the top-ranked result or was
manually positioned. Re-pin entry to tabs when they rank higher,
but preserve manual selection.

* Consolidate browser focus requests and simplify selection state

- Extract requestBrowserFocus to handle queueing + event dispatch atomically
- Simplify omnibox selection tracking with single pinnedOptionId state
- Optimize host resolution in tab search to compute once per query

* Report dead browser workspaces correctly and fold dedupe case by host

Two readiness-checklist fixes for open-tab search:

- Browser page activation checked page/workspace before the worktree, but
  deleting a worktree purges its browser workspaces and pages too, so a dead
  workspace surfaced as "Browser page no longer exists". Check the worktree
  first; routing already maps missing-worktree to the workspace wording.

- Editor-tab/file dedupe compared paths with separator normalization only, so
  a Windows worktree offered both "Switch to tab" and "Open file" for the same
  path in different case. Fold by the worktree path's syntax via the new
  isCaseInsensitiveRuntimeRoot, keeping WSL, POSIX and SSH roots case-sensitive,
  and add NFC so a macOS NFD listing matches an editor's composed path.

* Fix tab deduplication and resolve worktree host collisions

- Only editor tabs should suppress file entries; check contentType instead
  of relying on path being empty for non-editor tabs.
- Add executionHostId to simulator search results to disambiguate when
  the same worktree id exists on multiple execution hosts.
2026-08-08 17:46:11 -07:00
Jinjing 8a773a5e3f Focus search inputs for immediate typing (#13264)
* Focus search inputs for immediate typing

- Autofocus inputs in AutomationListSearchField, SettingsSidebar, and WorktreeParentPickerPopover
- Only autofocus Settings search when opening directly, not via deep-link
- Use modal mode and explicit focus management in popover for proper restoration
- Forward CommandInput ref and add autofocus test coverage

* Restore focus when closing worktree parent picker popover

- Find the nearest focusable ancestor of the anchor row to restore focus
  to instead of letting it drop on the detached input element
- Simplify focus assertion in AutomationListSearchField test to verify
  actual focus behavior rather than autofocus attribute presence
2026-08-08 17:15:43 -07:00
Jinjing 05c30166f4 fix(ci): stop hourly prune from deleting the just-published release (#13266)
* fix(ci): stop hourly prune from deleting the just-published release

Hourly prune sorted non-draft releases by createdAt, but nearly every
orca-hourly release shares one createdAt from bulk import. At the retain
cap, stable sort + reverse put the newest release past the window and
immediately deleted it with --cleanup-tag.

Sort by publishedAt (tagName as tie-break) and hard-skip the tag this run
just published so prune cannot self-delete.

* fix(ci): harden hourly prune protect without retain+1 drift

Review found that skipping only in the delete loop under-prunes when sort
is wrong, and excluding TAG before the retain slice would permanently keep
retain+1 releases. Force this run's tag to the front of the sorted list
before slicing so it always has a retain seat and oldest builds still prune.
Also gate prune on publish_live success and warn if TAG still appears stale.
2026-08-08 16:01:48 -07:00