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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
* 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
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>
* 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#10258Fixes#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>
* 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.
* 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.
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>
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>
* 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>
* 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.
* 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
* 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.
* test(wsl): drive hook-relay reattach bench through real PTY spawn
Follow-up to #13139: stop calling ensureWslHookRelayForReattach from the
benchmark and instead reattach a surviving WSL PTY via main's
registerPtyHandlers path, so a missing or misplaced integration in pty.ts
fails the bench.
* refactor(bench): isolate reattach relay refresh measurement and verify s
- Wrap benchmark in try-finally for reliable cleanup
- Add jiti module graph duplication detection to catch missing pty.ts integration
- Track relay refreshes only during reattach phase to avoid false positives from earlier phases
- Disable agent-status hooks during PTY spawn (reattach path doesn't gate on them)
- Improve error messages and make cleanup safe with optional chaining
* fix(cmd-j): fit large palette on short screens and sync overflow copy
Cap dialog/list height against the viewport so the input, filter chips, and
footer stay visible after the larger Cmd+J shell, and align the English catalog
overflow hint with the multi-primary “scroll or keep typing” wording.
* fix(cmd-j): re-emit section headers for interleaved palette remainder ro
When both open tabs and worktrees overflow their first-screen slice, the
layout interleaves remainder rows. The trailing-section header renders
before the leading remainder, so unlabeled rows read as the wrong section.
Re-emit headers before each remainder with a distinct suffix for React keys.
Refactor type-alias matching into a reusable utility that prefers earliest
match position over declaration order.
* fix(cmd-j): stabilize palette memos and gate heavy builders when closed
P1-a: Move quickActionContext filtering into a useMemo with stable
primitive deps (activeView, activeWorktreeId, sshConnectionStates, etc.)
instead of calling buildQuickActionContext() inline every render — the
fresh object identity defeated the middleItems memo on every keystroke.
P1-b: Guard browserSortedWorktrees, browserPageEntries,
simulatorTabEntries, and workspaceTabEntries on paletteStatusInputsActive
so the always-mounted palette stops rebuilding full open-tab indexes
after every store write while closed.
P2-a: Derive sortedWorktrees from browserSortedWorktrees by filtering
out archived worktrees — both called sortWorktreesSmart with identical
deps, so one sort + filter replaces two.
P2-b: Pre-index agent metadata by tabId once per build via
buildAgentMetadataTabIndex, replacing the O(tabs × map entries) scan in
collectAgentMetadataForTerminal with O(1) lookups per tab.
P2-e: Assert in the interleaved test that rendered selectable row order
matches orderMultiPrimaryPaletteItems, keeping listEntries and the
layout function as a single source of truth.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Allow automation deletion without SSH connection
Remove external source entries from the list and only show jobs.
This allows users to delete and manage automations even when the
remote host is not connected.
* Update test: list jobs from unavailable automation manager
Remove availability constraints (status, error, canManage) from test to
verify jobs can be listed regardless of manager connection state.
* fix(terminal): repair stale-dpr WebGL canvas backing on reveal and fit
When devicePixelRatio changes while a pane is hidden (window moved between
retina and non-retina displays, worktree then revealed), xterm's WebGL
renderer re-measures cell dimensions but its canvas keeps the old backing
store — the addon's device-pixel observer misses changes that land while the
element has no box. The browser composites the stale-scale bitmap into the
css box: half/double-size or smeared text until a manual resize. Reproduced
deterministically (2160px backing behind a 1080px css box at dpr 1) — this is
the mechanism behind the field reports of a normal pane going blurry after
switching back to a worktree.
A repair check now runs on every successful fit (via the fit-success hook)
and on the light tab-resume path (which never fits): when the canvas backing
diverges from cssWidth x devicePixelRatio beyond rounding tolerance, it
replays xterm's own dpr + resize path to rebuild the backing at the current
scale, then refreshes. Verified live: the same break sequence now self-heals
on reveal with no user action. A webgl-canvas-dpr-repair diagnostic records
each repair with the stale and expected backing widths.
* fix(terminal): keep dpr repair off the layout path
* fix(native-chat): resolve WSL Codex transcripts so Chat UI renders responses
Codex reports a guest Linux transcript path. On a Windows host `existsSync` resolved it against the current drive (`C:\home\...`) and discarded it, and the id-based fallback only searched host roots. `resolveSessionFilePath` returned null forever while the watcher reported `watching: true`, leaving Chat UI permanently empty.
Translate the guest path to its host-readable UNC twin, classifying **before** any `existsSync` probe on win32 so the `C:\home` false positive cannot fire. Adapted from #10639 with one required correction: it uses the **async cached** WSL seams (`listWslDistrosAsync` / `getWslHomeAsync`) rather than the `execFileSync` ones, which would stall the Electron main thread for up to 5s per tick of the resolve-poll loop on a cold distro.
Fixes#10326
Co-authored-by: Orca <help@stably.ai>
* fix(native-chat): stop the WSL transcript probe firing every poll tick
Three follow-ups from review of the WSL Codex transcript fix:
- The UNC translation was retried on every fast resolve-poll tick (measured 10
sync UNC stats per 100ms) because only a successful result was memoized. Gate
the retry to the slow fallback cadence.
- A non-empty WSL home list was cached for the process lifetime, so a distro
that was still booting during the first probe stayed excluded forever. Expire
both branches; getWslHomeAsync caches successes, so a refresh only re-spawns
wsl.exe for the distros that actually failed.
- codexSessionsDirs enumerated every distro's home eagerly, waking distros the
user left stopped even for native-Windows panes. Make the WSL roots a lazy
tier consulted only after the host's own Codex roots miss.
The resolve-poll suite became platform-dependent and only passed off Windows;
pin the platform and add explicit win32 coverage.
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
`mapTerminalFilePath` derived the WSL distro only from the *shape* of `worktreePath`. A worktree on a native Windows drive whose project runs under the WSL runtime gets a shell whose paths are POSIX, so no distro was found, the path went verbatim to a Win32 stat probe, and the candidate was dropped — no underline, no tooltip, inert Ctrl+click.
Resolve the pane's distro from the execution runtime, falling back to the old worktree-shape derivation so existing behaviour is unchanged. Note the half of #8156 covered by merged #8215 (worktree on the WSL filesystem) was already fixed; this closes the remaining gap.
Fixes#8156
Co-authored-by: Orca <help@stably.ai>
On a fresh session the Won rewrite silently did not fire — real-hardware
capture on macOS 26.5.2 with 2-Set Korean read 3 of 3 Backquote presses as
e2 82 a9 at the PTY, and the feature only started working after the first
press or an input-source round trip.
Cause: the reader returns null for 'no signal' — the IPC is not exposed yet at
startup — as well as for a genuinely absent source, and refreshInputSourceId
committed that as isKoreanInputSourceId(null) === false. An unknown became a
confirmed negative that nothing retried, because a keystroke-triggered refresh
is async and cannot classify the press that triggered it.
Leaves the cache unknown on a null read and retries the startup probe with
bounded backoff, so the gate is warm before the first key. Bounded because each
probe spawns defaults export | plutil | plutil.
Co-authored-by: Orca <help@stably.ai>