Resolves four CodeRabbit findings on the remote-desktop viewer width
ownership change, plus a round-trip race surfaced while fixing the leak:
- Leak: the one-shot terminal.updateViewport RPC has no disconnect hook,
so it must never create a width floor (nothing releases it, pinning the
host at a stale width). Add refreshRemoteDesktopViewer, which only
refreshes floors the client already owns via its stream subscription
(matched by clientId), mirroring the mobile updateMobileViewport
no-op-without-subscription invariant. The one-shot handler now refreshes
instead of registering.
- Round-trip replay: with the one-shot fallback now refresh-only, a resize
landing during the subscribe round-trip (connected, stream not yet
current) was dropped. The transport now replays the latest viewport over
the stream once it becomes current.
- Reclaim: keep the host reclaim target unless the reclaim resize actually
landed (result.ok), so a failed reclaim can retry against true host
geometry instead of a stale remote width.
- Snapshot drain: drain pendingRemoteDesktopViewport in the
sendRequestedSnapshot finally block; a viewer resize parked during a
SnapshotRequest buffering window was otherwise dropped until the next
resize.
- Key scope: scope the multiplex width-floor key by connectionId so two
connections reusing the same client-local streamId can't overwrite or
release each other's floor.
Tests: refresh-never-creates-floor, reclaim-target-retained-on-failed-resize,
SnapshotRequest parked-resize drain, connection-scoped key assertions, and a
round-trip replay race test (verified to fail without the flush).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A remote (relay/shared-control) desktop viewer resizes the host source PTY to
its own width, but was never registered as a width owner. So the host's own fit
cascade (window resize, split drag, tab reveal, "+"-new-tab re-render) freely
resized the viewed PTY back to the host-local width with no signal to the
viewer. The viewer kept its narrower grid while the host streamed wider
alt-screen frames -> cell-layout garble ("porridge") until a manual resize.
The mobile "presence lock" already solves this shape for phones by suppressing
the host's pty:resize while the phone drives. This does the same for remote
desktop viewers WITHOUT joining the mobile driver state machine (a viewer needs
only resize suppression, not input lock / phone-fit / driver banners), and it
routes every PTY geometry change through the existing enqueueLayout/applyLayout
serialization path rather than resizing the PTY ad hoc.
Design:
- Registry `remoteDesktopViewers: Map<ptyId, Map<subscriptionKey, viewport>>`,
keyed per SUBSCRIPTION so duplicate streams of one client cannot release each
other. isPtyResizeDrivenRemotely() = mobile driver OR any viewer present;
pty:resize (host fit cascade) bails on it. INPUT is never locked (shared
control: host and viewers can both type), unlike mobile.
- New internal layout target { kind: 'remote-desktop' } in applyLayout: clears
terminalFitOverrides like 'desktop', resizes only when dims changed, emits no
fit-override notifications, and does not call onExternalPtyResize (so a
viewer's width never pollutes desktop restore state). enqueueLayout remains
the sole serialized writer of PTY dimensions.
- Smallest client wins: applyRemoteDesktopLayout() sizes the PTY to the SMALLEST
attached viewer, so viewers with different screen sizes never overflow the
narrowest grid.
- No snapshot/replay race: a viewport that arrives while the initial scrollback
snapshot is being serialized is buffered and applied as ONE serialized resize
before serialization, so a mid-repaint alt screen is never baked into the
snapshot.
- Reclaim on detach: when the last viewer leaves, applyRemoteDesktopLayout
resizes the PTY back to the host's own width via enqueueLayout({kind:'desktop'}),
so the host reflows to full width without a manual window jiggle. The host
width is captured at the pty:resize suppression point (the host's own resize
attempts) — a source a remote viewport never pollutes.
- Mobile coexistence: a phone outranks a viewer; when the phone leaves, the
mobile-release paths call applyRemoteDesktopLayout so a surviving viewer keeps
the PTY, else the host reclaims.
Verified live (two local desktop instances): correct render on connect, no
garble on viewer-window resize (synchronous), and host reclaims full width on
disconnect. Unit tests cover the registry lifecycle, smallest-client-wins,
reclaim-via-layout, and mobile coexistence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
* fix(mobile): bump Android versionCode to 7 for 0.0.27
Android 0.0.26 shipped with versionCode 6. Align Android on marketing
version 0.0.27 with a higher versionCode so side-loaded upgrades install.
* test(mobile): expect direct cmd syntax for live Windows resume
Resume commands are typed into the host terminal; when that shell is
already cmd, wrap with cmd /d /s /c is wrong. Align the mobile unit
test with shared buildAiVaultResumeShellCommand behavior.
* fix(win): resume quoted cmd.exe startup commands via stdin, not /K
Resuming an AI Vault session into a cmd.exe tab on Windows failed with
"'...' is not recognized as an internal or external command" and never
ran the resume. The queued command for a cmd live shell is the
self-contained `cmd /d /s /c "cd /d ""cwd"" && claude ""--resume"" ""id"""`
form, which is correct when typed into cmd's interactive parser (its ""
doubling is cmd's convention). But the local PTY provider embedded it in
the `/K` launch argument, where node-pty's C-runtime argv escaping emits
backslash-escaped quotes (\") that cmd.exe cannot parse — the command
arrived as `\"cd /d \"\"cwd\"\" && ...` and was rejected wholesale.
Unlike PowerShell's -EncodedCommand, cmd.exe has no robust argv-quoting
path, so any startup command containing a double quote now falls back to
stdin delivery, where cmd's interactive parser handles the "" doubling
correctly (verified end-to-end against a real ConPTY via node-pty).
Quote-free commands keep the `/K` fast path.
* fix(win): preserve cmd resume command contracts
* fix(win): match copied resume commands to shell
---------
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
* 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>