Root cause: main gates every pty:data send on a global + per-PTY
in-flight counter that only renderer ACKs decrement. If ACKs are lost
across a system suspend, the counters pin at the cap and every PTY —
old and newly created — is silently gated forever while output piles up
in pendingData. A focus-preserving display wake also fires no renderer
focus/visibilitychange events, so terminal wake recovery (and the WebGL
context-loss latch clear) never runs. Only a renderer reload recovered.
Three fixes:
- ACK-stall watchdog (src/main/ipc/pty.ts): if sends stay gate-blocked
for 10s with zero ACK progress while the renderer webContents is
alive, warn once, reset the in-flight delivery counters, and flush
held pendingData. Armed lazily on the first gate-blocked send and
disarmed by every ACK, so it can never fire under healthy heavy load.
- Renderer lifecycle reset now also zeroes the in-flight counters — a
reload/navigation destroys the renderer dispatcher, so outstanding
ACKs can never arrive and stale counters would gate the new renderer.
- System-resume wake IPC: main relays powerMonitor 'resume' as
system:resumed to live windows (plus forceRepaint); preload exposes
ui.onSystemResumed; the terminal wake-recovery hook runs the same
recovery path as window focus/visibilitychange.
Co-authored-by: Orca <help@stably.ai>
* Fix CDP printToPDF proxy
* Harden CDP PDF stream handling
* Fix PDF margin units
* refactor(cdp): extract printToPDF param mapping + stream store into cdp-print-to-pdf
Move the CDP Page.printToPDF option translation and the ReturnAsStream handle
store out of cdp-ws-proxy into a focused cdp-print-to-pdf module, mirroring the
existing cdp-screenshot sibling. Keeps the over-budget proxy from growing
further and gives the PDF logic direct unit coverage.
No behavior change: all existing proxy tests pass unmodified.
Co-authored-by: Orca <help@stably.ai>
* fix(cdp): drop print result if client disconnected during printToPDF
A ReturnAsStream print that resolved after the requesting client
disconnected (or was replaced) registered its buffer in pdfStreams after
the disconnect's clear() already ran, orphaning a potentially large PDF
until the 5-minute TTL. Re-check isActiveClient after the await, mirroring
navigateWithLifecycle/reloadWithLifecycle.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <foxtrot-eng@stably.ai>
Co-authored-by: Orca <help@stably.ai>
* Allow arbitrary hostnames in manual network address entry
parseManualNetworkAddress only accepted an IPv4 address or a Tailscale
MagicDNS (*.ts.net) hostname, so users behind a dynamic residential IP
who rely on a DDNS domain or self-hosted relay had no way to enter it
in the desktop UI short of bypassing validation via DevTools/IPC.
The main process already resolves any host: resolvePairingEndpoint
and parsePairingAddressOverride in src/main/runtime/runtime-rpc.ts
accept an arbitrary hostname and an optional host:port. This change
brings the renderer-side validation in line with what pairing already
supports: any RFC 1123 hostname (a superset that still covers
*.ts.net), optionally suffixed with :port (1-65535). IPv4 validation
is unchanged, including still rejecting malformed dotted-numeric input
instead of silently treating it as an all-digit hostname.
Updates the custom-address dialog copy in NetworkInterfacePicker.tsx
to describe the wider grammar.
* Polish manual-address takeover: fix bare-numeric guard, sync 5 locales, lint
- Require a dot in the IPv4-typo guard so a bare numeric label (`123`)
validates as a legal RFC 1123 hostname, matching the code's own comment
and the main-process resolver; add coverage.
- Update en.json + es/ja/ko/zh placeholder/hint to the broadened grammar
(translate() reads en.json before the TSX fallback, so the copy change
was previously inert; the other locales described the old ts.net-only rule).
- Replace indexOf(...)!==-1 with includes() to satisfy oxlint.
Co-authored-by: Orca <help@stably.ai>
* Keep validator a strict subset of the backend resolver
Review surfaced two ways the renderer could accept an address the main
process handles differently:
- All-numeric hosts (bare `123` and dotted `256.0.0.1`) are now rejected.
The WHATWG URL host parser downstream reinterprets a numeric host as IPv4
(`123` -> `0.0.0.123`), so accepting one would validate an address the
pairing resolver silently dials as a different host.
- Ports with leading zeros are rejected. `^[0-9]+$` let an arbitrarily long
zero-padded string past the range check and inflate the returned address
beyond the hostname length cap that the old whole-string check enforced.
Co-authored-by: Orca <help@stably.ai>
* Reject any numeric final label, not just fully-numeric hosts
WHATWG URL host parsing treats a host whose last label is numeric
(`foo.123`, `foo.0x1`) as an IPv4 signal, so the pairing resolver would
fail to parse it and silently dial a fallback host. Widen the ambiguous-IP
guard to a single last-label check that subsumes the earlier all-numeric
case, keeping the renderer a strict subset of what the backend resolves
correctly. Normal hostnames whose last label merely contains digits
(`host2.example.com`) are unaffected.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(sidebar): keep independent same-host checkouts of one repo as separate projects
Re-regression of #5374: PR #6320 removed the per-host-surface separation
added by #5375, so independent local clones of the same repo (shared origin
=> same github:owner/repo identity) collapse into one sidebar project again.
Restore the surface-key separation in worktree-list-groups.ts, scoped by
ProjectHostSetup.setupMethod: 'provisioned' (ephemeral-VM runtime copies)
still nest under the project header (preserving #6320), while real user
checkouts (legacy-repo / imported-existing-folder / cloned) each get their
own header when duplicated on one host surface.
Revert the two tests #6320 inverted and add coverage for a provisioned copy
nesting alongside a same-host user checkout.
* test(cmd-j): update palette grouping tests for same-host checkout split
The Cmd+J palette shares getProjectGroupingForRepo (via
getProjectHeaderRevealTarget) with the sidebar, so restoring same-host
checkout separation also splits independent `cloned` checkouts into
per-setup palette entries. Update the multi-setup test to assert the split
and add coverage that a `provisioned` runtime copy still nests under one
project key.
* test(sidebar): cover split + provisioned-nest on one host together
Guard the intersection of #5374 and #6320: when a project has two real
same-host user checkouts AND a provisioned copy, the real checkouts each get
their own per-setup header while the provisioned copy stays under the plain
project key (never a path-scoped ::setup: header). Locks the two features'
combined behavior so a future change can't silently regress either half.
* refactor(sidebar): reuse shared Windows-path helper, single-pass surface count
Take-over polish of #7237:
- Use shared isWindowsAbsolutePathLike instead of an inline drive-letter
regex, deduplicating the pattern and also bucketing UNC \\server\share
paths as a Windows surface instead of falling through to default.
- Collapse buildProjectGroupingIndex's two passes over projectHostSetups into
one, computing each surface key once and flagging ambiguous projects from
the aggregated counts.
- Tighten why-comments to AGENTS.md's two-line rule.
No behavior change to the fix; all 123 sidebar/palette grouping tests pass.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix: block resuming host-stored AI Vault sessions into SSH workspaces
AI Vault scans session history from this machine's disk only (host home
dirs plus local WSL homes), but since #6685 resume actions accepted any
SSH workspace as a target. Resuming a host-stored session there queues
cd '<local cwd>' && <agent> --resume <id> into a remote shell where
neither the cwd nor the session file exists, so it always fails.
Gate resume targets on where the session file lives: local targets stay
unrestricted, SSH targets now require a WSL-stored session (the
SSH-to-local-WSL setup #6685 was built for). Applies to the sidebar
resume buttons, direct resume, and pane drag-and-drop, with a toast
explaining the host mismatch. Catalog sync also picked up two
pre-existing drifts (missing failedUnnestWorkspace fallback, two
unreferenced SSH-timeout keys).
* i18n: translate host-session SSH-resume block message (es/ja/ko/zh)
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* test(agent-completion): cover done after waiting pause
Co-authored-by: Orca <help@stably.ai>
* test(agent-completion): assert attention fires alongside done-after-waiting
Wire dispatchAttention into the done-after-waiting regression test so it
verifies both halves of the #5698 fix: the mid-turn permission pause
surfaces as attention exactly once, and the later genuine 'done' still
completes exactly once. Matches sibling tests that assert on both paths.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: brennanb2025 <brennankbenson@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(tabs): restore deferred tab activation so dragging a terminal tab doesn't switch panes mid-gesture
PR #5927 shipped terminal-pane drag (reorder tabs, move a tab into another
pane, edge-split into a new column). It deliberately DEFERRED tab activation
to pointer-up and suppressed it during a drag, so pressing a tab to drag it
never switched the active tab or stole terminal focus mid-gesture.
PR #6395 (d32d62a395) deleted tab-strip-pointer-activation.ts and made
SortableTab/EditorFileTab/BrowserTab activate eagerly on pointerdown — fixing
click-to-switch-after-reorder but regressing the drag: grabbing a tab now
flips the active tab + focused pane and yanks xterm keyboard focus before the
drag is even recognized (aggravated by PR #6210 raising the dnd-kit threshold
5px->12px). The move itself still lands, but the gesture feels broken.
Restore the deferred-activation hook, but gate it on measured pointer
DISPLACEMENT instead of the drag-active context ref the old hook used — that
ref clears asynchronously relative to the drop's pointerup, which is exactly
what made #6395's click-after-reorder misfire. Displacement mirrors dnd-kit's
own activation threshold: a release within TAB_DRAG_ACTIVATION_DISTANCE_PX is
a click (activate); crossing it is a drag (suppress). Because each press
measures its own gesture, a click after a reorder always activates.
- Recreate src/renderer/src/components/tab-bar/tab-strip-pointer-activation.ts
(displacement-gated) + unit tests, incl. an explicit #6395 regression case.
- Rewire onPointerDown in SortableTab, EditorFileTab, BrowserTab to use it.
- BrowserTab.test.tsx shallow-renders via mocked React hooks; add useRef/
useCallback stubs so the new hook works under that harness.
Verified end-to-end in a dev build via CDP: pressing a tab no longer
activates/steals focus on pointerdown; a click still activates on release;
dragging a tab into another pane still moves it; clicking tabs after a drag
still switches (the #6395 guard in tests/e2e/tabs.spec.ts stays green because
it uses zero-displacement clicks). 233 unit tests pass; oxlint + renderer
typecheck clean.
* fix: restore pane detach tab placement
Claude Code 2.1.199 renders the Fable plan bucket as Current week (Fable), while Orca only parsed a standalone Fable heading. Accept weekly-style Fable headings as the distinct Fable weekly meter while keeping broader Fable-weekly copy as a section boundary to avoid false positives.
Validation:
- pnpm exec vitest run --config config/vitest.config.ts src/main/rate-limits/claude-pty.test.ts src/main/rate-limits/claude-fetcher.test.ts src/main/rate-limits/service.test.ts src/renderer/src/components/status-bar/tooltip.test.ts src/renderer/src/components/status-bar/inline-usage-bars.test.tsx
- pnpm run typecheck
- pnpm exec oxlint src/main/rate-limits/claude-pty.ts src/main/rate-limits/claude-pty.test.ts src/main/rate-limits/claude-fetcher.ts src/main/rate-limits/claude-fetcher.test.ts src/main/rate-limits/service.ts src/main/rate-limits/service.test.ts src/renderer/src/components/status-bar/tooltip.test.ts src/renderer/src/components/status-bar/inline-usage-bars.test.tsx
- git diff --check HEAD~1..HEAD
- live local Claude fetcher returned fableWeekly: 62
Fix the per-tab CDP proxy reload path so raw Page.reload keeps the Orca browser tab alive. Lifecycle priming is bounded and root reloads use the same safer webContents reload behavior as Orca's own browser reload path.
Fixes #6972.\n\nPreserves mobile terminal buffered/live input mode across Android terminal re-entry and session refreshes. Includes follow-up hardening for pre-hydration preference edits and failed storage reads.
* Recover exported terminal handles from live PTYs
* Guard terminal-handle adoption: first-wins, no collisions
Discovery adoption of ORCA_TERMINAL_HANDLE is now skipped when the pty
already has a handle bound this session (re-keying would strand waiters
registered under the old handle) or when the reported handle is already
bound to a different pty (provider-reported values are not trusted to be
collision-free). Also adds the relay why-comment and PtyProcessSummary
type alias from review feedback.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Fix Vim alternate-screen terminal redraw recovery
* Decide alternate-screen atlas recovery from parsed buffer state
Replace the raw-chunk scan for alternate-screen enter sequences with the
xterm parser's own verdict: in-place rewrite chunks check
buffer.active.type (plus a parse-time buffer-switch count) inside the
write callback, where the buffer is authoritative. Covers enter/exit
sequences split across PTY chunk boundaries and full enter-exit cycles
coalesced into one write, and deletes the hand-rolled CSI parser.
Adds regression tests from a real captured vim session (including a
CSI sequence split at a 1024-byte PTY read boundary) and a contract
pin against the real @xterm/headless parser.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(emulator): recycle iOS simulators that wedge-boot without a display framebuffer
CoreSimulator can report a device Booted while its display IO ports never
came up (HID alive, no com.apple.framebuffer.display port), so
ensureSimulatorBooted passes and serve-sim --detach dies with 'No
framebuffer display descriptor found'. Reconnecting hits the same Booted
early-return, so the pane could never recover without a manual
simctl shutdown/boot.
startSession now recognizes that helper failure signature, recycles the
device once (shutdown + boot), and retries; if the display still fails to
come up it surfaces an actionable erase/recreate message instead of the
raw helper log dump.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: harden simulator framebuffer recovery
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Fix mirrored Codex relative config paths
Orca mirrors ~/.codex/config.toml into a managed CODEX_HOME before launching Codex. Relative path-valued Codex settings were then resolved from the runtime home instead of the user's real Codex home, which made config loading fail in Orca while the same CLI worked in a normal terminal. Rewrite known relative path settings to absolute paths rooted at the system Codex home while preserving runtime-owned trust sections.
* Dedupe Codex TOML line scanner and include path rewrite in CLI tsconfig
* Harden Codex config path rewrite and cover managed account homes
- Track multiline arrays in the shared TOML line scanner so array lines
are never mistaken for table headers or path keys
- Escape control characters and reject lone-surrogate unicode escapes so
the rewritten runtime config always stays valid TOML
- Extend the rewrite allowlist with profiles.* file settings and
debug.config_lockfile.* (both can abort Codex config loading)
- Rewrite relative paths when mirroring the canonical config into
managed account homes (codex login CODEX_HOMEs), anchoring WSL
accounts to the Linux-side ~/.codex with posix join semantics
---------
Co-authored-by: Neil <neil@stably.ai>