* feat(agent-status): show Claude subagent child rows and gate premature done
A Claude pane that spawned background subagents/teammates showed a green
done check the moment the lead's turn ended, even while a background
review loop was still running. Orca now tracks the pane's live children
from Claude hook events and:
- keeps the pane 'working' while at least one child is working (Stop is
gated; Claude wakes the lead when a child finishes, so the pane
resolves to done on the follow-up Stop with an empty roster)
- renders the children as indented child rows under the pane's sidebar
row (name/type + working/idle dot), reusing the existing lineage UI
Tracking is lifecycle-primary: SubagentStart/SubagentStop/TeammateIdle
(newly registered hooks) plus child-origin tool events (they carry
agent_id) own the roster. Stop's background_tasks is folded only where
unambiguous — verified live on Claude Code 2.1.207 that teammates report
status "running" while idle-alive and their task ids never match
lifecycle agent_ids, so the list cannot decide teammate working-ness.
Child-origin events no longer overwrite the lead's tool/prompt caches
(a live AskUserQuestion card survives child churn); a child's own
PermissionRequest records waitingAgentId so only that child's progress
or death clears the wait. The interrupted flag survives the gated
window, inferred interrupts sync the lead record and refuse while a
child works, and hydration reseeds the roster after a restart.
* fix(agent-status): drop identity icon on subagent child rows
The child's agentType carries its NAME (e.g. "pr-reviewer"), which is not
an iconable agent and rendered the unknown "?" glyph. Nesting under the
parent row already conveys identity.
* fix(agent-status): restore displaced lead state and reconcile phantom subagents
Four review findings from the adversarial pass on the subagent child-row
feature:
- Stash the lead state a child-induced wait displaces
(ClaudeLeadTurnState.stateBeforeWait) and restore it when the wait
clears, instead of inventing 'working' — a lead that had already
stopped left the pane spinning forever after the roster drained,
since the done-gate only ever downgrades done → working.
- Tag snapshot-seeded and background_tasks-recreated roster entries
(backgroundTasksAuthoritative) and demote them when a PRESENT
background_tasks list omits their id. A phantom child seeded before a
restart could otherwise gate the pane 'working' indefinitely in teams
sessions, whose task list is never empty. Live activity clears the
tag so lifecycle-tracked teammates keep their state.
- Match teammate ids with a hyphen-free suffix after `a<name>-` so
TeammateIdle for "lane" cannot idle "lane-hooks"'s rows or clear its
pending permission wait.
- Route turn-boundary events (Stop/StopFailure/UserPromptSubmit) that
carry a KNOWN child agent_id through the child-driven re-emit instead
of adopting them as lead state, and tie the prompt-cache new-turn
reset to lead-origin events so child refreshes can't blank the
prompt label.
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* Prevent mobile screen locking during voice dictation
Integrate expo-keep-awake to prevent the mobile device from locking or
sleeping while a voice dictation session is active.
- Modularize useMobileDictation logic into separate helper files for
keep-awake, audio chunking, session state, and desktop startup.
- Acquire keep-awake lock only after successfully establishing a
desktop session to avoid locking on stale start attempts.
- Release the keep-awake lock on all completion, cancellation, error,
and unmount paths.
- Add source invariant unit tests to verify keep-awake ownership and
strict cleanup ordering.
* serialize keep-awake operations and avoid stale dictation start races
- Implement a global execution queue and tag tracking for keep-awake
operations to prevent concurrent races and stale deactivations.
- Track failed native deactivations and retry them when a replacement
hook owner mounts or starts a new dictation session.
- Ensure stale or canceled desktop dictation starts do not reset the
UI state or propagate outdated start/keep-awake failures.
- Reuse the audio chunk queue wiring in useMobileDictation to avoid
allocating new closure objects on the high-frequency microphone path.
- Add comprehensive unit tests for the keep-awake and desktop start hooks.
* Commit native recording during dictation session startup
Commit native recording in the same continuation as the final session
stale check. This prevents a queued cancellation from resurrecting the
microphone recording after cleanup has already run. If microphone
initialization fails or throws, acquired resources (like keep-awake
locks and the remote desktop session) are properly rolled back.
* Make keep-awake acquisition best-effort with a bounded startup timeout
- Recording start no longer blocks (or fails) on keep-awake acquisition:
a hung or failing native call is capped at a short budget and logged
instead of delaying or aborting dictation.
- Add native-call timeouts, orphan-tag tracking, and reacquire/drain
logic in mobile-dictation-keep-awake.ts so Activity recreation on
Android and stale tags no longer wedge the keep-awake queue.
- Add useMobileDictationForegroundKeepAwake to refresh the wake tag on
Android foreground and retry failed refreshes/deactivations.
- Hold the wake tag through chunk drain and the finish RPC so a screen
lock can't suspend the app before the transcript arrives, and keep
cleanup running even if native recording shutdown throws.
- Loosen expo-keep-awake to a caret range to unblock the patch pulling
in these native fixes.
* Fix cancellation races in mobile dictation keep-awake handling
- Run wake-lock release and dictation cancel concurrently on stale
starts so a hung acquisition no longer delays the native cancel
- Guard foreground reacquire retries with a run token so a stale
retry chain can't deactivate a wake lock reacquired by a newer
AppState transition
* Update source invariant test for concurrent stale-start cleanup
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Fix tui-idle detection for Cursor Agent lanes with dismissed trust dialo
Cursor Agent's persistent TUI never emits an idle OSC title, so a
dismissed trust dialog stayed in scrollback and kept the wait blocked
even once Cursor reached its idle "→" prompt. Detect Cursor's active
prompt (busy or idle) to clear a stale trust hit, and detect its idle
state specifically (prompt present, no braille spinner) to satisfy
tui-idle, so busy lanes keep polling instead of returning a stale
blocked result.
* Add test coverage for Cursor Agent trust-dialog banner detection
Verify lastIndexOf picks the later ready banner over an earlier
"Cursor Agent" mention in the trust dialog body, for both the
idle-resolution and busy-detection cases.
* chore(i18n): sync localization catalog for aiVaultSessionLogOpen keys
Pre-existing gap from #8205 (ai-vault view log) that fails `pnpm lint`'s
localization-catalog check on origin/main; unrelated to the tui-idle fix but
committed here to unblock CI lint on this branch.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Fix mobile source control drawer overflow and branch-compare state loss
- Render BottomDrawer in a native Modal so it covers the full viewport
even when mounted inside a ScrollView.
- Move the conflict/Abort row onto its own line so it never overflows
the branch card, and enlarge the Abort hit target.
- Show the committed-on-branch footer even when the changed-files
SectionList has no sections, since RN skips ListFooterComponent for
empty sections.
- Stop branch-compare state from collapsing to idle/error on transient
base-ref resolution failures when a ready result should be preserved.
* Add mobile source control drawer reload screenshot
Attaches an evidence screenshot for the mobile source control drawer overflow / branch-compare state loss fix.
* Remove stray temp screenshot file
Accidentally committed debug artifact from mobile source control drawer work; not needed in the repo.
* fix(source-control): keep push enabled for same-repo linked review with real upstream
Push/Force Push (and Pull/Sync/Publish) were wrongly disabled when a branch
had a genuine git upstream but an open linked PR whose push target had not
yet hydrated. `hasUsableHostedReviewPushTarget` hard-returned false for any
resolvable review link without a hydrated `pushTarget`, so
`resolveHostedReviewActionUpstreamStatus` synthesized `hasUpstream:false`
and the whole remote menu treated the branch as unpublished.
A same-repo review's head IS the checked-out branch, so a real upstream
already tracking that branch is that head and is safe to use before the
resolver hydrates a target. Treat that upstream as usable; keep blocking the
fork-head/cross-repo case (upstream tracks a differently-named head) and the
no-upstream case until the resolver proves the real target. Provider-agnostic
(GitHub PR / GitLab MR) and covers the SSH linked-review path.
Adds regression tests to source-control-hosted-review-push-target.test.ts and
a composed pipeline test in source-control-dropdown-items.test.ts.
* docs(source-control): trim verbose comments on linked-review push-target fix
Co-authored-by: Orca <help@stably.ai>
* refactor(source-control): use canonical gitRefTargetsBranchName for linked-review upstream match
Replace the hand-rolled upstreamTracksBranch leaf-split with the shared
gitRefTargetsBranchName primitive, which trims and rejects malformed
remote-qualified refs. The remote stays unknown until the resolver hydrates
the push target, so this remains a branch-leaf match; the strict remote+branch
check takes over once the target is known.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* perf(mobile): replace worktree name polling with events
* fix(worktrees): push rename invalidation to remote clients
worktrees:updateMeta deliberately skips the renderer notifier (PR #209),
but paired mobile clients no longer poll for titles, so a manual rename
would never reach them. Emit the remote-only worktreesChanged client
event (with resolved-cache invalidation), gated on displayName so
per-click isUnread writes stay event-free.
Co-authored-by: Orca <help@stably.ai>
* test(worktrees): add missing runtimeStub type member for typecheck
Co-authored-by: Orca <help@stably.ai>
* fix(worktrees): derive rename event repoId with the shared non-throwing parser
getRepoIdFromWorktreeId matches the mobile client's event filter exactly
and cannot throw after the meta write already persisted.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Add read-only View Log tabs so AI Vault agent sessions open inside Orca'
- Adds a `readOnly` flag on OpenFile that hard-blocks edits, autosave, dirty
state, rename, and drafts, and persists/restores it safely across sessions
- Wires AI Vault's View Log/Open Log actions to open logs as permanent
read-only local tabs instead of shelling out to the OS, gated to
local, single-file, non-synthetic session paths
- Registers a dedicated `jsonl` Monaco language (JSON-style coloring without
whole-document JSON validation) and forces read-only tabs to render as raw
source, bypassing markdown/mermaid/csv/notebook viewers
* Add live-tail streaming for local AI Vault View Log tabs
- Extend the fs:readFile snapshot path with byte-stable file identity so a
read-only tab can resume appending exactly where the snapshot left off.
- Add a ranged local log tail reader plus IPC (read/start/stop watch) that
streams only newly appended bytes, detects truncation/rotation, and
cleans up watchers on tab close or renderer destruction.
- Add a renderer-side UTF-8/line-boundary decoder and useLocalLogTail hook
that appends completed lines into the existing Monaco model, falling
back to a full reload on reset/rotation, keeping snapshot behavior
unchanged when liveTail is not opted in.
- Thread the new `liveTail` flag through OpenFile/PersistedOpenFile,
workspace session persistence/restore, and the AI Vault "View Log" open
path so live tail survives restarts and stays read-only-safe.
* Remove stale planning brief for the live-tail feature
The AI-VAULT-VIEW-LOG-LIVE-TAIL.md pick-up brief is no longer needed now that the live-tail streaming work has landed (c7fdef7b1).
* fix(terminal): send CSI-u Shift+Enter to kitty TUIs (droid) on Windows (#7620)
On Windows, Shift+Enter was always sent as the Alt+Enter byte ESC+CR (added in
#2418 for Codex, which reads win32-input-mode and ignores CSI-u). droid speaks
the kitty keyboard protocol, parses CSI-u directly, and treats ESC+CR as a plain
Enter — so Shift+Enter SUBMITTED the message instead of inserting a newline.
droid works in other terminals (Windows Terminal, Warp) because those honor
win32-input-mode / kitty; Orca (xterm.js) withholds kitty from local Windows
ConPTY panes and emits neither.
Make the Windows Shift+Enter byte pane-aware: latch whether a pane's program
advertised the kitty keyboard protocol (query CSI ? u, push CSI > .. u, or set
CSI = .. u) and send CSI-u (\x1b[13;2u) to those panes, keeping the
Codex-compatible ESC+CR for win32-input-mode-only TUIs. Non-Windows is unchanged
(always CSI-u).
Verified end-to-end against the real droid and Codex CLIs through the actual
production functions: droid now newlines, Codex still newlines.
* fix(terminal): route Windows Shift+Enter safely for Droid
---------
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(mobile): recover terminal state after iOS resume
* Refactor terminal record merge to extract snapshot-reconciliation helper
Split the inline merge logic in mergeTerminalRecordsByCurrentOrder into a
named mergeTerminalSnapshotWithKnownRecord function for clarity, preserving
the existing behavior of keeping the last known theme when a snapshot omits it.
* perf(memory): scope boot worktree scans to live sessions
* fix(memory): refresh daemon hydration snapshots
* fix(memory): rescan repos referenced only by the re-read snapshot
Repo selection previously came solely from the first daemon listing, so a
session that only became visible on the post-enumeration re-read (e.g. a
briefly unreachable adapter) could never be registered for the life of the
process. Loop enumeration until the latest daemon snapshot references no
unresolved repos, so selection and registration always share a snapshot.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Cap automationRuns retention to stop unbounded state file growth
`automationRuns` was the only unbounded durable collection in
`orca-data.json`, and the whole blob is re-serialized and rewritten on
every save. On a machine running four `* * * * *` automations it had
grown to 11,184 rows / 21 MB of a 28.5 MB file — all of them
`skipped_precheck` no-ops — so each synchronous `flush()` blocked the
Electron main thread for 190-210 ms and macOS filed 24 `disk writes`
diagnostic reports against Orca (12-30 MB/s sustained, 549 GB/session).
Prune to the newest 100 runs per automation, on load and on append. This
mirrors the retention that `pruneLocalTerminalScrollbackBuffers` and
`pruneWorkspaceSessionBrowserHistory` already apply to their
collections; `automationRuns` was simply missed.
The load-path prune marks state dirty, so an oversized file heals on
first load. Without that flag the shrink lives only in memory: the sole
load-time save trigger is `normalized.changed || loadNeedsSave ||
adaptedProjectGroups`, and `normalized` covers pane identity only. A
user who took the documented workaround (`automations edit --disabled`)
fires no runs, so nothing would ever rewrite the file.
Measured against the affected 28.5 MB file: flush() 190 ms -> 9 ms,
bytes written per save 28.5 MB -> 1.3 MB.
Pruning breaks the old `runNumber` derivation, which counted retained
runs, so every run after the cap would have been titled "run 101".
Carry the ordinal on the run itself and derive the next number from the
highest survivor. Legacy rows are numbered from the highest number their
automation already carries, not from their append position: a downgrade
to a pre-`runNumber` build appends unnumbered runs after pruned
survivors numbered 101+, and a position would reissue one of those,
giving two runs the same title.
Fixes#8118
* Never evict in-flight automation runs from retention
A dispatched run's completion can land hours later (renderer round-trip or
headless completion promise); pruning it makes updateAutomationRun throw
'Automation run not found.'. Only final-status runs are evictable now, with
the final-status predicate shared between retention and the service.
Co-authored-by: Orca <help@stably.ai>
* Skip the usage write when retention evicted the run mid-collection
markDispatchResult finalizes a run, awaits usage collection, then writes
usage by id. The run is final during that await, so a concurrent
create-time prune can evict it and the write threw 'Automation run not
found.' — in the headless path that cascaded into an unhandled rejection.
Co-authored-by: Orca <help@stably.ai>
* Pin backfill-before-prune ordering with true legacy fixture rows
The heal-on-load fixture rows carried runNumber, so swapping backfill and
prune passed every test while renumbering real legacy survivors 1..100 and
re-minting colliding titles. Seed unnumbered rows and assert healed ordinals.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Previously a failed resize during reclaimTerminalForDesktop left the
override/lock in place (#7588 semantics), but for an explicit "take
back all terminals" gesture that could strand banners on background
panes whose resize can't converge. Now the take-back unconditionally
releases the driver and clears any held fit-override via a new
releaseDesktopTakeBack helper, while auto-restore and phone-initiated
paths keep the original keep-lock-on-failure behavior.
Also extract the local mapWithConcurrency helper out of
workspace-cleanup.ts into a shared, tested src/shared/map-with-concurrency.ts
and use it to bound concurrent desktop-fit reclaims in
terminal-fit-restore.ts.
* Redesign mobile search field as a shared, raised component
- Extract MobileSearchField from duplicated Search icon + TextInput + clear
button markup in worktree list and tasks screens into a reusable component
- Give the field a raised bgRaised shell with focus/disabled states so it
reads as a tappable control instead of blending into panel chrome
- Fix delayed autoFocus via InteractionManager + timeout so the keyboard
reliably appears after the search bar opens
- Preserve per-screen clear behavior (preset/query fallback for GitHub,
project-view filter) via configurable showClear/onClear props
* Simplify GitHub project search state checks and fix stuck clear button
- Extract `isGithubProjectSearch` to dedupe repeated `provider === 'github' && githubMode === 'project'` checks
- Fix showClear so an explicit empty applied override doesn't leave the clear button visible forever