Commit Graph
265 Commits
Author SHA1 Message Date
OrcaWinandBrennan Benson 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>
2026-07-20 18:35:14 -07:00
Brennan Benson 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.
2026-07-20 17:46:09 -07:00
Brennan Benson 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
2026-07-20 17:11:23 -07:00
NeilandOrca 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>
2026-07-20 03:34:44 -07:00
JinjingandOrca 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>
2026-07-19 16:10:32 -07:00
Brennan BensonandOrca 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>
2026-07-17 00:21:42 -07:00
Jinjing 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
2026-07-16 17:12:44 -07:00
Jinjing 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.
2026-07-16 13:06:21 -07:00
Jinwoo HongandOrca 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>
2026-07-15 13:32:13 -07:00
gatsby74andNeil 9dc1f253ad feat(editor): add file editor word wrap preference (#8423)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-07-12 15:17:06 -07:00
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>
2026-07-12 14:09:15 -07:00
Myungjoo Jang 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
2026-07-12 01:40:39 -07:00
Neil 8a4ec4e856 feat(terminal): expose right-click paste on every platform (#8322)
* WIP: Changes before auto-review fixes

* fix: preserve terminal paste defaults across platforms
2026-07-11 15:02:46 -07:00
Jinjing 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
2026-07-11 14:49:49 -07:00
Neil 800967278e feat: promote APFS shared paths (#8318) 2026-07-11 14:24:39 -07:00
Mariusandgatsby74 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>
2026-07-11 13:45:52 -07:00
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 (ee540f32d, perf-branch only),
failing PR Checks 'verify' lint. Mechanical global→Number.* rewrite via
oxlint --fix; behaviorally identical. Pre-existed the latest main merge.

Co-authored-by: Orca <help@stably.ai>

* fix(test): update stale terminal test stubs/expectations to current runtime

Three pre-existing perf-branch test failures (red before the latest main
merge; unrelated to it) — all stale test scaffolding lagging behind
perf-branch features, no production code changed:

- provider-dispatch.test.ts: electron mock missing powerMonitor, which
  pty.ts installPowerSignalBreadcrumbs now calls on registerPtyHandlers.
  Added powerMonitor:{on:vi.fn()} to match pty.test.ts.
- runtime-terminal-stream.test.ts: onSnapshot now receives a second
  { pendingEscapeTailAnsi } meta arg (#7329); updated the three exact-arg
  toHaveBeenCalledWith assertions.
- terminal-multiplex-escape-tail.test.ts: stubRuntime missing
  registerRemoteTerminalViewSubscriber (subscribe path calls it now, erroring
  before snapshot serialize); added the stub used by sibling multiplex tests.

Co-authored-by: Orca <help@stably.ai>

* Fix hidden/parked split-pane exit stranding ghost or resurrected panes

Deterministic e2e repro of the field 'ghost blank pane' incident (a
closed/finished setup-split leaf persisted in root with no binding and
remounted as a permanently blank pane) found two teardown gaps at the
hidden-view parking boundary:

1. The kept-exit guard ('freshly split pane can lose its newborn PTY
   during setup') fired for HIDDEN panes. The hidden-delivery gate
   withholds their bytes, so a hidden split always looks output-less and
   its dead pane was kept — a binding-less ghost that dead-session
   reconcile can never reach (no PTY id to prove dead). The keep is a
   visible-failure UX; gate it on isVisibleRef and close hidden panes.

2. A PTY exit landing while the tab is PARKED reached only the parked
   watcher's exit sidecar (hosts' onPtyExit requires a mounted
   TerminalPane), which only disposed the watcher. The leaf's stale
   binding then reattached on reveal and the daemon re-created the
   exited session id as a fresh shell — silent pane resurrection. The
   sidecar now collapses the dead leaf out of the stored layout via
   detachTerminalLayoutLeaf (the observed-exit teardown's data half).

New e2e suite terminal-pane-close-layout-consistency.spec.ts sweeps
close/shell-exit at every hidden/park lifecycle phase and asserts
leaves(root) == bindings == live panes; all 7 scenarios pass. Unit
regressions added for both fixes.

Co-authored-by: Orca <help@stably.ai>

* Harden terminal delivery and snapshot recovery

Co-authored-by: Orca <help@stably.ai>

* Fix inherited lint failures

Co-authored-by: Orca <help@stably.ai>

* Align merged runtime recovery tests

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Orca <help@stably.ai>
2026-07-10 17:27:47 -07:00
BingZandJinwoo Hong 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>
2026-07-10 13:16:55 -07:00
PP 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.
2026-07-06 18:35:42 -07:00
Brennan BensonandOrca 3826169c11 Keep Claude live-PTY refresh gate closed across restarts; recover wiped runtime credentials (STA-1246) (#7483)
Co-authored-by: Orca <help@stably.ai>
2026-07-06 01:16:23 -07:00
guihirsch f11a2109c8 feat(rate-limits): add MiniMax token control (#7387) 2026-07-04 23:33:25 -07:00
Neil 185b768609 Add rich Markdown spellcheck setting (#7103) 2026-07-02 21:59:41 -07:00
Jinwoo HongandOrca f8c1f0fdf8 Fix delayed TUI mouse wheel reports (#7060)
Co-authored-by: Orca <help@stably.ai>
2026-07-01 19:54:53 -07:00
Eddie JaoudeandJinjing e0a7f0eadd fix: reordered default columns in kanban board (#6934)
* fix: reordered default columns in kanban board

* Introduce dedicated flag to repair reversed default workspace statuses

- Add `_workspaceStatusesReorderedDefaultRepaired` to decouple the
  one-shot repair from the initial status order migration.
- Ensure the repair runs for users who saved the reversed default
  payload (with "Done" on the left) during a short-lived broken build.
- Support both "Completed" and "Done" labels when identifying default
  status shapes to migrate or repair.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-01 13:17:52 -07:00
Jinwoo HongandOrca d7010353ca Move per-workspace environments to Experimental settings (#6926)
Co-authored-by: Orca <help@stably.ai>
2026-06-30 12:57:39 -07:00
Nicholas Zolton 4febefd454 feat(browser): add Helium as a Chromium cookie-import source (#6768)
Register Helium (imput, bundle id net.imput.helium) so it is auto-detected
and importable from the picker on macOS like Chrome/Edge/Arc/Brave/Comet,
instead of only via the "From File..." JSON path. Reuses the existing macOS
Chromium decryption path unchanged.

Helium quirks (verified on a real install): its Keychain entry is service
"Helium Storage Key" / account "Helium" (not the usual "<Browser> Safe
Storage"), it stores data under its bundle id, and it keeps cookies at the
legacy <Profile>/Cookies path. Mac-only for now; Windows/Linux data dirs are
unverified so winRoot/linuxRoot are omitted and the source defaults off there.

Claude-Session: https://claude.ai/code/session_01SZzX6ktZaaCUg5PWnzniuC
2026-06-29 13:21:20 -07:00
a56da57bb8 Add worktree labels for localhost ports (#6424)
* Add worktree labels for localhost ports

* Fix localhost label test fixtures

* Extend localhost worktree labels to agents

* Address localhost label review feedback

* Remove localhost label page injection; stream responses untouched

Drop the title/favicon HTML injection from the localhost label proxy.
The proxy now only relabels the hostname and pipes responses straight
through, so app CSP/cookies/bodies are preserved and large/streamed
responses are no longer buffered in memory.

- Normalize wildcard bind hosts (0.0.0.0 -> 127.0.0.1, :: -> ::1) before
  using them as a proxy connect target.
- Delete the favicon SVG generator and dead repoIcon/badgeColor plumbing.
- Remove orphaned LocalhostLabelMock i18n keys.

Co-authored-by: Orca <help@stably.ai>

* Harden localhost label proxy from review

- IPC register: restrict proxy target to loopback or a matching scanned
  workspace port (close open-proxy/SSRF surface from untrusted renderer).
- Proxy: guard against ERR_HTTP_HEADERS_SENT on mid-stream upstream error;
  add error/cleanup listeners on client request/response and upgrade socket.
- labelLocalhostUrl: fall back to the raw URL when the proxy rejects a
  target (e.g. https) instead of throwing.
- Port label route: include worktreePath so button-open and terminal/CLI
  paths produce the same label.
- Terminal OSC link hover: discard stale async tooltip results via a hover
  token, matching the WebLinks path.

Co-authored-by: Orca <help@stably.ai>

* Match default-port advertised hosts in localhost label target check

Co-authored-by: Orca <help@stably.ai>

* Consolidate duplicated localhost label helpers

- Move the loopback host set, loopback-URL parser, and wildcard
  connect-host normalizer into shared/localhost-worktree-labels.ts; proxy,
  runtime, terminal link routing, and the IPC guard now share one copy.
- Extract the port -> repo -> worktree -> project label-route lookup into
  workspace-port-localhost-label-selector.ts (a hook plus an imperative
  resolver), replacing the block triplicated across the ports surfaces.

Co-authored-by: Orca <help@stably.ai>

* Extract command-code prompt-status seed to its own module

Keeps launch-agent-in-new-tab.ts under the max-lines limit after the
localhost-hint additions, without a lint disable.

Co-authored-by: Orca <help@stably.ai>

* Remove agent-facing localhost mechanism

Orca does not mutate user prompts or inject prompt snippets, so drop the
localhost-open agent hint entirely:

- Remove appendLocalhostOpeningHint / includeLocalhostOpeningHint and the
  hint constant from tui-agent-startup; agent prompts are no longer rewritten.
- Remove the ORCA_LOCALHOST_OPEN env var from local agent terminals (it was
  only discoverable via the now-removed hint).
- Remove the orca localhost label|open CLI commands, their workspacePorts RPC
  methods, and the runtime labelLocalhostUrl/openLocalhostUrl methods.

The feature is now purely structural: the loopback label proxy plus the
ports-panel and terminal-link 'Open in Browser' paths, which surface a
clickable labeled URL without touching agent prompts.

Co-authored-by: Orca <help@stably.ai>

* Make localhost worktree labels opt-in (default off)

Serving a dev app under a different host than localhost:<port> can break
apps that bind cookies/sessions to localhost, so the feature should not
change existing users' Open-in-Browser behavior on upgrade.

- Default localhostWorktreeLabelsEnabled to false.
- Flip the gates to enable only when explicitly true (=== true / !== true)
  instead of treating undefined as enabled.
- Update the setting switch to checked only when explicitly enabled.
- Drop the now-unused runtime store settings field.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-29 12:27:30 -07:00
lvfenandJinjing b776506809 Support submodule diffs and upstream-base compares in Source Control (#6350)
* feat(source-control): show submodule diffs with lazy expansion

Dirty submodules now expand inline in Source Control to reveal their
inner changes, with file-level diffs that are read-only from the parent
worktree. Inner status is fetched lazily only when a submodule is
expanded, so status polling never recurses into (possibly nested)
submodules. Adds a submodule-status path across local and SSH runtimes
and git providers.

* feat(source-control): add compare-against-current-branch setting

Adds a global setting (default off) that defaults the Source Control
compare base to the current branch's upstream so the panel prioritizes
local changes instead of the full delta versus the repository default
branch. When the branch has no upstream, the compare view falls back to
working-tree-only. This affects only the compare/diff view; the Pull
Request and rebase merge target are unchanged.

* refactor(source-control): extract submodule status hook and entry-action gates

Moves the lazy submodule-expansion state into a useSourceControlSubmoduleStatus
hook and centralizes per-row stage/unstage/discard eligibility into
source-control-entry-actions, shrinking SourceControl.tsx and keeping the
read-only submodule rules consistent across the row UI, bulk actions, and tests.
The hook adds a generation guard so a slow submodule-status response from a
previous worktree (common over SSH) can't write stale status into the current
panel. On the relay side, configured submodule paths are read through a
short-TTL per-instance cache so a burst of diff clicks does not re-read
.gitmodules over the SSH link. Adds tests for the new modules.

* fix(source-control): address submodule/compare review feedback

- Degrade git.submoduleStatus to an actionable reconnect hint when an older
  SSH relay lacks the RPC, mirroring clone()/worktreeIsClean fallbacks.
- Keep the branch-compare summary while upstream status is still loading so
  it no longer flickers when switching worktrees with prefer-upstream on.
- Mark the compare-base switch as type="button" to avoid form submission.
- Add diff base / source control keywords to the Git settings search catalog.
- Assert the compare-base toggle's own switch state and updateSettings call.

* fix(source-control): address second-round submodule/compare review feedback

- Route submodule inner diffs through resolveSubmoduleWorktreePath so a
  crafted .gitmodules path can't escape the selected worktree
- Clear statusReadsInFlight alongside the diff dedupe on git mutations so a
  post-mutation getStatus() can't join a stale in-flight read
- Clear the SSH diff dedupe in getSubmoduleStatus to mirror getStatus
- Derive list-view selection from the submodule-injected rows so expanded
  submodule children are selectable
- Refresh commit history when the upstream compare base changes

* Support staged submodule expansion and refine default compare base

- Support expanding and diffing staged submodule changes (HEAD vs index) independently of unstaged changes (index vs worktree).
- Track submodule expansion states using a compound key of area and path to prevent conflicts between staged and unstaged listings.
- Update the compare-against-upstream setting to a segmented control for the "Default Compare Base" policy.
- Fall back to the repository default branch when comparing a branch with no upstream, preventing comparison views from unexpectedly disappearing.

* Fix submodule staging behavior, WSL caching, and double-click toggles

- Namespace submodule path cache per WSL distro to prevent cross-distro
  collisions.
- Preserve the staged area of child entries when expanding unstaged
  submodules so staged inner changes do not open empty diffs.
- Prefix oldPath with the submodule path for renamed inner entries.
- Ignore click events where detail > 1 to prevent double-clicks from
  instantly collapsing newly expanded submodules.

* Secure submodule path resolution and prevent stale status updates

* Extract and centralize submodule path validation into a new
  `resolveSubmoduleWorktreePath` helper to prevent path traversal
  exploits when resolving paths from untrusted `.gitmodules` files.
* Invalidate submodule expansion state and increment the query
  generation whenever the active runtime environment or connection
  route changes, preventing out-of-order responses from writing
  stale data.

* Set git identity via CLI config options in test commits

- Extract test email and name into constants.
- Use `-c` config flags to pass user identity to `git commit` dynamically.
- This ensures commits succeed in submodule checkouts or CI environments
  where a local or global identity is not configured.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-06-28 23:43:33 -07:00
42b2ecc5c6 feat(emulator): Android emulation via scrcpy (cross-platform, iOS parity) (#6434)
* docs: add Android emulation design spec

Adds the design for first-class Android emulator support as a cross-platform
peer of the iOS simulator feature: an extracted EmulatorBackend interface
(iOS + Android), full AVD lifecycle management via the Android SDK, a live
scrcpy H.264 pane decoded in-renderer with WebCodecs, the full control surface
(tap/gesture/type/buttons/rotate), accessibility tree, app install/launch,
runtime permissions, logcat, and a dedicated orca-emulator-android skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(emulator): add EmulatorBackend interface + backend/codec session tags

First step of multi-backend emulator support: introduce the EmulatorBackend
type and tag each session with its backend kind + stream codec, defaulting to
ios/mjpeg so existing serve-sim behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(emulator): extract IosEmulatorBackend and make the bridge a router

Move the serve-sim/simctl device + helper + input mechanics out of
EmulatorBridge into IosEmulatorBackend (implementing EmulatorBackend). The
bridge now owns the session registry and lifecycle orchestration and routes
each command to the backend that owns the target device. iOS behavior is
unchanged; the existing bridge tests pass untouched and the backend gains its
own input-op coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): add pure Android leaf modules (sdk/adb/avd/scrcpy/input/ax)

Dependency-injected building blocks for the Android emulator backend, each unit-
tested in isolation: SDK + tool discovery, adb device/output parsing, AVD list +
boot arg building, scrcpy control-socket byte encoders, normalized<->pixel +
keycode mapping, and a uiautomator XML accessibility-tree parser. Not yet wired;
AndroidEmulatorBackend composes these in the next phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(emulator): gate availability on the iOS backend + lock it with tests

inspectEmulatorAvailability now decides iOS host support via the registered iOS
backend instead of a bare platform literal, routing the decision through the
multi-backend seam. Output shape and all messages are unchanged (the settings
pane still reads simctl/serveSim). Adds the previously-missing regression tests
covering the unsupported, ready, no-devices, and tool-failure paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): add Android app/permissions/logcat arg builders

Pure adb arg-builders + a logcat line parser for app install/launch, runtime
permission grant/revoke/reset, and logcat capture. Unit-tested in isolation;
wired into AndroidEmulatorBackend's capability verbs in a later phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): AndroidEmulatorBackend device management + unified device list

Adds the Android backend (registered alongside iOS in the bridge): SDK-gated
host support, device/AVD discovery and merge, AVD boot + boot-completion wait,
shutdown, and tap/swipe/type/button/rotate/exec via `adb shell input` so control
works without the scrcpy server (the live H.264 stream lands in the streaming
phase). Surfaces everything through a new cross-platform `orca emulator devices`
command (RPC emulator.listDevices -> bridge.listAllDevices) with a platform
column. Device inventory is split into its own module to keep files focused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): Android capability verbs (install/launch/permissions/ax/logcat)

Wires the Android capability operations into AndroidEmulatorBackend and exposes
them through a capability-gated bridge router (runCapability), RPC, and CLI:
- orca emulator install/launch/permissions/ax/logcat
Capabilities are advertised per backend; calling one on a backend that lacks it
(e.g. iOS) fails with emulator_unsupported instead of a silent no-op. Input ops
and capability ops are split into focused modules to keep files under the
line cap; the runtime shares one target-param type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(skill): add orca-emulator-android skill + cross-ref from iOS skill

Documents the cross-platform Android emulator control surface (devices, input,
hardware buttons, rotate, install/launch, permissions, ax, logcat) driveable via
the orca CLI today, and notes the live visual pane is in development. Points the
iOS skill's "when not to use" at the new Android skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): Android live-pane streaming scaffolding (scrcpy + WebCodecs)

Builds the H.264 video path as scaffolding: scrcpy frame/codec-meta parsing,
server-deploy arg builders, control-protocol encoders (committed earlier), the
stream session (server + sockets), a video pub/sub registry, the
emulator:videoStream* IPC channel, and a renderer WebCodecs->canvas hook. Pure
framing/deploy/registry are unit-tested; the socket/WebCodecs/jar integration is
clearly flagged UNVERIFIED and the remaining wiring (startSession, preload,
pane codec branch, packaging the jar) is documented in
docs/android-emulation-streaming.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fix streaming notes doc path in video-stream hook comment

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(emulator): add diagnostic probes for Android testing

Adds an emulator-probe logger (console + temp file at
os.tmpdir()/orca-android-emu-probe.log) and wires probes at four layers so
errors surface during manual testing: every emulator.* RPC call + error (RPC
dispatcher), every adb/emulator command + non-zero exit (command runner), and
the scrcpy session + video-stream IPC lifecycle. Temporary diagnostics; remove
or gate behind a flag once the Android pane is validated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): show Mobile Emulator settings cross-platform + aggregate Android availability

The Mobile Emulator settings section is no longer macOS-gated (Android works on
Windows/Linux), and inspectEmulatorAvailability now aggregates the iOS and
Android backends: Android devices/AVDs appear in the device list and a host
without iOS gets the Android setup message instead of "requires macOS".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): show Mobile Emulator sidebar nav entry on non-mac desktops

The settings sidebar nav registered the Mobile Emulator entry behind isMac, so
it stayed hidden on Windows/Linux even after the section content was ungated.
Widen it to showDesktopOnlySettings to match the section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): wire Android startSession to scrcpy + client-downloaded jar

AndroidEmulatorBackend.startSession now boots the device, ensures the scrcpy
server jar (downloaded by the client into the per-user cache on first use, not
bundled), starts a ScrcpyStreamSession, and feeds its H.264 frames to the video
registry; stopHelperForDevice tears it down. Sessions carry their backend kind
so worktree-active routing picks the right backend. Boot, host SDK discovery,
and the stream starter are split into focused modules to stay under the line cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): stop the iOS backend from claiming Android devices off-mac

iOS ownsDevice now returns false unless the host supports it, so on Windows an
Android serial routes to the Android backend instead of erroring with
"requires macOS". Backend-for-device fallback prefers a host-supported backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): scrcpy scid 31-bit + retry video socket until server delivers

Two fixes validated against a real emulator: scrcpy parses scid as a signed
32-bit hex int, so mask to 31 bits + pad to 8 digits (8-byte values overflowed
and the server exited). And adb accepts the forwarded TCP connection before the
server's abstract socket exists then resets it, so retry the video socket until
it actually delivers the dummy byte before connecting control. H.264 meta now
arrives (576x1280). Adds socket/server-exit diagnostics probes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): render the Android H.264 pane via WebCodecs

Wires the live Android pane end-to-end: preload exposes emulator video stream
APIs; the pane's device list uses the unified emulator.listDevices (Android +
iOS); and emulator-screen-stream-content renders a WebCodecs <canvas> for
scrcpy:// sessions (H.264, SPS/PPS prepended to the first keyframe) instead of
the MJPEG <img>. The video hook reports the stream size for the device frame.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): buffer the current GOP for late video subscribers

The renderer subscribes after attach already started the scrcpy stream, so the
registry now caches the current GOP (keyframe + following deltas) alongside the
codec meta and config, and replays it on subscribe. A pane opened mid-stream
decodes from the keyframe immediately instead of showing black until scrcpy's
next periodic keyframe (~10s). Refreshes the now-validated session doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): show New Mobile Emulator tab action off macOS

The tab create menu and its dropdown item gated the New Mobile Emulator action
on isMacOs, hiding it on Windows/Linux where Android emulation is now supported.
Gate on mobileEmulatorEnabled + onNewSimulatorTab (already cross-platform) so
the action appears wherever a mobile emulator backend is available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): open the Mobile Emulator tab off macOS

openMobileEmulatorTab and ensureSimulatorTab both returned null unless the host
was macOS, so the New Mobile Emulator action no-opped on Windows/Linux even
though the menu entry showed. Drop the isMacOsHost early-returns; the
mobileEmulatorEnabled setting and backend availability already gate the feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): resolve a default attach device across backends

emulatorAttach with no device fell back only to the iOS listSimulators picker
(empty on Windows/Linux), so the pane's no-device launch flow errored. Extract
resolveDefaultAttachDevice: iOS default first, else the first booted (else
first) device across host backends, so Android attaches without an explicit
device. Split into its own module to stay under the line cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): render the pane off macOS instead of an unavailable wall

EmulatorPane short-circuited to the "macOS only" EmulatorUnavailablePane on any
non-Mac host, blocking the now-working Android pane. Always render the pane
content; its device discovery and error surface handle a missing backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): let attach boot a shut-down AVD with a stale active session

getReusableActiveForWorktree called resolveDeviceId on the requested device,
which throws for a not-yet-booted Android AVD, aborting the attach. Guard it so
a resolve failure means "not the active device" and the attach falls through to
a fresh boot — so picking a shut-down AVD in the pane and hitting Connect boots
it via ensureBooted instead of erroring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): launch the AVD detached instead of via execFile

bootAndroidDevice started the emulator through the command runner (execFile with
a timeout + 1MB stdout maxBuffer), which kills the long-running, verbose emulator
process — so booting an AVD from the pane never actually came up. Spawn it
detached with no stdio and unref it so it outlives the call, mirroring how the
scrcpy server is launched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): dedupe concurrent attaches into one scrcpy stream

Extract AndroidStreamController to own the per-serial scrcpy lifecycle and
dedupe starts: concurrent attaches (e.g. the pane's auto-attach racing the tab
launch) now share one in-flight start and reuse the live stream instead of
spawning a second scrcpy server that fights for the port and kills the first.
Also initialize the registry GOP buffer in register() (latent type error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): boot the AVD headless without a console window

The detached spawn opened a Windows console (showing the emulator's verbose
qemu/netsim logs) and a redundant native emulator window. Pass windowsHide and
run the emulator with -no-window so it boots headless — the scrcpy pane is the
view, matching how iOS hides Simulator.app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): boot the AVD with a hidden console, not detached

detached: true sets DETACHED_PROCESS, which gives the console-subsystem emulator
no console — so it and its qemu/netsim children pop their own visible cmd window
that windowsHide can't suppress. Drop detached and rely on windowsHide
(CREATE_NO_WINDOW = hidden console) + unref; spawn already keeps it alive past
the launch call, and managed emulators are shut down on app quit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): keep Android emulators alive when switching devices

Attaching a different device shut down the active one (shutdownDevice: true),
which for Android meant killing the running emulator and cold-booting the target
(~60s) on every switch — and switching back. Add bridge.stopActiveForSwitch:
Android emulators stay running for instant switch-back, while iOS simulators are
still replaced. Switching to an already-running emulator is now immediate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(emulator): only resize the video canvas when dimensions change

The decoder output handler set canvas.width/height on every frame, which
reallocates the canvas backing store and forces an object-contain reflow each
frame — a needless per-frame cost. Resize only when the frame dimensions
actually change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): genericize copy + add Android Studio setup link

Replace iOS-only wording (Xcode/Simulator/iPhone) in the pane and settings with
backend-neutral copy so Android reads correctly on every platform. When no
emulator is available, the Mobile Emulator settings now show a "Download Android
Studio" link plus setup guidance (ANDROID_HOME / default install path). Removes
the now-unused, macOS-only EmulatorUnavailablePane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): show emulator SDK status in settings

The backend availability now reports the resolved Android SDK path, aggregated
into emulator.availability as an `android` block. The Mobile Emulator settings
render an "Emulator SDKs" card showing Android SDK (detected at <path> / not
found, with a Download Android Studio link) and, on macOS, iOS Simulator (Xcode)
status — mirroring the agent-control card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(emulator): locate a custom Android SDK folder from settings

Add an androidSdkPath setting and a "Locate SDK folder…" / Clear action in the
emulator SDK status card. The path is applied as the highest-priority discovery
candidate (falls back if invalid), and the backend's SDK is re-resolved on use
via a new AndroidSdkState — so locating or installing the SDK takes effect on
Refresh without restarting Orca. Guards the status card against older runtimes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): settle the scrcpy video socket once to stop retry storms

A failed TCP connect emits both 'error' and 'close', so retry ran twice and
scheduled openVideoSocket(attempt+1) twice — fanning out into an exponential
connection storm while waiting for the server to start listening. A runaway
chain could then hit attempt 100 and fail/close a stream that had already
connected. Replace the delivered flag with a single settled latch so each
socket retries (or delivers) exactly once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): address CodeRabbit review findings

- avd-boot: handle spawn 'error' (an unhandled ChildProcess error crashed the
  main process); validate the target is a known AVD before launching.
- capability-ops: propagate adb non-zero exits for launch/permission/logcat and
  check the uiautomator dump before reading (avoids stale XML).
- scrcpy-video-registry: actually replay the buffered GOP on subscribe so late
  subscribers decode immediately.
- android-sdk-state: re-resolve host discovery every call so a changed SDK path
  takes effect live (no restart).
- android-sdk-discovery: require both adb and the emulator binary.
- emulator-bridge: fall back to the platform-primary backend (Android off-mac)
  so setup errors aren't iOS/CoreSimulator on Windows/Linux.
- scrcpy-server-download: dedupe concurrent first-use downloads + add a timeout.
- scrcpy-stream-session: idle-socket connect timeout; surface control-socket
  errors instead of swallowing them.
- android-exec: pass the whole command so the device shell parses quotes/pipes.
- avd-manager: match emulator log prefixes exactly (keep AVD names like
  PixelWARNINGTest).
- permissions: `pm reset-permissions` is global and takes no package argument.
- stream controller/starter: drop stale handles for dead streams; idempotent
  teardown. use-emulator-video-stream: stopVideoStream returns Promise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(emulator): populate the GOP buffer and reuse live scrcpy streams

Self-audit follow-ups in the same class as the CodeRabbit GOP-replay finding:

- scrcpy-video-registry: pushFrame never wrote to entry.gop, so the replay
  loop added for late subscribers iterated an empty array — a no-op. Build the
  GOP on ingest (start at each keyframe, append following deltas; don't buffer
  deltas before the first keyframe). Adds tests for population, reset, and the
  pre-keyframe guard.
- android backend: isSessionReusable was stubbed to always return false with a
  "no persistent stream yet" note, but scrcpy streams are persistent now — so
  every renderer remount tore down and respawned the server. Reuse a live
  stream (scrcpyVideoRegistry.has) so remounts reconnect, matching iOS. The
  device-mismatch check still runs first, so device switching is unaffected.
- Refresh stale comments that implied unfinished/unverified work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* WIP: Changes before auto-review fixes

Co-authored-by: Orca <help@stably.ai>

* Refine mobile emulator availability settings

Co-authored-by: Orca <help@stably.ai>

* Address emulator review follow-ups

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-28 21:01:09 -07:00
Jinjing b6c0cb4f62 Migrate terminal scrollback setting from bytes to rows (#6593)
* Migrate terminal scrollback setting from bytes to rows

Transition terminal scrollback configuration from a byte-based (MB)
limit to a row-based count to align with standard terminal emulator
behavior.

- Introduce a shared scrollback policy to handle normalization and
  safe migration of legacy byte presets to row equivalents.
- Update persistence logic to automatically convert and clean up legacy
  settings on startup and write-back.
- Refactor the advanced settings interface and localization strings
  to let users configure scrollback in terms of rows (up to 50k).
- Live-apply row limit changes directly to mounted xterm instances
  without recreating panes or restarting PTY sessions.

* Migrate terminal scrollback setting from bytes to rows

Transition terminal scrollback configuration from a byte-based (MB)
limit to a row-based count to align with standard terminal emulator
behavior.

- Introduce a shared scrollback policy to handle normalization and
  safe migration of legacy byte presets to row equivalents.
- Update persistence logic to automatically convert and clean up legacy
  settings on startup and write-back.
- Refactor the advanced settings interface and localization strings
  to let users configure scrollback in terms of rows (up to 50k).
- Live-apply row limit changes directly to mounted xterm instances
  without recreating panes or restarting PTY sessions.

* defer updating terminal scrollback rows until blur or Enter

Avoid committing terminal scrollback row setting changes on every
keystroke, which can trigger rapid updates with incomplete or
invalid numbers. Instead, manage a local draft state and commit to
settings only when the input is blurred or the user presses Enter.
2026-06-28 16:13:16 -07:00
b916248294 Polish desktop native chat view (#6641)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-06-28 15:43:07 -07:00
Brennan BensonandOrca a278a30b1c Let agents wait for setup when requested (#6298)
Co-authored-by: Orca <help@stably.ai>
2026-06-28 13:50:00 -07:00
Jinwoo HongandOrca 0be512fc23 Add diff word wrap toggle (#6214)
Co-authored-by: Orca <help@stably.ai>
2026-06-24 19:26:50 -07:00
Neil c2353fd98c Tune terminal scroll defaults (#6068)
* Tune terminal scroll defaults

* Align TUI mouse wheel scrolling

* Add terminal scroll speed controls
2026-06-22 17:30:50 -07:00
067c88fe55 feat(source-control): make group order configurable (#2188)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: brennanb2025 <brennankbenson@gmail.com>
2026-06-19 18:50:05 -07:00
Brennan BensonandOrca c442f3aa00 Keep new workspace cards consistent after compact upgrades (#5843)
Co-authored-by: Orca <help@stably.ai>
2026-06-19 15:34:46 -07:00
Janderson Fagner e7af3676f2 feat(tray): Windows system tray with minimize-to-tray on close
Adds Windows system tray support and a minimize-to-tray setting for close behavior.
2026-06-18 22:30:44 -07:00
Brennan Benson a34801934d Keep automation off by default in compact workspace cards (#5740) 2026-06-18 16:28:37 -07:00
Brennan BensonandOrca 674639205c Show and Filter Automation-Created Workspaces (#5697)
Co-authored-by: Orca <help@stably.ai>
2026-06-18 13:05:57 -07:00
AJ becaea5734 feat(tabs): confirm before closing pinned tabs (#5536)
Add a configurable confirmation before pinned tabs are closed, route terminal/browser/CLI close paths through the guard, and add a one-shot Don't ask again option backed by the existing setting.

Verified with focused unit tests, typecheck, lint, Electron validation, and the PR verify workflow.
2026-06-17 18:27:01 -07:00
Brennan Benson 3eb0dbdd55 Sync Linear statuses from the workspace board (#5430) 2026-06-17 16:53:18 -07:00
0ec3882cb8 Add project Windows runtime selection (#5519)
* Add project Windows runtime selection

* Fix project Windows runtime selection

Co-authored-by: Orca <help@stably.ai>

* fix: preserve WSL shell variables

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Neil <neil@stably.ai>
2026-06-17 16:08:14 -07:00
Jinwoo HongandOrca 25d2053b25 Add Codex reset credit controls (#5631)
Co-authored-by: Orca <help@stably.ai>
2026-06-17 15:41:43 -07:00
Brennan BensonandOrca 167257a99b Simplify sidebar worktree card modes (#5470)
Co-authored-by: Orca <help@stably.ai>
2026-06-17 14:07:36 -07:00
Jinjing 62fbb51eb2 Refine terminal close confirmation (#5541)
* Refine terminal close confirmation

* Translate terminal close dialog strings
2026-06-16 17:42:48 -07:00
OrcaWinandNeil 68abadba81 Add Windows terminal onboarding step (#5488)
Co-authored-by: Neil <neil@stably.ai>
2026-06-16 14:07:26 -07:00
Jinjing ffd340c264 Add setup guide and intro callout for mobile emulator agent control (#5511)
Provides an optional, inline setup walkthrough within the emulator pane
and an introductory tab callout to guide users through registering the
Orca CLI and installing the necessary skill for agent control.

- Adds inline setup guide components, visibility hooks, and unit tests
- Introduces tab intro callout on macOS with Keep/Hide actions
- Displays a persistent toast when the emulator pane is hidden
- Ensures settings and tabs remain synced on user preference changes
2026-06-16 10:15:33 -07:00
Jinjing 996284241d Implement resizable Markdown table of contents panel (#5444)
* Implement resizable Markdown table of contents panel

Introduce resizable behavior to the Markdown table of contents panel,
allowing users to adjust its width via a draggable handle.

- Persist the custom panel width in the UI state and main process store.
- Clamp the width to a safe range (200px to 600px) based on layout space.
- Ensure the remaining editor workspace does not shrink below 320px
  by computing constraints dynamically against the parent container width.

* Clone process env without undefined values for Windows preflight

- Filter out undefined values when cloning process.env to prevent
  potential serialization or typing issues.
- Update buildLocalPreflightEnv return type to Record<string, string>.
2026-06-15 19:59:44 -07:00
Brennan Benson 04f2121ee9 Add experimental agent hibernation (#5347) 2026-06-15 14:36:10 -07:00
Brennan BensonandOrca 4880b56998 Show attached worktrees in folder workspace sidebar (#5289)
Co-authored-by: Orca <help@stably.ai>
2026-06-15 12:48:08 -07:00