#8522 stopped one-shot subagents from squatting as idle rows, but named
background agents (Workflow/orchestration/ultracode lanes and agent-teams
teammates) still piled up permanent "Idle - <type>" child rows for the rest
of the session — the reported regression (11 idle rows under an
"Orchestration Messages" pane, all idle 3-6h after finishing).
Root cause, confirmed against live hook captures (claude 2.1.210): named
agents get teammate-shaped ids (a<name>-<hex>) AND now appear in Stop's
`background_tasks` as `type: "teammate"` entries whose status stays "running"
forever — even after the agent finished. The old code read that shape as a
"resumable teammate", so SubagentStop only marked it idle and the fold never
reaped it (a present teammate task kept hasTeammateTypedTask true). The rows
never left.
Fix: the roster now tracks only WORKING children.
- SubagentStop removes the child outright (teammate-shaped or not) — it is
the reliable finish signal; the teammate task's "running" status is not.
- TeammateIdle removes by name as the fallback when a SubagentStop is lost.
- A lead Stop's background_tasks still reaps unlisted children: hyphen-free
one-shots always, and teammate-shaped rows once a complete inventory shows
no teammate-typed task at all. A live named agent whose id never appears is
kept only while a teammate-typed task is still present (the done-gate).
- Hydration drops persisted idle snapshots so a restart can't re-pile them.
Verified live in a dev Electron instance driving a real Claude TUI that spawns
four named background agents: pre-fix the pane resolves to done with four
persistent "Idle - <probe>" rows; post-fix each row disappears the instant its
agent finishes and the roster drains to empty (done, zero child rows).
Tests: roster + row-lifecycle + hook-listener suites rewritten to the
working-only semantics, grounded in the captured 2.1.210 hook stream
(126 passing). Typecheck + oxlint clean.
Two renderer paths let Promise rejections escape unhandled, where the
crash-breadcrumb system retains them and grows the heap until the renderer
white-screens (#8260):
- The terminal file-link provider fires a `void Promise.all(...)` whose async
mappers probe path existence over SSH. On disconnect these reject with
"Remote connection dropped/reconnecting"; add a `.catch` that resolves the
callback with no links so the rejection is handled.
- "ResizeObserver loop completed" is a benign Chromium quirk; suppress it in
crash-diagnostics so it stops filling the breadcrumb buffer and inflating
the renderer error count.
Scope note: the original fix for #8260 also proposed Windows pty:kill signal
handling (killPtyProcess / isPtyAlreadyGoneError) and a ResizeObserver
double-rAF. Those landed on main independently (Windows kill handling by
v1.4.135; the pane-title-overlay rect-equality guard from #2756 already breaks
the loop at the source), so this change carries only the two guards still
missing upstream.
Co-authored-by: Yixian HU <yixian@YixiandeMacBook-Air.local>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(speech): resume interrupted voice model downloads and surface real errors
Voice model downloads that died late in the transfer (flaky networks,
scanning proxies) failed at the UI's 90% progress cap and restarted from
byte zero on every retry, so affected users could never complete any
model download (#8620 / STA-1775).
- Retry transient download failures (net::ERR_* transfer cuts, resets,
timeouts, HTTP 5xx/429) with an HTTP Range resume from the bytes
already on disk, restarting from the canonical URL each attempt since
signed CDN redirect URLs expire; bounded backoff, gives up after
repeated zero-progress attempts with a diagnosable error.
- Correct catalog sizeBytes to exact upstream asset sizes (parakeet
v2/v3 are 482/487 MB, not 170/180; paraformer is 1,047 MB, not 115) —
also the progress denominator when content-length is missing.
- Show the underlying failure cause in the download-error toast and log
it in main; it was previously invisible everywhere.
All 7 pinned catalog SHA-256 hashes verified against current upstream
assets. Live-verified end to end: a transfer killed mid-body resumes
with a 206 and completes through checksum, extraction, and validation.
Fixes#8620
* fix(speech): harden resumed model downloads
* fix(speech): validate resumed download completion
* fix(speech): keep segmented download resumes progressing
* fix(speech): don't abandon a download that keeps making progress
The retry loop gave up after 8 total failures regardless of progress, so
a large model (e.g. the ~1GB Paraformer) over a network that resets every
few hundred MB was abandoned mid-download even though every attempt was
resuming and advancing — the exact 'stuck then fails' symptom, just later.
Replace the all-time failure budget and the separate advancing-segment
cap with a single rule: give up only on a genuine stall (repeated
attempts with no forward progress) or an absolute request ceiling that
bounds a pathological tiny-segment server. Any download that keeps
advancing now runs to completion.
Verified live under Electron 43: a transfer reset mid-stream on every
attempt but advancing each time previously gave up at 67% after 8
attempts; it now completes. All prior resume/cancel/timeout scenarios
still pass.
* Add in-place screenshot markup for the browser pane
Adds a Draw button to the browser toolbar (local and remote/SSH panes) that
freezes the visible viewport into a still image and overlays a drawing canvas.
Users can mark it up with pen, highlighter, arrow, rectangle, ellipse, and text
(color, thickness, undo/redo), then copy the composited PNG to the clipboard to
paste into their agent — reusing the existing clipboard screenshot path, so it
works for local and remote agents alike.
The drawing layer is a renderer-side canvas, so the base image is the only
environment-specific piece: local panes use webview.capturePage(), remote panes
snapshot the already-displayed screencast frame. Drawing model, compositing, and
shape rendering are isolated, unit-tested modules.
* Fix browser markup clipboard, transparency, text input, and toggle
- Copy now always produces a PNG (the clipboard handler accepts PNG only;
a JPEG fallback silently produced an empty clipboard) and targets the
clipboard size limit so realistic viewports stay full-resolution.
- Flatten the captured base image onto opaque white so transparent page
backgrounds no longer ghost the live view through the frozen backdrop.
- Keep markup text-input keystrokes local (focus the field explicitly and
stop propagation) so the browser pane's global handlers can't swallow them.
- Make the Draw toolbar button toggle markup mode off on a second click.
* Add markup text sizing, object re-editing, and text-input fixes
- Add a font-size control (text tool) to the markup toolbar.
- Add a Select tool to re-edit committed shapes: click to select, drag to
move, change color/width/font-size of the selection, Delete to remove, and
double-click text to re-edit its content. Undo/redo now snapshots the whole
shape list so every edit (not just adding) is reversible.
- Fix the text input: transparent ink-colored field (no dark theme box over the
screenshot) and ignore Enter during IME composition so Japanese conversion no
longer commits the annotation early.
- Split the grown overlay logic into focused modules (editor hook, pointer +
keyboard hooks, canvas renderer, document edits, hit-testing) to stay within
the file-size budget.
* Improve markup text commit and move toolbar to the bottom
- Clicking elsewhere while a text box is open now commits the text and
switches to the Select tool, instead of discarding it (the input
unmounted before its blur fired) and opening a new box at the click.
- Move the drawing toolbar to the bottom (stacked above the actions bar)
and open the color/size popover upward.
* Fix markup text re-editing alignment, doubling, and selection bounds
- Hide the text shape being re-edited from the canvas so the live input
isn't doubled by its committed render.
- Drop the input's padding/border and use the same font as the canvas so
the typing preview sits exactly where the text renders (no shift);
field-sizing keeps the box hugging the text.
- Estimate text width with full-width (CJK) awareness so the selection box
and hit bounds track Japanese text instead of clipping it.
- Move the font-size control out of the color popover into its own toolbar
button showing the current size.
* Unify markup text selection and edit framing
Draw the selection box on the canvas even while a text shape is being
re-edited (the shape stays hidden so it isn't doubled), and strip the box
off the text input so it renders only the editable glyphs. The frame is now
identical whether selecting or editing, and the text overlays the same
position with the same font, so there's no shift between the two.
* Align markup text-edit baseline with the committed render
Force the text input's line-height to 1 and zero its height/padding so its
top edge matches the canvas textBaseline:'top' render — the editing text no
longer sits slightly lower than the committed/selected text.
* Address review: markup hit-testing, clear reset, and async guards
- Hit-test highlights against their rendered (4x) thickness so the select
tool doesn't miss large highlight strokes.
- Reset transient editor state (pending text, selection, in-progress drag) on
Clear all so a pending input blur can't re-add text and no stale selection
lingers over the emptied canvas.
- Guard markup completion with the capture token so a stale onDeliver can't
reset or error a session the user already cancelled or restarted.
- Skip the grab-element keyboard shortcut while the markup overlay is open,
matching the already-disabled grab toolbar buttons.
- Localize the markup error messages and surface them via a toast (the
controller's error was otherwise never shown).
* Trim browser markup to a draw-only tool and fix two delivery bugs
Reshape the screenshot-markup overlay to match its actual use — a throwaway
scribble the user copies once into their agent — and drop the re-editable
vector-editor machinery that made it an outlier in scope.
- Remove the select/move/restyle/re-edit subsystem: hit-testing, per-shape
document edits, drag preview, selection frame, double-click text re-edit,
Delete-to-remove, and the CJK text-width heuristic / multi-line text plumbing
that only fed selection bounds. Keep pen, highlighter, arrow, rect, ellipse,
and text with color/thickness/font-size and undo/redo/clear.
- Fix silent data loss on HiDPI: the compositor budgeted only bytes, so a large
Retina viewport could produce a PNG under the byte limit but over the
clipboard handler's pixel ceiling, which the handler drops silently while the
UI still reported success. Composite now scales down to fit maxPixels too.
- Fix a stranded UI after a capture failure: the controller stayed "active" with
no overlay and an inert Escape. It now returns to idle (capture failure) or
back to the drawing surface (compose failure) after the error toast.
Net ~660 fewer lines and 3 fewer modules; delivery (freeze → composite PNG →
existing clipboard-image paste) is unchanged. Unit tests updated.
* Add a one-time discovery hint for the browser Draw button
Highlight the new screenshot-markup Draw button the first time it's usable so
users — including existing ones — notice the tool. A localStorage-gated,
show-once tooltip + highlight ring, independent of the browser contextual tour
(which is capped at three steps and only shows to users who haven't seen it).
- New use-markup-draw-hint hook: opens once per install when the button is
enabled and idle, auto-dismisses, and dismisses on click; skips cleanly when
storage is unavailable.
- MarkupDrawButton forces its tooltip open with the discovery copy and rings the
button while the hint shows.
- Localize the hint (drawHint) across en/es/ja/ko/zh; normalize the markup key
order alphabetically.
* Align the markup copy button label with "Copy Screenshot"
The browser pane's two image-copy actions read inconsistently in English —
"Copy Screenshot" (the grab flow) vs "Copy markup" (the overlay). Match the new
markup button to the existing one: "Copy Markup". English only; other locales
already use their own sentence-case convention consistently, so they're
unchanged.
* Drop the markup Draw hint auto-timeout; dismiss on act only
The one-time Draw-button hint no longer auto-dismisses on a timer the user might
miss. It now stays open until the user clicks Draw or the button stops being
usable (grab started, markup open, or a blank tab), which also fixes a case
where losing eligibility mid-hint left the forced-open tooltip stuck over a
disabled button.
* Optimize markup rendering and harden compose delivery
Performance:
- Rasterize committed shapes once into an offscreen layer; per-frame paint blits
that layer and draws only the in-progress shape, so a fast pointermove stream
never re-strokes the whole scene.
- Encode the composite PNG off the main thread via canvas.toBlob (a large
synchronous toDataURL froze the renderer), with a rAF yield so the "composing"
UI paints first.
Correctness / review fixes:
- Don't overwrite the clipboard after cancel: re-check the capture token between
the (now async) compose and onDeliver, closing the window where Escape during
composing still delivered the markup.
- Keep the offscreen layer and visible canvas in lockstep by threading one
measured devicePixelRatio through both paints, and re-measure on window resize
so a monitor move (dpr change without a CSS-box change) repaints at the new
scale. Blit stretched to the canvas box so a transient mismatch never clips.
- Drop the stale "or JPEG fallback" note; compose is PNG-only.
* Fix test fixtures for markup pixel-budget clamping to use a truly oversi
The old 3840×2160×dpr2 fixture (33.2M px) was already under the 32×1024×1024
(~33.55M) ceiling, so the "never over budget" assertions weren't exercising
the clamp path. Switch to 4000×3000×dpr2 (48M px), which genuinely exceeds
the ceiling.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Surface Grok unified-billing monthly usage instead of a permanent warning
Unified-billing Grok accounts have no weekly credits: the
/billing?format=credits view returns a config without
creditUsagePercent, so the status bar was stuck on 'Grok billing
response did not include credit usage' even though the account has a
real quota. The default (format-less) /billing view reports it as an
included monthly budget (monthlyLimit/used with the billing period).
When the credits view has no weekly credit usage, read the default view
and surface monthly usage as the provider's 30-day window (already
supported by the tooltip and chip visibility for OpenCode Go). If the
fallback read fails, the previous 'unavailable' presentation stands
rather than escalating to an error chip.
* Review fixes: chip renders monthly-only usage; fallback failures keep stale data
- StatusBar ProviderSegment: monthly window is chip-visible when it is the
sole window (Grok unified billing); fetching/error no-data guards and the
icon-only dot now count monthly, matching tooltip.tsx. OpenCode Go chips
are unchanged (monthly stays tooltip-only next to session/weekly).
- grok-fetcher: monthly-fallback request failures propagate as 'error' so
applyStalePolicy keeps the last good monthly snapshot; 'unavailable' is
reserved for a successful response without monthly fields.
* Settings: show Grok monthly usage row for unified-billing accounts
Why: the Grok accounts section only rendered the weekly-credits row, so
unified-billing accounts showed a signed-in state with no usage at all.
* Use generated localization keys for Grok monthly copy
* Stop usage chips flashing and back off retries for failing providers
Two behaviors made the status bar unusable when any provider was
persistently failing (bad auth, unsupported plan):
1. Every refetch repainted all providers as 'fetching', so a settled
error chip flashed to a loading "…" chip and back on every cycle.
withFetchingStatus now keeps settled states (ok/error/unavailable)
visible until the new result lands; only providers with no settled
state (first load, explicit account-switch clear) show loading.
2. Error providers on the fast activation-retry lane (claude/codex/
grok) were retried every 30s on any focus/show/restore event —
forever. Repeated hits drove Claude's tight-budget usage endpoint
into 429s, flipping the chip between 'Limited' and its actual error.
Retries now back off exponentially per consecutive applied failure
(30s, 60s, 120s, … capped at the 15-minute poll cadence) and reset
on success or account/target switch.
* Count full fetches as failure-lane retries and keep Grok pane refresh feedback
- Stamp failing providers' activation-retry clocks when a stale-driven full
fetch runs, so the individual failure lane does not fire a redundant retry
right after the full fetch already retried them.
- GrokUsagePane: manual refresh spinner/disable is now renderer-local, since
settled snapshots no longer repaint as 'fetching' during refetches.
- Strengthen the backoff-reset test so it distinguishes a reset streak from a
stale retry timestamp (CodeRabbit), and add a regression test for the
full-fetch retry stamping.
* Fall back to legacy keychain when stale scoped Claude credentials 401
Claude Code maintains the legacy 'Claude Code-credentials' Keychain item
for the default config dir, but a scoped item (service suffixed with
sha256(configDir)) can be left behind by sessions that ran with
CLAUDE_CONFIG_DIR set. Once its access token expires, nothing refreshes
it: readFromKeychain returned the scoped token unconditionally, every
usage fetch 401'd as 'stale-token', and system-default auth has no CLI
recovery lane — so the status bar showed 'Refreshing sign-in' forever
while the legacy item held a perfectly valid token.
Two changes:
- readFromKeychain: an actual access token from the legacy item now
beats scoped refresh-only credentials (Orca cannot refresh tokens
itself, so refresh-only must not shadow a working token).
- On a stale-token OAuth failure with scoped-keychain credentials, retry
once with the legacy item's token before classifying the failure.
Host system-default auth only — managed/WSL credentials never fall
back to the host user's legacy keychain item.
The legacy retry runs before CLI repair, so a readable working token is
preferred over launching a hidden 25s claude PTY.
* Pin WSL-gate and same-token short-circuit for legacy keychain retry
The legacy-keychain retry must never answer a WSL target with the host
user's keychain account, and must not double the usage request when the
legacy item mirrors the failed scoped token. Add tests locking both.
* Classify Codex app-server chatgpt-auth-required as an auth error
When auth.json holds only an OPENAI_API_KEY (no ChatGPT tokens), the
app-server RPC rejects account/rateLimits/read with "chatgpt
authentication required to read rate limits". That string matched none
of CODEX_AUTH_ERROR_PATTERNS, so fetchCodexRateLimits fell through to
the hidden PTY /status probe, which cannot render usage for such
accounts and burned the full 15s PTY timeout on every refresh cycle,
surfacing as a permanent "Refresh failed — PTY timeout" status chip.
Classify the message as an auth error so the RPC result is returned
directly (fast, accurate) and no PTY is spawned.
* Keep auth-required usage errors from rendering as a rate-limit Limited label
The new Codex app-server error 'chatgpt authentication required to read
rate limits' mentions rate limits only as the object it failed to read,
but the status bar's rate-limit classifier matched the phrase and
labeled the chip 'Limited'. Classify authentication-required messages as
auth failures so they get the standard softened refresh copy instead.
* Suppress Git Credential Manager OAuth popup on git clone (fixes#7652)
Orca's git runner disables the interactive credential prompt on every git
call that goes through gitExecFileAsync/gitStreamStdout, but the two raw
'git clone' spawns (desktop repos:clone and the runtime clone path) passed
no env, so they inherited process.env with no guard. On Windows a clone
that needs GitHub auth then makes Git Credential Manager pop its
'Connect to GitHub' OAuth window, and in a network-restricted intranet the
browser/device flow never completes while git's credential retry re-pops it.
Apply nonInteractiveGitEnv() to both clone spawns so the prompt is
suppressed (GCM_INTERACTIVE=never, credential.interactive=false,
GIT_TERMINAL_PROMPT=0). The credential *helper* is kept, so cached-token
clones for private repos still work; only the interactive fallback popup is
disabled and the clone fails fast with a clear error instead.
* Suppress GCM OAuth popup in agent terminals and setup hooks too (#7652)
The clone-spawn fix stopped Orca's own managed git from popping Git
Credential Manager, but git run in terminals and setup scripts inherited
process.env with no guard. That is the more likely source of the reported
loop: agents are told to run 'git pull --rebase'/'git fetch'/retry 'git
push' (preamble + conflict/push-failure prompts), and each retry re-pops
GCM's 'Connect to GitHub' window in a network-restricted intranet.
Apply the credential-prompt guard to:
- setup/archive/hook scripts (hooks.ts non-WSL exec env), which run
unattended on worktree create/archive.
- the shared PTY host env (buildPtyHostEnv), via a small
applyTerminalGitCredentialPromptGuard helper. Agent terminals are
guarded unconditionally (they cannot dismiss a GUI popup); user
terminals are guarded by default via the new
terminalSuppressGitCredentialPrompt setting so power users can opt out.
The credential helper is kept, so cached gh auth still works; only the
interactive fallback prompt is disabled. Verified end-to-end in a real
Orca terminal (GIT_TERMINAL_PROMPT=0 + GCM_INTERACTIVE=never by default;
absent when the opt-out is set).
* Scope user-terminal credential guard to Windows, add settings toggle, forward guard into WSL (#7652)
* Retrigger PR checks (Actions dropped the synchronize dispatch for 57e7ce249)
* Keep shell locale out of the terminal/hook credential guard (#7652 review fix)
* Fix Fable review findings: guard WSL hook branch, wire settings search, catalog keyword keys, sparse-env askpass, one-shot agent classification (#7652)
* fix(terminal): harden Git credential popup guard
* test(pty): cover SSH credential guard setting
* fix(git): guard remote clones and setup runners
* fix(git): scope credential guards to unattended work
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* fix(settings): collapse Projects list to one row + pane per project (#8566)
Settings and its nav enumerated repo rows, so a project set up on multiple
execution hosts (local + a Remote Orca Server, or the same repo cloned on two
machines) rendered two nav rows + two panes that collided (duplicate React
key/DOM id) or mirrored. Derive the Settings list from the project projection
(repos-only, deterministic) so nav, panes, and the Cmd+J palette all collapse
to one entry per project. Deep-link repoId targets resolve to the project's
representative section.
* fix(settings): switch project host in place, host-scoped edits + deep links (#8566)
Add an ephemeral per-project host selection driven by the pane's "Available
Hosts" switcher, so the single collapsed pane shows the selected host's setup
(path, worktree base, runtime, fork-sync, hooks, source-control AI). Route
edits to the selected host by threading an optional hostId through updateRepo
(mirrors removeProject's host routing), fixing the same-id/self-pair case where
a local + runtime share one repo id. Couple Settings deep links to the switcher
so host-specific subsection anchors resolve, load hooks for the selected host,
and make pane-level Remove Project remove every host setup.
* fix(settings): isolate selected project host state
* fix(settings): review follow-ups for per-project Projects pane (#8566)
- Remove Project copy now states it removes the project on all
configured hosts (the button removes every host setup); new catalog
key translated across all 5 locales
- ensureHooksConfirmed forwards hostId to readRuntimeIssueCommand so
the issue-command trust read resolves duplicate repo ids the same way
as its sibling checkRuntimeHooks call
- translate the multi-host "N hosts" nav description
- extract removeSettingsProjectFromAllHosts with unit tests; drop the
now-unused getRuntimeTargetIdentity
* fix(terminal): prevent Windows multiline paste submission
* fix(terminal): normalize chunked forced-paste line endings
Windows multiline pastes over TERMINAL_PASTE_DIRECT_MAX_BYTES (64 KiB)
take the chunked plan and stream plainText straight to the PTY, skipping
wrapTerminalBracketedPasteText — so raw CRLF/LF still reached ConPTY and
Codex treated the LF as submit, the exact bug the direct path just fixed.
Wire the plan's newlinePolicy field: forced bracketed plans get
'terminal-cr' and the chunk iterator normalizes the full text before
chunking (a per-chunk pass could split a CRLF across a boundary and leak
the LF half). Non-forced chunked pastes keep their documented
preserve-newlines behavior, so macOS/Linux bytes are unchanged.
* fix(terminal): normalize programmatic paste newlines
* test(e2e): harden Windows Codex paste spec per review
Poll for the idle composer placeholder as a positive ready signal (the
negative boot-state check alone can pass on an empty screen), and grow
the large-paste payload so it stays above the 64 KiB direct-max after
newline normalization, keeping the chunked lane covered even if
planning ever measures post-normalization bytes.
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(terminal): certify replay wedges only after a quiet window and re-kick recovery on reveal
A slow-but-alive xterm parse (hidden-restore backlogs) could be certified
"wedged" by the replay guard's flat probe deadline, opening the guard while
replay bytes were still parsing (auto-replies leak into the agent's stdin)
and handing healthy panes to recovery. The wedge deadline now extends while
parse progress is observed and certifies only after a fully quiet window.
Two self-heal holes in the same family: a restore requested on a
certified-dead pipeline silently returned false — if certification's own
recovery request was budget-declined, a revealed dead pane kept its stale
pre-death frame forever; it now re-kicks pane recovery once per xterm
instance. And remountTerminalTabForRecovery returning false was the one
unbreadcrumbed recovery outcome; it now leaves a trace.
* fix(terminal): make replay progress generation-based
* fix(terminal): separate write failure from parse progress
* fix(terminal): skip output for certified-dead renderers
* fix(terminal): close recovery retry races
* fix(terminal): bind recovery to xterm lifecycle
* fix(terminal): invalidate stale parse probes
* fix(terminal): retry every dead split renderer
Codex, Antigravity, and Devin launch their agent-hook `command` as a program
(argv[0]), not through cmd.exe. PR #8430 changed wrapWindowsCmdHookCommand to
emit an `if exist "path\." (drain) else if exist "path" (call "path") else
(drain)` compound whose argv[0] is the cmd builtin `if` — unspawnable — so every
Codex/Antigravity/Devin hook (SessionStart, UserPromptSubmit, Stop, ...) failed
with "hook exited with code 1" on Windows starting in v1.4.138.
Revert the cmd-safe fast path to the bare, directly-spawnable .cmd path (the
proven pre-#8430 form). A cmd-builtin drain and direct-spawnability are mutually
exclusive, and nesting cmd.exe /d /c breaks large-payload draining; the
missing-script stdin drain stays on the encoded-PowerShell fallback (used for
spaced/non-ASCII paths). Upgrades self-heal on first launch: startup install()
unconditionally rewrites the command and Codex trust entry, sweeping the old
compound form.
Add a platform-independent regression guard (launcher must resolve to a real
file, never a cmd-builtin fragment), update the lifecycle test + docs, and fix a
stale Devin comment.
* perf(terminal): defer cold worktree activation tab mounts until first reveal
Activating a worktree mounted a TerminalPane for every saved terminal tab
in one render pass. Each mount replays scrollback through xterm, attaches
a WebGL renderer, and issues a sync-IPC snapshot read, so a worktree with
many agent-session tabs froze the renderer for tens of seconds (field
trace: 200+ replay-guard stall releases inside one activation window,
plus restore-marker feedback loops re-fetching snapshots at ~4/sec).
Cold activations now mount only the tabs the user can see (active tab,
each split group's active tab, activity-portal tabs, pending spawns) plus
tabs that are already live or that parked byte watchers cannot cover.
Every other tab defers like a cold-parked tab from birth: no view until
first reveal, with the parked byte watchers owning bell/title/completion
side effects meanwhile. The restriction reuses the targeted background
mount mechanism and lifts once every tab has been revealed.
Deferral only engages when more than four tabs would mount cold, and only
while hidden-view parking is enabled, so small worktrees and the parking
kill switch keep today's behavior.
* test(terminal): prove cold-activation deferral end-to-end; unblock it past hydration's blanket spawn flag
The new e2e (child worktree, 8 tabs, renderer reload against live daemon
sessions) caught the deferral never engaging: session hydration marks
every persisted tab pendingActivationSpawn, and treating that flag as
must-mount-now put all tabs in the immediate set. Only explicit queued
startups (pendingStartupByTabId) mount eagerly; a deferred tab's reveal
consumes the hydration flag exactly like an activation mount would.
Sleep/wake respawn flows go through targeted background mounts and are
unaffected (terminal-sleep-wake-restore passes).
The spec asserts the visible tab mounts, deferred tabs stay unmounted
with parked byte-watcher coverage, and a revealed tab mounts on demand.
* fix(terminal): harden deferred activation lifecycle
* docs(reliability): correct SSH startup assertion evidence
* Add version-matched bundled skill guides
* Clarify skill freshness rollout PRs
* Add canonical skills show alias
* fix(skills): address guide review feedback
* fix(skills): make guide commands cross-platform
* fix(skills): apply the ORCA convention to the emulator guides
Review follow-up: the emulator guides still instructed literal
`orca emulator ...` in sh fences with no Linux disambiguation, so on
unmanaged Linux they could launch the GNOME screen reader — the exact
failure the executable-selection preamble prevents. Both emulator
guides now carry the preamble and ORCA placeholder across fences,
tables, and prose, and the cross-platform safety test covers all four
converted guides. Also replaces computer-use's "unless a block names a
shell" carve-out, which contradicted its own POSIX example, with the
unconditional placeholder rule.
The shell-ready zsh wrapper restored ZDOTDIR from the env-imported
`${ZDOTDIR}`. On Windows+WSL the wrappers are generated with a Windows
path baked in but sourced via /mnt/c, and for non-ASCII Windows
usernames (e.g. a Korean login) zsh corrupts environment values whose
UTF-8 bytes fall in its 0x84-0x9D token range while processing startup
files. The corrupted `${ZDOTDIR}` failed the self-check, so the wrapper
fell back to the unusable baked Windows literal and the user's ~/.zshrc
never loaded — a bare `HOSTNAME%` prompt with no theme/aliases/PATH.
Derive the wrapper dir from `${${(%):-%x}:h}` instead — %x is zsh's
internal script name for the file being sourced and is not subject to
the env-import corruption. `${ZDOTDIR}` is kept only as a fallback when
%x expansion yields nothing; the existing final restore still validates
with -f before trusting the value. On native macOS/Linux the derived
value equals the old one, so behavior there is unchanged.
Covers local PTYs, the daemon, and the Windows→WSL launch path, which
all share getZshEnvTemplate. Adds a live-zsh regression test that sources
the wrappers from a non-ASCII (token-range) runtime path.
Supersedes the ORCA_ORIG_ZDOTDIR path-normalization approach with a wrapper self-location fix that also covers the non-ASCII (token-range) corruption trigger.
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* fix(daemon): replace a permanently wedged daemon instead of preserving it forever (#8689)
A daemon whose socket accepts connections but whose event loop never answers the
'hello' handshake was adopted by the launcher unconditionally and never re-evaluated,
so every terminal spawn failed with 'DaemonProtocolError: Hello response timed out'
with no recovery.
- daemon-init.ts: bound the launcher's 'preserve any unresponsive-but-connectable
daemon' with a grace window. A transient wedge (Windows update-relaunch AV/disk
pressure) drains within ~20s and is preserved WITH its live sessions; a permanent
wedge exhausts the grace and is replaced. Stays well under the 60s local-PTY
fail-open cap.
- daemon-pty-adapter.ts: isDaemonGoneError now treats 'Hello response timed out' as
daemon-gone, so a runtime wedge triggers withDaemonRetry's respawn (re-entering the
same grace-bounded launcher) instead of failing every spawn until app restart.
Tests pin the grace magnitude so it cannot be silently shrunk.
Co-authored-by: Orca <help@stably.ai>
* fix(daemon): widen wedged-daemon grace window to ~60s
Bump WEDGED_DAEMON_GRACE_RETRIES 3 -> 11 (~20s -> ~60s) to keep live-session
loss on the transient-wedge (Windows update-relaunch) path as close to zero as
possible. A transient wedge drains early and stays under the 60s fail-open cap;
only a permanent wedge runs the full window. Export the constant and pin its
floor in tests so it can't be silently shrunk.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Take-over of #8605 (issue #8591). Ships #8498 (worktree resync + pull-to-refresh + cache write-through) and #8129 (idempotent notification replay on reconnect). Fixes the original PR's field mismatch (seq vs notificationSeq) and adds the missing notifications.getMissedSince mobile RPC allowlist entry. #6784 and #4500 held back to avoid conflicting with the relay work (#8536). Co-authored-by: Brandon Bennett (@branben).
fix(sidebar): guard kanban worktree sort against undefined displayName (crash 99657ab1)
A worktree reached the sidebar with an undefined displayName, so the
Manual-sort comparator ran `a.displayName.localeCompare(...)` and threw
`TypeError: Cannot read properties of undefined (reading localeCompare)`,
unmounting the sidebar.worktrees error boundary.
Route both kanban comparators through a null-safe compareDisplayName so a
missing name only affects tie-break order instead of crashing. Behavior is
unchanged when displayName is present.
Adds regression tests for both the manual and recent sort paths.
@
* fix(github): pin work-item list ordering to updated-desc so cursor pagination reaches every page
The Tasks page paginates work items with an updatedAt cursor
(updated:<oldest-item), but the underlying gh calls never pinned a sort:
'gh issue list' defaults to created-desc and '--search' defaults to
best-match. Items created long ago but updated recently therefore never
appeared on any page — page 0 (created order) skipped them and every
later page excluded them via the cursor — so the pager advertised pages
the fetch chain could never reach, clicks on them clamped to the last
real page, and cross-page ordering was scrambled.
Append sort:updated-desc to every list/search invocation so the fetch
order matches the cursor field on the first and all subsequent pages.
Verified against a live 588-issue repo: the cursor chain previously
died around page 5; it now traverses 585/588 unique issues (the
remainder is the pre-existing strict '<' boundary edge for items
sharing the cursor's exact timestamp).
Fixes#8649
* fix(github): make work-item cursor pagination lossless at updatedAt boundaries
Builds on the sort-pin fix: switch the pagination cursor from strict
'updated:<' to inclusive 'updated:<=' so items sharing the boundary row's
exact updatedAt are no longer skipped between pages (the residual 3/588 edge
in #8649).
The inclusive bound re-fetches the boundary rows, so dedupe them by repoId+id
(a bare item.id like 'issue:9' collides across repos). Extract the page
accumulation out of the 12k-line TaskPage component into a pure, unit-tested
helper (accumulateWorkItemPages) that dedupes and backfills: it accumulates
fresh rows across fetches and emits uniform pageSize pages, so deduped pages
never shrink below the size totalPages (count / effectivePageSize) assumes —
which would otherwise strand the tail items and break the no-count degraded
pager.
Also hoist the updated-desc ordering into a named WORK_ITEM_LIST_SORT_QUALIFIER
constant so the cursor's ordering contract has one home.
Tradeoff: when per-repo fetch size equals pageSize, the boundary dedupe costs
one extra fetch per page; acceptable for interactive pagination and bounded by
the gh rate-limit guard. Persisting the cursor/buffer across calls is a
possible follow-up.
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Follow-up to the pane self-heal: its input-undeliverable detector is
gated off while a transport connect is settling (correct — remounting
mid-reattach used to kill the resolving shell), but the gate had no
timeout. A connect that never settles (the SSH RPC-timeout class) left
the one state recovery still could not reach: no output flows, so no
other detector fires, and every keystroke is silently dropped forever.
Two changes that only make sense together:
- the gate becomes a 60s settle grace; past it, undeliverable input may
request recovery again (still pty:hasPty-confirmed).
- the transport's destroyed-while-connecting check now kills only what
the connect CREATED. A fresh spawn is ours to reap; a reattach
(sessionId) resolves to a pre-existing session owned by the tab
lifecycle — tab close kills it by id through its own path, so killing
it here turned a remount racing a slow reattach into the loss of a
live shell. This is what makes the grace safe: recovery firing at 60s
against a connect that resolves at 61s now costs a wasted view
rebuild, not the user's session.
Verified: transport unit test pins the reattach/fresh-spawn kill split;
the recovery e2e pair passes 6/6 consecutive iterations with this
change (an earlier flaky signal bisected to machine load: failures only
under parallel-build load, two unrelated failure modes, 4/4 then 6/6
green on quiet hardware with and without the change re-applied).
* fix(terminal): self-heal panes whose renderer dies while the PTY stays alive
A pane's xterm write pipeline can die while its shell keeps running: a
synchronous throw escaping an unguarded write callback wedges WriteBuffer
(issue #2836), and write() on a disposed terminal silently drops its
completion callback (verified against vendored xterm 6.1.0-beta.287 — it
does NOT throw, falsifying the output scheduler's disposed-race catch).
Every recovery path we have (dead-session reconcile #6514/#7002,
hibernation wake #7145, the allDead activation generation bump) gates on
the PTY being dead, so these panes stayed fossils: last frame painted,
every keystroke and byte of output silently dropped, delivery ack credits
leaking, until the user reloaded the window (issue #8104 class).
Detection is probe-certified, mirroring replay-guard.ts:
- a scheduler write whose completion stalls gets an empty probe write; a
probe that never parses certifies the pipeline dead (catches both the
wedged WriteBuffer and the disposed-terminal case) and credits the
queued deliveries so main's in-flight window no longer leaks
- the replay guard's existing wedged release ("pane likely needs
recovery") now actually hands the pane to recovery
- user input rejected by an unbound transport (detached during a
remount/move and never rebound) arms recovery after confirming the PTY
is alive via pty:hasPty
Recovery reuses the proven remount seam: bump the tab's generation so
TerminalPane unmounts, detach() preserves the live PTY, and the remounted
pane builds a fresh xterm that reattaches and replays the daemon
snapshot — no shell restart, capped per tab to prevent remount storms.
The new e2e spec pins both phenotypes end-to-end (wedge → recover,
dispose-under-live-bindings → recover); both fail on main and pass with
the fix, and the same arc was validated live in a pnpm dev instance.
* fix(terminal): make pane recovery strictly best-effort in timer contexts
Recovery fires from stall-watch timers, replay-guard releases, and onData —
contexts where a throw becomes an unhandled error (CI verify caught this:
pty-connection.test.ts mocks a partial store, other tests advance fake
timers past the stall window, and the certification path hit a missing
remountTerminalTabForRecovery). Guard the store action call and the
ptyIdsByTabId reads so a partial surface yields a false return, never a
throw, and pin it with a regression test.
* fix(terminal): guard pane recovery against in-flight reattach and remote liveness blind spots
Review findings on the self-heal (adversarial pass):
1. HIGH: typing during an in-flight connect/reattach (startup restore,
app-SSH) hits sendInput while the transport is legitimately unbound; an
input-undeliverable remount there destroys the unbound transport (no
ptyId yet, so unmount cannot detach), and pty-transport's destroyed
check then kills the PTY the resolving reattach returns — the live
shell recovery exists to preserve. Gate the input detector on a
transport-connect-in-flight flag (set around all three connect sites)
and on disposed, so "not deliverable YET" never remounts. The fossil
case (detached and never rebound) has no pending connect and still
recovers.
2. pty:hasPty answers null for ids the local registry does not own, which
made the liveness gate inert for remote panes: a disconnected remote
runtime would remount-churn on every cooldown window while typing.
Remote panes (connection-tagged or remote:-prefixed) now require an
authoritative true; local panes keep the lenient null-proceeds gate.
Flagged for follow-up, not changed here: pty-transport's destroyed check
kills reattached sessions without discriminating isReattach — a
pre-existing hazard that tab-close covers by killing per id anyway.
* fix(terminal): keep certification throw-proof end to end
Guard the two remaining throw paths in the certification chain — the
entry-discard callback and the recovery handler — so nothing can escape a
timer as an unhandled error, and a throwing discard cannot suppress the
recovery notification it exists to precede. Pinned by two new tests.
* fix(terminal): breadcrumb swallowed recovery failures
A store-action throw in recovery returns false without consuming budget,
so the detector retries each cooldown — an invisible loop unless it
leaves a trace. Breadcrumb it (the recorder is self-guarded and cannot
throw where recovery runs). Also invoke transport.isConnected optionally
so partial test transports fail the gate quietly instead of logging a
contained TypeError.
* fix(terminal): end the zombie-pane replay loop at its root
Root-caused the production "wedged release drip" (1,302 breadcrumbs in one
day on one machine, 4-write bursts on a fixed timer phase, idle-required):
once a pane's xterm pipeline dies while its connection lives, the delivery
watchdog's heal (60s cooldown, fires only while idle because a dead xterm
never ACKs its in-flight bytes) re-delivers restore markers, the hidden
output restore replays 3-4 chunks into the dead parser, each write arms a
replay guard destined for another wedged release — and nothing ever
learns. The loop runs forever and re-forms after app restart.
Three fixes so the loop learns:
- replayIntoTerminal/Async short-circuit on a probe-certified dead
pipeline: no more futile writes, so no more guard drips, and awaited
restore chains resolve instead of hanging.
- requestHiddenOutputRestoreIfNeeded is gated the same way, so the
watchdog heal stops refetching snapshots for a pane recovery owns.
- a window-cap recovery decline now schedules one retry for when the
budget window reopens (deduped per tab, cancelled by any successful
remount). Without it, the certified-dead latch plus the new write
silence made a capped pane a permanent zombie: nothing would ever
re-request recovery. Cooldown declines deliberately do not retry —
remounts are tab-scoped, so the just-made remount already replaced
every pane's xterm in the tab.
Still open (tracked separately): the deterministic wedge surface that
creates the dead pipeline on the release build in the first place — an
unguarded, unreported parse-path throw or silent disposed-write; zero
guard breadcrumbs fired all day, so the trigger predates the guards'
coverage.
* feat(terminal): name the silent zombie producer in breadcrumbs
The last unproven link in the zombie-pane chain is HOW a pane's xterm
dies on the release build. Field discriminators eliminated every
reporting channel: zero terminal guard breadcrumbs and zero xterm-stack
renderer_error/unhandled_rejection events across days of logs, while the
drip re-formed after a clean app restart. Every content-triggered wedge
surface would have reported; the only fully silent mechanism left is a
restore write into an already-disposed xterm instance (write() drops its
completion callback without a throw — verified against the vendored
6.1.0-beta.287).
Instrument that exact moment: a version-pinned disposal probe (its test
runs against the real vendored build so an upgrade that moves the private
field fails loudly), a terminal_restore_write_target_disposed breadcrumb
where startup scrollback restore would write into a disposed instance,
and a terminal_restore_write_failed breadcrumb replacing the fully silent
restore catch. The next zombie formation logs its own root cause.
The unread-activity bell landed with a hardcoded aria-label, and the
localization-coverage gate now fails on main itself (verify does not run
on pushes to main, so the break landed silently and blocks every PR's
CI). Route it through translate() with catalog entries for all five
locales.
* Redesign native chat interactive UI to match assistant-turn styling
- Restyle approval and question cards as bordered cards docked in the
composer region instead of top-border strips, and widen the chat
column (max-w-3xl → max-w-4xl) to match the rest of the app
- Rework the question card into a numbered pick-list with an always-
present free-text row and explicit Next/Skip/Send action, replacing
the prior "Other…" toggle flow
- Hide the composer while a question card is active since it supplies
its own answer input, and hold the card open until the paced answer
send settles instead of dismissing on click
- Auto-repin the message list to the bottom during in-place streaming
growth via a ResizeObserver on the content, not just the viewport
- Widen question-answer pacing (800ms→1000ms step, 300ms→500ms buffer)
for reliability on slower machines / higher-latency SSH sessions
- Restyle tool-run/tool-line disclosure to a right-side hover chevron
and add a formatted (pretty-printed) detail view for diff-less calls
- Add "Skip" i18n string across all locales
* Extend paste bridge and question-card fixes for native chat interactive
- Route pane-level Paste to the question card's free-text input when
the composer is unmounted, so Cmd/Ctrl+V keeps working while a
question prompt is showing.
- Fix a stale-timer bug where a new interactive prompt could inherit
the previous card's dismiss/submitting state and swallow its first
answer.
- Only render tool-run detail when there's actually content to show,
and mark selected question options with aria-pressed for
accessibility.
The WorktreeStatus union feeding activityDotState widened, and the
switch-exhaustiveness gate now fails on main itself (verify does not run
on pushes to main, so the break landed silently and blocks every PR's
CI). Cover the two glyph-less states explicitly — matching the
function's documented intent — and drop the now-unreachable default.
* Fix stacked-worktree PR creation targeting a local-only parent branch
- Resolve the eligibility default base to a remote-tracking ref instead
of blindly trusting the submitted parent branch, since a stacked
worktree's base is often a local-only branch the remote can't resolve
- Add a create-time hard block (base_not_on_remote) so a stale or
unpushed submitted base fails with actionable copy instead of the
provider's opaque error
- Update the dialog's default-base resolution and blocked-action/
dropdown copy to match the new remote-validated default
* Split hosted-review-creation.test.ts to fix max-lines lint error
Moved getHostedReviewCreationEligibility tests to a separate file (hosted-review-creation-eligibility.test.ts) to reduce the original file size from 880 to 579 lines, satisfying the max-lines lint constraint.
Co-authored-by: Orca <help@stably.ai>
* Fix Create PR intent flow to use remote-validated eligibility default fo
Prefer eligibilityDefaultBaseRef over the raw compare base when resolving
the review base for the one-click Create PR intent flow, since eligibility
is recomputed from the same compare base right before creation and already
corrects a local-only stacked parent to the repo default. Falls back to
the compare base only when eligibility supplies no default.
* Simplify base-ref remote existence check into a single for-each-ref call
Combine the wildcard and exact-tracking-ref lookups into one for-each-ref
invocation with multiple patterns instead of two sequential git calls,
removing the redundant rev-parse fallback path.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): preserve terminals during headless desktop activation
* rm design doc
* Fix desktop activation launch ordering and blocked-window status resolut
- Check desktopWindowStatus before spawning the Orca app so a blocked
runtime no longer launches a doomed second instance.
- Reuse resolveDesktopWindowStatus for remote runtime status so it
honors the same authoritativeWindowId fallback as local status.
- Re-check the authoritative window at spawn time instead of trusting
a possibly-stale snapshot, since it can be destroyed mid-await.
- Harden the e2e activation spec against silent spawn failures.
---------
Co-authored-by: bbingz <zzb@gxsmjx.com>
* fix(editor): avoid undo history for read-only live tails
* Add reliability-gate evidence for read-only live-tail undo-history fix
- Records the passing vitest run that verifies live-tail appends leave
canUndo false while ordinary external updates stay undoable, backing
the recent editor undo-history fix.
* fix watcher lifecycle cancellation bounds
* fix(review): guard remote watcher installs against post-shutdown resurrection
closeAllWatchers aborted the in-flight install *tokens* it could see, but a
same-key joiner awaiting a 'cancelled' resolution (and a fired retry tick)
calls installRemoteWatcher directly and, on the fresh-generation recursion,
builds a brand-new non-aborted AbortController and calls provider.watch()
after teardown — leaking an SSH watcher into the just-cleared remoteWatchers
map. Latch the subsystem shut in closeAllWatchers and refuse installs while
latched; a genuine new fs:watchWorktree clears it. Adds a regression test
(fails without the latch) plus a test for the same-tick handoff-revival guard
that had no coverage.
Also extract the duplicated isolated-quarantine-vs-fuse branch shared by
retireSlot and releaseFailedRoot into quarantineOrFuse (behavior-preserving).
* Add lifecycle generation guard to refuse stale remote-watcher joiners
- A boolean latch alone can't distinguish a pre-shutdown joiner from a
fresh call once a genuine new watch reopens the subsystem, letting a
stale joiner recurse and register a post-shutdown provider.watch()
- Each installRemoteWatcher call now captures a generation counter that
closeAllWatchers bumps, so a waiter that resumes after a later
shutdown+reopen is refused instead of resurrecting
- Adds a regression test covering the shutdown-then-reopen race
Silently swallowing chmod failures let installs proceed with a
non-executable launcher, surfacing as a confusing runtime error later
instead of a clear install-time failure.
* fix(mobile): validate Windows firewall remote scope
* fix(review): simplify string-guard ternary to boolean AND
The ternary returned only boolean literals, so cond ? f() : false is
equivalent to cond && f() (addressScopeIsSufficient returns boolean).
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): accept dotted-netmask firewall scopes and lock in fail-safe edges
- Parse dotted-netmask CIDR (192.168.0.0/255.255.255.0) via a contiguous-mask
check, failing closed on holey masks.
- Factor subnetFromParsed so CIDR parsing no longer re-parses the address.
- Document why single-host (/32, /128) subnets and family-specific keywords with
an unknown interface family fail closed, and add regression tests covering
those deliberate false-deny edges plus policy-defined keywords (Intranet, DNS).
Co-authored-by: Orca <help@stably.ai>
* Add explanatory comment on why firewall scope check isn't unioned
Documents the fail-safe rationale behind checking coverage per rule
instead of merging rule scopes, so future edits don't "fix" this
into a less conservative union check.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(cli): harden Windows launcher transports
* Fix csc.exe compile failures on space-bearing Windows install paths
- Legacy csc.exe mangles absolute paths containing spaces, so the
compile step now cd's into the bin directory and passes bare
file names for /out and the source file instead of full paths
* feat(mobile): show usage reset countdown on accounts screen
Surface the rate-limit reset time ("5h resets in 3h 54m · 7d resets in
6d 7h") under the usage bars on the mobile accounts screen, matching the
desktop status-bar tooltip copy. The resetsAt timestamps already arrive
in the accounts.subscribe snapshot; this only adds the presentation.
Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ
* docs(mobile): JSDoc for new usage reset selectors
Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ
* refactor(mobile): per-bar reset countdown instead of combined line
Drop the redundant "5h/7d" prefixes — each countdown now renders under
its own bar ("Resets in 3h 54m"), matching the desktop tooltip copy
exactly.
Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ
* Extract shared reset-countdown formatter for desktop and mobile
- Move duration/countdown formatting out of tooltip.tsx into
src/shared/rate-limit-reset-format.ts so mobile's account-usage-state
can reuse it instead of a duplicated copy (with tests).
- Re-export formatResetCountdown from tooltip.tsx to avoid touching
existing import paths.
- Resend the pairing deep link once more in start-emulator.mjs since
the first can arrive before the Expo app's JS router is ready.
---------
Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
The store bumps agentStatusEpoch at the 30m stale boundary without
replacing the agentStatusByPaneKey map. Keying the flag cache on map
reference alone kept serving stale flags (old timestamp, abandoned
spinning), while the sidebar — which keys on agentStatusEpoch — correctly
de-spun. Invalidate on either changing.
Co-authored-by: Orca <help@stably.ai>
* Show agent status in terminal tabs
* Resolve terminal tab activity via the canonical worktree-status engine
The first pass hand-rolled resolveTerminalTabAgentActivityState, a fourth
parallel copy of the pane-iteration/freshness/title-heuristic loop that already
lives in smart-attention.ts and worktree-agent-activity-summary.ts. It diverged
from every existing surface (novel blocked>waiting split, a phantom 'interrupted'
red state the sidebar treats as idle) and re-scanned the global agentStatusByPaneKey
map per tab per store write (O(tabs*agents)).
Replace it with resolveTerminalTabActivityStatus, which reuses resolveWorktreeStatus
(the WorktreeCard resolver: freshness gate, live-PTY liveness, per-leaf title dedup,
permission>working>done priority) over a per-tab flag summary bucketed once per store
snapshot (O(tabs+agents)). Tabs now speak the same WorktreeStatus vocabulary as the
sidebar, so their live states can't disagree with the worktree card.
- Map WorktreeStatus -> AgentStateDot: working=spinner, permission=amber, done=check;
active/inactive fall through to the agent/shell identity icon.
- Parse legacy numeric pane keys too, matching the sidebar summary, so restored/
imported sessions light the tab.
- Drop the bespoke resolver + its tests; add focused coverage for the new resolver
and the leading-icon component.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>