Skip to content

Sync upstream c5d40565a (conflicts) - #2

Open
WrittenByAI wants to merge 219 commits into
mainfrom
sync/upstream-c5d40565a
Open

WrittenByAI wants to merge 219 commits into
mainfrom
sync/upstream-c5d40565a

Conversation

@WrittenByAI

Copy link
Copy Markdown
Owner

Upstream c5d40565a merged into the fork with conflicts.

Unresolved markers remain in: (none, but agent was unsure)

Fork files upstream also touched: package.json, pnpm-lock.yaml, src/main/github/client.ts, src/main/ipc/github.test.ts, src/main/ipc/github.ts, src/main/runtime/orca-runtime.ts, src/main/runtime/rpc/methods/github.ts, src/main/runtime/runtime-rpc.ts, src/preload/api-types.ts, src/preload/index.ts, src/renderer/src/assets/rich-markdown-editor.css, src/renderer/src/components/editor/EditorContent.tsx, src/renderer/src/components/right-sidebar/SourceControl.tsx, src/renderer/src/i18n/locales/en.json, src/renderer/src/i18n/locales/es.json, src/renderer/src/i18n/locales/ja.json, src/renderer/src/i18n/locales/ko.json, src/renderer/src/i18n/locales/zh.json, src/renderer/src/lib/language-detect.ts, src/renderer/src/store/slices/diffComments.test.ts, src/renderer/src/web/web-preload-api.test.ts, src/renderer/src/web/web-preload-api.ts, src/shared/types.ts


Fork sync conflict report — upstream c5d40565a

  • Merge base: 792e11372 (v1.4.142-rc.1, the fork base recorded in CLAUDE.md)
  • Upstream head: c5d40565a — "Clarify Orca Mobile pairing connection paths (Clarify Orca Mobile pairing connection paths stablyai/orca#9425)"
  • Fork branch: sync/upstream-c5d40565a (tracking fork/main)
  • Merge state: in progress (MERGE_HEAD present), all conflicts already resolved
  • Scale: 1544 files changed, +136238 / −15060

Headline

There were no conflicts left for me to resolve. By the time I inspected the
worktree, git reported "All conflicts fixed but you are still merging":

  • git ls-files -u → empty (no unmerged index entries)
  • no <<<<<<< / ======= / >>>>>>> markers anywhere in the working tree
  • MERGE_RR is empty, so rerere did not silently auto-apply any recorded
    resolution — nothing was resolved from a cached prior decision

So this merge resolved textually on its own. I made no edits. I did not run
git add, git commit, or git merge.

Because a clean textual merge of this size can still break the fork
semantically (upstream refactors a file, git merges it fine, and the fork's
hook is left orphaned), I spent the effort on verifying the fork's integration
points instead. Results below.

Files changed by both sides (the real auto-merge collision candidates)

Of the 50 files the fork changed since the merge base, 19 were also changed
by upstream. Every one auto-merged:

File Verified outcome
package.json, pnpm-lock.yaml tldraw and @tldraw/assets still pinned to 5.2.4
src/renderer/src/lib/language-detect.ts see below — no real interaction
src/shared/types.ts fork issue-image types retained
src/main/ipc/github.ts gh:uploadIssueImage handler intact (line 429)
src/main/github/client.ts still re-exports uploadIssueImage (line 94)
src/main/runtime/rpc/methods/github.ts github.uploadIssueImage registered (line 512)
src/main/runtime/runtime-rpc.ts method present in allowlist (line 239)
src/main/runtime/orca-runtime.ts runtime wrapper intact (lines 403, 14751)
src/preload/index.ts, src/preload/api-types.ts bridge intact (index line 1326)
src/renderer/src/web/web-preload-api.ts (+ test) web/SSH route intact
src/renderer/src/components/editor/EditorContent.tsx lazy TldrawViewer intact
src/renderer/src/assets/rich-markdown-editor.css no fork-specific rule at risk
src/renderer/src/i18n/locales/{en,zh}.json fork canvas keys survived (see below)
src/main/ipc/github.test.ts, src/renderer/src/store/slices/diffComments.test.ts fork test cases retained

Notably, the two files carrying the fork's canvas-notes logic —
src/shared/diff-comments-format.ts and
src/renderer/src/store/slices/diffComments.ts — were not touched by
upstream at all
, so the source: 'canvas' design is uncontested.

Per-area detail

1. Canvas viewing (.tldr → tldraw)

  • Upstream changed: in language-detect.ts, only the .proto mapping
    ('protobuf' → 'proto'), at line ~69.
  • Fork changed: added '.tldr': 'tldraw' at line 28.
  • Why they collided: same file, same map literal — but different, distant
    entries. Git merged both.
  • What I did: nothing; verified '.tldr': 'tldraw' is still present and
    that isTldraw still flows editor-panel-render-model.ts:162 →
    EditorPanelShell.tsx:106,147 → EditorContent.tsx:863 → lazy
    TldrawViewer (EditorContent.tsx:48).
  • Human decision: none.

2. Canvas notes (DiffComment with source: 'canvas')

  • Upstream changed: nothing in the canvas-notes files.
  • Fork changed: the source === 'canvas' predicate and the
    selectedText shape-summary description.
  • What I did: verified diff-comments-format.ts still carries the canvas
    branch and the "Source: canvas" serialization.
  • Also verified the fork's documented footgun holds:
    tldraw-canvas-notes.ts:1 is still import type { Editor, TLShape } from 'tldraw'
    — a type-only import. Per CLAUDE.md, a runtime binding here would drag in
    tldraw's rich-text module and break under Vitest/Node ESM resolution.
  • Human decision: none.

3. Canvas → GitHub issue

This is the surface that crosses main/preload/runtime, and upstream touched
every file in the chain. I traced the whole path end to end and it is
complete and consistent:

renderer  TldrawIssueDialog.tsx / tldraw-issue-image.ts
  ├─ electron  preload/index.ts:1326 ──> ipc 'gh:uploadIssueImage'
  │            main/ipc/github.ts:429 ──> github/client.ts:94 ──> issue-images.ts:130
  └─ web/SSH   web-preload-api.ts:2164 ─> rpc 'github.uploadIssueImage'
               runtime-rpc.ts:239 (allowlist) ─> rpc/methods/github.ts:512
               ─> orca-runtime.ts:14753 ──> issue-images.ts:130
  • CANVAS_ASSET_BRANCH = 'orca/canvas-screenshots' still set
    (issue-images.ts:22), matching the fork's design and its tests.
  • Both transports survive, which matters because AGENTS.md requires the SSH
    case to keep working — the web/SSH branch is the one that would have failed
    silently if upstream's RPC-registry refactor had dropped the fork's entry.
  • Human decision: none.

4. Localization

  • Upstream changed: en.json and zh.json.
  • Fork changed: all five locales, adding namespaces TldrawViewer,
    TldrawSelectionOverlay, TldrawIssueDialog, plus canvas strings.
  • What I did: verified all three fork namespaces are present in all five
    locales (en, es, ja, ko, zh) — parity holds, which is what
    verify:localization-catalog / verify:localization-coverage enforce in
    pnpm lint. Also confirmed upstream did not add any new locale file that
    would be missing the fork's keys.
  • Human decision: none, pending the lint run noted below.

5. max-lines ratchet

AGENTS.md forbids disabling max-lines, so a merge-induced overflow would need
a real refactor. Checked: every file both sides changed that is large
(EditorContent.tsx, orca-runtime.ts, ipc/github.ts, web-preload-api.ts)
is already listed in config/max-lines-baseline.txt as inline-exempt. The
fork's own files are under budget (largest is EditorPanelHeader.tsx at 362
raw lines against a 400/600 budget that skips blanks and comments), and
upstream did not touch them, so their size is unchanged from the pre-merge
state that was already passing.

What a human must still decide

Nothing about conflict resolution — there is no ambiguous hunk awaiting a call.
The open items are all verification, not decisions:

  1. The gates were not run. pnpm typecheck, pnpm lint, and pnpm test
    all require command approval and this session was non-interactive, so
    I did not run them and cannot claim they pass. My verification above is
    static (grep/diff-based). Given the size of this jump — 1544 files, a
    +136k-line diff, including upstream changes to the runtime RPC layer — the
    gates should be run before this merge is committed. Per the fork's own note,
    the test gate requires Node 24.
  2. Upstream's .proto language-id rename ('protobuf' → 'proto') is a
    behavior change in a file the fork also edits. It does not affect .tldr,
    but if anything in the fork keys off the string 'protobuf', confirm it.
  3. Merge is left uncommitted, by instruction. MERGE_HEAD is still present
    and the index is fully staged. Concluding the merge is the human's call.

Verdict

No conflict markers remain, and no fork intent was lost that I could detect by
static inspection. The residual risk is limited to what only a full
typecheck/lint/test run can surface.

Jinwoo-H and others added 30 commits July 14, 2026 16:00
Co-authored-by: Orca <help@stably.ai>
…tablyai#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.
…ning (stablyai#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
…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>
…l errors (stablyai#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 (stablyai#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 stablyai#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.
* feat(mobile): add notification opt-in onboarding

* fix(mobile): deliver alerts despite desktop focus
…ons (stablyai#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 (stablyai#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 stablyai#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 stablyai#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>
…ai#8789)

* Fix mobile cutover activation and usage refresh loops

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

* Fix restored mobile terminal state parity

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

* Fix migrated PTY workspace attribution

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

* Fix overlapping mobile terminal surface swaps

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

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
…isi (stablyai#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.
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
…#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>
…tablyai#8842)

* fix(terminal): reset Windows focus reporting after agent turns

* fix(terminal): preserve focus mode across Windows Codex turns
…s for pairing (stablyai#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 stablyai#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.
…port (stablyai#7507)

* feat(ssh): support Kerberos/GSSAPI hosts via the system OpenSSH transport

ssh2 has no gssapi-with-mic support, and adding it would mean forking its
protocol layer plus packaging the kerberos native module for three
platforms. Instead, route GSSAPI hosts through the existing system-OpenSSH
transport, which delegates Kerberos (tickets, SSPI on Windows) to the
platform ssh binary.

Two tiers, because RHEL-family distros enable GSSAPIAuthentication
globally in /etc/ssh/ssh_config and ssh -G therefore reports it for every
host:

- Targets whose ~/.ssh/config Host block explicitly sets
  GSSAPIAuthentication yes (imported as target.gssapiAuthentication) try
  system ssh first, falling through to ssh2 so key auth and credential
  prompts still work when no ticket is available.
- When ssh2 exhausts key/agent auth and the ssh -G-resolved config
  enables GSSAPI, retry over system ssh before prompting for credentials,
  so Kerberos-only hosts on distro-default configs connect without a
  password prompt. Hosts where keys work never leave the ssh2 path.

Manual targets flagged for GSSAPI pass -o GSSAPIAuthentication=yes
explicitly since they bypass ssh_config. Both tiers work headless (no
credential callbacks required).

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

* fix(ssh): harden GSSAPI transport selection (review fixes for PR stablyai#7507)

Review fixes on top of the Kerberos/GSSAPI feature branch (s546126/kerberos-ssh):

- HIGH: reset useSystemSshTransport on the ssh2 fall-through. doSystemSshProbe
  sets the flag before spawnSystemSshCommand, which throws synchronously when no
  system ssh binary is on PATH (outside the probe try/catch). The proactive
  fall-through previously reset only 2 of 3 transport fields, so exec/sftp kept
  routing through the failed transport - breaking GSSAPI on Windows-with-Git-ssh
  and headless Linux.
- MEDIUM: throw a cancellation error (not the stale ssh2 authError) when a
  disconnect supersedes the reactive probe mid-flight, and guard connect()'s
  catch on disposed, so a deliberate disconnect is not overwritten with
  auth-failed.
- MEDIUM: skip the encrypted-key passphrase prompt when the GSSAPI fallback
  applies, so a Kerberos ticket is tried before prompting; the general prompt
  still fires if the probe fails.

Adds 3 mutation-verified regression tests and hardens two existing tests to
assert the probe actually ran. Not connected to any PR remote.

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

* fix(ssh): isolate GSSAPI system transport

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

---------

Co-authored-by: s546126 <268420947+s546126@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
…840)

Re-pairing a desktop that was already paired created a duplicate host card (STA-1840). Pairing now resolves the durable host identity by the desktop's pinned publicKeyB64 and reuses the existing id/name, collapses any already-stored duplicates for that key, clears stale relay overlays on a direct-only re-pair, fails closed on unreadable storage, and closes the host's client on pairing success so a reused id reconnects on the newly-paired endpoint. Mobile only. Full mobile suite (1,719 tests) + typecheck/lint/format pass.
…blyai#8709)

Reconstruct remote POSIX commands with bounded printf arguments so non-POSIX SSH login shells can forward them without requiring remote base64. Preserve relay and system-SSH stdin, and centralize login-shell flag selection for csh/tcsh compatibility.\n\nValidated against real csh and tcsh OpenSSH targets with built-in and system SSH, including cold relay deployment, stdin upload, PTY I/O, file mutation, and reconnect.
…he sidebar roster (stablyai#8825)

* fix(agent-status): reap finished Claude named agents/teammates from the sidebar roster

stablyai#8522 stopped one-shot subagents from squatting as idle rows, but named
background agents (Workflow/orchestration/ultracode lanes and agent-teams
teammates) still piled up permanent "Idle - <type>" child rows for the rest
of the session — the reported regression (11 idle rows under an
"Orchestration Messages" pane, all idle 3-6h after finishing).

Root cause, confirmed against live hook captures (claude 2.1.210): named
agents get teammate-shaped ids (a<name>-<hex>) AND now appear in Stop's
`background_tasks` as `type: "teammate"` entries whose status stays "running"
forever — even after the agent finished. The old code read that shape as a
"resumable teammate", so SubagentStop only marked it idle and the fold never
reaped it (a present teammate task kept hasTeammateTypedTask true). The rows
never left.

Fix: the roster now tracks only WORKING children.
- SubagentStop removes the child outright (teammate-shaped or not) — it is
  the reliable finish signal; the teammate task's "running" status is not.
- TeammateIdle removes by name as the fallback when a SubagentStop is lost.
- A lead Stop's background_tasks still reaps unlisted children: hyphen-free
  one-shots always, and teammate-shaped rows once a complete inventory shows
  no teammate-typed task at all. A live named agent whose id never appears is
  kept only while a teammate-typed task is still present (the done-gate).
- Hydration drops persisted idle snapshots so a restart can't re-pile them.

Verified live in a dev Electron instance driving a real Claude TUI that spawns
four named background agents: pre-fix the pane resolves to done with four
persistent "Idle - <probe>" rows; post-fix each row disappears the instant its
agent finishes and the roster drains to empty (done, zero child rows).

Tests: roster + row-lifecycle + hook-listener suites rewritten to the
working-only semantics, grounded in the captured 2.1.210 hook stream
(126 passing). Typecheck + oxlint clean.

* fix(agent-status): prune persisted idle Claude children

* fix(agent-status): persist Claude idle-row hydration cleanup

* fix(agent-status): avoid ambiguous teammate idle cleanup

* fix(agent-status): reconcile replacement children at roster cap
AmethystLiang and others added 30 commits July 18, 2026 22:10
* Guard add-review-note chord to prevent remount (product B)

- Scoped guards consume chord at composer level to prevent remount and leakage
- Handle OS key-repeat: ignore when no draft, consume when one is mounted
- Clear stale block keys in markdown preview when content renumbers
- Flush pending selection before reading targets to fix timing races

* fix(editor): repair add-review-note guard tests and close remaining chord gaps

- Update the product-B guard tests to the Mod+Shift+A default binding
  (stablyai#9257 retired Mod+Alt+N as AltGr-unsafe); they asserted the old chord,
  so seven landed red and the rich-editor repeat test passed vacuously.
- Monaco: recompute the annotation target from the live selection at
  keydown instead of the render-lagged ref, so a chord right after a drag
  cannot open on the previous selection or miss a fresh one.
- Preview: key the stale-block-key cleanup on renderedContent (the DOM
  the block keys live in), which can lag content during external-edit
  section preservation.

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

* fix(editor): mirror shortcut-guard refs in effects instead of render body

CodeRabbit on stablyai#9412: render-body ref writes can leak from a render pass
React replays and discards. Move the state->ref mirrors for
commentPopoverRef / shouldShowMarkdownAnnotationsRef (MonacoEditor) and
activeAnnotationBlockKeyRef (MarkdownPreview) into effects; same-tick
keydown paths keep their eager event-handler writes.

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

---------

Co-authored-by: Orca <help@stably.ai>
…tablyai#9420)

Raise the floating terminal panel from z-30 to z-45 so it sits above
notification cards (z-40) but below the modal layer (z-50), preventing
notifications from burying the terminal. Raise the toggle button to z-46
to keep it clickable where it overlaps the panel.
* feat(ssh): download folders from remote explorer

* fix(ssh): harden remote folder downloads

* Add missing getRepo stub to worktree cwd test mock

Restoring headless mobile tabs looks up the repo for the active
worktree id; the mock lacked getRepo, so the test only passed
incidentally. Add it explicitly and return undefined since wt-1
is a worktree id, not a registered repo.

* Enable SSH folder downloads, gated for system-SSH connections

- Folder downloads require SFTP, unavailable on system SSH (which
  offers only raw file operations). Add supportsFolderDownload flag
  to gate the feature in the UI layer.
- Reject symlinks at directory-entry level, preventing tree escapes
  and eliminating unnecessary stat calls.
- Check abort signal before opening dialog for better responsiveness
  when renderer closes.
- Log cleanup errors without re-throwing to preserve underlying
  transfer failures.

* Gate SSH folder downloads to SFTP-capable connections

Enforce fail-closed gating and add Windows path traversal validation to
ensure downloads are only available when explicitly supported and safe.

* Gate SSH folder downloads to SFTP-capable connections

Enforce fail-closed gating and add Windows path traversal validation to
ensure downloads are only available when explicitly supported and safe.

* fix(ssh): keep provider types under max-lines after main merge

Move FolderDownloadOptions next to the SFTP download implementation and
narrow IFilesystemProvider.downloadFolder options to AbortSignal only so
types.ts stays within the 300-line oxlint budget when merged with main.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
…#9070)

Only offer 'Proceed Anyway (Unsafe)' if the connected remote runtime
advertises browser.certificate-trust.v1 support. Older runtimes cannot
honor the request and would fail silently, creating a false affordance.

Centralize certificate error normalization to prevent divergence
between main and renderer certificate matching, and unify URL redaction
for Kagi session token stripping across load-error paths.
…lyai#5785)

* Allow hiding Orca Mobile sidebar button once a device is paired

Introduce a shared external store (`paired-mobile-devices.ts`) to cache
and synchronize the paired mobile device list across the sidebar,
Settings pane, and Mobile page. This prevents redundant backend IPC
requests when multiple surfaces mount simultaneously.

Using this shared pairing status, display an inline "Hide from sidebar"
control next to "Orca Mobile" in the navigation sidebar once a device
has been paired. This allows users to easily declutter their sidebar
after completing setup.

* Handle device load errors with recovery on focus/reconnect

- Add error flag to distinguish failed device loads from empty device lists
- Recover from transient startup IPC failures by retrying on window focus or online
- Validate revoke device success before routing or showing success toast
- Only show hide control after first device is actually paired
- Use snapshot getter to avoid manual device ref tracking in MobilePane

* Handle device load errors with recovery on focus/reconnect

- Shared module-level listeners for failed load recovery reduce IPC spam
  compared to per-consumer event listeners on every focus/online event.
- Optimistically remove revoked devices if post-revoke reload fails,
  keeping success toast and intro routing correct.
- Prevent repo/branch label overflow with CSS text truncation.
- Fix HTML entity rendering: use plain spaces instead of &nbsp; in
  translations so React text nodes render them correctly.
- Add comprehensive tests for the paired devices hook covering load,
  navigation, revoke, and error recovery paths.
…9437)

Resolve Microsoft Store PowerShell App Execution Aliases to their real package executable before ConPTY spawn, while preserving explicit PowerShell choices and the existing fail-safe fallback chain.
…tablyai#9398)

* fix(composer): show full worktree path on hover in Run on dropdown

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

* refactor(composer): use native title tooltip for Run on paths

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

* fix(composer): reliable native-style tooltip for Run on paths

The native title attribute won't re-trigger between adjacent rows in
Chromium, so the tooltip flickered/dropped as you moved down the list.
Use the shared (controlled) Tooltip instead, styled compact + arrowless
to read like an OS tooltip; the app-level provider's skip-delay makes
row-to-row hovers reliable.

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

* fix(composer): stop Run on path tooltip wedging shut

Radix's uncontrolled hover state could flash the tooltip open then
wedge it closed inside the cmdk list (shared pointer-transit state
getting stuck), so re-hovering never reopened it. Drive open state
with a controlled hover-intent handler (open after a delay, close
after a short grace) so re-entry always reopens and transient
enter/leave thrash debounces to a stable state.

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

* fix(composer): rebuild Run on path tooltip as a portal (no loop/wedge)

Both the native title and Radix Tooltip (controlled or not) misbehaved
inside the cmdk list: flashing, wedging shut, or opening/closing in a
loop. Replace with a self-contained tooltip that renders a fixed,
pointer-events-none portal on document.body and only changes state on
pointer enter/leave/down (never pointermove). Opening it cannot reflow
the list or become the hover target, so it structurally cannot create
the enter/leave feedback loop.

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

* fix(composer): trigger Run on path tooltip on the path line, always below

- Scope the hover trigger to the truncated path line itself (not the
  whole row), so only hovering the path reveals it.
- Always position the tooltip below the hovered line for predictable
  placement (was flipping above near the viewport bottom).

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

* fix(composer): anchor Run on path tooltip under the path line

The old placement anchored to the window's right half when the trigger
sat past viewport-center, which shoved the tooltip to the far right edge
of the window, disconnected from the dropdown. Always left-align it
directly under the hovered path line and cap max-width to the remaining
viewport width so long paths wrap instead of overflowing.

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

---------

Co-authored-by: Orca <help@stably.ai>
… Windows (stablyai#7742) (stablyai#9311)

* test(e2e): prove the terminal daemon survives a main-process crash on Windows (stablyai#7742)

Add a win-crash-survival e2e harness (sibling to win-update-e2e) that
force-kills ONLY the packaged app's real Electron main (resolved via
app.evaluate -> process.pid, /F no /T) and asserts the detached
orca-terminal-daemon.exe plus its ConPTY shell survive with no pwsh
0xE9 FailFast, then that a relaunch re-adopts the SAME daemon and the
reattached UI binds to the SAME survivor shell (proved via a per-shell
env sentinel read back through the restored terminal).

This guards the stablyai#7742 fix (standalone relocated daemon that outlives
main death) against regression. A directional `--expect orphaned`
profile fails on a fixed build, keeping the survival assertions honest.

Windows-only; reuses win-update-e2e app-driver/daemon-process modules.

* test(e2e): harden Windows crash-survival proof

* test(ci): keep crash survival gate durable

* test(e2e): tolerate restart hydration navigation

* test(e2e): prove exact shell input after crash

* perf(ci): avoid crash harness installer rebuilds

* test(ci): harden crash survival evidence and cost

* test(e2e): fail closed on authoritative crash target

* test(e2e): fail closed on crash liveness evidence

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(claude-accounts): block adding a duplicate Claude account (stablyai#6616)

Adding the same Claude subscription via Settings -> AI Provider Accounts
appended a second managed account with an identical email/organization,
producing duplicate rows that confused account selection and rate-limit
tracking.

doAddAccount now checks the captured identity against existing accounts
before appending and rejects the add with a clear message when it matches.
Identity is keyed on email + organizationUuid scoped to the runtime
(host/WSL distro), so the same email can still be added under a different
organization, and both sides are normalized so a legacy account persisted
before the runtime fields existed still matches.

* fix(claude-accounts): skip rollback I/O for duplicates
…ablyai#8911)

The right-sidebar Checks panel merge control merged the PR/MR immediately when
a strategy was chosen from its dropdown, with no confirmation — so opening the
dropdown to switch strategies (e.g. Squash -> Merge commit) merged on the spot.
Every other PR/MR merge surface (PullRequestPage, GitHubItemDialog, TaskPage)
already confirms first, and the close/reopen path in this same hook confirms too;
only the hosted-review merge path was missing the gate.

Add the same confirmation dialog to the hosted-review handleMerge, provider-aware
so it reads "Squash and merge PR #N?" / "Squash and merge MR !N?". Selecting a
strategy now asks before merging; the merge still runs when explicitly confirmed.

Fixes stablyai#7943
…idebar (stablyai#9463)

When a runtime host identity (runtime:<envId>) is removed from the saved
list, retire the repos, project host setups, and worktree rows it owned so
the same physical checkout stops duplicating in the sidebar.

setRuntimeEnvironments now diffs the previous in-memory saved list against
the new one and purges state owned by any environment that just left, routed
through the existing worktree purge cascade (tabs/PTY/browser/editor/agent
maps). Scoped to the removal diff, not an absolute keep-set, so a serving
instance's locally-persisted runtime-stamped repos (whose env id was never in
this instance's saved list) are never torn down.

A tombstone set (removedRuntimeEnvironmentIds) guards the three repo-catalog
merges so an in-flight fetch for a just-removed env can't re-add purged repos,
while a runtime env merely absent from a not-yet-hydrated saved list still
merges normally.

Refs stablyai#8881
…ng (stablyai#9292)

* fix(source-control): bound Create PR eligibility probe so it can't hang

The local Electron eligibility path had no renderer-side timeout, so a hung
main-process git/gh subprocess (plausible on Windows) left the Create PR header
stuck in its "Checking whether this branch can create a pull request..." loading
state with the button permanently disabled. The runtime-hosted path was already
bounded by callRuntimeRpc's 30s timeout; the local path was not.

- Wrap the local window.api.hostedReview.getCreationEligibility call in a 30s
  timeout so a never-settling probe rejects instead of hanging.
- On probe failure/timeout, synthesize a local-status blocker snapshot
  (dirty -> no_upstream -> needs_sync -> needs_push, mirroring the main-process
  ordering) so a dirty branch still offers the commit/preparation intent and a
  clean unpublished branch the publish intent, instead of an inert disabled
  button. Fall back to the retryable failed state only when local status can't
  determine a blocker.

Extracts the eligibility snapshot builders into their own module to stay under
the max-lines cap. Provider-aware copy and host-scoped (local/SSH/runtime)
behavior preserved; no git commands changed.

* fix(source-control): guard synthesized eligibility fallback (default branch, provider parity)

Mirror the main-process canReturnLocalBlocker guard in the renderer's
probe-failure fallback so a timed-out/failed probe can't synthesize a
'dirty'/commit blocker on the default branch or a detached HEAD — which
would have surfaced an enabled Create PR that commits onto the base
branch. The ahead-only needs_push case is likewise dropped to match main
(it won't offer a push it can't auth-check first).

Infer the provider from the repo remote host so a GitLab (etc.) repo with
no linked review shows its own review copy during loading and after a
failure instead of the GitHub default.

Also: assert the timeout error by type, and rename the snapshot test to
match its module.

* fix(source-control): keep eligibility timeout stable

* fix(source-control): keep eligibility timeout bounded

* test(source-control): assert eligibility effect dependencies

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* Fix diff editor flashing "Loading…" on every save

Saving a file open in a single-file diff tab blanked the editor to a centered "Loading…" for a frame on every content-changing save. The DiffViewer's React key embedded the modified-content signature, so each save changed the key and forced a full unmount/remount of the Monaco diff editor — which shows its built-in loading placeholder while re-initializing, and also discarded scroll position and undo history.

The modifiedModelKey → modifiedModelPath rotation already refreshes diff content in place without a remount, so the signature in the outer key was redundant. Drop it; keep the view-state scope and explicit reload nonce for a stable per-tab identity.

Adds a regression test asserting a save does not remount the diff editor.

* Harden in-place diff model refresh

* Preserve diff view state across model swaps

* Fix retained diff model disposal race
…abases (stablyai#8924)

* fix(agent-history): keep the app responsive while scanning huge OpenCode databases (stablyai#8864)

Opening or refreshing Agent Session History froze the entire app when
OpenCode's opencode.db had grown to multiple GB (reporter: 29 GB with
20+ live opencode writers). All OpenCode SQLite reads ran synchronously
(node:sqlite DatabaseSync) on the Electron main-process event loop, the
discovery query evaluated a COUNT+json_extract subquery for every
session before LIMIT, and the preview query JSON-parsed every part blob
of each session. Live writers bump session.time_updated continuously,
so the mtime parse cache missed every 15s/focus/manual refresh and the
multi-tens-of-seconds freeze repeated forever.

Fix:
- Run OpenCode SQLite discovery and per-session parsing on a persistent
  worker thread (lazy spawn, unref'd, idle teardown, FIFO dispatch,
  per-call timeouts, crash-loop cap). Faults surface as per-source scan
  issues instead of stalls; other providers' results always arrive.
- Fall back to the in-process reader when no worker bundle exists,
  surfacing a degraded-mode scan issue so a persistent fallback cannot
  silently reintroduce the hang.
- Bound the queries: sort+LIMIT sessions before computing message
  counts, and source previews from the newest 100 messages via the
  (session_id, time_created, id) index instead of scanning every part.

Measured on a 22 GB synthetic DB matching the reporter's shape: main-
process IPC RTT during a fully cache-invalidated scan went from a
32.8 s continuous block (UI click timeout) to 1 ms max, with the panel
populating normally (previews and message counts intact).

* fix(agent-history): drop the cached worker handle after a clean exit and document worker-client exports

A worker that exits cleanly on its own left this.worker pointing at the dead
thread; the next request would post into it and stall to its timeout instead
of respawning. Clean idle exits now drop the handle without counting as a
death. Also adds JSDoc to the new exported scan-worker surfaces.

* fix(agent-history): keep OpenCode scans off the main thread

* fix(agent-history): align OpenCode recency ordering
…d false exits (stablyai#9258)

* fix(terminal): confirm the agent from ConPTY console presence to avoid false exits

On Windows the foreground scan is a whole-process-table PowerShell fork
that, under load, exceeds its timeout or returns an incomplete snapshot —
the completion coordinator then reads the shell as the foreground and
fires a false "agent done" while the agent is still working.

While a recognized agent is still active, confirm it with a cheap ConPTY
console-membership read instead: a child process still attached to the
console means the agent is working, so keep it without the whole-table
scan. Fall through to the authoritative scan only when the console is
shell-only (the agent likely exited). No-op off Windows, and never worse
than the existing degraded-scan fallback.

* fix(terminal): apply the ConPTY console-presence check on the daemon foreground path

Windows PTYs are hosted by the terminal daemon, whose foreground-identity
refresh (not the local provider) is what runs by default — it retired the
cached agent on a timed-out or incomplete CIM scan, so foreground reads fell
back to the shell and fired a false "agent done" while the agent was working.

Mirror the local-provider fix on the daemon path: a degraded scan
(available:false) no longer retires the identity; an authoritative scan that
resolves no agent is confirmed against a ConPTY console-membership read before
retiring (an incomplete snapshot with a child still attached keeps the agent);
and the sync foreground read serves the cached agent across a shell fallback on
Windows (an unreliable exit signal under load) until the background refresh
authoritatively retires it. The membership read stays off the sync path.

* fix(terminal): exclude the console-list helper's own process from ConPTY membership

The console-membership helper attaches to the console to read it, so
GetConsoleProcessList counts the helper's own forked process. A bare shell
therefore read as [helper, shell] and looked like it still had a child, so a
genuine shell-only console (an exited agent) was never detected — the foreground
refresh held the exited agent's identity indefinitely. Drop the helper's own pid
before judging membership; a remaining set of only the shell (or the
AttachConsole-failure fallback) is not child proof. Fixes real-exit detection on
both the daemon and local foreground paths.

* fix(terminal): require conclusive ConPTY exit evidence

* fix(terminal): absorb delayed ConPTY helper errors
* Make plain-text file:// links clickable in the terminal

Printed file:// URIs (e.g. a report path echoed by a tool or agent) were
neither http links nor bare filesystem paths, so the terminal's URL and
local-path detectors both skipped them and the link was dead.

Orca already resolves and opens file:// URIs for OSC 8 hyperlinks. Reuse
that exact resolver for plain-text URIs so a printed file:// behaves the
same whether or not the emitter wrapped it in an escape sequence:

- Promote the (dependency-pure) file-url target resolver into src/shared
  so the OSC path and the new plain-text path share one implementation.
- Add a file:// detector that decodes the URI to a filesystem path and
  routes it through the existing file-link pipeline (existence probe +
  openDetectedFilePath), so line/col anchors, %20, Windows drive paths,
  html-in-browser, editor reveal, and SSH/runtime resolution all just work.

Lines without file:// are unchanged: the pass short-circuits to the prior
result, so only file://-bearing lines gain a link.

- Add unit + integration coverage for detection, decoding, and no-double-link.

* Harden plain-text file URI detection

* Split terminal file link detection modules
* Standardize per-repo Source Control AI save UX with global recipe patter

Extract draft/save logic into `useRepositorySourceControlAiGlobalUx` hook and
serial persist queue, matching how global action recipes save: selects persist
immediately, CLI args and command template draft until per-action Save.

* Support draft/commit for per-repo custom-command input

- Drafts custom commands locally on keystroke without backend writes
- Commits to persistence only on blur or mode selection change
- Adds forceRepoMode local state to fix mode-select snapping with empty fields
- Mirrors existing pattern used for action recipe text editing
- Improves persist queue with repo-switch safety (returns boolean, pins repoId at schedule time)
- Adds comprehensive test coverage for settings hook and persist queue

* Fix forceRepoMode logic to preserve REPO intent mid-edit

Always set forceRepoMode based on field content instead of
only when a repo command is present. This preserves REPO
intent when the field is empty during editing, only exiting
when a value is explicitly entered. Add test for draft
discard and revert behavior.
* Restructure mobile pairing setup into a clear stepped layout

* Rework mobile pairing UI: radio-style path selector with sign-in gate fo

* fix(mobile): resolve mobile-pairing review findings

Applies the code-review findings for the reworked pairing UI:

- Clear a displayed Relay QR on sign-out in both MobilePage and MobilePane
  (the rework dropped the wasSignedInRef watcher, leaving a stale Relay QR
  next to a "sign in required" prompt). Anywhere stays selected; the QR
  re-mints as local-only. [#1, #3]
- Extract useMobilePairingConnectionMode so both panes resolve the saved
  preference identically instead of duplicating the state + resync effect. [#6]
- Delete ~24 i18n keys the rework left unreferenced; sync locale catalogs
  to parity. [#4]
- Give the connection-path radiogroup roving tabindex + arrow-key nav and
  document why it diverges from SettingsSegmentedControl. [stablyai#7]
- Add MobilePane.test.tsx (previously untested safety logic) and a
  MobilePage saved-local-only restore test. [#2, #5]

Verified: 22 targeted tests pass, typecheck + oxlint clean, localization
catalog/coverage and max-lines ratchet green.

* fix(mobile): resolve adversarial-review findings for pairing UI

Address accepted findings from the mobile pairing UI rework:

- MobilePane: add a request-generation epoch so a late getPairingQR
  response can't paint a stale Relay QR after sign-out, a mode switch,
  or an address change; arm rotation when discarding a pending mint.
- MobilePage: on sign-in, upgrade a signed-out local-only fallback QR to
  Relay (invalidate + rotate-regenerate); handle null->connected too.
- Extract useMobilePairingQrInvalidation so both sign-in/out edges and
  cross-window preference syncs invalidate/re-mint the QR consistently.
- MobilePane: clear + rotate the QR when the selected address changes
  (manual pick or refresh-driven) so it can't encode the old endpoint.
- MobilePairingConnectionOptions: guard the Sign in CTA on configured;
  show an Unavailable panel on unconfigured builds instead of dead CTA.
- Drop the duplicate sign-in helper from MobilePairingSetupSection.
- Remove dead i18n keys (title, recommended, signInToGenerate) and add
  relayUnavailable across all locale catalogs.
- Add tests: deferred sign-out/mode-switch races, sign-in upgrade,
  cross-window sync, unconfigured build, arrow-key radiogroup.

TODO left for the relay-label-honesty finding: getPairingQR does not
expose the actually-encoded mode when an automatic offer degrades to
local-only, so the mismatch can't be surfaced without a new return field.

* fix(mobile): resolve relay-pairing deep-review regressions

- MobilePane: invalidatePairing now clears loading so a superseded
  mid-flight generate can't wedge Generate disabled forever
- MobilePage: stop auto-minting a local-only QR under the Relay label
  when signed out with Anywhere; gate Step 2 auto-generate and the
  Generate button on a shared canMintMobilePairingOffer helper, align
  with Settings, clear QR + loading on sign-out, mint Relay on sign-in
- qr-invalidation: clear pairQrDataUrl (and loading) on every
  invalidation path so a stale QR can't stay scannable during rotation
- Strengthen MobilePane/MobilePage tests for the aligned behavior and
  stuck-loading coverage
- Translate relayUnavailable in es/ja/ko/zh

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

* fix(mobile): refuse to mint QR when signed-out with Anywhere selected

Replace silent degradation of Anywhere mode to local-only QR with explicit
refusal. Add canMintMobilePairingOffer guard across all mint paths (address
change, network invalidation, connection mode switch). This ensures the UI
honestly encodes the selected pairing path. Improve keyboard focus visibility
on the path selector by adding a persistent focus ring.

* fix(mobile): detect and flag Relay provisioning degradation

When Relay provisioning fails during automatic (Anywhere) pairing offer creation, the offer silently degrades to local-only. This confuses users who selected Anywhere expecting cellular capability.

Add connectionMode field to the pairing offer result to expose what the offer actually encodes. The UI now flags degradation when the offered mode mismatches the user's selection.

Also move credential rotation logic to the main process: rotate when requested mode differs from the pending token's encoded mode. This ensures QR codes displayed under an old policy can't pair under a new one, and windows reminting after preference sync converge on one token.

Rename MobileRelayBetaAvailability → MobileRelayBetaNotice.

* test(mobile): verify pairing codes don't flash during Relay mint

Assert that the pairing QR and URL remain hidden while the Relay mint is
pending, preventing confusing intermediate states when signing in unlocks
Relay.

---------

Co-authored-by: Orca <help@stably.ai>
Resolved by fork-sync agent; see report.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.