Commit Graph
3866 Commits
Author SHA1 Message Date
OrcaWin 9e2c63ec7c fix(mobile): detect and repair overriding Windows Firewall Block rules for pairing (#8846)
* fix(mobile): detect and repair overriding Windows Firewall Block rules for pairing

Firewall inspection now reports an overriding inbound Block rule as
blocked instead of false success, the UAC repair removes only
conflicting rules for the current Orca executable, TCP pairing port,
and Private profile before recreating the scoped allow rule, and the
notice re-inspects Windows policy after repair instead of optimistically
reporting success. Stale focus-triggered inspections can no longer
overwrite a newer result during UAC elevation.

Fixes #8371

* fix(mobile): inspect the ActiveStore so GPO firewall rules are visible

Without -PolicyStore ActiveStore the NetSecurity queries read only the
local persistent store, so a GPO-applied Block rule was invisible and
the post-repair re-inspection could report a false success on managed
hosts.
2026-07-15 01:40:41 -07:00
OrcaWin c13441123a fix(terminal): filter stale Windows Codex focus reports after turns (#8842)
* fix(terminal): reset Windows focus reporting after agent turns

* fix(terminal): preserve focus mode across Windows Codex turns
2026-07-15 01:28:25 -07:00
JinjingandOrca bf0b65a049 feat(editor): add Toggle Heading 2, 3, 4 to markdown editor (#8822)
* feat(editor): add Toggle Heading 2, 3, 4 to markdown editor

Generalize the heading-1-only collapsible-toggle plumbing to support
heading levels 1-4, mirroring the plain heading scales the slash menu and
toolbar dropdown already offer.

- Introduce ToggleHeadingVariant type + parseToggleHeadingVariant helper
- Widen markdown parse/render/sanitize allowlists from heading-1 to 1-4
- Add Toggle Heading 2/3/4 slash commands
- Add matching summary sizing in editor + preview CSS
- Extend round-trip tests for the new levels

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

* fix(editor): preserve toggle heading variants

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-14 23:14:22 -07:00
Jinwoo HongandOrca f7926c11f1 fix(terminal): keep legacy daemon PTYs mounted (#8817)
Co-authored-by: Orca <help@stably.ai>
2026-07-14 22:53:33 -07:00
Jinwoo HongandOrca 1c275336ed chore(mobile): prepare 0.0.31 releases (#8820)
Co-authored-by: Orca <help@stably.ai>
2026-07-14 22:51:02 -07:00
Jinjing 2519e51b8e Fix markup draw hint to use dismissable popover and respect surface visi (#8812)
- Replace the force-open Tooltip discovery hint with a Popover (title,
  body, Try it/Got it actions) that can be dismissed via outside
  click, Escape, or window blur — Radix's tooltip couldn't handle
  Electron webview clicks, which never reach the renderer document.
- Gate the hint on a new `surfaceActive` prop so it only opens for the
  visible pane, preventing a portaled layer from anchoring against a
  hidden/zero-size trigger at the viewport origin.
- Add locale strings for the new badge/try/dismiss copy across all
  supported languages and cover the hook's eligibility/dismiss
  behavior with unit tests.
2026-07-14 22:05:34 -07:00
70e16cb900 fix: stop renderer heap leak from unhandled terminal teardown rejections (#8279)
Two renderer paths let Promise rejections escape unhandled, where the
crash-breadcrumb system retains them and grows the heap until the renderer
white-screens (#8260):

- The terminal file-link provider fires a `void Promise.all(...)` whose async
  mappers probe path existence over SSH. On disconnect these reject with
  "Remote connection dropped/reconnecting"; add a `.catch` that resolves the
  callback with no links so the rejection is handled.
- "ResizeObserver loop completed" is a benign Chromium quirk; suppress it in
  crash-diagnostics so it stops filling the breadcrumb buffer and inflating
  the renderer error count.

Scope note: the original fix for #8260 also proposed Windows pty:kill signal
handling (killPtyProcess / isPtyAlreadyGoneError) and a ResizeObserver
double-rAF. Those landed on main independently (Windows kill handling by
v1.4.135; the pane-title-overlay rect-equality guard from #2756 already breaks
the loop at the source), so this change carries only the two guards still
missing upstream.

Co-authored-by: Yixian HU <yixian@YixiandeMacBook-Air.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-14 19:19:43 -07:00
Brennan Benson 33a8df93fc fix(speech): resume interrupted voice model downloads and surface real errors (#8775)
* fix(speech): resume interrupted voice model downloads and surface real errors

Voice model downloads that died late in the transfer (flaky networks,
scanning proxies) failed at the UI's 90% progress cap and restarted from
byte zero on every retry, so affected users could never complete any
model download (#8620 / STA-1775).

- Retry transient download failures (net::ERR_* transfer cuts, resets,
  timeouts, HTTP 5xx/429) with an HTTP Range resume from the bytes
  already on disk, restarting from the canonical URL each attempt since
  signed CDN redirect URLs expire; bounded backoff, gives up after
  repeated zero-progress attempts with a diagnosable error.
- Correct catalog sizeBytes to exact upstream asset sizes (parakeet
  v2/v3 are 482/487 MB, not 170/180; paraformer is 1,047 MB, not 115) —
  also the progress denominator when content-length is missing.
- Show the underlying failure cause in the download-error toast and log
  it in main; it was previously invisible everywhere.

All 7 pinned catalog SHA-256 hashes verified against current upstream
assets. Live-verified end to end: a transfer killed mid-body resumes
with a 206 and completes through checksum, extraction, and validation.

Fixes #8620

* fix(speech): harden resumed model downloads

* fix(speech): validate resumed download completion

* fix(speech): keep segmented download resumes progressing

* fix(speech): don't abandon a download that keeps making progress

The retry loop gave up after 8 total failures regardless of progress, so
a large model (e.g. the ~1GB Paraformer) over a network that resets every
few hundred MB was abandoned mid-download even though every attempt was
resuming and advancing — the exact 'stuck then fails' symptom, just later.

Replace the all-time failure budget and the separate advancing-segment
cap with a single rule: give up only on a genuine stall (repeated
attempts with no forward progress) or an absolute request ceiling that
bounds a pathological tiny-segment server. Any download that keeps
advancing now runs to completion.

Verified live under Electron 43: a transfer reset mid-stream on every
attempt but advancing each time previously gave up at 67% after 8
attempts; it now completes. All prior resume/cancel/timeout scenarios
still pass.
2026-07-14 17:49:13 -07:00
ishihara takashiandJinjing 6ee2af357b Add an in-place screenshot markup tool to the browser pane (#6335)
* Add in-place screenshot markup for the browser pane

Adds a Draw button to the browser toolbar (local and remote/SSH panes) that
freezes the visible viewport into a still image and overlays a drawing canvas.
Users can mark it up with pen, highlighter, arrow, rectangle, ellipse, and text
(color, thickness, undo/redo), then copy the composited PNG to the clipboard to
paste into their agent — reusing the existing clipboard screenshot path, so it
works for local and remote agents alike.

The drawing layer is a renderer-side canvas, so the base image is the only
environment-specific piece: local panes use webview.capturePage(), remote panes
snapshot the already-displayed screencast frame. Drawing model, compositing, and
shape rendering are isolated, unit-tested modules.

* Fix browser markup clipboard, transparency, text input, and toggle

- Copy now always produces a PNG (the clipboard handler accepts PNG only;
  a JPEG fallback silently produced an empty clipboard) and targets the
  clipboard size limit so realistic viewports stay full-resolution.
- Flatten the captured base image onto opaque white so transparent page
  backgrounds no longer ghost the live view through the frozen backdrop.
- Keep markup text-input keystrokes local (focus the field explicitly and
  stop propagation) so the browser pane's global handlers can't swallow them.
- Make the Draw toolbar button toggle markup mode off on a second click.

* Add markup text sizing, object re-editing, and text-input fixes

- Add a font-size control (text tool) to the markup toolbar.
- Add a Select tool to re-edit committed shapes: click to select, drag to
  move, change color/width/font-size of the selection, Delete to remove, and
  double-click text to re-edit its content. Undo/redo now snapshots the whole
  shape list so every edit (not just adding) is reversible.
- Fix the text input: transparent ink-colored field (no dark theme box over the
  screenshot) and ignore Enter during IME composition so Japanese conversion no
  longer commits the annotation early.
- Split the grown overlay logic into focused modules (editor hook, pointer +
  keyboard hooks, canvas renderer, document edits, hit-testing) to stay within
  the file-size budget.

* Improve markup text commit and move toolbar to the bottom

- Clicking elsewhere while a text box is open now commits the text and
  switches to the Select tool, instead of discarding it (the input
  unmounted before its blur fired) and opening a new box at the click.
- Move the drawing toolbar to the bottom (stacked above the actions bar)
  and open the color/size popover upward.

* Fix markup text re-editing alignment, doubling, and selection bounds

- Hide the text shape being re-edited from the canvas so the live input
  isn't doubled by its committed render.
- Drop the input's padding/border and use the same font as the canvas so
  the typing preview sits exactly where the text renders (no shift);
  field-sizing keeps the box hugging the text.
- Estimate text width with full-width (CJK) awareness so the selection box
  and hit bounds track Japanese text instead of clipping it.
- Move the font-size control out of the color popover into its own toolbar
  button showing the current size.

* Unify markup text selection and edit framing

Draw the selection box on the canvas even while a text shape is being
re-edited (the shape stays hidden so it isn't doubled), and strip the box
off the text input so it renders only the editable glyphs. The frame is now
identical whether selecting or editing, and the text overlays the same
position with the same font, so there's no shift between the two.

* Align markup text-edit baseline with the committed render

Force the text input's line-height to 1 and zero its height/padding so its
top edge matches the canvas textBaseline:'top' render — the editing text no
longer sits slightly lower than the committed/selected text.

* Address review: markup hit-testing, clear reset, and async guards

- Hit-test highlights against their rendered (4x) thickness so the select
  tool doesn't miss large highlight strokes.
- Reset transient editor state (pending text, selection, in-progress drag) on
  Clear all so a pending input blur can't re-add text and no stale selection
  lingers over the emptied canvas.
- Guard markup completion with the capture token so a stale onDeliver can't
  reset or error a session the user already cancelled or restarted.
- Skip the grab-element keyboard shortcut while the markup overlay is open,
  matching the already-disabled grab toolbar buttons.
- Localize the markup error messages and surface them via a toast (the
  controller's error was otherwise never shown).

* Trim browser markup to a draw-only tool and fix two delivery bugs

Reshape the screenshot-markup overlay to match its actual use — a throwaway
scribble the user copies once into their agent — and drop the re-editable
vector-editor machinery that made it an outlier in scope.

- Remove the select/move/restyle/re-edit subsystem: hit-testing, per-shape
  document edits, drag preview, selection frame, double-click text re-edit,
  Delete-to-remove, and the CJK text-width heuristic / multi-line text plumbing
  that only fed selection bounds. Keep pen, highlighter, arrow, rect, ellipse,
  and text with color/thickness/font-size and undo/redo/clear.
- Fix silent data loss on HiDPI: the compositor budgeted only bytes, so a large
  Retina viewport could produce a PNG under the byte limit but over the
  clipboard handler's pixel ceiling, which the handler drops silently while the
  UI still reported success. Composite now scales down to fit maxPixels too.
- Fix a stranded UI after a capture failure: the controller stayed "active" with
  no overlay and an inert Escape. It now returns to idle (capture failure) or
  back to the drawing surface (compose failure) after the error toast.

Net ~660 fewer lines and 3 fewer modules; delivery (freeze → composite PNG →
existing clipboard-image paste) is unchanged. Unit tests updated.

* Add a one-time discovery hint for the browser Draw button

Highlight the new screenshot-markup Draw button the first time it's usable so
users — including existing ones — notice the tool. A localStorage-gated,
show-once tooltip + highlight ring, independent of the browser contextual tour
(which is capped at three steps and only shows to users who haven't seen it).

- New use-markup-draw-hint hook: opens once per install when the button is
  enabled and idle, auto-dismisses, and dismisses on click; skips cleanly when
  storage is unavailable.
- MarkupDrawButton forces its tooltip open with the discovery copy and rings the
  button while the hint shows.
- Localize the hint (drawHint) across en/es/ja/ko/zh; normalize the markup key
  order alphabetically.

* Align the markup copy button label with "Copy Screenshot"

The browser pane's two image-copy actions read inconsistently in English —
"Copy Screenshot" (the grab flow) vs "Copy markup" (the overlay). Match the new
markup button to the existing one: "Copy Markup". English only; other locales
already use their own sentence-case convention consistently, so they're
unchanged.

* Drop the markup Draw hint auto-timeout; dismiss on act only

The one-time Draw-button hint no longer auto-dismisses on a timer the user might
miss. It now stays open until the user clicks Draw or the button stops being
usable (grab started, markup open, or a blank tab), which also fixes a case
where losing eligibility mid-hint left the forced-open tooltip stuck over a
disabled button.

* Optimize markup rendering and harden compose delivery

Performance:
- Rasterize committed shapes once into an offscreen layer; per-frame paint blits
  that layer and draws only the in-progress shape, so a fast pointermove stream
  never re-strokes the whole scene.
- Encode the composite PNG off the main thread via canvas.toBlob (a large
  synchronous toDataURL froze the renderer), with a rAF yield so the "composing"
  UI paints first.

Correctness / review fixes:
- Don't overwrite the clipboard after cancel: re-check the capture token between
  the (now async) compose and onDeliver, closing the window where Escape during
  composing still delivered the markup.
- Keep the offscreen layer and visible canvas in lockstep by threading one
  measured devicePixelRatio through both paints, and re-measure on window resize
  so a monitor move (dpr change without a CSS-box change) repaints at the new
  scale. Blit stretched to the canvas box so a transient mismatch never clips.
- Drop the stale "or JPEG fallback" note; compose is PNG-only.

* Fix test fixtures for markup pixel-budget clamping to use a truly oversi

The old 3840×2160×dpr2 fixture (33.2M px) was already under the 32×1024×1024
(~33.55M) ceiling, so the "never over budget" assertions weren't exercising
the clamp path. Switch to 4000×3000×dpr2 (48M px), which genuinely exceeds
the ceiling.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-14 17:25:10 -07:00
Brennan Benson a4cfc82d69 Surface Grok unified-billing monthly usage instead of a permanent warning (#8769)
* Surface Grok unified-billing monthly usage instead of a permanent warning

Unified-billing Grok accounts have no weekly credits: the
/billing?format=credits view returns a config without
creditUsagePercent, so the status bar was stuck on 'Grok billing
response did not include credit usage' even though the account has a
real quota. The default (format-less) /billing view reports it as an
included monthly budget (monthlyLimit/used with the billing period).

When the credits view has no weekly credit usage, read the default view
and surface monthly usage as the provider's 30-day window (already
supported by the tooltip and chip visibility for OpenCode Go). If the
fallback read fails, the previous 'unavailable' presentation stands
rather than escalating to an error chip.

* Review fixes: chip renders monthly-only usage; fallback failures keep stale data

- StatusBar ProviderSegment: monthly window is chip-visible when it is the
  sole window (Grok unified billing); fetching/error no-data guards and the
  icon-only dot now count monthly, matching tooltip.tsx. OpenCode Go chips
  are unchanged (monthly stays tooltip-only next to session/weekly).
- grok-fetcher: monthly-fallback request failures propagate as 'error' so
  applyStalePolicy keeps the last good monthly snapshot; 'unavailable' is
  reserved for a successful response without monthly fields.

* Settings: show Grok monthly usage row for unified-billing accounts

Why: the Grok accounts section only rendered the weekly-credits row, so
unified-billing accounts showed a signed-in state with no usage at all.

* Use generated localization keys for Grok monthly copy
2026-07-14 17:02:06 -07:00
Brennan Benson dfabd85513 Stop usage chips flashing and back off retries for failing providers (#8772)
* Stop usage chips flashing and back off retries for failing providers

Two behaviors made the status bar unusable when any provider was
persistently failing (bad auth, unsupported plan):

1. Every refetch repainted all providers as 'fetching', so a settled
   error chip flashed to a loading "…" chip and back on every cycle.
   withFetchingStatus now keeps settled states (ok/error/unavailable)
   visible until the new result lands; only providers with no settled
   state (first load, explicit account-switch clear) show loading.

2. Error providers on the fast activation-retry lane (claude/codex/
   grok) were retried every 30s on any focus/show/restore event —
   forever. Repeated hits drove Claude's tight-budget usage endpoint
   into 429s, flipping the chip between 'Limited' and its actual error.
   Retries now back off exponentially per consecutive applied failure
   (30s, 60s, 120s, … capped at the 15-minute poll cadence) and reset
   on success or account/target switch.

* Count full fetches as failure-lane retries and keep Grok pane refresh feedback

- Stamp failing providers' activation-retry clocks when a stale-driven full
  fetch runs, so the individual failure lane does not fire a redundant retry
  right after the full fetch already retried them.
- GrokUsagePane: manual refresh spinner/disable is now renderer-local, since
  settled snapshots no longer repaint as 'fetching' during refetches.
- Strengthen the backoff-reset test so it distinguishes a reset streak from a
  stale retry timestamp (CodeRabbit), and add a regression test for the
  full-fetch retry stamping.
2026-07-14 16:35:31 -07:00
Brennan Benson ba5fa7d909 Classify Codex app-server chatgpt-auth-required as an auth error (#8765)
* Classify Codex app-server chatgpt-auth-required as an auth error

When auth.json holds only an OPENAI_API_KEY (no ChatGPT tokens), the
app-server RPC rejects account/rateLimits/read with "chatgpt
authentication required to read rate limits". That string matched none
of CODEX_AUTH_ERROR_PATTERNS, so fetchCodexRateLimits fell through to
the hidden PTY /status probe, which cannot render usage for such
accounts and burned the full 15s PTY timeout on every refresh cycle,
surfacing as a permanent "Refresh failed — PTY timeout" status chip.

Classify the message as an auth error so the RPC result is returned
directly (fast, accurate) and no PTY is spawned.

* Keep auth-required usage errors from rendering as a rate-limit Limited label

The new Codex app-server error 'chatgpt authentication required to read
rate limits' mentions rate limits only as the object it failed to read,
but the status bar's rate-limit classifier matched the phrase and
labeled the chip 'Limited'. Classify authentication-required messages as
auth failures so they get the standard softened refresh copy instead.
2026-07-14 15:28:04 -07:00
Brennan Benson a5cea59933 fix(settings): render Projects settings per-project across hosts (#8566) (#8650)
* fix(settings): collapse Projects list to one row + pane per project (#8566)

Settings and its nav enumerated repo rows, so a project set up on multiple
execution hosts (local + a Remote Orca Server, or the same repo cloned on two
machines) rendered two nav rows + two panes that collided (duplicate React
key/DOM id) or mirrored. Derive the Settings list from the project projection
(repos-only, deterministic) so nav, panes, and the Cmd+J palette all collapse
to one entry per project. Deep-link repoId targets resolve to the project's
representative section.

* fix(settings): switch project host in place, host-scoped edits + deep links (#8566)

Add an ephemeral per-project host selection driven by the pane's "Available
Hosts" switcher, so the single collapsed pane shows the selected host's setup
(path, worktree base, runtime, fork-sync, hooks, source-control AI). Route
edits to the selected host by threading an optional hostId through updateRepo
(mirrors removeProject's host routing), fixing the same-id/self-pair case where
a local + runtime share one repo id. Couple Settings deep links to the switcher
so host-specific subsection anchors resolve, load hooks for the selected host,
and make pane-level Remove Project remove every host setup.

* fix(settings): isolate selected project host state

* fix(settings): review follow-ups for per-project Projects pane (#8566)

- Remove Project copy now states it removes the project on all
  configured hosts (the button removes every host setup); new catalog
  key translated across all 5 locales
- ensureHooksConfirmed forwards hostId to readRuntimeIssueCommand so
  the issue-command trust read resolves duplicate repo ids the same way
  as its sibling checkRuntimeHooks call
- translate the multi-host "N hosts" nav description
- extract removeSettingsProjectFromAllHosts with unit tests; drop the
  now-unused getRuntimeTargetIdentity
2026-07-14 15:00:52 -07:00
OrcaWinandBrennan Benson 62c644a8bc fix(terminal): prevent Windows multiline paste submission (#8688)
* fix(terminal): prevent Windows multiline paste submission

* fix(terminal): normalize chunked forced-paste line endings

Windows multiline pastes over TERMINAL_PASTE_DIRECT_MAX_BYTES (64 KiB)
take the chunked plan and stream plainText straight to the PTY, skipping
wrapTerminalBracketedPasteText — so raw CRLF/LF still reached ConPTY and
Codex treated the LF as submit, the exact bug the direct path just fixed.

Wire the plan's newlinePolicy field: forced bracketed plans get
'terminal-cr' and the chunk iterator normalizes the full text before
chunking (a per-chunk pass could split a CRLF across a boundary and leak
the LF half). Non-forced chunked pastes keep their documented
preserve-newlines behavior, so macOS/Linux bytes are unchanged.

* fix(terminal): normalize programmatic paste newlines

* test(e2e): harden Windows Codex paste spec per review

Poll for the idle composer placeholder as a positive ready signal (the
negative boot-state check alone can pass on an empty screen), and grow
the large-paste payload so it stays above the 64 KiB direct-max after
newline normalization, keeping the chunked lane covered even if
planning ever measures post-normalization bytes.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-14 14:40:40 -07:00
Brennan Benson 5b90c42f37 fix(terminal): certify replay wedges only after a quiet window and re-kick recovery on reveal (#8691)
* fix(terminal): certify replay wedges only after a quiet window and re-kick recovery on reveal

A slow-but-alive xterm parse (hidden-restore backlogs) could be certified
"wedged" by the replay guard's flat probe deadline, opening the guard while
replay bytes were still parsing (auto-replies leak into the agent's stdin)
and handing healthy panes to recovery. The wedge deadline now extends while
parse progress is observed and certifies only after a fully quiet window.

Two self-heal holes in the same family: a restore requested on a
certified-dead pipeline silently returned false — if certification's own
recovery request was budget-declined, a revealed dead pane kept its stale
pre-death frame forever; it now re-kicks pane recovery once per xterm
instance. And remountTerminalTabForRecovery returning false was the one
unbreadcrumbed recovery outcome; it now leaves a trace.

* fix(terminal): make replay progress generation-based

* fix(terminal): separate write failure from parse progress

* fix(terminal): skip output for certified-dead renderers

* fix(terminal): close recovery retry races

* fix(terminal): bind recovery to xterm lifecycle

* fix(terminal): invalidate stale parse probes

* fix(terminal): retry every dead split renderer
2026-07-14 14:14:42 -07:00
Jinwoo HongandOrca 77b154d5dd Add Orca Relay desktop and mobile transport (#8536)
* feat(mobile): define relay protocol groundwork

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

* feat(mobile): implement replay-safe E2EE v2 sessions

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

* test(auth): lock cloud refresh single-flight

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

* test(mobile): complete E2EE v2 adversarial coverage

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

* refactor(runtime): unify mobile socket wiring

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

* feat(runtime): add relay control and data clients

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

* feat(runtime): coordinate desktop relay sessions

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

* fix(auth): fence stale cloud session mutations

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

* feat(runtime): add relay pairing and durable revoke

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

* feat(runtime): add relay credential pairing RPCs

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

* feat(settings): show Orca Relay sign-in status

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

* test(relay): prove desktop lifecycle and E2EE splice

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

* feat(mobile): persist relay pairing state

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

* feat(mobile): race direct and relay pairing

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

* feat(mobile): recover pairing through relay director

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

* fix(relay): preserve origin controls during drain

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

* feat(mobile): recover interrupted relay pairing

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

* feat(mobile): add stable relay RPC sessions

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

* feat(mobile): supervise direct and relay endpoints

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

* Cover mobile relay director fallback matrix

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

* Fix relay settings component test isolation

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

* Remove unrelated merge formatting drift

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

* Update runtime connection count integration assertion

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

* Run mobile typecheck through pnpm

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

* feat(relay): gate desktop controls on mobile demand

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

* test(mobile): cover served relay recovery

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

* feat(mobile): upgrade direct pairings to relay

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

* fix(relay): harden mobile reconnect and teardown

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

* fix(auth): clarify account sign-in state

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

* fix(auth): polish sign-in completion flow

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

* fix(auth): clarify sign-out confirmation

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

* fix(auth): simplify sign-in completion page

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

* feat(mobile): add per-device pairing connection mode

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

* fix(mobile): stabilize pairing option layout

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

* fix(mobile): give pairing choices stable space

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

* fix(mobile): stabilize pairing QR regeneration

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

* Animate mobile pairing flow height

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

* Configure auth in packaged builds

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

* Make Orca Relay pairing an opt-in beta

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

* Show Relay beta details on hover

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

* Refine mobile relay pairing choice

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

* Polish Orca Relay pairing controls

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

* Keep mobile contract fallback test additive

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-14 11:47:05 -07:00
Brennan Benson 9d51732523 perf(terminal): defer cold worktree activation tab mounts until first reveal (#8597)
* perf(terminal): defer cold worktree activation tab mounts until first reveal

Activating a worktree mounted a TerminalPane for every saved terminal tab
in one render pass. Each mount replays scrollback through xterm, attaches
a WebGL renderer, and issues a sync-IPC snapshot read, so a worktree with
many agent-session tabs froze the renderer for tens of seconds (field
trace: 200+ replay-guard stall releases inside one activation window,
plus restore-marker feedback loops re-fetching snapshots at ~4/sec).

Cold activations now mount only the tabs the user can see (active tab,
each split group's active tab, activity-portal tabs, pending spawns) plus
tabs that are already live or that parked byte watchers cannot cover.
Every other tab defers like a cold-parked tab from birth: no view until
first reveal, with the parked byte watchers owning bell/title/completion
side effects meanwhile. The restriction reuses the targeted background
mount mechanism and lifts once every tab has been revealed.

Deferral only engages when more than four tabs would mount cold, and only
while hidden-view parking is enabled, so small worktrees and the parking
kill switch keep today's behavior.

* test(terminal): prove cold-activation deferral end-to-end; unblock it past hydration's blanket spawn flag

The new e2e (child worktree, 8 tabs, renderer reload against live daemon
sessions) caught the deferral never engaging: session hydration marks
every persisted tab pendingActivationSpawn, and treating that flag as
must-mount-now put all tabs in the immediate set. Only explicit queued
startups (pendingStartupByTabId) mount eagerly; a deferred tab's reveal
consumes the hydration flag exactly like an activation mount would.
Sleep/wake respawn flows go through targeted background mounts and are
unaffected (terminal-sleep-wake-restore passes).

The spec asserts the visible tab mounts, deferred tabs stay unmounted
with parked byte-watcher coverage, and a revealed tab mounts on demand.

* fix(terminal): harden deferred activation lifecycle

* docs(reliability): correct SSH startup assertion evidence
2026-07-14 02:58:34 -07:00
fsdwen 2b8cec36d9 fix(i18n/zh): 修复3处翻译质量错误 (#8693) 2026-07-14 01:21:05 -07:00
Brennan Benson 36cd8a3347 fix(terminal): retire sessions when tabs close (#8628)
* fix(terminal): retire sessions when tabs close

* fix(terminal): close remaining session lifecycle gaps

* fix(terminal): close review-discovered lifecycle gaps

* fix(terminal): revalidate bulk session retirement

* fix(terminal): harden retirement review edges

* fix(agent): reverify restored pane authority

* test: make terminal retirement gate portable on Windows

* test: use POSIX join in Linux PATH assertion

* test: keep simulated Linux PATH host-consistent
2026-07-14 00:15:34 -07:00
NeilandOrca a2b5431ca3 fix(markdown-preview): paint Find matches without mutating react DOM (crash 237acef1) (#8678)
Co-authored-by: Orca <help@stably.ai>
2026-07-13 23:34:13 -07:00
OrcaWin 3bb6f917e4 @ (#8683)
fix(sidebar): guard kanban worktree sort against undefined displayName (crash 99657ab1)

A worktree reached the sidebar with an undefined displayName, so the
Manual-sort comparator ran `a.displayName.localeCompare(...)` and threw
`TypeError: Cannot read properties of undefined (reading localeCompare)`,
unmounting the sidebar.worktrees error boundary.

Route both kanban comparators through a null-safe compareDisplayName so a
missing name only affects tie-break order instead of crashing. Behavior is
unchanged when displayName is present.

Adds regression tests for both the manual and recent sort paths.
@
2026-07-13 23:30:48 -07:00
NeilandOrca 9a324bf333 fix(agent-status): guard against re-entrant pending-status flush (crash 9fc89529) (#8671)
Co-authored-by: Orca <help@stably.ai>
2026-07-13 23:08:19 -07:00
moseohandOrcaWin 7d8c4fdca1 fix(github): pin work-item list ordering to updated-desc so cursor pagination reaches every page (#8658)
* fix(github): pin work-item list ordering to updated-desc so cursor pagination reaches every page

The Tasks page paginates work items with an updatedAt cursor
(updated:<oldest-item), but the underlying gh calls never pinned a sort:
'gh issue list' defaults to created-desc and '--search' defaults to
best-match. Items created long ago but updated recently therefore never
appeared on any page — page 0 (created order) skipped them and every
later page excluded them via the cursor — so the pager advertised pages
the fetch chain could never reach, clicks on them clamped to the last
real page, and cross-page ordering was scrambled.

Append sort:updated-desc to every list/search invocation so the fetch
order matches the cursor field on the first and all subsequent pages.

Verified against a live 588-issue repo: the cursor chain previously
died around page 5; it now traverses 585/588 unique issues (the
remainder is the pre-existing strict '<' boundary edge for items
sharing the cursor's exact timestamp).

Fixes #8649

* fix(github): make work-item cursor pagination lossless at updatedAt boundaries

Builds on the sort-pin fix: switch the pagination cursor from strict
'updated:<' to inclusive 'updated:<=' so items sharing the boundary row's
exact updatedAt are no longer skipped between pages (the residual 3/588 edge
in #8649).

The inclusive bound re-fetches the boundary rows, so dedupe them by repoId+id
(a bare item.id like 'issue:9' collides across repos). Extract the page
accumulation out of the 12k-line TaskPage component into a pure, unit-tested
helper (accumulateWorkItemPages) that dedupes and backfills: it accumulates
fresh rows across fetches and emits uniform pageSize pages, so deduped pages
never shrink below the size totalPages (count / effectivePageSize) assumes —
which would otherwise strand the tail items and break the no-count degraded
pager.

Also hoist the updated-desc ordering into a named WORK_ITEM_LIST_SORT_QUALIFIER
constant so the cursor's ordering contract has one home.

Tradeoff: when per-repo fetch size equals pageSize, the boundary dedupe costs
one extra fetch per page; acceptable for interactive pagination and bounded by
the gh rate-limit guard. Persisting the cursor/buffer across calls is a
possible follow-up.

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-07-13 22:37:46 -07:00
Brennan Benson 8b86e7ead7 fix(terminal): let pane recovery reach input stuck behind a never-settling connect (#8673)
Follow-up to the pane self-heal: its input-undeliverable detector is
gated off while a transport connect is settling (correct — remounting
mid-reattach used to kill the resolving shell), but the gate had no
timeout. A connect that never settles (the SSH RPC-timeout class) left
the one state recovery still could not reach: no output flows, so no
other detector fires, and every keystroke is silently dropped forever.

Two changes that only make sense together:
- the gate becomes a 60s settle grace; past it, undeliverable input may
  request recovery again (still pty:hasPty-confirmed).
- the transport's destroyed-while-connecting check now kills only what
  the connect CREATED. A fresh spawn is ours to reap; a reattach
  (sessionId) resolves to a pre-existing session owned by the tab
  lifecycle — tab close kills it by id through its own path, so killing
  it here turned a remount racing a slow reattach into the loss of a
  live shell. This is what makes the grace safe: recovery firing at 60s
  against a connect that resolves at 61s now costs a wasted view
  rebuild, not the user's session.

Verified: transport unit test pins the reattach/fresh-spawn kill split;
the recovery e2e pair passes 6/6 consecutive iterations with this
change (an earlier flaky signal bisected to machine load: failures only
under parallel-build load, two unrelated failure modes, 4/4 then 6/6
green on quiet hardware with and without the change re-applied).
2026-07-13 22:33:00 -07:00
Brennan Benson 9924b07bcc fix(terminal): self-heal panes whose renderer dies while the PTY stays alive (#8630)
* fix(terminal): self-heal panes whose renderer dies while the PTY stays alive

A pane's xterm write pipeline can die while its shell keeps running: a
synchronous throw escaping an unguarded write callback wedges WriteBuffer
(issue #2836), and write() on a disposed terminal silently drops its
completion callback (verified against vendored xterm 6.1.0-beta.287 — it
does NOT throw, falsifying the output scheduler's disposed-race catch).
Every recovery path we have (dead-session reconcile #6514/#7002,
hibernation wake #7145, the allDead activation generation bump) gates on
the PTY being dead, so these panes stayed fossils: last frame painted,
every keystroke and byte of output silently dropped, delivery ack credits
leaking, until the user reloaded the window (issue #8104 class).

Detection is probe-certified, mirroring replay-guard.ts:
- a scheduler write whose completion stalls gets an empty probe write; a
  probe that never parses certifies the pipeline dead (catches both the
  wedged WriteBuffer and the disposed-terminal case) and credits the
  queued deliveries so main's in-flight window no longer leaks
- the replay guard's existing wedged release ("pane likely needs
  recovery") now actually hands the pane to recovery
- user input rejected by an unbound transport (detached during a
  remount/move and never rebound) arms recovery after confirming the PTY
  is alive via pty:hasPty

Recovery reuses the proven remount seam: bump the tab's generation so
TerminalPane unmounts, detach() preserves the live PTY, and the remounted
pane builds a fresh xterm that reattaches and replays the daemon
snapshot — no shell restart, capped per tab to prevent remount storms.

The new e2e spec pins both phenotypes end-to-end (wedge → recover,
dispose-under-live-bindings → recover); both fail on main and pass with
the fix, and the same arc was validated live in a pnpm dev instance.

* fix(terminal): make pane recovery strictly best-effort in timer contexts

Recovery fires from stall-watch timers, replay-guard releases, and onData —
contexts where a throw becomes an unhandled error (CI verify caught this:
pty-connection.test.ts mocks a partial store, other tests advance fake
timers past the stall window, and the certification path hit a missing
remountTerminalTabForRecovery). Guard the store action call and the
ptyIdsByTabId reads so a partial surface yields a false return, never a
throw, and pin it with a regression test.

* fix(terminal): guard pane recovery against in-flight reattach and remote liveness blind spots

Review findings on the self-heal (adversarial pass):

1. HIGH: typing during an in-flight connect/reattach (startup restore,
   app-SSH) hits sendInput while the transport is legitimately unbound; an
   input-undeliverable remount there destroys the unbound transport (no
   ptyId yet, so unmount cannot detach), and pty-transport's destroyed
   check then kills the PTY the resolving reattach returns — the live
   shell recovery exists to preserve. Gate the input detector on a
   transport-connect-in-flight flag (set around all three connect sites)
   and on disposed, so "not deliverable YET" never remounts. The fossil
   case (detached and never rebound) has no pending connect and still
   recovers.

2. pty:hasPty answers null for ids the local registry does not own, which
   made the liveness gate inert for remote panes: a disconnected remote
   runtime would remount-churn on every cooldown window while typing.
   Remote panes (connection-tagged or remote:-prefixed) now require an
   authoritative true; local panes keep the lenient null-proceeds gate.

Flagged for follow-up, not changed here: pty-transport's destroyed check
kills reattached sessions without discriminating isReattach — a
pre-existing hazard that tab-close covers by killing per id anyway.

* fix(terminal): keep certification throw-proof end to end

Guard the two remaining throw paths in the certification chain — the
entry-discard callback and the recovery handler — so nothing can escape a
timer as an unhandled error, and a throwing discard cannot suppress the
recovery notification it exists to precede. Pinned by two new tests.

* fix(terminal): breadcrumb swallowed recovery failures

A store-action throw in recovery returns false without consuming budget,
so the detector retries each cooldown — an invisible loop unless it
leaves a trace. Breadcrumb it (the recorder is self-guarded and cannot
throw where recovery runs). Also invoke transport.isConnected optionally
so partial test transports fail the gate quietly instead of logging a
contained TypeError.

* fix(terminal): end the zombie-pane replay loop at its root

Root-caused the production "wedged release drip" (1,302 breadcrumbs in one
day on one machine, 4-write bursts on a fixed timer phase, idle-required):
once a pane's xterm pipeline dies while its connection lives, the delivery
watchdog's heal (60s cooldown, fires only while idle because a dead xterm
never ACKs its in-flight bytes) re-delivers restore markers, the hidden
output restore replays 3-4 chunks into the dead parser, each write arms a
replay guard destined for another wedged release — and nothing ever
learns. The loop runs forever and re-forms after app restart.

Three fixes so the loop learns:
- replayIntoTerminal/Async short-circuit on a probe-certified dead
  pipeline: no more futile writes, so no more guard drips, and awaited
  restore chains resolve instead of hanging.
- requestHiddenOutputRestoreIfNeeded is gated the same way, so the
  watchdog heal stops refetching snapshots for a pane recovery owns.
- a window-cap recovery decline now schedules one retry for when the
  budget window reopens (deduped per tab, cancelled by any successful
  remount). Without it, the certified-dead latch plus the new write
  silence made a capped pane a permanent zombie: nothing would ever
  re-request recovery. Cooldown declines deliberately do not retry —
  remounts are tab-scoped, so the just-made remount already replaced
  every pane's xterm in the tab.

Still open (tracked separately): the deterministic wedge surface that
creates the dead pipeline on the release build in the first place — an
unguarded, unreported parse-path throw or silent disposed-write; zero
guard breadcrumbs fired all day, so the trigger predates the guards'
coverage.

* feat(terminal): name the silent zombie producer in breadcrumbs

The last unproven link in the zombie-pane chain is HOW a pane's xterm
dies on the release build. Field discriminators eliminated every
reporting channel: zero terminal guard breadcrumbs and zero xterm-stack
renderer_error/unhandled_rejection events across days of logs, while the
drip re-formed after a clean app restart. Every content-triggered wedge
surface would have reported; the only fully silent mechanism left is a
restore write into an already-disposed xterm instance (write() drops its
completion callback without a throw — verified against the vendored
6.1.0-beta.287).

Instrument that exact moment: a version-pinned disposal probe (its test
runs against the real vendored build so an upgrade that moves the private
field fails loudly), a terminal_restore_write_target_disposed breadcrumb
where startup scrollback restore would write into a disposed instance,
and a terminal_restore_write_failed breadcrumb replacing the fully silent
restore catch. The next zombie formation logs its own root cause.
2026-07-13 21:30:10 -07:00
Brennan Benson 6c4bb67415 fix(i18n): localize the terminal tab unread-completion aria-label (#8663)
The unread-activity bell landed with a hardcoded aria-label, and the
localization-coverage gate now fails on main itself (verify does not run
on pushes to main, so the break landed silently and blocks every PR's
CI). Route it through translate() with catalog entries for all five
locales.
2026-07-13 21:25:49 -07:00
Jinjing 9875b5528a Improve native chat style (#8654)
* Redesign native chat interactive UI to match assistant-turn styling

- Restyle approval and question cards as bordered cards docked in the
  composer region instead of top-border strips, and widen the chat
  column (max-w-3xl → max-w-4xl) to match the rest of the app
- Rework the question card into a numbered pick-list with an always-
  present free-text row and explicit Next/Skip/Send action, replacing
  the prior "Other…" toggle flow
- Hide the composer while a question card is active since it supplies
  its own answer input, and hold the card open until the paced answer
  send settles instead of dismissing on click
- Auto-repin the message list to the bottom during in-place streaming
  growth via a ResizeObserver on the content, not just the viewport
- Widen question-answer pacing (800ms→1000ms step, 300ms→500ms buffer)
  for reliability on slower machines / higher-latency SSH sessions
- Restyle tool-run/tool-line disclosure to a right-side hover chevron
  and add a formatted (pretty-printed) detail view for diff-less calls
- Add "Skip" i18n string across all locales

* Extend paste bridge and question-card fixes for native chat interactive

- Route pane-level Paste to the question card's free-text input when
  the composer is unmounted, so Cmd/Ctrl+V keeps working while a
  question prompt is showing.
- Fix a stale-timer bug where a new interactive prompt could inherit
  the previous card's dismiss/submitting state and swallow its first
  answer.
- Only render tool-run detail when there's actually content to show,
  and mark selected question options with aria-pressed for
  accessibility.
2026-07-13 21:24:33 -07:00
Brennan Benson 5a3e4b44f8 fix(lint): cover active/inactive in the terminal tab activity switch (#8660)
The WorktreeStatus union feeding activityDotState widened, and the
switch-exhaustiveness gate now fails on main itself (verify does not run
on pushes to main, so the break landed silently and blocks every PR's
CI). Cover the two glyph-less states explicitly — matching the
function's documented intent — and drop the now-unreachable default.
2026-07-13 21:10:01 -07:00
JinjingandOrca 1ef0551bc1 Create pr not working for stacked worktree (#8651)
* Fix stacked-worktree PR creation targeting a local-only parent branch

- Resolve the eligibility default base to a remote-tracking ref instead
  of blindly trusting the submitted parent branch, since a stacked
  worktree's base is often a local-only branch the remote can't resolve
- Add a create-time hard block (base_not_on_remote) so a stale or
  unpushed submitted base fails with actionable copy instead of the
  provider's opaque error
- Update the dialog's default-base resolution and blocked-action/
  dropdown copy to match the new remote-validated default

* Split hosted-review-creation.test.ts to fix max-lines lint error

Moved getHostedReviewCreationEligibility tests to a separate file (hosted-review-creation-eligibility.test.ts) to reduce the original file size from 880 to 579 lines, satisfying the max-lines lint constraint.

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

* Fix Create PR intent flow to use remote-validated eligibility default fo

Prefer eligibilityDefaultBaseRef over the raw compare base when resolving
the review base for the one-click Create PR intent flow, since eligibility
is recomputed from the same compare base right before creation and already
corrects a local-only stacked parent to the repo default. Falls back to
the compare base only when eligibility supplies no default.

* Simplify base-ref remote existence check into a single for-each-ref call

Combine the wildcard and exact-tracking-ref lookups into one for-each-ref
invocation with multiple patterns instead of two sequential git calls,
removing the redundant rev-parse fallback path.

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-13 20:10:50 -07:00
NeilandOrca 6e1aa8503c perf(renderer): pause always-on idle timers while the window is hidden (#8528)
Co-authored-by: Orca <help@stably.ai>
2026-07-13 19:19:17 -07:00
Jinjing 2801c6b03b P2 live log undo memory (#8644)
* fix(editor): avoid undo history for read-only live tails

* Add reliability-gate evidence for read-only live-tail undo-history fix

- Records the passing vitest run that verifies live-tail appends leave
  canUndo false while ordinary external updates stay undoable, backing
  the recent editor undo-history fix.
2026-07-13 19:13:19 -07:00
c408a3d852 feat(mobile): show usage reset countdown on accounts screen (#7954)
* feat(mobile): show usage reset countdown on accounts screen

Surface the rate-limit reset time ("5h resets in 3h 54m · 7d resets in
6d 7h") under the usage bars on the mobile accounts screen, matching the
desktop status-bar tooltip copy. The resetsAt timestamps already arrive
in the accounts.subscribe snapshot; this only adds the presentation.

Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ

* docs(mobile): JSDoc for new usage reset selectors

Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ

* refactor(mobile): per-bar reset countdown instead of combined line

Drop the redundant "5h/7d" prefixes — each countdown now renders under
its own bar ("Resets in 3h 54m"), matching the desktop tooltip copy
exactly.

Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ

* Extract shared reset-countdown formatter for desktop and mobile

- Move duration/countdown formatting out of tooltip.tsx into
  src/shared/rate-limit-reset-format.ts so mobile's account-usage-state
  can reuse it instead of a duplicated copy (with tests).
- Re-export formatResetCountdown from tooltip.tsx to avoid touching
  existing import paths.
- Resend the pairing deep link once more in start-emulator.mjs since
  the first can arrive before the Expo app's JS router is ready.

---------

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-13 18:56:36 -07:00
JinjingandOrca 1c6098c214 fix(terminal-tabs): invalidate activity-status cache on freshness update (#8641)
The store bumps agentStatusEpoch at the 30m stale boundary without
replacing the agentStatusByPaneKey map. Keying the flag cache on map
reference alone kept serving stale flags (old timestamp, abandoned
spinning), while the sidebar — which keys on agentStatusEpoch — correctly
de-spun. Invalidate on either changing.

Co-authored-by: Orca <help@stably.ai>
2026-07-13 18:52:09 -07:00
67fdb1c2b9 Show agent status in terminal tabs (#8142)
* Show agent status in terminal tabs

* Resolve terminal tab activity via the canonical worktree-status engine

The first pass hand-rolled resolveTerminalTabAgentActivityState, a fourth
parallel copy of the pane-iteration/freshness/title-heuristic loop that already
lives in smart-attention.ts and worktree-agent-activity-summary.ts. It diverged
from every existing surface (novel blocked>waiting split, a phantom 'interrupted'
red state the sidebar treats as idle) and re-scanned the global agentStatusByPaneKey
map per tab per store write (O(tabs*agents)).

Replace it with resolveTerminalTabActivityStatus, which reuses resolveWorktreeStatus
(the WorktreeCard resolver: freshness gate, live-PTY liveness, per-leaf title dedup,
permission>working>done priority) over a per-tab flag summary bucketed once per store
snapshot (O(tabs+agents)). Tabs now speak the same WorktreeStatus vocabulary as the
sidebar, so their live states can't disagree with the worktree card.

- Map WorktreeStatus -> AgentStateDot: working=spinner, permission=amber, done=check;
  active/inactive fall through to the agent/shell identity icon.
- Parse legacy numeric pane keys too, matching the sidebar summary, so restored/
  imported sessions light the tab.
- Drop the bespoke resolver + its tests; add focused coverage for the new resolver
  and the leading-icon component.

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

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-13 18:32:08 -07:00
NeilandOrca 2266c1b03a fix(sidebar): scope project display labels by execution host to prevent cross-host path collision (#8345) (#8511)
* fix(sidebar): scope project display labels by execution host to prevent cross-host path collision (#8345)

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

* review: harden #8345 fix — pin cross-host key separateness for identical path+name

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-13 18:16:33 -07:00
NeilandOrca b667b72eb2 Route diffs by explicit worktree owner, not focused runtime (#8484) (#8509)
Co-authored-by: Orca <help@stably.ai>
2026-07-13 18:15:56 -07:00
NeilandOrca f96fa32ad8 fix(codex): debounce auto-resolved 'Approve for me' approval notifications (#8387) (#8519)
* fix(codex): debounce auto-resolved approval notifications (#8387)

Codex fires its PermissionRequest hook at the human-input boundary before
the approval decision, so under 'Approve for me' the review agent approves
and Codex resumes within ~1s. The completion coordinator dispatched the OS
attention notification immediately on that 'waiting'/'blocked' hook, so the
self-resolving pause raised a false 'approval required' notification.

Debounce the Codex OS attention notification behind a 1500ms quiet window
and cancel it when a working/completion hook lands inside the window. Scoped
strictly to agentType==='codex'; every other agent's pause still notifies
immediately. The visual sidebar status is set upstream (store.setAgentStatus)
before the coordinator runs, so it stays immediate.

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

* review: harden #8387 fix — protect debounce evidence + cancel on title resume

Two adversarial-review hardening fixes to the Codex attention debounce, both
preserving the fail-open contract (a genuine 'needs input' pause must always
notify):

- handleProcessInspectionResult: extend the evidence-teardown guard to also
  hold while a pendingCodexAttentionTimer is armed. A transient null/shell
  foreground blip (or a remote/SSH inspection that returns null foreground)
  could otherwise tear down agent evidence mid-window and make the timer's
  hasAgentRunEvidence guard silently drop a genuine pause banner — or fire a
  false process-exit completion racing the pause. Mirrors the pendingHookDone
  guard.
- recordTitleWorking: cancel the debounced attention on a genuine title-driven
  resume (placed after the replay guard, so a stale post-completion title
  replay never drops a still-pending genuine pause banner).

Adds 6 discriminating fake-timer tests (guard, title resume, blocked parity,
done cancels attention, dispose clears timer, second distinct pause re-arms).

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-13 18:14:50 -07:00
NeilandOrca 5886ffab63 Fix orchestration skill coverage for provider-home skill roots (#8256) (#8510)
Co-authored-by: Orca <help@stably.ai>
2026-07-13 18:13:36 -07:00
NeilandOrca 1450db85ba fix(grok): don't tell users to re-run grok login on refreshable token expiry (#8508)
Co-authored-by: Orca <help@stably.ai>
2026-07-13 17:55:47 -07:00
moseoh 6c489a4379 fix(source-control): keep push-signal status refreshes alive while the huge flag is set (#8570)
Once git status hit the 10,000-entry limit, the huge flag disabled every
status-refresh lane — including the push signals (repo metadata watcher,
terminal command-finished) that carry the evidence needed to clear it.
The flag only clears when a fresh non-huge status result arrives, so the
worktree deadlocked into stale Source Control and explorer badges until
an app restart.

Split the gate: evidence-free interval polling stays paused while huge
(preserving the #7983 idle-CPU fix), but push-signal refreshes now ride
the coalesced change-signal lane, so a commit made in the integrated
terminal clears the flag and resumes normal polling. A visibilitychange
listener (active only while huge pauses polling) catches up signals
dropped behind a hidden window, matching the becoming-visible catch-up
the normal lane already has.
2026-07-13 17:52:25 -07:00
Jinjing dbe2481d57 Being able to reply to any comment from bot (#8621)
* Allow replying to any comment in a thread, not just the root

Reply state now tracks a comment id instead of a group id, and reply composers/handlers are threaded through each comment row (root and replies alike) so any comment can receive an inline reply rather than only the thread root.

* Fix reply cancellation clearing the wrong comment's reply box

When multiple comment threads had reply boxes open, cancelling one
reply cleared replyingCommentId unconditionally, closing whichever
box was open instead of the one actually cancelled. onCancelReply now
threads through the specific commentId so cancellation only clears
state if it matches the currently open reply.
2026-07-13 17:37:16 -07:00
Jinwoo HongandOrca 83588b43a7 fix(terminal): stop phantom pinned-viewport pins from freezing follow-output (#8625)
Co-authored-by: Orca <help@stably.ai>
2026-07-13 16:36:59 -07:00
Neil 469018ec78 fix(sidebar): decouple agent-list expansion from the child-worktrees toggle (#8527)
Agent-list expansion (compact "N agents" summary + per-parent lineage folds) lived in local useState on WorktreeCardAgents, so the WorktreeCard remount triggered by the child-worktrees toggle (a virtual-row key flip between item and lineage-group) and by virtualizer scroll-recycle wiped it — folding one section reset the other. Lift it into a session-scoped, LRU-bounded per-worktree module cache (useWorktreeAgentExpansionState), mirroring pr-comments-list-selection. Adds unit + integration + e2e regression tests. Reviewed for regressions and performance.
2026-07-13 16:15:44 -07:00
NeilandOrca 398fc79c4f Show markdown front-matter by default in the rich editor and preview (#8623)
Flip the default so the front-matter banner is visible unless the user
explicitly hides it. The per-file visibility map now stores only hide
overrides (false) instead of show overrides: read defaults resolve to
true, the setter writes false / deletes on show, session persistence
preserves the real value, and hydration keeps false entries while
dropping legacy true entries back to the visible default.

Co-authored-by: Orca <help@stably.ai>
2026-07-13 16:13:29 -07:00
833724830f Fix folder workspace Git status path (#8326)
* Fix folder workspace Git status path

* fix(git): resolve folder-workspace path for all local filesystem ops

Extend the folder-workspace suffix stripping beyond git status: every
local Git subprocess cwd, the WSL-context probe, the first-work
branch/folder rename hooks, and the renderer session placeholders now
resolve the synthetic `::workspace:<uuid>` instance id to its backing
folder via splitWorktreeIdForFilesystem. Without this they would spawn
Git (or fs ops) against a nonexistent directory (ENOENT).

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

* Preserve folder workspace rename instance ids

* Read folder workspace suffix from worktree id

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-13 15:47:17 -07:00
Brennan BensonandBrennan Benson 53a09afbef feat(mobile): match desktop's Smart workspace source picker exactly (#7985)
* feat(mobile): start a workspace from a branch, issue/PR, or Linear ticket

Unify mobile workspace creation with desktop. The "+" Create Workspace
modal now has a primary "Start from" field that opens a tabbed search
drawer (Branch · GitHub · GitLab · Linear), letting a user start a
workspace from an existing/new git branch, a GitHub issue/PR, a GitLab
issue/MR, or a Linear ticket — in addition to the default blank workspace.

No new backend is required: the search RPCs (github.listWorkItems,
gitlab.listWorkItems, linear.searchIssues/listIssues, repo.searchRefs) and
the worktree.create linked-item params were already used by the mobile
Tasks screen. This surfaces them in the create flow, reusing the existing
pure modules (buildTaskWorkspaceCreateParams, shouldResolveHostedReviewStartPoint,
filterAvailableTaskProviders).

Details:
- New pure modules: workspace-source-selection, use-workspace-source-search,
  source-workspace-create, worktree-create-retry, blank-workspace-create
  (the blank/retry path extracted from the modal for reuse + line budget).
- New UI: WorkspaceSourcePickerDrawer (+ row) and SetupHookTrustDrawer
  (extracted from the modal).
- Older paired desktops (missing the mobile.tasks.v1 capability) degrade to
  Branch + Blank only; GitLab/Linear tabs appear only when available.
- GitHub/GitLab sources pin their repo; switching repos resets the source.
  PR/MR sources resolve their base branch at create time; SSH repos gate
  search until connected (Linear search is repo/SSH-independent).

* fix(mobile): hydrate settings/trust before availability probes settle

Review fixes for #7985: setTrustedOrcaHooks/setRuntimeSettings no longer
wait on status.get/preflight.check/linear.status (a first-open
preflight.check can take seconds, widening the spurious setup-trust
re-prompt window). Also adds param-parity tests for createBlankWorkspace
and a GitLab MR base-resolve test.

* feat(mobile): match desktop's Smart source picker exactly

Rework the mobile create-workspace source picker to be a faithful port of
desktop's Smart picker instead of the earlier divergent "Start from" drawer.

The mobile field is now the workspace-name input AND the source search, with the
exact desktop tabs — Smart · GitHub · Linear · GitLab · Branch · Name. "Smart"
fans out across GitHub + GitLab + Linear + branches, prepends a "Use '<name>'"
row, and resolves pasted URLs / #123 / STA-42 to exact items (with a cross-repo
switch prompt). Selecting a source shows a pill and moves the editable name into
Advanced. The invented "Blank workspace" concept is removed — the neutral state
is just a typed/empty name (blank submit still yields a creature name).

DRY: the pure desktop logic (smart-workspace-source-results, -command-value,
github-links, gitlab-links, work-item-link-query-bounds, github-work-item-identity)
moves to src/shared/new-workspace/ with re-export shims at the old renderer paths,
so both renderer and mobile share one implementation. composer-branch-selection
and workspace-name were already shared and are reused directly.

Two read-only lookup RPCs are allowlisted for mobile so pasted GitLab URLs and
cross-repo GitHub URLs resolve to exact items (github.workItemByOwnerRepo,
gitlab.workItemByPath).

New mobile modules are split for max-lines: use-mobile-composer-source (selection
state + desktop-parity handlers, PR/MR base resolve), use-smart-workspace-source
+ smart-source-fan-out/-search-requests/-paste-intent (RPC orchestration),
composer-linked-work-item / work-item-lookup-text / mobile-smart-source-modes
(pure logic), and SmartWorkspaceSourceField/Drawer/Row + SmartWorkspaceAdvancedFields.
Replaces WorkspaceSourcePickerDrawer/Row, workspace-source-selection,
use-workspace-source-search, and MobileWorkspaceNameInput.

Reviewed by three adversarial agents + re-reviewed after fixes: GitHub search now
returns issues AND PRs (not issues-only), Linear defaults to assigned, create-branch
preserves slashy names, cross-repo PR base resolves against the item's own repo,
displayName is suppressed for user-edited names, and the smart-mode GitHub fan-out
respects availability. tsc/oxlint/max-lines-ratchet clean; 1328 mobile tests pass.

* fix(mobile): keep smart source drawer fully visible

* refactor: share workspace creation behavior across clients

* fix: address workspace creation review findings

---------

Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
2026-07-13 15:38:02 -07:00
Jinjing 280103aa04 Allow replying to any comment in a thread, not just the root (#8562)
Reply state now tracks a comment id instead of a group id, and reply composers/handlers are threaded through each comment row (root and replies alike) so any comment can receive an inline reply rather than only the thread root.
2026-07-13 15:20:43 -07:00
Brennan Benson 65c65c37e8 fix(terminal): stop stale PTY resize after worktree reveal (#8502)
* fix(terminal): stop stale PTY resize after worktree reveal

The visibility-resume size readback captures xterm's pre-reveal grid as
its resize target while the applied-size read is in flight. A reveal fit
or snapshot-restore resize can change the grid mid-flight without
queuing a newer request, so the resolved callback "repaired" the PTY
back to the pre-reveal grid — an idle TUI (Claude Code) then redraws
for the wrong grid until a manual resize (refs #7951, #7240).
Instrumented traces show the stale capture on every reveal; correctness
relied on the read winning FIFO against the fit's resize, which busy
daemons and SSH/relay round-trips lose.

Re-measure xterm at resolve time and re-run against the fresh grid
instead of forwarding a stale target. Adds an e2e seam that delays the
readback dispatch to reproduce the losing ordering, unit repros that
fail without the guard, and a hidden-resize/reveal-cycle e2e spec.

* test(terminal): cover single-flight convergence under an oscillating grid

* docs(e2e): note the WINCH bar is illustrative, not a Windows ground truth

The reveal repro asserts on pty:getSize convergence; the bottom-bar TUI
only makes the pane WINCH-reactive. Record that a long-lived process can
miss Node's stdout 'resize' event under Windows ConPTY even when the OS
PTY was resized, so future readers don't add a flaky bar-content check.
2026-07-13 14:17:49 -07:00
Jinjing 5be44d1553 fix(native-chat): target native chat to the active split leaf (#8569)
* fix(native-chat): target native chat to the active split leaf

* fix(native-chat): harden split leaf reassignment

* fix(native-chat): tolerate missing layout state
2026-07-13 13:07:52 -07:00
Jinjing 39b2ec832e fix(native-chat): scope composer sends to the active turn (#8568)
* fix(native-chat): scope composer sends to the active turn

* fix(native-chat): close pending send lifecycle gaps

* fix(native-chat): deduplicate pending message derivation
2026-07-13 13:07:25 -07:00