Commit Graph
3883 Commits
Author SHA1 Message Date
NeilandOrca 891a456b69 fix(sidebar): don't scroll to an unfocused worktree when pinning/unpinning it (#8930)
Co-authored-by: Orca <help@stably.ai>
2026-07-15 19:07:49 -07:00
Neil 49e2ac6f59 fix(cmd-j): show cross-project dialog on paste + restore focus on Escape (#8931) 2026-07-15 19:06:37 -07:00
fsdwenandJinjing 78d2b958bf fix(issues): replace cursor-based pagination with page-number Search API (#8680)
* fix(issues): replace cursor-based pagination with page-number Search API

Problem
=======
Issue pagination (#8649) had two bugs:
1. Pages 6-16 were unreachable — clicking page 16 highlighted page 5;
   clicking 6/7 did nothing. The old cursor-based approach
   (updated:<CURSOR) broke with Search API's relevance sorting —
   pages after the first few returned no items even though more
   issues existed.
2. Issue numbers appeared out of order on loaded pages (e.g. #1082
   between #1308 and #1499), because client-side sort used
   updatedAt instead of issue number.

Root Cause
==========
The pagination used two separate GitHub API strategies:
- Initial page 0 load: REST endpoints (repos/:owner/:repo/issues,
  repos/:owner/:repo/pulls) sorted by updatedAt
- Subsequent pages: Search API with cursor (updated:<DATE)

These two sources returned items in different orders, causing items
to go missing or appear on wrong pages across page boundaries.

Solution
========
1. Unified on GitHub Search API for all pages — initial load and
   pagination both use search/issues?q=...&page=N, eliminating the
   REST-vs-Search inconsistency.
2. Changed from cursor-based (update:<DATE) to page-number-based
   pagination (page=N), which the Search API supports natively.
3. Switched client-side sort from updatedAt to issue number
   (sortWorkItemsByNumber), matching GitHub's default Issues view.
4. Parallelized page fetches in handleLoadNextPage — clicking page
   16 now fetches all intermediate pages concurrently (~2s) instead
   of sequentially (~30s).
5. Cleaned up dead legacy gh issue list / gh pr list code path,
   extracted quoteForSearch helper, shortened overlong comments.

Files changed: 11 files, +140/-127 lines

Closes #8649

* chore: remove unrelated merge formatting

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-15 18:52:37 -07:00
Jinjing 0da3d392dc Reorganize heading slash commands and rename toggle heading labels (#8917)
Move toggle heading commands (toggle-h1..h4) into their own "Toggle headings"
group, separate from plain heading commands, so the slash menu distinguishes
static headings from collapsible toggle headings. Also shorten toggle heading
labels from "Toggle Heading N" to "Toggle HN" for consistency with the plain
heading labels.
2026-07-15 18:48:34 -07:00
BingZ 24dc165b3c fix(sidebar): map header drags to the nearest boundary slot instead of a dead zone (#8891)
* fix(sidebar): map header drags to the nearest boundary slot instead of a dead zone

Fixes #8879

* fix(sidebar): bound header edge drops to measured content
2026-07-15 17:14:10 -07:00
f7ef467cc9 fix: pr-bug-scan validated finding from #6335 (#8804)
* fix: address pr-bug-scan validated finding from #6335

Replaced unguarded crypto.randomUUID() with the established createBrowserUuid() fallback in both markup shape-id call sites.

* fix(review): cover markup crypto fallback call sites with regression tests

PR #8804 replaced two unguarded crypto.randomUUID() calls with
createBrowserUuid(), matching the convention established in #5471, but
shipped without extending that PR's regression-test pattern to the new
call sites. Add hook-level tests for useMarkupPointerHandlers.onPointerDown
and useMarkupEditor.commitPendingText under a simulated non-secure-context
crypto global.

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

* fix(review): consolidate annotation bridge token onto createBrowserUuid

BrowserPane had its own inline crypto.randomUUID guard with a
Date.now()+Math.random() fallback, duplicating the getRandomValues-based
fallback already centralized in createBrowserUuid() and used at every
other call site in the renderer.

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

---------

Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-15 16:47:18 -07:00
Jiyong Jung 5b329026d5 Update Korean translations for diff word wrapping (#8867)
Fixed misleading Korean translation of "on"/"off" and "word wrap"
2026-07-15 16:39:00 -07:00
Brennan BensonandOrca 5739e1844b Reopen closed tabs with Cmd+Shift+T, including terminals (#7445)
* Reopen closed tabs with Cmd+Shift+T, including terminals

Cmd/Ctrl+Shift+T now reopens the most recently closed tab of any kind
(terminal, browser, or editor) in true recency order, walking back
through close history on repeated presses — Chrome/Ghostty semantics.
Terminal tabs previously had no reopen at all, and the shortcut picked
browser-vs-editor by focused surface instead of recency.

Terminal closes capture a snapshot (startupCwd, shellOverride,
customTitle, color) on explicit user closes only; PTY exits, launch
cleanup, onboarding, and floating-panel closes are excluded. Reopen
spawns a fresh shell through the same createTab path as the "+" button.
A per-worktree cross-type kind list (new recently-closed-tabs slice)
orders the three existing per-type stacks; both new maps are purged on
worktree removal and re-keyed on rename with startupCwd prefix remap.
createNewTerminalTab moved to terminal-tab-create.ts to keep
terminal-tab-actions.ts under the max-lines budget.

Requested via Discord feedback; STA-1287.

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

* Update close-tab assertions for captureRecentlyClosed flag

Floating-panel, launch-failure, and stale-agent-tab closes now thread
{ captureRecentlyClosed: false } so system-initiated closes don't pollute
the reopen stack. Update the affected test expectations to match the new
call shape.

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

* fix(reopen): skip local terminal reopen on remote-runtime worktrees

reopenClosedTerminalTab recreated terminals via a raw local createTab
without the isWebRuntimeSessionActive guard every sibling terminal-create
path uses, so Cmd+Shift+T on a remote-runtime-owned worktree (paired-web
or desktop-attached headless-serve) spawned an unbacked local phantom tab
not tied to the host session. Guard at reopen time via the cycle-free
getRuntimeEnvironmentIdForWorktree — importing web-runtime-session into a
store slice creates a store-index init cycle. Remote terminal reopen stays
deferred; the cross-type dispatcher falls through to browser/editor.

Adds a regression test and corrects an inaccurate removal-path comment.

* fix(test): seed runtime host id for remote-reopen guard test

The prior test set settings.activeRuntimeEnvironmentId by spreading the
store's settings, which widened workspaceDir to string|undefined and broke
`pnpm typecheck` (GlobalSettings requires string). Seed the worktree with a
`runtime:` hostId instead — getRuntimeEnvironmentIdForWorktree reads it
directly, so no settings spread is needed.

* Remove unrelated formatter changes

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-15 16:14:44 -07:00
Brennan Benson a1778d93d3 Fix PR checks sticking to a stale linked PR after a terminal branch switch (#8760)
* Fix PR checks sticking to a stale linked PR after a terminal branch switch

A worktree's linked PR is a branch-scoped hint, but two refresh paths race
when a terminal switches branches: the git-status identity path clears
branch-scoped review links, while the worktree-listing path rehydrates the
new branch together with the stale persisted link and clears nothing. When
the listing lands first (the common case — worktree listing is much faster
than git status), the identity path sees no branch change and the stale
link survives. Every subsequent refresh then re-fetches the linked PR by
exact number, which ignores the branch, so Checks stays pinned to the old
branch's PR and the Refresh button cannot recover.

Two-part fix:
- Prevention: listing refreshes now route observed branch switches through
  updateWorktreeGitIdentity before merging, so the existing link clear and
  tombstone machinery runs no matter which refresh path wins. Gated on the
  entry still carrying branch-scoped review context so a stale listing row
  cannot roll back a newer branch identity.
- Recovery: PRInfo now carries headRefName, and a fetch that returns the
  linked OPEN PR whose head branch matches neither the current branch, the
  worktree push target, nor the worktree HEAD clears the durable link and
  re-resolves by branch. Wired into both fetchPRForBranch and the
  background refresh coordinator, mirroring the merged-PR divergence clear.
  This also heals wedged workspaces persisted by earlier builds.

* Harden stale PR recovery across refresh races

* Avoid duplicate PR recovery refresh work

* Index linked PR refresh aliases once
2026-07-15 15:52:41 -07:00
Brennan Benson 64be819790 fix(runtime): harden watcher and PTY teardown ownership (#8661)
* fix(runtime): retain watcher and PTY teardown ownership

* fix(runtime): restore watchers after interrupted cleanup

* fix(runtime): prevent stale watcher revival

* test(runtime): cover watcher shutdown ownership

* test(daemon): model physical PTY exit

* fix(daemon): keep shutdown terminating when disposal cannot prove exit

A rejecting host.dispose() (unreapable child past its exit deadline) left
the shutdown RPC without its process.nextTick(shutdown) and skipped socket
cleanup in shutdown(), stranding the daemon as an unreachable orphan after
the stale-daemon replacement flow unlinks its socket. Log and continue:
daemon exit reparents the child to init instead of blocking on it.

* fix(runtime): keep local watching alive after an idle-kill deadline miss

An idle child that outlived the exit deadline set shutdownRequested on the
shared desktop supervisor, which has no retire-and-replace path — every
later subscribe rejected supervisor_disposed and the roots were cached
unwatchable, silently ending local file watching for the session. The idle
path owns zero records, so there is no double-watch hazard; the zombie
keeps its capacity reservation until physical exit and the next subscribe
gets a fresh child.

* fix(renderer): resync replayed paired-web file watches

Transparent replay removed the implicit resync the old close-and-rebuild
path provided: a replayed files.watch only reports changes from its own
native setup, so changes during the reconnect gap were silently lost.
Deliver a conservative overflow to consumers once the replayed watch is
ready, matching the overflow-after-interruption contract everywhere else.

* fix(runtime): address teardown review findings

* fix(runtime): retry watches after teardown deadlines

* Fix PTY descendant leaks on forced teardown

* Fix jitter-sensitive terminal lifecycle test
2026-07-15 15:23:35 -07:00
Brennan Benson 8a7cd84bda fix(crash-reporting): coalesce repeated renderer error breadcrumbs (#8800)
* fix(crash-reporting): coalesce repeated renderer error breadcrumbs

The crash-breadcrumb ring holds only 30 entries, and renderer breadcrumbs
arrived via the plain uncoalesced path. A repeating renderer error — like the
ResizeObserver and SSH-rejection storms in #8260 (50+ identical events) —
flushes the entire ring in seconds, erasing the pre-crash trail exactly when a
crash report is about to snapshot it.

Route renderer_error and renderer_unhandled_rejection breadcrumbs through the
existing recordCoalescedCrashBreadcrumb, keyed by name plus message prefix
with a 30s window. Repeats collapse into one entry carrying
suppressedSinceLast, so a storm shows up as "error X fired N times" alongside
29 slots of surrounding context instead of 30 identical copies.

Other breadcrumb names keep the plain path; telemetry trace spans are
unchanged. Renderer breadcrumb routing tests move to a focused
crash-reporting-renderer-breadcrumbs.test.ts (the main suite is at the
max-lines ceiling).

* fix(crash-reporting): coalesce durable renderer traces

* fix: harden renderer crash guards

* fix(crash-reporting): preserve distinct error sources
2026-07-15 14:30:33 -07:00
gatsby74andBrennan Benson 0690be5656 fix(status-bar): sync usage meters with display mode (#8582)
* fix(status-bar): sync usage meters with display mode

* docs(status-bar): correct barColor comment now that fill follows display mode

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-15 14:12:12 -07:00
Brennan Benson 2de4604d76 fix(native-chat): deliver AskUserQuestion answer by option number, not label (STA-1860) (#8840)
* fix(native-chat): deliver AskUserQuestion answer by option number, not label

Claude Code's AskUserQuestion is an arrow-navigate selector: a bare Enter
commits the HIGHLIGHTED default (the first option), and pasted label text
does not move the highlight. Native chat delivered the answer as the option
LABEL text + Enter, so every non-first pick was silently committed as the
first option (STA-1860 Bug A).

Deliver the answer by each option's stable 1-based number instead (the marker
the selector commits on), paced so each keystroke lands after the selector
step renders. Covers single/multi-select, multi-question, and free-text
('Type something') answers. Verified live against Claude Code 2.1.210.

- native-chat-interactive-prompt.ts: buildAskAnswerKeys / AskAnswerSelection /
  AskAnswerKeyGroup / hasAskAnswer; formatAskAnswer kept (index-based) for the
  non-Claude label path
- native-chat-runtime-send.ts: sendNativeChatAskAnswer (paced keystroke groups)
  replaces the old label-line sender
- card + interactive hook thread structured { indices, other } selections

STA-1860

* fix(native-chat): harden AskUserQuestion answer lifecycle

* fix(native-chat): stop stale paced answer writes

* fix(native-chat): cancel paced answers during commit

Cancel stale AskUserQuestion timers before replacement UI can commit, and localize the multi-step sending state in every shipped locale.
2026-07-15 13:41:55 -07:00
Brennan Benson ed2135d248 Preserve original markdown style when editing in the rich editor (#8862)
* Preserve original markdown style when editing in the rich editor

The rich markdown editor re-serialized the whole document into a canonical
style on the first edit, so a one-character change rewrote every non-canonical
construct (`_x_`->`*x*`, `__x__`->`**x**`, `* item`->`- item`, dropped trailing
newline) and produced a large, unexpected diff.

Reconcile the canonical getMarkdown() output back toward the original source
bytes at every disk-bound serialize site (debounced autosave, flush, Cmd/Ctrl+S)
so untouched regions keep their markup and only the edited region changes. A
divergent-base fuzzy patch (diff-match-patch) carries the user's edit onto the
original style; a safety re-parse requires the result to render-equal what the
editor shows, else it falls back to today's canonical output — so content
semantics can never change. The rich editor stays the default for markdown.

Fixes #6080

* Bound markdown reconciliation diff work
2026-07-15 13:33:11 -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
Asurada 40c006122d fix(i18n): correct Chinese translations including styling syntax and terminology (#8723) 2026-07-15 13:05:16 -07:00
a381567bab fix: close Add Project host selector when an SSH credential prompt appears (#8491)
* fix: close Add Project host selector when an SSH credential prompt appears

The SSH password/passphrase dialog (z-50) renders below popovers (z-60),
so a host selector left open during a connect attempt covered the
credential prompt and its input field. Close the selector as soon as a
credential request is queued so the modal prompt is visible and usable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: raise SSH credential prompt above popovers instead of closing the host selector

The credential prompt is a preemptive global modal, but DialogContent's
default z-50 sits below popovers (z-60) and menus (z-70), so any surface
left open while a prompt fires covers the focused password input — the
Add Project host selector was one reproducible case (#8490).

Raise the prompt's overlay/content to the frontmost tier (!z-[140] /
!z-[150], matching link-routing-preference-dialog) so it renders above
every popover/menu, and drop the host-selector close effect: the picker
can stay open and is available again after cancel for an immediate retry.

---------

Co-authored-by: jonghoonlee <jonghoonlee@inocras.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-15 12:45:41 -07:00
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