mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 00:02:34 +00:00
99d19d4635c40e23b2ef546136b22fa3efeb7367
294
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
953cfab635 |
fix(terminal): make the bold font weight its own setting (#14368)
* fix(terminal): make the bold font weight its own setting
Deriving bold as max(700, regular + 200) silently destroyed bold. A family
exposes only a few real faces: the monospace the default chain resolves to on
macOS has exactly two, splitting at 600. Measured by rasterizing each weight to
a canvas — 100-500 are byte-identical (ink 3023) and 600-900 are byte-identical
(ink 3855), at every weight the same advance. So any base weight at or above 600
put both values in the same face and bold stopped existing, on 4 of the 9
positions the slider offers, with no error and nothing the user could do.
Arithmetic cannot fix it — on a two-face family there is no heavier face to
escape to. So bold is now user-owned: a new terminalFontWeightBold setting with
its own control, defaulting to 700. The default pair (500/700) straddles the
boundary, so existing profiles render exactly as before; a collision is now a
choice the user can see and undo.
The old test asserted 800 -> {800, 900} as 'keeps bold heavier', which is where
this hid: numerically heavier, identically rendered.
* fix(terminal): surface bold face collisions accurately
|
||
|
|
c86418eaad |
rm git shim (#14141)
* rm git shim Drops the terminal git/gh PATH wrapper and its settings toggle. Renames the no-marker shell-ready launch config after what it does. Co-authored-by: Orca <help@stably.ai> * rm git shim: clear stale state from older installs Deletes the orphaned wrapper dir and scrubs inherited env/PATH, so a daemon that outlives the upgrade cannot keep seeding it. Drops a now-unread spawn option. Co-authored-by: Orca <help@stably.ai> * rm git shim: cover the daemon and headless paths Scrub after the PATH prepends (they re-read process.env on the sparse daemon env) and run the cleanup above the serve branch so remote hosts get it too. Retry a locked removal; match PATH case-insensitively. Co-authored-by: Orca <help@stably.ai> * rm git shim: keep the scrub final Refuse to re-prepend a legacy entry during agent-teams PATH promotion, which runs after the scrub. Cover the removal guard. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
5e3a2d25f7 |
Filter remote workspaces by creating device (#13718)
* Filter remote workspaces by creating device * Fix workspace origin filter reconnect behavior * Shorten workspace origin filter label * Refine remote workspace filter UX --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
ce8c3267b7 | feat(terminal): add anchored link action popovers (#13414) | ||
|
|
3ec48a74d5 |
Gate artifact publishing behind off-by-default capability (#13368)
* fix(artifacts): gate agent artifact publishing behind an off-by-default capability Public artifact sharing was reachable by any agent through `orca artifacts share`: the Artifacts settings toggle only controlled sidebar visibility, and nothing in the main process checked a capability before minting a public URL. Add `artifactSharingEnabled` (default off) and enforce it in ArtifactCloudService.share/update — before auth, network, or the share-record write — so the CLI, relay-forwarded remote CLI, and IPC paths are all denied. The denial carries a stable `artifact_sharing_disabled` code plus next steps through the RPC error allowlist, so the CLI prints actionable guidance. list, unshare, and delete stay ungated: turning publishing off must not strand already-published links. The capability is absent from the `settings.update` RPC schema, so an agent cannot grant it to itself — only the desktop UI can. Co-authored-by: Orca <help@stably.ai> * fix(artifacts): gate agent artifact publishing behind an off-by-default Publishing is blocked until enabled in Settings → Artifacts. CLI preflights the capability before reading files to avoid unnecessary uploads. RPC surface rejects capability grants so callers cannot self-grant. UI shows opt-in workflow and recovery path when publishing is off. Web clients mirror the host's setting read-only. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
394e4bf1c0 |
Polish Artifacts management UI (#13356)
* refactor(artifacts): polish artifact management UI * fix(artifacts): address UI polish review --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
c6ded160b2 |
Revert the Korean Won to backquote mapping (#13312)
* Revert "fix(terminal): detach the Korean input-source probe when its setting goes off (#13283)" This reverts commit |
||
|
|
24003936a5 |
feat(terminal): Korean Won (₩) → backquote key mapping for Korean keyboards (#13104)
* feat(terminal): map Korean Won (₩) key to backquote on macOS Korean keyboard users type markdown code fences and shell backquotes on the key that US layouts reserve for ` — 두벌식 and 세벌식 390 put ₩ there, 세벌식 최종 puts *, so there was no way to type a backquote without switching layouts. Add a Mac-only terminal setting, "Korean Won (₩) to Backquote (`)", that rewrites the plain backquote-position keystroke to backquote while a Korean input source is active. It sits in Terminal → Advanced, right below the existing JIS Yen (¥) to Backslash (\) mapping. The rewrite keys on the keystroke position alone — no character or layout-variant knowledge — and follows the live input source through the existing MacNativeTextInputSourceTracker, which refreshes on focus and keyboard activity (Caps Lock / 한영 input switches never blur the window). Modified chords and IME-composed events pass through untouched. Covered by resolver and input-source tracker unit tests; verified manually in the GUI. * docs(terminal): clarify Korean Won mapping scope and add docstrings State in the setting copy that the backquote rewrite applies only while a Korean input source is active, and add JSDoc to the Korean Won resolver exports (addresses CodeRabbit pre-merge docstring coverage and copy-clarity findings). |
||
|
|
c991bb27d3 | Add account-backed artifact sharing (#13012) | ||
|
|
8ddf575fe6 |
Revert "Remove source control group order preference (#12785)" (#12955)
This reverts commit
|
||
|
|
a30e3b9f61 |
feat(dashboard): add experimental agent map view (#12168)
* feat(dashboard): add experimental agent map view
* fix(dashboard): harden agent map behavior
* fix(dashboard): harden agent map recovery
* fix(dashboard): close map selection on view change
* fix(agent-map): center sparse layouts
* fix(agent-map): align completion and workspace actions
* Polish agent map interactions and repo labels
* feat(agent-map): add worktree lineage and project actions
* fix(agent-map): use marker for unread agents
* fix(agent-map): compact orchestrated families
* fix(dashboard): harden agent map actions and layout
* fix(agent-map): bound layout work and preserve interactions
* fix(agent-map): move unread marker to ring top-right
* fix(agent-map): seat unread marker on the ring's top-left edge
* feat(agent-map): restore the agent launcher and declutter map labels
Three gaps in the experimental Agent Map:
- The "start a new agent" picker was split onto a preserved branch during the
08-02 rebase (
|
||
|
|
ae1ed5e886 |
Remove source control group order preference (#12785)
* Reorder source control to show staged changes first by default Stages are closest to the commit action and most relevant to the commit workflow. Merges untracked files into Changes visually while preserving their Git area. Removes the untracked-first preset and includes migration logic for existing user settings. * Drop source control group order user preference Remove the sourceControlGroupOrder setting and related UI, migrations, and persistence logic. The source control view now always displays sections in the order: staged changes, unstaged changes, untracked files. * Reorder source control to show changes before staged Aligns with the edit-stage-commit workflow by showing unstaged changes (active edits) before staged changes (queued for commit). |
||
|
|
e59a319ffe |
fix(sidebar): keep each project's entry-point workspace visible under "Hide sleeping" (#12257)
"Hide sleeping" swept each project's main workspace out of the sidebar as soon as it had no live PTY, browser tab or agent — even with "Hide default branch" off. For a project whose only row is that workspace (a folder workspace, a fresh clone, a detached-HEAD main), the entire project vanished with no in-place way back. Adds a shared `isSleepingSweepExemptWorkspace` predicate keyed on `isMainWorktree` rather than the branch name, so folder workspaces (no branch), detached-HEAD mains, and SSH rows whose head/branch are blanked while a provider is disconnected all stay put. Wired into `computeVisibleWorktreeIds` (sidebar, Cmd+1-9, workspace board), the jump palette's duplicate inline pass, and mobile's `filterWorktrees`. Ships default-on with an escape hatch: a persisted `alwaysShowDefaultBranchWorkspace` setting surfaced as "Except default branch" under "Hide sleeping". Explicit "Hide default branch" still wins, since it filters before the sleeping sweep. Mobile reads the setting but never writes it back, so a desktop opt-out can't be clobbered by a filter tap before the ui.get roundtrip lands. Combines the two PRs open against #8873. #8966's exempt set is a strict subset of this one, so its production diff was subsumed rather than ported; its jump-palette render harness and e2e spec were carried over, and are the only such coverage here. Fixes #8873 Closes #8966 Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
93a2ad8fd8 |
fix(browser): bound retained webview guests across worktree switches (#12194)
* fix(browser): bound retained webview guests across worktree switches * fix(browser): merge duplicate imports flagged by import/no-duplicates * fix(browser): evict guests without unmounting the worktree surface Unmounting the surface disposed every parked terminal byte watcher for the evicted worktree (bell/title/agent-completion facts dropped for good after the 15s handoff TTL) and skipped the force-park scrollback capture remote panes rely on. Destroy the guests only: hidden slots mount no BrowserPane, so nothing resurrects a destroyed guest before the next visit, and no slot ever unmount-detaches a live guest (STA-3228). Terminal-state vetoes and the eviction re-render bump are no longer needed; the only veto left is a guest an automation/mobile controller is actively driving. * fix(browser): veto eviction for downloading pages; keep zoom; add kill switch Main cancels a page's active downloads when its guest unregisters (tab-close semantics), so eviction now vetoes worktrees with a page still writing a download, tracked app-wide because download state was pane-local and hidden panes are unmounted. Eviction is not a user close: re-remember explicit zoom past the destroy-path forget so a revisit reasserts the user's zoom instead of resetting same-host siblings through Chromium's partition-wide HostZoomMap. browserGuestWorktreeRetentionBudget mirrors the terminal budget kill switch. |
||
|
|
7c7167028c |
feat(voice): allow selecting a microphone for dictation (#12119)
* feat(voice): allow selecting a microphone for dictation Persist a preferred audioinput device in Voice settings and pass it into getUserMedia, falling back to the system default when the device is gone. * fix(voice): resolve mic preference by label and detect mid-capture loss Drop Chromium's 'default'/'communications' aliases from the picker — pinning one behaved exactly like system default and silently defeated the setting. Resolve a stored preference against the live device list before capturing: a unique label match heals an id that Chromium re-salted, a known-missing device skips the doomed getUserMedia attempt that clipped the first words, and an unreadable list no longer reads as "unplugged". Surface the input ending mid-dictation instead of feeding silent zeros, add a permission affordance so the picker is not empty before mic access, and toast the fallback once per preference rather than once per utterance. Co-authored-by: Orca <help@stably.ai> * add e2e tests * add e2e tests --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
ed00ab0f34 | fix(ssh): restore relay ownership after app restart (#11860) | ||
|
|
e698241aab |
fix(mobile): remember custom pairing addresses (#11741)
* fix(mobile): remember custom pairing address * fix(mobile): stabilize custom pairing address sync * fix(mobile): update pairing refresh refs after commit * feat(mobile): manage saved custom pairing addresses * fix(mobile): harden custom address selection |
||
|
|
e20554bfd7 | fix(terminal): reduce inactive pane dimming (#11591) | ||
|
|
c74fb3c71b |
feat(browser): let Shift invert link routing instead of always forcing the system browser (#10991)
* feat(browser): let Shift invert link routing instead of always forcing the system browser Shift+Cmd/Ctrl-click has always meant "open in the system browser", which is a no-op when that is already where links go. Users who keep Link Routing off have had no gesture to pull a single link into Orca's built-in browser. Adds "Hold Shift to open in ___", a nested toggle under Link Routing that makes the modifier open a link the opposite way from the setting. It ships off, so the one-way escape hatch is unchanged for every existing user. The title and description name the destination the modifier actually reaches and flip with the parent setting, since "the opposite" is meaningless on its own. - openHttpLink gains modifierHeld; resolveModifierRouting owns the decision so every surface (terminal URLs, OSC 8, xterm web links, markdown preview) routes identically. forceSystemBrowser stays for callers that must bypass settings. - The terminal hover hint names Orca when the modifier would open there, and is re-resolved per hover so toggling applies without recreating panes. - Link Routing's own copy drops "always uses your system browser", which the new toggle can falsify; the nested row states the live destination instead. * fix(browser): route the Checks panel hosted-review link through the shared modifier The "Open on GitHub" button had its own Shift+Cmd/Ctrl escape hatch that passed forceSystemBrowser directly, so it kept the old one-way behavior while every other surface honored the invert setting. Route it through modifierHeld like the terminal and markdown paths. Also wraps the modifier row's description in translate(); the title in the same file was localized but the description returned raw English (caught in review). * fix(browser): make link-routing modifier copy true in every state Review follow-ups on the Shift-inverts-routing change. - The nested row promised "⇧⌘+click opens one in Orca" in the present tense while its own toggle was off, so the out-of-box state described behavior the user did not have. Phrase it as enabled-state copy, matching sibling rows. - The parent Link Routing description gained "opens a link the other way", which is false in the default state and contradicts the child row when inverting is on. No fixed sentence there is true in every state, so the child row — which knows the live destination — now owns the claim. That leaves getBrowserLinkRoutingShortcutLabel unused, so drop it. - The rich markdown editor still forced the system browser while the preview of the same file honored the modifier, so one link routed two ways depending on which view it was clicked in. - A remote runtime pins every link to the system browser, so the hover hint could promise Orca for a click that lands elsewhere. Gate the hint on the same condition openHttpLink uses. - Index both modifier titles: the search entry is built with openLinksInApp false, so the row was unfindable by the title it renders when routing is on. - Drop the ariaLabel that shared no words with the visible label (WCAG 2.5.3) and add the four missing settings-search keyword keys to all five catalogs. * test(editor): pin the rich markdown Shift+click routing hop The editor half of the modifier fix had no coverage — reverting it to forceSystemBrowser left the suite green while the same link routed one way in the markdown preview and the other way in the rich editor. Also pins that a non-local source owner survives the hop, since that is what keeps an SSH file's links out of Orca's browser. * test(editor): cover the Ctrl chord for rich markdown link routing This file is the only test of handleRichMarkdownEditorClick, and it exercised metaKey alone, so the isMac branch of modKey had no coverage off macOS. Also stop claiming the source-owner case proves SSH links stay out of Orca — it proves the owner survives the hop; http-link-routing.test.ts enforces the rest. * style: trim review comments to the one-line house rule Both explained the change adequately in two lines; the extra lines were worked examples, not information. * fix(browser): keep Link Routing copy unchanged until inverting is enabled Removing the "⇧⌘-click always uses your system browser" sentence outright reworded the row for every user on upgrade, including everyone who never turns the opt-in on. Restore it verbatim in the default state and only hand the chord sentence to the nested row once inverting makes "always" untrue. * revert(editor): keep rich markdown Shift+click on the system browser Per Brennan: the editor's Shift path hands the link to the client OS and should not follow the invert setting — the preview opening in Orca is the intended divergence, not a bug. Restores main's call exactly; the test now pins the divergence so a future consistency pass cannot erase it silently. * fix(browser): surface the inverted modifier on the hosted-review link The hosted-review click path now passes modifierHeld, so with inverting on and Link Routing off the chord opens in Orca — but the hint stayed gated on openLinksInApp, hiding a live gesture. Resolve the destination instead of a boolean. Default-off output is unchanged. * test(browser): pin the inert modifier hint when links already open in Orca * refactor(terminal): require the pane link hint so dropping the wiring fails to build The optional option fell back to a duplicated copy of the legacy hint string, so deleting the hook wiring reverted the tooltip silently with every test green. * fix(browser): trim the runtime id before hiding the hosted-review modifier hint openHttpLink and terminalUrlOpenHintOptionsFor both trim, so a blank runtime id hid the hint while the click still reached Orca. |
||
|
|
a7c8b8e071 |
fix(terminal): bound SSH & remote hidden-worktree terminal retention (C1) (#10625)
* fix(terminal): park SSH worktrees like local ones (C1 retention, slice A) SSH ptys were blanket-excluded from hidden-view parking, so a hidden SSH worktree retained every pane forever (C1: renderer heap climbs to the V8 ceiling). SSH bytes transit local main — fact-mode watchers already cover them, and main keeps a headless model served over pty:getMainBufferSnapshot that the SSH reattach path never consulted. - isParkRestorableTerminalPty: snapshot-backed OR (SSH + policy); threaded through both park verdicts, both selectors, watcher coverage, and the watcher start guard. Remote-runtime/fail-open/foreign/null unchanged. - Parked-SSH reveal paints from main's headless model (dimension-matched, ~5k rows) and degrades to the relay 100KiB replay unless the snapshot is a non-empty source==='headless' payload — never a blank/stale paint. - Kill switch: settings.terminalSshViewParking (default on). DESIGN.md records the approved plan and the H1 magnitude non-claim. Co-authored-by: Orca <help@stably.ai> * fix(terminal): bound hidden-worktree retention with a force-park budget (C1, slice B) Un-parkable worktrees (remote-runtime ptys, uncoverable tabs, SSH with the slice-A switch off) had unlimited retention: the parking cap/TTL only ever saw eligibility-passing worktrees, so one bad tab pinned a whole worktree's panes forever. Retention is now memory-bounded, not eligibility-bounded. - terminal-hidden-worktree-retention.ts: retention budget (12 hidden / 45min TTL, sized from the measured 2.5-19MB per-pane V8 cost, DESIGN.md §2) over hidden worktrees ordinary parking can never evict; reuses the hot-retain ranking so last-active exemption, deterministic ties, and deadline-driven rechecks hold. Fail-open/foreign-pty tabs are eviction-exempt (a remount would fresh-spawn and orphan the live shell). - Terminal.tsx: force-parked ids join the parked set AFTER the coverage veto (darkness for uncoverable tabs is the accepted cost); buffers captured via the sleep-flow registry before the unmount render; retention TTL added to the recheck deadlines for budget candidates only. - Verdict stays out of its own effect deps; policy test asserts idempotence and time-monotone membership (flip-loop dwell regression). - Kill switch: settings.terminalHiddenWorktreeRetentionBudget (default on). Co-authored-by: Orca <help@stably.ai> * fix(terminal): demote hidden scrollback for eviction-exempt worktrees (C1, slice C) The retention budget (slice B) must exempt worktrees holding fail-open or foreign-worktree ptys — a remount would fresh-spawn and orphan the live shell — which would leave that class unbounded again. Instead, past the same 45min retention TTL their hidden panes drop to the minimum scrollback tier (measured: ~19MB -> ~1.3MB V8 heap per 50k-row pane; trimmed history is gone by design, reveal restores the configured cap for future output). - terminal-hidden-scrollback-demotion.ts: module-state verdict registry (parked-watcher pattern) with content-equality notify damping; applied in the existing scrollback-rows effect in use-terminal-pane-lifecycle. - selectScrollbackDemotedTerminalWorktrees: pure, TTL-gated, time-monotone. - Retention TTL wakeups now also cover exempt worktrees so demotion fires. - Kill switch: settings.terminalHiddenScrollbackDemotion (default on). Co-authored-by: Orca <help@stably.ai> * fix(terminal): paint the SSH model snapshot inline, not via nested coordinator (C1 slice A fix) applyMainBufferSnapshot runs its own structuralReplayCoordinator.run; calling it from applyReattachPayload (already inside the coordinator when a relay replay exists) deadlocks on the coordinator's tail chain. The model paint now mirrors the daemon-snapshot branch inline (folded scrollback + rehydrate + screen, dimension-matched, escape tail last) and arms the restored-snapshot seq baseline so deferred/live chunks the snapshot covers dedupe instead of double-painting. Also falls through (no early return) so reattachPayloadApplied still latches. Adds the folder-workspace id parity unit case. Co-authored-by: Orca <help@stably.ai> * test(terminal): SSH park+reveal e2e round-trip + as-built design notes (C1) Docker-gated (ORCA_E2E_SSH_DOCKER=1) spec: SSH tab parks behind a decoy and reveal restores marker content at multi-viewport scrollback depth. DESIGN.md records the as-built deltas (inline paint, force-park shape, last-active floor) and the residuals so follow-ups aren't lost. Co-authored-by: Orca <help@stably.ai> * fix(terminal): paint SSH reveal from main's model even when the relay replay is empty (C1 review #1) A relay restart empties the replay buffer; the reveal previously painted nothing even when main's headless model held the session. The reattach now prefetches the model snapshot when no structural replay exists (SSH-shaped ptys only) and paints it inside the coordinator; emptiness is judged on the composed payload (scrollbackAnsi + data + pendingEscapeTailAnsi) so an alt-screen snapshot with an empty screen frame still paints. Co-authored-by: Orca <help@stably.ai> * fix(terminal): decouple scrollback demotion (slice C) from the retention-budget switch (C1 review #2) Per the approved contract each slice reverts behind its own switch: slice C now requires only the master terminalHiddenViewParking plus its own terminalHiddenScrollbackDemotion flag. The TTL wakeup timer fires for demotion candidates even with the budget switch off. No DEFAULT_SETTINGS entries exist for sibling flags (defaults are the '!== false' optional pattern), so no explicit defaults are added. Co-authored-by: Orca <help@stably.ai> * fix(terminal): scope eviction exemption to the tab, not the worktree (C1 review #3) One eviction-exempt tab (fail-open/foreign pty) previously vetoed force-park for its whole worktree, pinning co-located remote-runtime tabs forever. The worktree now force-parks while exempt tabs keep their mounted panes via a per-tab exclusion mirroring the Activity-portal pattern (legacy watcher sync, legacy render, and the overlay cold-parking hook). Ordinary parking is untouched — a worktree with an exempt tab still cannot ordinary-park. Slice C now also demotes exempt tabs' panes as soon as their worktree force-parks under the count budget (they are the only panes left mounted). Co-authored-by: Orca <help@stably.ai> * fix(terminal): demote un-parkable worktrees the force-park lever spared (C1 review #4) The last-active exemption means a single hidden un-parkable worktree never force-parks — and slice C previously only targeted exempt-tab worktrees, so its panes held full scrollback forever. Demotion now also covers un-parkable non-exempt worktrees past the retention TTL that are absent from the force-parked set (last-active spared, or slice B switched off). Membership stays time-monotone for fixed inputs; covered by new idempotence/monotone selector tests. Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep the hidden clock running through transient background-measure windows (C1 review #5) Whole-worktree background mounts (browser-automation bootstrap lease, mobile mounts, agent wakes) open a ~3s self-clearing measure window that previously deleted hiddenSince — every remount restarted the 30s hysteresis and the 45min retention TTL, so a periodically re-mounted force-parked worktree never re-parked. The measure window still pauses parking/eviction verdicts (all selectors skip measuring candidates); only the clock survives, so the prior verdict resumes as soon as the window closes. Visible and portal-holding worktrees still reset the clock. Co-authored-by: Orca <help@stably.ai> * test(terminal): make the SSH park+reveal depth assertion prove the model paint (C1 review #6a) Pad the session with ~180KB of output after the numbered markers so the earliest marker falls outside the relay's 100KiB rolling replay buffer while staying inside main's ~5k-row headless model; asserting marker_1 after reveal now proves the headless-model paint rather than passing under the relay fallback. Co-authored-by: Orca <help@stably.ai> * docs(terminal): rewrite DESIGN.md as the single as-built C1 contract (review #7) One contract matching the code: status IMPLEMENTED around force-park (not the unmount proposal), real kill-switch names with coupling + revert matrices, the true retention-floor formula with measured per-pane and demotion numbers, an explicit when-OOM-is-still-possible paragraph naming the H2 pendingSideEffects residual, the applyMainBufferSnapshot deadlock constraint inside the slice-A section, stable-signal phrasing instead of a capability latch, fail-open AND foreign-worktree exemption class, verified cites, and a planned/landed/follow-up test matrix. Co-authored-by: Orca <help@stably.ai> * fix(terminal): resolve the eviction exemption per pane, not per tab (C1 review #8) isEvictionExemptTerminalTab read only tab.ptyId — the FIRST leaf's pty — while the coverage veto that makes a worktree a retention candidate walks every pane. A split tab whose second leaf held an unrestorable pty therefore failed coverage (→ force-park target) yet looked exempt-free, so force-park unmounted it and orphaned the live shell. The exemption now resolves panes through the same resolveParkedTerminalPaneCandidates, keeping tab.ptyId in the union for the no-layout/no-capture case. Also from the same review round: - force-park's capture passes includeLocalBuffers:false like every other shutdownBufferCaptures caller; it was serializing up to 512KB/pane of scrollback into the store inside a fix meant to bound renderer heap. - Terminal.tsx unmount resets the scrollback-demotion registry — module state with no reset path, read by a pane effect that runs before the host effect that would clear it, so a stale verdict trimmed restore replays. - memoize watcher coverage per tab within the parking pass; the retention candidates re-asked it for every mounted worktree, not just the parked few. * docs(terminal): drop DESIGN.md — the as-built C1 contract moves to the PR body Co-authored-by: Orca <help@stably.ai> * fix(terminal): cap the deferred PTY side-effect queue (C1 residual H2) pendingSideEffects grew without bound under background timer throttling (~64 drained/s vs hundreds queued/s overnight). Cap at 512 entries with oldest-first eviction: titles drop (last-wins), a pending bell latches onto the next survivor, agent-status payloads collapse onto the survivor keeping the newest 16 (last-wins store state, KB-scale strings). Co-authored-by: Orca <help@stably.ai> * fix(terminal): carry command-lifecycle facts through parked watchers (C1 follow-up) Parked fact-mode watchers omitted onCommandFinished/onCommandCode*, so OSC 133;D and Command Code scrape signals went dark while parked. New parked-terminal-command-status.ts ports the store-level subset: git-UI nudge on every command finish, same-turn status-row drop for SSH PTYs (exact mounted-path parity — the foreground tracker refuses SSH ids), and the Command Code working seed / 1500ms done settle. Byte mode scans the same shared parsers for authority-off parity. Local-PTY status drops stay with the mounted pane: they need pty-connection's process-confirm ladder to tell a leaked nested-shell 133;D from a real agent exit. Co-authored-by: Orca <help@stably.ai> * test(terminal): retention-budget force-park e2e with a retentionLimit override (C1 6b) ORCA_E2E_TERMINAL_RETENTION_LIMIT flows preload → e2e-config → getTerminalParkingPolicyOverrides (exposeStore-gated, positive-integer only) so a spec can shrink the force-park budget to 1. The Docker-gated spec opens two remote worktrees on one relay target (second pre-seeded remote repo), disables terminalSshViewParking to make both un-parkable, hides both behind the local context, and proves the older one force-parks while the last-active exemption spares the newest; re-activating the evicted worktree restores the marker tail via relay replay. Co-authored-by: Orca <help@stably.ai> * test(terminal): retention-budget e2e via same-repo remote worktrees (passes docker lane) The first draft added a second remote repo mid-session, whose pane pty spawn misroutes to the local daemon with the remote cwd (pre-existing multi-repo issue, reproducible without any retention override — a seeded local repo plus one remote repo shows the same misroute). The spec now budgets across three worktrees of the ONE connected repo, created through the product createWorktree path (an external git-worktree-add only lands as a detected worktree needing adoption) and polled through the relay's transient post-connect reconnect window. Verified green on the local Docker lane in 20.8s. Co-authored-by: Orca <help@stably.ai> * fix(terminal): prevent remount thrashing during post-measure cool-down ( Implements the C1 retention contract: preserve worktree `hiddenSinceMs` through a background-measure window (so TTL/ranking stay honest), but re-park waits for a full `coldParkDelayMs` cool-down after the measure ends. Without the cool-down, every ~3s measure lease on a past-deadline worktree thrashes remount/reattach. Core changes: - Terminal.tsx: add measure clock (measuringTerminalWorktreeIdsRef) and post-measure cool-down tracking (terminalWorktreeParkCooldownUntilRef); gate parking candidates until cool-down expires. - Extract snapshot replay choreography to shared terminal-snapshot-replay-paint.ts (used by SSH reattach + daemon restore paths). - Add SSH model snapshot timeout (750ms) with fallback to relay replay. - Move cold-park recheck deadline logic to terminal-cold-park-recheck-deadlines.ts; add cool-down deadline to scheduling. - useTerminalTabColdParking: implement matching measure-clock contract with per-tab cool-down gate to keep tab deadlines synced with worktree retention clock. - Add resolveTerminalMountScrollbackRows() to demote new xterms under demoted worktrees (pane births during demotion must take the demoted tier at create). - Add kill switches: terminalSshViewParking, terminalHiddenWorktreeRetentionBudget, terminalHiddenScrollbackDemotion. * fix(terminal): detect Command Code completion in parked mid-turn panes Seed the byte watcher with in-flight turn state from agent status: the watcher is recreated per park cycle with no startup command to arm it, and the banner scrolled away before parking. Also memoize eviction-exempt checks and use SSH PTY ID builder in tests. * fix(terminal): flush pending command-code settles on reveal remount When a parked pane reveals mid-Command Code turn, the new detector cannot re-observe the already-passed idle composer. Cancelling the settle leaves the row stranded at 'working', so dispose now flushes the pending settle instead. Extract readInFlightCommandCodeTurn to shared space and seed detectors with in-flight turns so remounts complete mid-flight commands. Also memoize SSH model probes to prevent double timeouts on reattach. * fix(terminal): remove scrollback demotion (C1 slice C) The scrollback demotion feature for eviction-exempt hidden worktrees is no longer needed. Retention budget limits are now sufficient without this additional bound. Remove the terminal-hidden-scrollback-demotion module, the selectScrollbackDemotedTerminalWorktrees function, and related per-pane demotion logic. * test(terminal): assert bounded probe during stalled reveal Add assertion to verify that a stalled reveal operation makes exactly one `getMainBufferSnapshot` call, ensuring retry logic doesn't introduce redundant probes that would extend the timeout window before relay fallback. * fix(terminal): implement C1 retention budget for hidden parked worktrees Addresses OOM regressions in hidden parked terminals by force-evicting worktrees past a retention budget: at most 12 mounted while hidden, none past 45 minutes (absolute, not exempted by last-active). Eviction is least-recently-hidden-first. Exempt tabs (unrestorable local PTYs) keep their panes to avoid orphaning shells; worktrees are force-parked even if they contain exempts, and their buffers released elsewhere. SSH/remote worktrees serialize buffers pre-eviction for reveal; local worktrees keep daemon snapshots. Command Code's done-settle window is transferred across park/reveal boundaries so the row cannot strand at 'working'. Model probe on SSH reattach is scoped to park-reveal only, not ordinary reconnects. Includes new E2E suite proving the budget actually releases memory. * memoize eviction-exempt terminal tabs to avoid redundant store reads Each tab's exemption check re-reads the store and walks the layout tree. Introduce selectEvictionExemptTerminalTabIds() to resolve all exempt tabs for a worktree in a single pass, then memoize the result in Terminal.tsx and useTerminalTabColdParking. This prevents O(n) store reads when checking exemptions across multiple tabs and ensures the set remains stable across unrelated re-renders. * refactor: reformat hidden-worktree retention comments Reflow to 80-character lines and remove internal ticket references (C1, C1 slice C). * fix(lint): split overlay slot and eviction-exempt tabs under max-lines Static analysis failed because TerminalPaneOverlayLayer (401) and terminal-parked-tab-watchers (304) exceeded oxlint max-lines. Extract the slot component and eviction-exempt helpers into dedicated modules. * test(terminal): stabilize retention budget e2e control arm Stage un-parkable remote pty ids only after both worktrees are hidden, and keep re-staging during the control-arm poll so a late updateTabPtyId cannot flip the decoy back to park-restorable and ordinary-park it before budget engages. * test(terminal): pin retention e2e decoy to a mounted pane snapshot Use the active pane-identity snapshot for the decoy tab instead of all worktree tabs, and re-assert un-parkable ids after the control-arm hold so a deferred/empty tab id cannot fail the budget-off mounted-count check. * fix: memoize terminal eviction exemptions on layout leaf PTYs Splits add leaf panes to the layout store without changing the tabs array. A memo keyed only on tabs misses this change, leaving new panes unexempted for unmount. Include layout leaf PTYs in the exemption memo key so it recalculates when splits occur or PTYs are re-minted. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
13c193a00a |
feat(dashboard): add agent status search board (#11042)
* feat(dashboard): add agent status search board * fix(dashboard): keep idle controls reachable * chore: drop merge-only formatting drift * fix(dashboard): compare sparse subagent snapshots safely * fix(dashboard): satisfy settings handler lint * fix(dashboard): address review feedback * fix(dashboard): complete search and localized status copy * fix(dashboard): pad active filter row * fix(dashboard): keep idle control in board settings * fix(dashboard): source filters from workspace state * fix(dashboard): clarify PR and MR status filter * fix(dashboard): preserve review and board parity |
||
|
|
1bd80931bd | fix(diff): stop file-tree navigation remounting combined diffs; make tree resizable (#11088) | ||
|
|
97e4776dfe |
feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental) (#8549)
* feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental) Adds Orca's experimental plugin system behind a settings flag: a supervised kernel, declarative content packs (VM recipes, commands and keybindings, language packs), sandboxed iframe panels, forked worker hosts, and a Git-backed marketplace v0 with consent, provenance and kill-list enforcement. Theme, icon-theme and terminal-theme contributions are deferred to a follow-up pass. * fix(plugins): make unsupported marketplace listings unreachable by key findPlugin() backs preview/install/previewInstalledUpdate via requireListing(), so filtering only listPlugins() hid the catalog card while leaving the dead install path reachable one click later. * fix(plugins): fan Pi session-only status out to plugin subscribers The providerSessionOnly early-return in applyNormalizedStatus emitted to onAgentStatus (main-window fanout) but skipped enrichedStatusListeners, so plugins subscribed to agent.status.changed silently missed every Pi session_start event. Route both emit sites through one helper so a future early return cannot drop the plugin tap again. Co-authored-by: Orca <help@stably.ai> * plugins: drop dead code and hoist duplicated trust-boundary patterns Cleanup pass over the P1 diff, no behavior change: - Delete `readPluginTreeSnapshot`/`readSnapshotFile` and their types, plus the now-vestigial `directories`/`signal` plumbing in `collectFiles`. - Delete `resolveContainedPluginDirectory` (no callers). - Delete `plugin-content-load-pool.ts`; it reimplemented the existing `mapWithConcurrency`, whose index arg also removes the pairing wrapper in `buildPluginList`. - Hoist `PLUGIN_CONTENT_HASH_PATTERN` and `PLUGIN_COMMIT_PATTERN` into the install-lockfile module; 11 sites hand-rolled these identically. - Point the new reliability gate at the PR instead of gitignored docs paths, matching every other gate's link form. * fix(plugins): retry plugin state renames on Windows AV/EPERM locks Six plugin write paths (lockfile, provenance, current pointer, kill list, marketplace cache, staged install dir) did a plain rename, so an antivirus or indexer holding the target open surfaced as a failed install. The repo already retries this hazard for issue #1507, but only through a sync helper; these paths are all async. Adds one bounded async retry + atomic write used by all six, and trims a consent-provenance header that restated its own JSX. * test(plugins): cover the Windows rename retry path The retry loop shipped untested: both existing cases hit the non-retry path, and the temp-cleanup test passed identically with the `finally` removed. Mock `rename` to queue errno codes so CI can exercise locks it cannot provoke. Co-authored-by: Orca <help@stably.ai> * fix(plugins): pin bundled plugin resources to LF Windows CI checks out with autocrlf, so the byte-hashed launch tree arrived as CRLF and verify-packaged-plugin-resources rejected it — the packaged build could never pass on Windows. Reproduced locally: CRLF yields the exact CI error, LF verifies clean. Files are already LF, so nothing renormalizes. Co-authored-by: Orca <help@stably.ai> * test: guard the bundled-plugin LF pin against a CRLF checkout The byte-hash mismatch only surfaced in Windows packaging CI. Assert the .gitattributes pin and that a CRLF tree is rejected, so a regression fails on any platform instead of waiting for a packaged Windows build. Co-authored-by: Orca <help@stably.ai> * ci: trigger packaged-build check on bundled plugin resource changes The launch tree is byte-hashed during packaging, but no trigger path covered it — so the CRLF fix for that check would not have re-run the check. Add the resources, verifier and .gitattributes paths that can break packaging. Co-authored-by: Orca <help@stably.ai> * perf(plugins): rebuild the panel frame only when its baked theme values change The revision keys the panel iframe, so every bump destroys the sandboxed frame and its in-panel state. It counted root attribute mutations, but --workspace-sidebar-live-width is written every rAF of a sidebar drag, so dragging with a panel open blanked it ~60x/sec. Compare the two values the shell actually bakes in instead. Co-authored-by: Orca <help@stably.ai> * test: stop pinning a plugin name in the CRLF guard The CRLF case rewrites every launch file, so the reported mismatch is whichever plugin sorts first. P2 adds theme plugins that sort ahead of orca-navigation-shortcuts, which broke the assertion there. Co-authored-by: Orca <help@stably.ai> * style: drop stray blank lines left by the rebase resolutions Both sides of the agent-hooks and orca-runtime conflicts contributed a trailing blank, which oxfmt rejects. Whitespace only. Co-authored-by: Orca <help@stably.ai> * test(plugins): stop the startup budget failing on machine load P95 runs 16-34ms idle but exceeds the 50ms bound under full-suite parallelism, so the gate flaked. Widen it to catch an order-of-magnitude regression instead; the no-worker/no-plugin-code assertions are the real guarantee. Verified a 400ms regression still fails. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
af708d3471 |
feat(sidebar): add a filter to hide detached-HEAD workspaces (#10786)
Adds "Hide detached HEAD" alongside the existing sidebar filters, wired through the same pipeline as Hide CLI-created: sidebar list, Cmd+J empty-query list, workspace board, active-filter badges, Clear/Reset Filters, and persisted UI state. The predicate reuses getWorktreeGitIdentityDisplay so the filter targets exactly what the card renders a Detached HEAD badge for. Requiring a real head (not just an empty branch) keeps folder workspaces and SSH-synthesized rows — which carry both empty — out of the filter. Activating a hidden detached workspace clears the filter, matching the existing reveal escape hatch for automation- and CLI-created workspaces. Splits the filter-state describes out of visible-worktrees.test.ts into sidebar-filter-state.test.ts to stay under the max-lines budget. Co-authored-by: Orca <help@stably.ai> |
||
|
|
28dfc13654 | feat(sidebar): distinguish and filter CLI-created workspaces (#10712) | ||
|
|
9042ef9792 |
fix(terminal): make Zellij/TUI OSC 52 clipboard copy work by default (#10588)
* fix(terminal): make Zellij/TUI OSC 52 clipboard copy work by default Zellij and other multiplexers copy via OSC 52. Empty Pc is a valid XTerm default for clipboard, but we rejected it, and the feature defaulted off so copy silently failed inside Zellij. Accept empty Pc as clipboard, default the setting on (query still blocked; size capped), and surface Zellij in settings. Closes #10567 * fix(review): make the OSC 52 default actually reach existing installs Review fixes for #10588: - Persistence: profiles saved under the old off default persisted `false`, which is indistinguishable from a real opt-out, so the default flip never reached #10567's reporter. Added the repo's one-shot stamp (terminalAllowOsc52ClipboardDefaultedOnForAllUsers) so unmigrated profiles flip once and a later opt-out sticks. - Replay: reattach/cold-restore re-writes recorded PTY bytes through the same parser, so a stale `\e]52;c;...` silently clobbered the clipboard on every restart. Gated behind isPaneReplaying via a new resolveOsc52ClipboardGate. - Blocked toast latches once per renderer session and could be burned by a pre-hydration read; it now fires only for a real opt-out. - An empty Pd decoded to '' and, with the gate default-on, silently blanked the clipboard. Now rejected as invalid. - Localization: en.json is bundled and the catalog beats the code fallback, so all three copy changes were inert. Resynced across five locales. - Corrected the empty-Pc rationale: tmux (not Zellij) emits `\e]52;;<b64>`. * test(terminal): cover the OSC 52 gate wiring and settings copy Extracts createOsc52OscHandler so the replay/hydration gate wiring is covered, not just the pure gate — dropping the isReplaying getter now fails a test instead of passing silently. Adds catalog assertions for the two OSC 52 settings strings. Only the toast key was pinned, so the same inert-copy regression (code fallback edited, bundled en.json not) could still ship for the settings pane. * docs(settings): note that the OSC 52 default only covers new profiles Co-authored-by: Orca <help@stably.ai> * fix(terminal): migrate the web settings store to the OSC 52 default-on flip The default-on flip only reached the Electron store. The web/remote client keeps its own settings in localStorage, so a profile that persisted the old `false` there stayed opted out — the same bug the Electron migration fixed, in the second store. Extract the migration into shared/osc52-clipboard-settings.ts and call it from both stores. Also coalesce OSC 52 writes onto a microtask so a hostile chunk of ~15-byte sequences cannot fan out into a million clipboard writes, and latch the blocked-write toast after it renders rather than before. * feat(terminal): tell users when the OSC 52 flip overrides their opt-out The default-on migration cannot distinguish a deliberate opt-out from a profile that simply never touched the setting — both persisted `false` under the old default. Flipping everyone is the only way to fix #10567 for existing installs, but doing it silently reverses a security choice the user made. Arm a one-shot notice at load when the migration overrides a persisted `false`, on both settings stores, and show it once the renderer hydrates. Profiles that never opted out are never notified. * fix(terminal): clear the OSC 52 notice after it renders, not before Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep the web OSC 52 notice armed against an unmigrated host The host store always projects osc52ClipboardDefaultOnNoticePending, so the plain spread in the web client's runtime UI merge overwrote an arm raised by its own localStorage settings migration — flipping the opt-out in silence. Co-authored-by: Orca <help@stably.ai> * fix(terminal): stop the OSC 52 notice overclaiming, and cover it Round-3 review fixes: - Rename the arming predicate to osc52ClipboardDefaultOnOverridesPersistedOff. Both stores rewrite the whole settings object on every save, so every profile saved under the old off default holds `false` — the deliberate-opt-out cohort is not distinguishable on disk. Name, docs and test names now say so. - Read settings before the UI snapshot in readLocalWebUIState: getStoredSettings() arms the notice, so reading first snapshotted a pre-arm state that callers wrote back, erasing an arm the stamp can never raise again. - Give the notice toast a stable id; StrictMode re-runs the effect against the same closure, so the early return cannot catch the second pass. - Restore guardParserHandler parity in the coalescer microtask. - Drop the unverified Zellij claim justifying all-selections routing; that routing predates this branch and PRIMARY routing stays an open question. - Cover the notice hook (order, single-fire, deep-link), the armed flag reaching disk and surviving a clear, and pin the notice catalog to its code fallbacks. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin OSC 52 setting discovery by product name The migration notice says to turn it off in Terminal settings, so searching Zellij/Grok/tmux has to find it. Also note why the OSC 52 write-back clauses stay despite an unrelated always-true clause in the same condition. Co-authored-by: Orca <help@stably.ai> * fix(terminal): consume the OSC 52 notice on close, and cover the guards it relies on The notice was cleared the moment the toast was enqueued, so a quit inside its 15s window spent the profile's only warning on a launch where nothing was ever seen — and the settings stamp means it can never re-arm. Clear on onAutoClose/onDismiss instead, plus explicitly in the action handler, because sonner's action path deletes the toast without firing onDismiss. Also closes three coverage gaps a review found: - ui.set must accept osc52ClipboardDefaultOnNoticePending. The update schema is strict, so dropping the key rejects the whole call rather than stripping it, and the renderer only logs that failure — every paired client would re-toast forever with nothing red. - the coalescer's try/catch and .catch had no test; the rejection case needs a plain function because vi.fn tracks settled results and hides the leak. - pin that every selection kind (including bare `p`) lands in the system clipboard, so routing PRIMARY separately later is a deliberate break. Co-authored-by: Orca <help@stably.ai> * test(web): pin that ui.get arms the OSC 52 notice when it runs the migration readLocalWebUIState reads settings before the UI blob so the migration's arm is in place before the snapshot every caller writes back. Seeding localStorage after install is what makes ui.get the first settings read, and therefore what makes swapping those two lines fail. Co-authored-by: Orca <help@stably.ai> * test(store): cover the OSC 52 notice clear and its hydration The clear sets local state before persisting so a rejected ui.set cannot leave the toast re-firing for the rest of the session; losing the persist only re-arms the notice next launch. Co-authored-by: Orca <help@stably.ai> * docs(terminal): state the real residual risk of default-on OSC 52 Three comment corrections from review: - the safety note claimed exfil was the risk; queries are blocked, so it isn't. The actual accepted risk is execute-on-paste: decoded text goes to the clipboard verbatim, newlines included. Filtering here would break multi-line TUI copies, which is the feature; bracketed paste is where that is handled, and kitty/Ghostty take the same posture. - the coalescer bounds a flood per parse yield, not overall. - the replay gate reads at parse time while queued live bytes are drained before the guard engages, so a copy racing a reattach is dropped silently. Co-authored-by: Orca <help@stably.ai> * test(terminal): close the four OSC 52 gaps a full revert walked through Mutation testing found four assertions that stayed green against the very change they were written to pin. The notice suite passed 8/9 against a complete revert to clear-at-enqueue: `calls[0][1][callback]?.()` is a silent no-op when the option is absent, and the call count was already satisfied by the enqueue-clear, so nothing separated "cleared by this callback" from "cleared earlier". Assert the option exists and the notice is unspent before invoking it. The stable toast id was deletable with all 9 green despite the adjacent comment calling it load-bearing for StrictMode. Pin it. The blocked toast's latch-after-throw fix was unproven: both orderings pass when `toast.info` succeeds. Only a throwing first call tells them apart. Deleting the hook call in App.tsx silenced the desktop notice with every suite green. Pin it alongside the static Toaster import, since sonner drops a toast enqueued before any Toaster subscribes and never replays it. Also retone the coalescer-latch comment, which claimed the reset ordering was load-bearing on its own; the try/catch reaches the same end, so the test binds the pair. All four verified green->red by mutation, then restored. * test(terminal): cover the OSC 52 notice and its guards Add tests pinning the static Toaster mount required to prevent notice dropout (#10567), the stable toast ID deduping StrictMode double-invokes, that the notice stays unspent on toast throws, and that flush-latch guards prevent silent consumption across error boundaries. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
4a71a0ecb2 |
feat(mobile): add safe Codex rate-limit resets (#9394)
* feat(mobile): add safe Codex rate-limit resets * fix(mobile): address reset credit review feedback * review: purge removed-account reset attempts, shared capability constant, rebase test mocks * review: preserve host compatibility and reset durability * fix(mobile): recover reset capability after cutover * fix(mobile): validate runtime capability payloads * fix(mobile): enforce capability payload contract * fix(mobile): route mock terminals to selected worktree * test(mobile): pin malformed probe retry behavior --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
d56e2fbbe4 |
feat(agent-dashboard): choose in-window board or pop-out window (#10243)
* feat(agent-dashboard): choose in-window screen popover or pop-out window
The experimental Agent Dashboard opened only as a separate pop-out
window. Add an "Open as" mode under the experimental toggle so it can
open as an in-window screen popover (new default) or a pop-out window
(prior behavior). The mode row appears only when the feature is on.
- New setting `experimentalAgentDashboardMode: 'in-window' | 'popout'`
(default in-window); sidebar entry branches on it.
- In-window: AgentDashboardOverlay renders the shared AgentKanbanBoard
in a near-fullscreen dialog, snapshot built locally via
useLiveDashboardSnapshot (the pop-out relays over IPC; in-window has
no relay). Ack/reveal act on the local store — the pop-out IPC
handlers are gated to the pop-out renderer.
- AgentKanbanBoard gains containerClassName/onAckAgent/onRevealAgent/
onClose props; defaults preserve the pop-out behavior.
- Extracted AgentDashboardExperimentalSetting to keep ExperimentalPane
under the max-lines cap.
* fix(agent-dashboard): admit main renderer to terminal-preview IPC for in-window dialog
The terminalPreview:* handlers gated every channel to the pop-out
renderer, so the in-window overlay's terminal dialog (running in the
main renderer) got { snapshot: null } from connect and falsely showed
"No live terminal — this agent's pane has closed." for live agents.
Accept the trusted UI renderer too — it already has full PTY access
through the regular terminal channels, so this adds no reach.
* fix(agent-dashboard): sync locale catalogs for new mode/close keys
verify:localization-catalog (part of lint CI) fails when en.json keys are
missing from the other locale catalogs; run sync:localization-catalog so
the six new agent-dashboard keys exist everywhere (English fallback text;
translated copy remains the documented follow-up).
* feat(agent-dashboard): present in-window mode as a companion board sheet
The in-window dashboard now uses the same non-modal left sheet as the
workspace kanban board — anchored to the sidebar edge, chrome/status-bar
bounds, sidebar stays interactive — instead of a near-fullscreen modal
dialog. Both companion boards are mutually exclusive; the sidebar entry
toggles the drawer. Removes the modal focus-restore timing coupling on
reveal.
* fix(agent-dashboard): ignore Radix dismiss requests like the workspace board
Non-modal Radix layers also request dismissal for interactions the drawer's
outside guards cannot classify — focus moving outside carries no pointer
coordinates, and clicks in the status bar / top chrome fall outside the
right-side dismiss band. Forward only open requests from the Sheet, matching
WorkspaceKanbanDrawer, so only the drawer's own escape/outside/close paths
close it.
* fix(agent-dashboard): guard reveal relay and refresh stale mode copy
CodeRabbit review: revealAgent lacked the ?. HMR-skew guard its sibling
ackAgent has (both channels shipped together, so a stale dev preload
lacks both). The es/ja/ko/zh catalogs also still described the dashboard
as pop-out-only in stale English, contradicting the new in-window
default; refreshed to the current English source.
* feat(agent-dashboard): add board settings menu to the in-window header
Mirrors the workspace board's settings gear: an Open as segmented control
in the board header so the mode is changeable without opening Settings.
Switching to pop-out hands the surface over (closes the drawer, opens the
window) instead of leaving a board the setting says should be a window.
In-window only via an optional headerActions slot - the pop-out renderer
has no store to drive it.
* fix(agent-dashboard): reset the settings-menu flag when the drawer closes
The pop-out hand-off closes the sheet while the menu is still open, so the
menu unmounts without Radix reporting onOpenChange(false). The stale
menuOpen=true then blocked outside-dismiss permanently on the next
in-window open. Mirror closeWorkspaceBoard by resetting the flag in close.
* fix(agent-dashboard): reset the menu flag on store-driven drawer closes
Cmd+B sidebar collapse and the workspace-board exclusivity effect close
the drawer via setAgentDashboardDrawerOpen directly, bypassing close();
a settings menu open at that moment unmounted without Radix reporting
onOpenChange(false), leaving menuOpen stuck true and outside-dismiss
disabled on the next open. Sync the flag to the open state so every
close path resets it.
|
||
|
|
91ee29a808 |
feat(editor): add an opt-in editor font family (default: follow terminal font) (#9658)
The code editor was hard-wired to the terminal font, so Nerd Font 'Mono' CJK glyphs overlapped in the editor. Adds an opt-in editorFontFamily setting; default is empty so the resolved value is byte-identical to today for every existing user. Closes #9628 |
||
|
|
6444be3a01 |
fix(accounts): follow the runtime for WSL provider account detection (#9537) (#9611)
* fix(accounts): follow the runtime for WSL provider account detection (#9537) On Windows + WSL, provider-account detection (usage recognition and the status-bar account switcher) was pinned to the Windows host even when the project runs in WSL, so WSL accounts were never recognized and the WSL switcher group never appeared. Root cause: `localAccountRuntime` hard-defaulted to 'host', which short-circuited `getInitialClaude/CodexRateLimitTarget` before the existing "follow the global Windows runtime default" branch could run. That branch was therefore dead for every real user. Fix: add an 'auto' value for `localAccountRuntime`, make it the default, and migrate the untouched legacy 'host' default to 'auto' once (guarded by `localAccountRuntimeDefaultedToAutoForAllUsers`; explicit 'wsl' is preserved). 'auto' resolves via a shared `resolveLocalAccountRuntimeTarget` helper: on a windows-host default it stays host (no behavior change); on a WSL default it follows WSL, so WSL accounts are recognized and the WSL group appears. Wired the shared resolver into the managed-account default target, the status-bar WSL-group gate, and the Accounts settings location toggle. Note: detection follows the global Windows runtime default, not the live active project's runtime (the fetch target is a single global value); the latter is a larger follow-up. * fix(accounts): align auto runtime consumers * fix(accounts): keep runtime polling aligned with settings --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
580f8eb492 |
feat(status-bar): consolidate agent usage into a single roster popover (#8761)
* feat(status-bar): consolidate agent usage into a single roster popover The footer usage cluster is now one quiet, borderless control: each agent shows its tightest window as neutral text (letter badges at icon-only width), and clicking anywhere opens a consolidated Usage popover listing every agent worst-first — icon, name, plan, soonest reset, and per-window threshold-colored bars. Claude/Codex rows drill into their existing account switcher, runtime toggle, and Codex reset credits via a submenu variant of ProviderDetailsMenu; all other providers drill into their detail panel. Row actions and the footer links route to Settings. Also: barColor's <60% band goes green -> neutral so color is reserved for approaching limits (threshold color now lives only in the popover, the always-visible bar stays monochrome), Codex plan_type is surfaced as ProviderRateLimits.planType for the "Codex · Plus" label, and the Fable weekly window is labeled "Fable" so it no longer collides with "wk". * fix(status-bar): single-line sign-in row + review dedup Signed-out roster rows now read as one line (name · "not signed in" · right-aligned Sign in) instead of a floating button over an orphaned second line. Review follow-ups: the non-submenu ProviderDetailsMenu branch reuses the extracted panelBody, and the icon-only letter badge is one shared component so its has-data dot condition can't drift. * fix(status-bar): harden usage roster interactions * chore: remove unrelated formatting change * chore(skills): refresh bundle manifest for rc.2 * fix(status-bar): preserve usage roster semantics * fix(status-bar): preserve roster menu interactions * chore: remove unrelated formatting changes * fix(status-bar): keep usage reset countdowns live * feat(status-bar): toggle compact usage summary * fix(status-bar): simplify usage mode toggle * feat(status-bar): replace usage footer toggle with Compact/Detailed segmented control Swap the bottom-of-popover on/off switch for a SettingsSegmentedControl at the top of the Usage popover (view-switcher pattern), so both modes are named and discoverable on first open. Reuses the repo's canonical Compact/Detailed vocabulary from the Workspace card-layout control. |
||
|
|
3468b434d6 |
feat(dashboard): add agent dashboard popout (#9604)
* feat(dashboard): add agent dashboard popout * fix(dashboard): gate and harden agent popout * test(ipc): isolate dashboard handler registration * fix(dashboard): drop diff status from bucket counts * fix(dashboard): address review feedback * perf(dashboard): ignore unrelated store churn |
||
|
|
0989128287 |
refactor(comments): slim verbose comments in shared/cli/relay/preload (#9544)
Collapse multi-line explanatory comment blocks into single-line "why" statements
per AGENTS.md ("Document the Why, Briefly"): drop restatements of the code and
mechanism narration; keep the non-obvious reason, external refs, and directives.
Comments-only — verified no code changed via a Babel/esbuild comment-strip
token-equality gate against origin/main; typecheck and oxlint clean.
Area: shared, cli, relay, preload. 26 files changed, 1039 insertions(+), 3300 deletions(-).
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
c5d40565af |
Clarify Orca Mobile pairing connection paths (#9425)
* Restructure mobile pairing setup into a clear stepped layout * Rework mobile pairing UI: radio-style path selector with sign-in gate fo * fix(mobile): resolve mobile-pairing review findings Applies the code-review findings for the reworked pairing UI: - Clear a displayed Relay QR on sign-out in both MobilePage and MobilePane (the rework dropped the wasSignedInRef watcher, leaving a stale Relay QR next to a "sign in required" prompt). Anywhere stays selected; the QR re-mints as local-only. [#1, #3] - Extract useMobilePairingConnectionMode so both panes resolve the saved preference identically instead of duplicating the state + resync effect. [#6] - Delete ~24 i18n keys the rework left unreferenced; sync locale catalogs to parity. [#4] - Give the connection-path radiogroup roving tabindex + arrow-key nav and document why it diverges from SettingsSegmentedControl. [#7] - Add MobilePane.test.tsx (previously untested safety logic) and a MobilePage saved-local-only restore test. [#2, #5] Verified: 22 targeted tests pass, typecheck + oxlint clean, localization catalog/coverage and max-lines ratchet green. * fix(mobile): resolve adversarial-review findings for pairing UI Address accepted findings from the mobile pairing UI rework: - MobilePane: add a request-generation epoch so a late getPairingQR response can't paint a stale Relay QR after sign-out, a mode switch, or an address change; arm rotation when discarding a pending mint. - MobilePage: on sign-in, upgrade a signed-out local-only fallback QR to Relay (invalidate + rotate-regenerate); handle null->connected too. - Extract useMobilePairingQrInvalidation so both sign-in/out edges and cross-window preference syncs invalidate/re-mint the QR consistently. - MobilePane: clear + rotate the QR when the selected address changes (manual pick or refresh-driven) so it can't encode the old endpoint. - MobilePairingConnectionOptions: guard the Sign in CTA on configured; show an Unavailable panel on unconfigured builds instead of dead CTA. - Drop the duplicate sign-in helper from MobilePairingSetupSection. - Remove dead i18n keys (title, recommended, signInToGenerate) and add relayUnavailable across all locale catalogs. - Add tests: deferred sign-out/mode-switch races, sign-in upgrade, cross-window sync, unconfigured build, arrow-key radiogroup. TODO left for the relay-label-honesty finding: getPairingQR does not expose the actually-encoded mode when an automatic offer degrades to local-only, so the mismatch can't be surfaced without a new return field. * fix(mobile): resolve relay-pairing deep-review regressions - MobilePane: invalidatePairing now clears loading so a superseded mid-flight generate can't wedge Generate disabled forever - MobilePage: stop auto-minting a local-only QR under the Relay label when signed out with Anywhere; gate Step 2 auto-generate and the Generate button on a shared canMintMobilePairingOffer helper, align with Settings, clear QR + loading on sign-out, mint Relay on sign-in - qr-invalidation: clear pairQrDataUrl (and loading) on every invalidation path so a stale QR can't stay scannable during rotation - Strengthen MobilePane/MobilePage tests for the aligned behavior and stuck-loading coverage - Translate relayUnavailable in es/ja/ko/zh Co-authored-by: Orca <help@stably.ai> * fix(mobile): refuse to mint QR when signed-out with Anywhere selected Replace silent degradation of Anywhere mode to local-only QR with explicit refusal. Add canMintMobilePairingOffer guard across all mint paths (address change, network invalidation, connection mode switch). This ensures the UI honestly encodes the selected pairing path. Improve keyboard focus visibility on the path selector by adding a persistent focus ring. * fix(mobile): detect and flag Relay provisioning degradation When Relay provisioning fails during automatic (Anywhere) pairing offer creation, the offer silently degrades to local-only. This confuses users who selected Anywhere expecting cellular capability. Add connectionMode field to the pairing offer result to expose what the offer actually encodes. The UI now flags degradation when the offered mode mismatches the user's selection. Also move credential rotation logic to the main process: rotate when requested mode differs from the pending token's encoded mode. This ensures QR codes displayed under an old policy can't pair under a new one, and windows reminting after preference sync converge on one token. Rename MobileRelayBetaAvailability → MobileRelayBetaNotice. * test(mobile): verify pairing codes don't flash during Relay mint Assert that the pairing QR and URL remain hidden while the Relay mint is pending, preventing confusing intermediate states when signing in unlocks Relay. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
c5d2275c35 |
Add preference to show pinned worktrees in original lists (#6216)
* Add setting for pinned worktree group display Co-authored-by: Orca <help@stably.ai> * Fix pinned worktree host metadata Co-authored-by: Orca <help@stably.ai> * Polish pinned worktree setting copy Co-authored-by: Orca <help@stably.ai> * Clarify pinned worktree setting copy Co-authored-by: Orca <help@stably.ai> * Use original lists in pinned setting copy Co-authored-by: Orca <help@stably.ai> * Fix pinned sidebar order and render churn * Fix pinned host and inbox placement * Fix worktree host ownership consistency --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
5b75a87dd3 |
feat(native-chat): add per-model session option pickers with verified Claude switch (#9085)
* Add per-model session-option pickers (model/effort/fast-mode) to native Introduces a shared agent session-option catalog (Claude/Codex/Gemini/Cursor) with model-scoped options, launch-command composition, mid-session dispatch via slash commands, and per-model persisted defaults. Wires the new NativeChatSessionOptionPickers UI into the composer, threads sessionOptions through every startup-plan builder (worktree creation, onboarding, source control actions, folder workspace, direct work-item launches), and adds localized strings and tests across the affected surfaces. * Add per-model session-option pickers with a verified Claude model switch Native chat's model/effort picker now dispatches option commands through a body-then-verified-Enter write path and, for Claude, arms a PTY observer that classifies the cached-history confirmation prompt as applied/rejected/needs interaction before the picker returns — falling back to the terminal only when Claude genuinely requires manual input, and clearing stale truth otherwise. Also reorders the composer's model/effort pills, disambiguates their tooltip labels, and switches disabled-reason strings to a closed enum so producer and localized copy can't drift. * Add live model/effort detection from Claude's TUI header - Reads the mounted xterm's main-buffer snapshot (falling back to the rendered screen when the alternate screen owns the buffer) to parse Claude's header for the currently active model and effort, so the native chat picker reflects reality instead of only dispatched state - Extracts command-apply recording and reported-value application into dedicated modules, and factors file-link click handling into a hook, to keep native-chat-pty-session-options.ts focused |
||
|
|
1536171fdb |
Add a native macOS menu bar status item with activity indicator (#9042)
* Add native macOS menu bar status item with settings toggle Extend the Windows-only system tray into a shared status-item module so macOS gets a template menu bar icon (Open/Settings/Check for Updates/Quit), a theme-aware attention dot, and a "Show Menu Bar Icon" appearance setting. Also fixes a startup race where a tray "Settings…" click could fire before the renderer's ui:openSettings listener attached, by queuing a one-shot pending-open-settings intent the renderer consumes on mount. * Fix Retina blur, race conditions, and menu-label duplication in tray Set - Rebuild the tray attention icon's @2x representation since toBitmap only read 1x pixels, blurring the glyph on Retina displays - Fix premultiplied-alpha math so light-glyph tinting uses per-pixel alpha instead of a flat 0xff, keeping antialiased edges valid - Always push ui:openSettings and leave a longer-lived pending flag, since there was no reliable signal that a renderer's listener was attached - Preserve tray attention state across macOS menu-bar hide/show toggles instead of resetting it on tray destroy - Route macOS tray creation through syncMacMenuBarIcon so startup and the live toggle share one visibility policy - Reuse app-menu translation keys for tray Settings/Check for Updates and drop the now-duplicate tray-scoped locale strings * Make menu bar icon default on and platform-independent in settings Previously showMenuBarIcon was hardcoded to darwin-only in both the default settings and the sanitize/load paths, so a profile written on macOS lost its opt-out when touched from another OS. Store the raw boolean everywhere and let darwin-specific consumers decide whether to act on it, so the value round-trips unchanged across platforms. * Fix Settings menu click being silently dropped after a slow cold rendere Replace the 60s pendingOpenSettings TTL with an untimed intent, since a cold renderer start can outrun any fixed timeout and cause the flag to expire before the click is consumed. webContents-id scoping plus consume-on-read still prevent the intent from leaking to an unrelated renderer. Adds a test covering the queued-before-mount pull path. |
||
|
|
a2d3451efd |
Fix manual project order across paired runtime hosts (#8894)
* fix(sidebar): persist manual project order across hosts Co-authored-by: Orca <help@stably.ai> * fix(sidebar): scope desktop repo reorder by host Co-authored-by: Orca <help@stably.ai> * fix(web): cover host-scoped repo reorder API Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
9dc1f253ad |
feat(editor): add file editor word wrap preference (#8423)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
7fe6742b8b |
fix(pr-comments): let users mark comment authors as bots for the Humans/Bots filter (#7598)
* fix(pr-comments): let users mark comment authors as bots for the Humans/Bots filter Some review bots post from regular user accounts that defeat both provider bot metadata and login heuristics, so their comments were misclassified as human. Adds a persisted prBotAuthorOverrides setting with a "Mark author as bot" comment action, applied consistently across desktop and mobile. Fixes #7597 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(pr-comments): address review feedback on bot-author overrides - Cap sanitized prBotAuthorOverrides at 500 entries so malformed payloads can't bloat GlobalSettings or slow comment classification - Reuse the shared normalizePRCommentAuthorLogin in isBotPRComment on desktop and mobile instead of duplicating the normalization inline - Pass botAuthorOverrides from CommentRow to CommentMoreMenu instead of re-subscribing per menu instance - Re-fetch mobile bot-author overrides alongside each PR refetch so they don't stay a stale one-shot snapshot for the whole session Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(pr-comments): harden bot author override sync * fix(pr-comments): bound and recover override updates * fix(pr-comments): merge overrides from canonical settings * fix(pr-comments): make bot override updates atomic * fix(pr-comments): surface rejected bot overrides * fix(i18n): translate bot override warning * fix(i18n): translate bot author actions --------- Co-authored-by: Dzmitry Bachko <dbachko@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local> |
||
|
|
8b8e1bbab2 |
fix: restore the active top-level view across renderer reload (#8265)
Persist and safely restore the active top-level view on startup. Unknown, removed, legacy, or unavailable views fall back to the terminal, while cross-window UI sync cannot navigate the current window.\n\nCloses #8264 |
||
|
|
8a4ec4e856 |
feat(terminal): expose right-click paste on every platform (#8322)
* WIP: Changes before auto-review fixes * fix: preserve terminal paste defaults across platforms |
||
|
|
6be8687c34 |
feat(status-bar): notify upgraded users usage meters show % used (#8319)
* feat(status-bar): notify upgraded users usage meters show % used Show a one-time status-bar callout when upgraded profiles still use the new percent-used default. Brand-new profiles and users who already chose remaining stay quiet; dismissing or changing the setting is permanent. * Add settings deep-link to expand Appearance's Window accordion for one-s - Replaces the searchQuery-based redirect (fragile, flashed filter UI) with a dedicated appearanceAccordionDeepLink store field that force-opens the correct accordion and scrolls to the target row - Rebuilds the status-bar change notice as a plain elevated card instead of a Popover, since PopoverContent's glass/backdrop-filter defaults fought the opaque callout styling and needed heavy overrides - Simplifies the light/dark card CSS tokens accordingly * Reposition status-bar usage-change notice via fixed-position portal Portal the one-shot callout to document.body with fixed positioning anchored via getBoundingClientRect, since the status-bar's overflow-hidden flex ancestors clipped or mispositioned the previous in-tree absolute layout. * Refine status-bar usage notice styling and test coverage - Replace hand-tuned light/dark card colors with existing design tokens (--popover, --border) and the documented floating elevation, so the callout stays in sync with the design system instead of duplicating its own palette - Add tests covering dismiss via X button, "Got it", and Escape - Scope the foreground-process confirm assertion to the pane's ptyId so an unrelated pane's delayed confirm can't cause a false failure |
||
|
|
800967278e | feat: promote APFS shared paths (#8318) | ||
|
|
6d3ab9f971 |
feat(status-bar): choose used or remaining usage (#7574)
Add a persisted Appearance setting that switches provider usage labels between percent used and percent remaining while keeping meter fill consumption-based. Cover desktop and web persistence, current providers, settings search, localized copy, and regression tests. Co-authored-by: gatsby74 <166927047+gatsby74@users.noreply.github.com> |
||
|
|
e84a8ddec9 |
Terminal performance initiative: pipeline fixes + term-speed-2 revival + PTY flow control (integration branch) (#7214)
* Skip legacy hidden skip grammar assertions
* Fix hidden TUI snapshot test setup
* Fix sleep wake history test contract
* Fix hidden delivery startup gate helper
* Fix hidden Latin skip branch predicate
* Fix hidden synchronized split-boundary replay
* Stabilize remote runtime mixed subscription test
* Keep hidden startup query parser active during window
* Stabilize raw emoji golden restore width
* Stabilize raw emoji golden fixture completion
* Keep terminals responsive under agent output load
* Add frozen-terminal repro harness and silent-drop regression tests
Investigation harness for the frozen-terminal reports (Discord
#performance, issue #2836): pane shows content, shell alive, daemon
output.log flat while typing.
- e2e: renderer crash -> auto-reload recovery and three restart/restore
shapes (live daemon, SIGSTOP-wedged daemon, daemon killed between
launches), each probing input at both drop layers. Post-crash phases
drive the renderer from the main process because a crashed target
severs Playwright's CDP session even though the app recovers.
- e2e helpers: layer-discriminating probes (direct pty.write vs
transport input, plus pty:listSessions ownership-rebuild revival).
- unit repro: vendored xterm 6.1.0-beta.287 WriteBuffer permanently
wedges when a sync throw escapes a write-completion callback or a
custom parser handler (xterm-write-buffer-stall.repro.test.ts).
- unit repros for both silent input-drop layers: main drops writes for
a live PTY once ptyOwnership loses the id (revived by listSessions),
and the renderer transport stays unbound after a failed connect.
- pty.test.ts: unregister every leaked SSH provider id in afterEach so
module-level provider state cannot leak across tests.
Co-authored-by: Orca <help@stably.ai>
* Harden xterm write pipeline against sync-throw wedge that freezes panes
A synchronous exception escaping xterm's WriteBuffer loop permanently
wedges that terminal: _innerWrite has no try/catch around the parse
action or the write-completion callback, the tail re-schedule never
runs, and write() only re-arms on an empty buffer. The pane stops
rendering and, if a replay was in flight, the replay guard latches and
pty-connection's onData silently eats every keystroke — matching the
field reports (Discord #performance, issue #2836: content visible,
shell alive, daemon output.log flat). Both vectors verified against
vendored xterm 6.1.0-beta.287 in xterm-write-buffer-stall.repro.test.ts.
Three layers of defense:
- Guard every write-completion callback Orca hands xterm at the two
choke points (writeForegroundTerminalChunk, writeBackgroundTerminalChunk),
with settle and onParsed guarded separately so a WebGL/renderer
failure during viewport settle cannot starve the replay-guard release.
- Guard all throwing-capable custom parser handlers (DA1, OSC 10/11,
CSI ?h/?l mode reports, OSC 52 clipboard, OSC 7 cwd), degrading a
throw to "not handled" — same escape class as
terminal-link-provider-guard.ts.
- Replay-guard watchdog: each engagement releases exactly once, from
xterm's completion or a 10s watchdog, so a lost completion (wedged
pipeline, disposed-terminal race) cannot latch the guard on a live
pane; replayIntoTerminalAsync resolves on either path so restore
chains cannot hang. Force-releases record a crash breadcrumb.
All guard trips record rate-capped crash breadcrumbs, so the next field
occurrence names the throwing stack instead of failing silently.
Co-authored-by: Orca <help@stably.ai>
* Cap unbounded terminal output buffers in main and the foreground queue
Field evidence (Discord #performance / #2836): renderer memory climbs to
~1.5 GB and terminals freeze; a force reload does not help until memory
recovers. Two unbounded buffers matched that shape:
- Main-process pendingData grew by string concatenation without bound
while the renderer could not receive (frozen, starved, mid-reload) —
main-heap bloat a renderer reload cannot clear. Now capped at 2 MB per
PTY: past the cap the buffered bytes are dropped and the entry stays
O(1) until the renderer ACKs again, then a droppedOutput sentinel is
delivered and the pane repaints from the authoritative main-owned
buffer snapshot (existing hidden-output restore path) instead of
continuing a stream with a silent gap.
- The renderer output scheduler capped only hidden-pane backlogs; the
foreground path could queue a visible pane's flood without bound when
the drain could not keep up. The 2 MB cap now applies to every
foreground enqueue branch too, with a foreground-specific skip notice.
Verified: new main-side cap test (starve → flood → sentinel → normal
flow resumes), renderer sentinel-to-snapshot-restore test, two
foreground scheduler cap tests; full pty/terminal-pane/pane-manager
suites (1981 tests) and typecheck pass.
Co-authored-by: Orca <help@stably.ai>
* Make replay-guard stall release probe-certified instead of time-based
The previous stall watchdog blindly released the input guard after 10s.
If a replay were genuinely still parsing on a starved machine, that
early release could leak xterm's auto-replies into the shell — and into
agent TUIs, where a leaked ESC reads as the user pressing Escape.
Replace the blind release with a probe: when a completion looks
overdue, enqueue an empty write behind the replay. xterm parses writes
in order, so every outcome is provably safe:
- probe parses after the replay completion ran: normal release already
happened; probe is a no-op.
- probe parses but the replay completion never ran: all replay bytes
have parsed, no further auto-replies can exist — the completion was
genuinely lost. Release + breadcrumb.
- probe never parses (bounded wait): the pipeline is wedged, and a dead
parser can never emit auto-replies, so releasing cannot leak input.
Release + breadcrumb naming the pane as needing recovery.
While the probe is pending — a slow-but-alive replay — the guard now
HOLDS instead of releasing early; that case is pinned by a regression
test.
Co-authored-by: Orca <help@stably.ai>
* Scale output backlog caps with the scrollback setting and breadcrumb drops
The 2 MB pending-output caps were flat, which risked dropping lines a
50k-row scrollback user would have retained. Both caps (main pendingData
and the renderer output queue) now derive from one shared policy:
max(2 MB, scrollbackRows x 120 chars) — 2 MB at the 5k default, 6 MB at
the 50k max. The main side reads the setting live via getSettings; the
renderer scheduler is configured where the terminal lifecycle already
reads the scrollback setting.
Every drop now records a rate-limited crash breadcrumb with dropped and
cap sizes (terminal_output_backlog_dropped in the renderer,
terminal_pending_output_dropped in main — no pty ids, session ids can
embed workspace paths). Field drop frequency and size decide whether the
cap constants need raising, replacing theory with data (#2836, #7017).
Backlog skip notices are now cap-agnostic since the limit varies.
Co-authored-by: Orca <help@stably.ai>
* Extract breadcrumb recording into a collection-safe leaf module
Playwright loads spec imports at collection time, and e2e specs import
terminal-module constants (e.g. terminal-attention.spec.ts pulls
POST_REPLAY_MODE_RESET from layout-serialization, whose chain reaches
replay-guard). The breadcrumb import added to the terminal modules made
that chain reach crash-diagnostics.ts, whose top-level import.meta.hot
and webview-registry import crash Playwright's transform
("ReferenceError: exports is not defined in ES module scope") — every
e2e shard failed at collection before running a single test.
Move recordRendererCrashBreadcrumb into crash-breadcrumb-recorder.ts
(type-only imports, no import.meta) and point the terminal modules and
their test mocks at it; crash-diagnostics re-exports for existing
callers. Full e2e suite collects again (262 tests / 94 files); unit
suites, typecheck, lint green. No runtime behavior change.
Co-authored-by: Orca <help@stably.ai>
* Add cross-terminal pipeline benchmark (DSR-fenced throughput + latency probe)
Run inside any terminal (Orca pane, iTerm2, Ghostty, Terminal.app, VS Code)
to measure its full byte path. DSR round-trip latency at idle and under a
paced agent-TUI load, plus fenced throughput over four deterministic
fixtures. The DSR fence forces 'all bytes parsed' before the clock stops so
xterm.js-class ingest queues can't flatter the result.
First piece of the terminal performance initiative's measurement rig.
Co-authored-by: Orca <help@stably.ai>
* Add terminal performance initiative plan
Working plan for the orca-performance branch: verified architecture
findings, workstreams (baselines, #7153 validation, term-speed-2 revival
with merge-scout numbers, stall fixes, flow control, rig extensions,
utilityProcess router, telemetry), benchmark protocol, sequencing, and
baseline-relative success criteria.
Co-authored-by: Orca <help@stably.ai>
* Add cross-terminal baseline results (Orca 1.4.91 prod vs Terminal.app vs Ghostty)
Headline: Orca DSR latency under 1MB/s agent-TUI load is p50 134ms / p99 292ms
vs 0.45ms (Terminal.app) and 0.21ms (Ghostty). Idle latency is fine (0.69ms
p50) — the problem is queueing under load, not the pipeline hop. agent-tui
fenced throughput: Orca 2.0 MB/s vs Terminal.app 37 MB/s, Ghostty 78 MB/s.
Co-authored-by: Orca <help@stably.ai>
* Add pipeline-loss decomposition benches (headless xterm + daemon ingest)
Both isolate layers of the 51x agent-tui gap found in baseline-jul02:
bare @xterm/headless parses agent-tui at 103 MB/s and daemon Session
ingest (emulator + pending-output recording + fanout) at 103 MB/s —
on the byte stream the full Orca pipeline delivers at 2.0 MB/s.
Parser and daemon are exonerated; the loss is in main per-chunk
processing, delivery/ACK pacing, or renderer layers above xterm.
Co-authored-by: Orca <help@stably.ai>
* Record baseline + decomposition findings in initiative plan
Co-authored-by: Orca <help@stably.ai>
* Add dev-build orca-performance bench result (confounded: dev mode, 282-col window, 3MB fixtures)
DSR under load p50 161ms — the #7139/#7150 branch does not move the
under-load latency class. Expected in hindsight: DSR replies are ordered
within the output stream, so the metric measures output-queue depth;
cooperative drain paces input responsiveness but cannot reorder the queue.
Shrinking the queue itself (producer flow control, task 6) and raising
agent-tui throughput (task 9) are the levers for this number.
Co-authored-by: Orca <help@stably.ai>
* Record dev-build #7153 check in findings log
Co-authored-by: Orca <help@stably.ai>
* Parse-clock high-priority terminal drains instead of fixed-nap dripping
Attribution (task #9): the drain loop wrote at most 2x16KB then slept
4/16ms regardless of parse speed — an isolation bench (new
pane-terminal-output-scheduler-throughput.bench.test.ts) measures that
drip at 1.9 MB/s background / 27 MB/s foreground against xterm's
~103 MB/s parse rate, matching the baseline-jul02 end-to-end numbers
(agent-tui 2.0 MB/s in prod 1.4.91).
Fix: high-priority (visible-pane) drains now re-arm on xterm's
parse-completion callback and carry 8 writes per tick; the isolation
ceiling rises 27 -> 117.6 MB/s (parse-limited). Background cadence is
deliberately unchanged (2 MB/s drip protects the focused pane; hidden
delivery is term-speed-2's job). DRAIN_TIME_BUDGET_MS still bounds
per-tick work, preserving #7139's cooperative-drain intent.
Validation: 621 scheduler/guard/pty tests green, typecheck clean.
Co-authored-by: Orca <help@stably.ai>
* Record task #9 attribution + parse-clock fix in findings log
Co-authored-by: Orca <help@stably.ai>
* Findings: 51x loss attributed to O(tail) retained-tail redraw path in main onPtyData
Co-authored-by: Orca <help@stably.ai>
* Window the retained-tail redraw path to the cursor's reach
Attribution (findings log 2026-07-03): main's onPtyData consumed ~93% of
the event loop under an agent-TUI flood, and the dominant term was
appendNormalizedToMultilineTailBuffer + finalizeRetainedTerminalRows
materializing ~2x tail-length row objects plus a per-row trailing-space
regex on every chunk — 0.888ms/chunk at the 2,000-line cap, on every
Claude-Code-shaped frame (cursor-up + erase-below).
The multiline algorithm now runs on a suffix window sized by the chunk's
maximum upward cursor excursion (plus the inherited redraw cursor and a
safety margin); the untouched prefix is shared by reference with a cheap
last-char trailing-space check to match the reference trim. Pathological
full-height cursor-ups fall back to the unwindowed implementation, which
is kept verbatim and exported as the reference for the 500-case
differential fuzz (retained-tail-redraw-window.equivalence.test.ts).
Micro-bench at a full 2,000-line tail: 0.888 -> 0.073 ms/chunk (12x).
1,415 runtime tests green, typecheck clean.
Co-authored-by: Orca <help@stably.ai>
* Add dev bench results: parse-clock and windowed-tail fixes
Co-authored-by: Orca <help@stably.ai>
* Record windowed-tail partial win + next-cycle recipe in findings log
Co-authored-by: Orca <help@stably.ai>
* Findings: remaining whale is the per-chunk blocked-reason check (~85% of onPtyData post-fix)
Co-authored-by: Orca <help@stably.ai>
* Throttle the terminal wait-blocked check off the PTY hot path
Post-windowed-tail attribution (findings log 2026-07-03): the blocked-
reason complex — two full-tail buildTerminalWaitText builds plus
toLowerCase and multi-pattern scans per chunk, existing only to stamp
waitBlockedAt — consumed ~85% of onPtyData's remaining cost (~700-790ms/s
under an agent-TUI flood).
The check now runs at a 50ms cadence over coalesced chunks (PTY chunk
boundaries are arbitrary, so coalescing preserves semantics), with a
trailing-edge timer so burst-final state is always evaluated, and an
immediate bypass when the incoming chunk (plus a 31-char split carry)
contains a prompt keyword — so actionable-prompt stamping stays
per-chunk-immediate while keyword-free flood frames skip the complex
entirely. Previous wait text is cached per pty instead of rebuilt, and
state is cleared at both pty teardown sites.
1,415 runtime tests green (including the cross-chunk prompt test, which
exercises the keyword bypass), typecheck and lint clean.
Co-authored-by: Orca <help@stably.ai>
* Findings + results: three stacked fixes unlock the pipeline (agent-tui 16x, DSR-load p50 161->18.8ms in dev)
Co-authored-by: Orca <help@stably.ai>
* Add producer flow-control design to initiative plan
Co-authored-by: Orca <help@stably.ai>
* Findings: revival branch green but perf-gated — daemon Session ingest regressed 103->40-48 MB/s (chain emulator restructure); merge blocked until blockedfix parity
Co-authored-by: Orca <help@stably.ai>
* Pre-filter daemon OSC/mouse scanners for introducer-free chunks
Skips the scan-tail copy and full-chunk walks when a chunk cannot contain
an OSC or private-mode sequence (single native includes() checks), with
split-sequence correctness preserved via explicit tail retention. Strictly
positive micro-optimization on the daemon per-chunk path; 641 daemon tests
green (1 pre-existing WSL failure unrelated).
Co-authored-by: Orca <help@stably.ai>
* Retract confounded daemon conviction; mandate load-controlled A/B protocol for the revival merge gate
Co-authored-by: Orca <help@stably.ai>
* Record A/B gate pass in findings log; add A/B result JSONs
Co-authored-by: Orca <help@stably.ai>
* Add producer-side PTY flow control (watermarks + protocol v19)
Main now pauses the actual PTY when a pane's renderer-pending backlog
crosses the 256KB high watermark and resumes once it drains below the
32KB low watermark (wide hysteresis band so a draining queue cannot flap
pause/resume per flush slice). node-pty pause() stops the pty fd read, so
the kernel/ConPTY buffer fills and a flooding shell blocks on write —
flood-induced buffered lag becomes shell blocking instead of unbounded
main-process buffering (terminal-performance-initiative §5).
Transport: new fire-and-forget pausePty/resumePty daemon notifications
(protocol v19; 18 added to PREVIOUS_DAEMON_PROTOCOL_VERSIONS), routed
DaemonServer -> TerminalHost -> Session -> subprocess pause()/resume().
LocalPtyProvider pauses node-pty directly. Router/degraded providers
forward; IPtyProvider gains optional pauseProducer/resumeProducer.
Safety invariants:
- Lost-resume failsafe: daemon Session auto-resumes 5s after a pause with
no matching resume; main re-asserts the pause at most once per 5s while
still above the high watermark, so a lost resume can never wedge a shell
and a sustained flood stays throttled.
- Resume on every teardown path: Session kill/exit/dispose/detach; main
releases on pty exit and on window-destroyed bookkeeping wipes; the
adapter owes paused sessions a resumePty on the next connect after a
socket drop.
- Providers without support (SSH relay, legacy protocol <= v18) no-op
silently, and the scrollback-scaled pending-output cap still bounds
main memory when pause is unavailable.
- Kill switch: PRODUCER_FLOW_CONTROL_ENABLED in ipc/pty.ts flips the
whole mechanism off in one line.
daemon-errors.ts is split out of types.ts to stay under the max-lines cap.
Tests: watermark transitions/hysteresis/re-assert (controller unit),
lost-resume failsafe + resume-on-kill/exit/dispose/detach (session),
notification routing + v18 gating + reconnect owed-resume (adapter),
direct pause/resume (local provider), and a flood test asserting pause
fires once, pending stays bounded at HIGH + one chunk, and resume fires
once after drain (ipc/pty).
Co-authored-by: Orca <help@stably.ai>
* Findings: flow control merged; definition-of-done accounting; prod verification re-scoped to packaged RC
Co-authored-by: Orca <help@stably.ai>
* Fix stray brace from revival merge in long-table-scroll-restore e2e spec (broke e2e transform in CI)
Co-authored-by: Orca <help@stably.ai>
* Prod verdict: v1.4.121-rc.0 bench — DSR-load p50 134->18.6ms (7.2x), agent-tui 2.0->11.2 MB/s, idle at Terminal.app parity; pipeline now cadence-bound
Co-authored-by: Orca <help@stably.ai>
* Recover terminal output delivery after system sleep
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>
* VS Code head-to-head: Orca beats/ties 5 of 6 metrics (16x idle, 5x styles-stress, better p99); load p50 gap attributed to ACK window + timer-clamped drain cadence
Co-authored-by: Orca <help@stably.ai>
* Schedule zero-delay terminal drains via MessageChannel
Chromium clamps nested setTimeout(0) to ~4ms, stacking dead gaps onto
every parse-clocked drain tick; the explicit 4ms high-priority re-arm
interval added more. A posted message is still a macrotask — input and
paint are serviced between posts — so cooperative yielding survives
without the clamp. Generation-tokened cancellation; vitest keeps the
timer path (fake timers can't advance channel posts) plus a real-timer
smoke test for the channel path. Standing-queue target: VS Code's ~7ms
class (measured us 18.6ms, them 7.18ms, same rig).
Co-authored-by: Orca <help@stably.ai>
* Cut daemon and main PTY batch windows 8ms -> 2ms
At 9% pipeline utilization the DSR-under-load latency is fixed batching
windows, not queue depth (proved by the MessageChannel drain lever
moving nothing). Both hops charged an expected half-window per chunk;
2ms keeps burst coalescing at negligible IPC overhead (~500 msgs/s
worst case vs MB/s payloads).
Co-authored-by: Orca <help@stably.ai>
* Findings + tests: batch windows were the DSR-load gap (19->8.0ms dev); timing tests updated to 2ms windows
Co-authored-by: Orca <help@stably.ai>
* Fix PR CI and guard resume relay during shutdown
Co-authored-by: Orca <help@stably.ai>
* Chain e2e specs 6/6 green — gate x drain validation debt paid
Co-authored-by: Orca <help@stably.ai>
* Replace ack-stall watchdog with cumulative ACKs + solicited delivery resync
Design review: the 10s blind-reset watchdog decided correctness from a
wall-clock threshold. Rework piece 1 into a deterministic two-part design
(pieces 2 and 3 — lifecycle-reset counter zeroing and powerMonitor wake
IPC — are unchanged):
- Cumulative ACKs (TCP-style): the renderer dispatcher now tracks a
monotonic per-pty total of processed chars (terminal-pty-ack-gate) and
sends it on every ACK alongside the legacy per-chunk delta. Main keeps
per-pty sentChars/ackedChars and max-merges received totals — idempotent
and reorder-tolerant, so a lost ACK self-heals when any later ACK
arrives instead of becoming permanent in-flight debt. Provider
(SSH/daemon) backpressure is credited only the derived delta, clamped,
never negative. Main tolerates both payload shapes keyed by field
presence (dev hot-reload can mix renderer/main versions); totals reset
on pty exit and renderer lifecycle reset on both sides.
- Solicited resync (replaces the blind reset): when new pty data arrives
while that pty's delivery is fully gated and no probe is outstanding,
main sends pty:requestDeliveryResync; the renderer replies with its
cumulative totals and main reconciles via max-merge, then flushes held
pendingData. Event-triggered, verified-state recovery — no wall-clock
threshold decides correctness. The only timer is a 5s request/response
hygiene timeout that clears the outstanding flag and logs one
diagnostic warn per silent streak; it never mutates counters (a
renderer that cannot answer has dead IPC — reload is the only cure).
The 10s corrective watchdog is deleted.
Co-authored-by: Orca <help@stably.ai>
* Starting point: prior agent's garble differential fuzz harness
Three files recovered (were untracked) from a prior agent killed by API
outages, plus a trivial curly-brace lint fix in the op dispatcher so the
pre-commit hook passes:
- src/shared/agent-tui-ansi-fuzz-stream.ts (seeded agent-TUI byte-stream gen)
- src/shared/terminal-restore-parity-fixture.ts (renderer-parity fixture)
- src/main/daemon/headless-emulator-fidelity.fuzz.test.ts (suite 1: differential
HeadlessEmulator vs @xterm/headless reference on identical bytes)
Co-authored-by: Orca <help@stably.ai>
* Suite 1 findings: two new serialize round-trip bugs (B bold-loss, C cursor)
Scanned seeds 1..2000. Beyond the pre-documented serialize wrap-null-cell bug
(A, 27 seeds, tolerated), the fuzz surfaced two NEW real @xterm/addon-serialize
0.15.0-beta.287 round-trip defects, both of which garble a revealed hidden pane:
- Bug B (seeds 435, 770, 1321): serializing a dim cell followed by a bold-only
cell emits \x1b[1;22m; SGR 22 clears bold too, so restored bold is lost.
Minimal repro: '\x1b[2mA\x1b[22m\x1b[1mB' -> restored 'B' loses bold.
- Bug C (seeds 454, 1696): a final content row filled to the right margin leaves
xterm wrap-pending; the serializer's relative cursor restore lands one column
short. Minimal repro: '0123456789\x1b[3;5H' at cols=10 -> cursor x=3 not x=4.
Both isolated to pure serializer replay (no Orca preamble), confirming upstream.
Parity fixture verified faithful to the renderer pane's buffer options. Each is
pinned as a standalone it.skip repro; full evidence + classification in
notes/garble-fuzz-divergences.md. Seed 113 (handoff's DECSC/DECRC case) does not
diverge on the current harness. No production code changed.
Co-authored-by: Orca <help@stably.ai>
* Add perf prerelease update check modifier
Co-authored-by: Orca <help@stably.ai>
* Suite 2: hidden-reveal seq-reconciliation fuzz + two new snapshot bugs (D, E)
Property-tests the reveal seq-reconciliation byte-stitch (getChunkDataAfterSnapshot
/ reconcileChunkAgainstRestoredSnapshot in pty-connection.ts), mirrored exactly:
N=200 seeded hide/reveal scenarios with a rich agent-TUI hidden prefix snapshot
and an append-only racing tail, chunked with seq/rawLength meta, seq-domain
restarts, unmetered chunks and droppedOutput markers. Asserts snapshot-at-S +
reconciled tail == snapshot-of-everything (seq-neutral) and == always-visible
(end-to-end). Runtime ~5s at 200; FUZZ_ITERATIONS override documented.
Two NEW real snapshot-limitation garbles found while building it, both distinct
from suite 1's serialize bugs and pinned as standalone it.skip repros:
- Bug D: the DECSC saved-cursor register is not serialized. A hidden TUI that
saves the cursor (ESC 7 / CSI s) and restores it on reveal (ESC 8 / CSI u)
lands the restore at home. Repro: 'AB\x1b7\x1b[4;10HCD' + '\x1b8X' -> 'XB' vs 'ABX'.
- Bug E: a snapshot taken mid-escape-sequence (a PTY read split an escape) drops
the partial sequence (it's parser state, not buffer), so the tail's
continuation renders literal. Repro: 'AB\x1b[3' + 'mCD' -> 'ABmCD' vs 'ABCD'.
Fired on ~24% of the corpus (tolerated + counted via prefixEndsMidSequence).
The append-only-tail design isolates seq reconciliation from these and the Bug C
cursor cascade. Full evidence + fix directions in notes. No production changes.
Co-authored-by: Orca <help@stably.ai>
* Suite 3: 25-cycle park/reveal drift e2e test
Extends terminal-hidden-view-parking.spec.ts with a deterministic 25-cycle
park->reveal test on a static rich alt-screen TUI frame (box drawing, SGR
colors, wide CJK/emoji). Baselines against the frame after the first snapshot
restore (so both sides pass through identical machinery — the alt-screen restore
correctly drops normal-buffer scrollback, which is contract not garble), then
asserts every subsequent reveal reproduces it byte-for-byte with no accumulated
drift and no hidden-skip banner. Exercises the real renderer teardown +
HeadlessEmulator snapshot restore + PTY reattach path the fuzz suites model in
isolation. Passes in ~29s (electron-headless, workers=1).
Co-authored-by: Orca <help@stably.ai>
* Fix two serialize round-trip bugs garbling hidden-terminal snapshot restore
BUG B (addon patch): @xterm/addon-serialize's SGR diff emitted bold/dim set
params before the shared intensity reset 22, so "1;22" wiped a freshly set
bold and a bare "22" dropped a still-set bold/dim. Patched via pnpm
patchedDependencies (config/patches) to diff bold+dim as one intensity
group with the clearing 22 emitted first. Other flag pairs (4/24, 3/23,
7/27, ...) have dedicated resets and were verified unaffected.
BUG C (Orca-side hardening): the addon restores the cursor with relative
moves computed from where it assumes replay leaves the cursor; a final row
filled exactly to the right margin leaves replay wrap-pending and the
restore lands one column short. New shared
serializeWithAbsoluteCursor appends an absolute CUP from the source
terminal's authoritative cursor at every restore/replay serialize site
(daemon/runtime HeadlessEmulator.getSnapshot, renderer mobile snapshot
serializer, shutdown layout capture). It skips empty snapshots and
wrap-pending sources so it never changes already-correct behavior.
Round-trip repros + non-regression coverage in
src/main/daemon/terminal-snapshot-serialize-roundtrip.test.ts (verified
failing with the fixes stashed). buildRehydrateSequences extracted to its
own module to keep headless-emulator.ts under the max-lines budget.
Co-authored-by: Orca <help@stably.ai>
* Gates: tolerate+count Bugs B/C in deep mode; drop inverse from reconciliation tail
- Fidelity suite: add snapshotHasSelfCancellingBoldReset (Bug B) and
isMarginWrapPendingCursorOffByOne (Bug C) predicates so the corpus tolerates +
counts them like Bug A. FUZZ_ITERATIONS=2000 is now green (~113s) and fails
only on genuinely new divergences; each tolerance keeps its <50% degeneracy
guard. Default 300 unchanged (~17s).
- Reconciliation suite: drop SGR 7 (inverse) from the append-only tail. Inverse
marks trailing blanks with an inverse-fg the serializer round-trips slightly
differently by capture depth — a Bug-B-class serialize nuance, not seq
reconciliation. FUZZ_ITERATIONS=1000 is now green; default 200 unchanged.
- Notes updated: every bug class is both pinned (skipped repro) and tolerated in
its corpus; combined default runtime ~19s.
Regex uses String.fromCharCode(27) to stay oxlint no-control-regex clean.
Co-authored-by: Orca <help@stably.ai>
* Keep RC update checks off perf prereleases
Co-authored-by: Orca <help@stably.ai>
* Fix snapshot DECSC register loss (Bug D) and mid-escape boundary drop (Bug E)
Bug D: the serialized screen cannot carry the VT100 DECSC saved-cursor
register, so a hidden ESC 7 followed by a post-reveal ESC 8 restored to
home and clobbered live cells. The snapshot epilogue now re-saves at the
source's saved position before the final absolute CUP
(readSavedCursorRegister + serializeWithAbsoluteCursor; the active
buffer's own register, so alt screens carry theirs). Position-only by
design; never-saved terminals are left untouched.
Bug E: a PTY read ending mid-escape leaves the sequence in the emulator's
parser, so serialize dropped it and the racing tail's continuation bytes
rendered literally after reveal (~24% of the fuzz corpus). The emulator
now tracks the unparsed trailing partial at ingest
(terminal-partial-escape-tail.ts, committed post-parse like the mouse
mirror) and ships it as TerminalSnapshot.pendingEscapeTailAnsi.
applyMainBufferSnapshot writes it LAST, after POST_REPLAY reset — any
later ESC would abort the dangling sequence. Seq accounting is unchanged:
the tail is a suffix of bytes the snapshot seq already counts, so
reconcile slicing needs no adjustment.
Fuzz suites: unskip the Bug B/C repros (fixed on this branch) and the new
D/E repros; remove the B/C/E tolerance predicates so regressions fail
loudly. Only Bug A (upstream wrap null-cell) stays tolerated + counted.
Green at FUZZ_ITERATIONS=2000 (fidelity) and 1000 (reconciliation).
Co-authored-by: Orca <help@stably.ai>
* Count suffixed RC tags (rc.N.perf) in the shared rc counter — second suffixed cut collided with the first
Co-authored-by: Orca <help@stably.ai>
* Classify suffixed rc tags (rc.N.perf) as rc telemetry identity in release builds
The build-identity guard only knew vX.Y.Z and vX.Y.Z-rc.N, so suffixed
perf RCs cut fine but every platform build refused the tag and the
releases published empty.
Co-authored-by: Orca <help@stably.ai>
* Cut the hidden-restore flood feedback loop (A) + query carve-out on drops (B)
(A) Under a foreground flood, the hidden-output-restore loop re-fetched
snapshots endlessly: each synchronous applyMainBufferSnapshot starved ACK
processing, main pinned at the in-flight cap, dropped at the pending cap,
and every droppedOutput/modelRestoreNeeded marker re-armed another
restore until the flood ended (rc.7.perf DSR timeouts).
- Restore loop: a foreground live-chunk queue overflow now abandons the
restore immediately (the stream is outrunning snapshot fetch+replay),
with a 3-iteration hard cap + lifecycle warn as backstop.
- Re-arm gate: drop markers/sentinels and reconcile seq-gaps on a visible
pane during its own in-flight/just-abandoned restore no longer re-arm;
live bytes write through and ONE deferred repaint (2s after the last
backpressure signal) heals the gap. Hidden-pane gate semantics are
unchanged.
- Query salvage: discarding queued restore bytes (overflow/refetch) now
extracts DSR/CPR/DA/OSC-color queries and replays them to xterm so
replies still flow.
(B) Main-side: dropOversizedPendingPtyData carves reply-eliciting query
sequences out of the dropped buffer (and out of post-drop latched data,
bounded) and ships them on the droppedOutput sentinel, so DSR probes
survive bulk drops. Query scanning moved to
src/shared/terminal-reply-query-extraction.ts, shared verbatim with the
renderer's hidden-startup query extraction.
Co-authored-by: Orca <help@stably.ai>
* ACK terminal output at parse-drain, not dispatcher enqueue (C)
The renderer credited main's per-PTY in-flight window the moment a
pty:data chunk entered the dispatcher, so the 512KB window meant "bytes
received", never "bytes parsed". Under flood the renderer write queue
grew unbounded behind instant ACKs; main saw no backpressure, crossed
the pending cap, and bulk-dropped output (rc.7.perf DSR timeouts).
Crediting is now parse-deferred: each delivery carries a fire-once
credit (deliverPtyDataWithDeferredAck); the pane's first scheduler write
claims it (writeTerminalOutput.ackCredit) and the output scheduler fires
it when the bytes are consumed — after terminal.write in the
parse-clocked drain, or on ANY discard path (backlog cap replacement,
discardTerminalOutput, disposed-terminal drops, flush recovery).
Deliveries that never reach the scheduler (reconcile drops, restore
queueing, pre-mount eager buffer) settle at handler return, so the
invariant holds: every delivered chunk credits exactly once, parsed or
discarded. E2E ack-gate hold/release and delivery-resync semantics are
unchanged (all crediting still routes through ackPtyData).
Main-side equilibrium: with ACKs at parse cadence, in-flight becomes
true backpressure — pendingData stays near the 256KB producer-pause
watermark, far under the >=2MB drop cap, so bulk floods block the shell
(node-pty pause) instead of dropping.
Co-authored-by: Orca <help@stably.ai>
* Synthesize salvaged query replies directly instead of replaying into xterm
The 10MB dev bench proved the write-back salvage insufficient: a
pending-cap drop always triggers a snapshot restore, whose replay guard
swallows xterm auto-replies and whose discardTerminalOutput races away
still-queued query writes — the salvaged DSR died both ways and the
fence still timed out.
Salvage now answers directly on the input path (immune to both): CPR
(CSI 6n) from the live buffer via transport.sendInput, DA1 with the
renderer's canned response, OSC color probes via the existing direct
responder. Rare queries (DECRQM, DA2) keep the best-effort xterm
replay.
Co-authored-by: Orca <help@stably.ai>
* Untrack branch-added bench result JSONs (20 files); keep numbers in the findings log
Files stay on disk; main's 7 pre-existing results are untouched.
Co-authored-by: Orca <help@stably.ai>
* Branch guide: document merge-not-rebase sync strategy and conflict pattern
Co-authored-by: Orca <help@stably.ai>
* Merge origin/main (#7316 tab-strip click-vs-drag fix); adapt #7290 recovery-reload tests to this branch's dual did-finish-load listeners
The three tests grabbed the FIRST did-finish-load listener; on this branch
the renderer delivery-gate reset registers before the orphan sweep, so the
sweep tests exercised the wrong handler (one failing, two vacuously green).
They now fire all listeners like a real reload.
Co-authored-by: Orca <help@stably.ai>
* Fix branch CI lint: split pane-interaction functions out of artificial-opencode-terminal-load.spec (815>800 lines), modernize perf-html-report script
No max-lines disable per repo rules; extracted to
artificial-opencode-pane-interactions.ts. toReversed() and
import.meta.filename replace reverse()/fileURLToPath.
Co-authored-by: Orca <help@stably.ai>
* Fix Windows update-relaunch killing the live terminal daemon
On a Windows update relaunch the daemon can be wedged past every RPC
budget (final checkpoint flush + installer/AV disk pressure), so the 3s
health check AND the 5s session-list hello both time out while sessions
are still alive - and the launcher failed closed, killing the daemon and
every terminal session it owned.
- Adopt an unresponsive daemon whose pipe still accepts a raw
connection; a new rejected health state keeps replacing daemons that
answered and refused the handshake (never adoptable).
- Give Windows pid files a real startedAtMs (daemon self-reports it in
the ready IPC message) and verify it via CIM CreationDate piggybacked
on the existing command-line query, so the pid-recycling guard is no
longer inert on win32.
- Only delete legacy daemon pid/token files when the pid-file process is
provably dead; deleting a live daemon''s token made its sessions
permanently unadoptable after a protocol bump.
- Capture agent resume records every 60s in the renderer (skipping
unchanged records) so hard kills still leave a fresh resume record.
* Heal blank terminals when main→renderer push delivery dies (renderer-pull delivery watchdog)
Field evidence (v1.4.121-rc.0 debug snapshot, 2026-07-06): a wedged window
held 530,115 un-ACKed in-flight chars — one PTY pinned at the 512KiB per-PTY
high water plus a fresh terminal's 245-char prompt that was sent and never
consumed — while the user ran the snapshot over invoke from that same window.
Main→renderer push delivery (pty:data and every sibling channel) was dead;
renderer→main→renderer invoke was alive. Upstream precedent for
one-directional IPC death: electron#37067 (suspected Mojo pipe disconnect,
stalled as need-info). Every terminal goes blank, new terminals are born
blank, and only a renderer reload recovered.
The existing recovery layers cover the OTHER variants of this bug family and
structurally cannot reach this one:
- The xterm write-pipeline sync-throw guards, output-buffer caps, and
probe-certified replay-guard release (#7150 family) run only after bytes
arrive in the renderer — here they never do. (The pending cap did work as
designed in the field: ~2.1MB pendingDroppedChars, bounded main heap.)
- Cumulative ACKs self-heal lost ACK messages and the solicited delivery
resync reconciles verified totals (4647df86a; #7260 on main) — but the
resync probe, the powerMonitor wake relay, and the droppedOutput restore
markers all ride main→renderer push, the direction that is dead. The
probe's unanswered path deliberately only logs.
This adds the missing lane, renderer-initiated and ridden entirely over
invoke — the direction the field snapshot proved alive:
- terminal-delivery-watchdog.ts: 15s heartbeat, free while output flows.
Hot-path cost is one Map upsert per received chunk; a tick does no IPC
unless the terminal plane was silent for the whole interval and a PTY
still expects delivery. Two consecutive silent ticks with main reporting
ACK-starved in-flight confirm the wedge; heals are one-shot per 60s
cooldown so a persisting wedge cannot repaint-storm.
- pty:reportRendererDeliveryState (invoke): always max-merges the renderer's
cumulative processed totals (a free extra repair lane for the lost-ACK
variant); with heal:true — and only after main has itself seen ≥10s of ACK
silence — writes off bytes the renderer provably never received
(received ≤ acked < sent; a received-but-unparsed backpressure window is
never written off), drops that PTY's pendingData (snapshot covers
everything ≤ markerSeq, hidden-drop parity), credits provider flow
control, and returns restore markers in the reply.
- The renderer re-attaches all push listeners (cures a detached-listener
variant outright; a safe no-op against a dead channel) and routes the
pulled markers through the existing pty:modelRestoreNeeded machinery —
panes repaint from the main-owned buffer snapshot with zero push
delivery involved.
- Field discrimination built in: the heal warn logs
ipcRenderer.listenerCount('pty:data') (listener detached vs channel dead)
with the full delivery snapshot, so the next occurrence names the root
cause without asking the user to run anything in a console.
Repro harness: the exposeStore-gated __terminalDeliveryWatchdog hook
blackholes pty:data ahead of the dispatcher — the field failure in
miniature (no receive count, no ACK credit, no dispatch).
terminal-push-delivery-loss-recovery.spec.ts proves the wedged output
repaints while the blackhole is still engaged and live flow resumes after
release, with no reload. Unit suites pin the watchdog state machine
(zero IPC under flow, two-tick confirm, cooldown), the dispatcher reattach
seam, and the main-side write-off semantics.
Perf: nothing added to main's send/flush path; the renderer data path gains
one integer/Map update per chunk; idle cost is one ~100-byte invoke per 15s
only during total terminal silence. Terminal perf e2e suite (typing
latency, redraw freeze, output scheduler, hidden TUI restore, artificial
opencode load) passes on this change; no watchdog activity occurs under
ack-gate pressure scenarios because receive-progress gates the heartbeat.
Co-authored-by: Orca <help@stably.ai>
* Expose the hidden-yet-visible delivery-gate contradiction in the debug snapshot
The v1.4.124-rc.2.perf blank-terminal field snapshot showed a different
state than the v1.4.121 transport wedge: no delivery gating at all
(ackGatedFlushSkipCount 0, in-flight 38KB, far under every cap) but TWO
ptys hidden-delivery-gated with 78MB dropped as hidden. The aggregate
counters cannot say whether the pane the user was staring at was one of
the gated ones — the one number that separates "normal background
dropping" from "main is starving a visible pane because the reveal
unmark never fired".
Add hiddenDeliveryGatedVisiblePtyCount / hiddenDeliveryGatedActivePtyCount
(overlap of the gate's hidden set with the renderer's visible/active
reports — a contradiction that must be zero) to the delivery debug
snapshot, and a once-per-minute warn when hidden-gated bytes are dropped
for a pty the renderer reports visible or active, with the full snapshot
attached. Zero cost outside the debug read and the already-dropping path.
Co-authored-by: Orca <help@stably.ai>
* Unlatch the hidden-delivery gate when user input disproves a stuck document.visibilityState
macOS occlusion tracking can wedge document.visibilityState at 'hidden'
after display sleep and never fire another visibilitychange. The hidden-
delivery gate then keeps dropping renderer-bound bytes for panes the user
is looking at (field snapshot 2026-07-06, v1.4.124-rc.2.perf: 78MB dropped
across 2 pane-level-visible ptys with a fully healthy transport), and every
recovery path (window focus, system-resume relay, backlog recovery) re-ran
syncHiddenRendererPtyDelivery only to recompute the same stale predicate —
nothing could ever clear the gate. The user sees a frozen terminal; typing
echo is dropped in main; only a reload recovers.
Real user input while the document claims hidden is a physical
contradiction: keystrokes and clicks only reach a focused, on-screen
window. stale-document-visibility.ts latches that proof, runs each pane's
existing visibilitychange resync (gate unhide + hidden-output snapshot
restore), and hands authority back to the occlusion tracker on the next
genuine visibilitychange. No timers; the failure bias is safe — a wrong
latch can only restore pre-gate delivery cost, never drop bytes. Hot path
unchanged: the foreground predicate still returns on the same single
comparison while the document is visible.
tests/e2e/terminal-stuck-occlusion-recovery.spec.ts pins the wedge
(visibilityState pinned hidden -> output dropped, not painted; the
hiddenDeliveryGatedVisiblePtyCount field discriminator reads >0) and the
recovery (one Shift keypress repaints the missed output from the main-owned
snapshot, no reload, while visibilityState still reads hidden). Negative
control verified: the spec fails without this fix. Typing-latency perf
gate passes; terminal-pane unit suites 366/366.
Co-authored-by: Orca <help@stably.ai>
* Add a one-paste terminal freeze report: __orcaTerminalFreezeReport()
Every field report of the frozen-terminal family so far has needed
follow-up asks (console output, main logs, second snapshots) because each
capture showed one process's counters at one instant. This makes a single
DevTools command sufficient: `await window.__orcaTerminalFreezeReport()`
returns renderer state (document.visibilityState + the stale-visibility
override, pty:data listener count, delivery-watchdog totals), main's debug
snapshot extended with a per-pty delivery table (sent/acked/pending, hidden
vs visible-set membership, last send/ACK ages, window focus flags, power
suspend/resume ages, app version), and bounded breadcrumb rings from BOTH
processes recording the transitions that matter: gate marks/unmarks,
visibilitychange and stale-visibility latches, watchdog stalls and heals,
restore markers, heal write-offs, and renderer lifecycle resets (so "user
already reloaded" is visible in the history).
Costs stay off the data path: breadcrumbs record only rare transitions into
a 100-entry ring with same-kind coalescing (a flood costs one slot per
second); the per-pty table is built only when the snapshot is read; the
per-send bookkeeping adds one Date.now() to existing accounting writes. Pty
ids are redacted to their `@@` suffix because daemon session ids embed
worktree paths. The report assembles over invoke IPC — the direction proven
alive in every observed wedge — and a failing invoke is captured as data
instead of sinking the report.
The stuck-occlusion e2e now also pins the report end-to-end: after the
wedge + keystroke recovery, the report must carry the stale-visibility
latch and gate transitions in the renderer ring, gate-mark/unmark in main's
ring, and a populated per-pty table. Suites: pty.test.ts 258, terminal-pane
1705, shared ring 5; typing-latency perf gate passes.
Co-authored-by: Orca <help@stably.ai>
* perf(daemon): keep-tail thin hidden panes' stream so agent floods never bury typing (STA multi-workspace lag)
Hidden panes are exempt from pendingData flow control (main gate-drops
their bytes after ingestion), so N background agents ran unbounded ahead
on the one shared daemon->main stream socket (measured 192MB user-space
backlog) and visible-pane echo waited FIFO behind it — typing appeared
seconds late whenever several agents burst on a loaded machine
(8x512KB/s + 12 CPU spinners: p50 293ms fix-off; 12x1MB/s: 6.1s).
Mechanism (replaces producer pacing — no reveal catch-up, ever):
- Shallow socket write gate (128KB) + per-session fairness bypass bounds
echo latency by construction; kernel-flush refill sentinel keeps held
bulk draining at full speed (drain-only refill capped at ~8MB/s).
- Backgrounded sessions' queued output is keep-tail dropped (newest
512KB kept, in-order dataGap replaces the middle); a ~2MB GLOBAL
budget shrinks per-session keep-tails (floor 64KB) so a worktree
switch never waits behind the aggregate. Reply-eliciting query bytes
(DSR/DA/OSC probes) are salvaged from dropped spans.
- Notifications are structurally lossless: the daemon runs the same
shared scanners main uses (bell/OSC 133/pr-link/2031) over every byte
BEFORE drop decisions and relays facts in byte order; ordered
background markers hand scan authority back and forth, seeded with the
emulator's partial escape tail so a sequence split across the handoff
neither phantom-fires nor goes missing. Titles/agent-status stay
main-side (kept-tail convergent).
- Main: background = hidden AND no remote view subscriber (a live
mobile/web view is never thinned); on dataGap main resets cross-chunk
parse carries, drops the headless mobile mirror, and reuses the
hidden-drop model-restore marker.
Wire: three new stream events, tolerated within protocol v19 (old mains
ignore unknown events; old daemons never see the trigger). Kill
switches: ORCA_DAEMON_BACKGROUND_STREAM_DROP=0,
ORCA_DAEMON_SHALLOW_SOCKET_GATE=0.
A/B (pnpm bench:multi-workspace-typing): 8x512KB/s + 12 CPU workers
p50 293ms/p90 647ms -> 15/21ms (= baseline); 12x1MB/s 6,146ms -> 20ms;
light loads unchanged; zero missing echoes. Latin hidden-restore e2e
green (probe-verified aggregate-drain root cause). New deterministic
repro harness: tests/e2e/terminal-multi-workspace-typing-latency.spec.ts
+ CPU pressure workers.
Co-authored-by: Orca <help@stably.ai>
* diag(terminal): breadcrumb WebGL context-loss/atlas + wake triggers into freeze report
Silent instrumentation (memory ring only, no new console lines) so the next
post-wake garble report attributes itself. Adds:
- shared/terminal-webgl-diagnostics.ts: lib-safe sink so pane-webgl-renderer
(lib) can record without importing the components-layer ring; wired to the
ring in terminal-freeze-breadcrumbs.
- webgl-context-loss crumb at onContextLoss, webgl-atlas-reset crumb at the
atlas registry reset — the pair that distinguishes 'atlas corrupted' from
'missed repaint'.
- wake-recovery:<source> crumb (focus/visibilitychange/system-resumed) with the
clearGlyphAtlases decision; source in the kind so distinct triggers don't
coalesce.
- per-pane WebGL state (getAllPaneRenderingDiagnostics) in the freeze report.
Gates: typecheck 0 errors; terminal suites 328 files pass; oxlint clean.
Co-authored-by: Orca <help@stably.ai>
* fix(lint): use Number.parseInt/parseFloat in terminal-view-attributes
oxlint unicorn(prefer-number-properties) flagged 24 global parseInt/parseFloat
calls in the terminal-view-attributes feature (
|
||
|
|
96d1fa1d62 |
fix(grok): clipboard, native chat, hooks, sessions, ConPTY KKP (#7944)
* fix(grok): restore clipboard and native-chat parity Grok CLI already supports argv prompts, OSC 52 copy, and image paste chips. Orca was blocking those paths: stdin-after-start keystroke injection, OSC 52 writes default-off, image-attachment denylist, and native-chat allowlist. - Launch Grok with positional argv prompts - Default OSC 52 TUI clipboard writes on (still user-toggleable) - Treat Grok as image-attachment capable - Parse ~/.grok/.../chat_history.jsonl for native chat OSC 52 clipboard *query* remains ignored by design (host clipboard exfil risk); xAI docs only require OSC 52 write for remote copy. * fix(grok): sync OSC 52 docs and locale catalog with default-on Update terminalAllowOsc52Clipboard type docs for the true default, and refresh locale strings so settings UI mentions Grok alongside other TUIs. * fix(grok): tool hook matcher, StopFailure, previews, AskUser waiting Grok tool-event matchers are real regexes; bare `*` failed as match-all. Install `.*` for Pre/Post tool hooks, add StopFailure for API-error ends, recognize Grok-native tool input keys, and map ask_user_question PreToolUse to waiting with interactivePrompt (Kimi-style live card path). * fix(grok): resolve chat_history under GROK_HOME and long-cwd layouts Centralize Grok session path helpers so hooks and native-chat honor GROK_HOME and find chat_history.jsonl by session id when the cwd group is slug-encoded (encoded name > 255 bytes) instead of only encodeURIComponent(cwd). * fix(terminal): keep Kitty keyboard for Grok on Windows ConPTY Local Windows ConPTY withholds KKP so CSI-u-blind CLIs (e.g. Antigravity) keep Enter/nav working (#2434). Grok needs KKP for Ctrl+Enter interject and modified-Enter newline chords; blanking the advertisement for Orca-launched Grok left those actions broken. - Prefer KKP when tuiAgent is grok despite ConPTY withhold - Wire launchAgent from tab/startup into keyboard protocol options * fix(grok): restore OSC52 default-off, split decoders, honor GROK_HOME hooks - Keep terminalAllowOsc52Clipboard default false (clipboard exfil risk) - Split transcript-line-decoders under max-lines without suppressions - Install local Grok hooks under resolveGrokHomeDir() / GROK_HOME * refactor(grok): share CLI home resolution * fix(grok): harden terminal and native chat integration * test(grok): align CI coverage with native chat support --------- Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> |
||
|
|
0b4196fc17 |
fix(ssh): stop deleted ~/.ssh/config hosts from reappearing on sync (#7302)
Deleting a config-sourced SSH target had no lasting effect: the Manage-SSH pane re-imports ~/.ssh/config on open, and the import was a pure upsert with no record of deletions, so the just-deleted host was re-inserted verbatim from the config that still exists on disk. Persist a `deletedSshConfigAliases` tombstone set: - Deleting a config-managed target (source 'ssh-config', or an adopted legacy import) records its alias; manual targets are never tombstoned. - The passive on-open sync skips tombstoned aliases, so a deleted host stays deleted. - Re-adding or editing a target reclaims its alias, and the explicit Import action (`reAdopt`) clears all tombstones to deliberately re-adopt config. This also fixes the edit-then-reappear case: editing a config host to `manual` already reserved its current alias, and reclaim covers alias changes. |
||
|
|
3826169c11 |
Keep Claude live-PTY refresh gate closed across restarts; recover wiped runtime credentials (STA-1246) (#7483)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
f11a2109c8 | feat(rate-limits): add MiniMax token control (#7387) |