Commit Graph
6883 Commits
Author SHA1 Message Date
NeilandOrca 2a80df44ea chore(editor): trim move-fix comments to the why; drop redundant rename quiesce
Co-authored-by: Orca <help@stably.ai>
2026-07-20 23:25:27 -07:00
NeilandOrca 13f83e894d fix(editor): prefix-suppress the move root so late tabs under a dir move aren't flagged (round 8)
Co-authored-by: Orca <help@stably.ai>
2026-07-20 22:04:47 -07:00
NeilandOrca cca28dc218 fix(editor): scope move rekey to the initiating execution host (round 6)
Co-authored-by: Orca <help@stably.ai>
2026-07-20 21:42:43 -07:00
NeilandOrca 0e6bf30abb fix(editor): don't consume move-echo provenance in the safety-net verify (round 5)
The round-3 proactive post-commit verify ran resolveLiveMoveVerification,
which consumed pendingSelfMoveEcho. On FSEvents/SSH the real destination
watcher event reliably lands AFTER the fast local read, so it then found no
provenance, took the immediate changed-on-disk mark (no baseline check when
there is no recent self-write), and raised a false conflict banner on the
just-moved dirty tab — the exact data-loss this change removes.

The proactive verify is a safety net: it now releases the autosave gate but
leaves the provenance, so a later destination event is still recognized as
the move's own echo and content-verified. Only a real watcher event consumes
the provenance. Keeping it is safe — every watcher consumer verifies by
content, which is strictly safer than the immediate mark.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 19:41:30 -07:00
NeilandOrca adbb913ae6 fix(editor): review round 4 (gate strand, cross-worktree verify path, leak)
- Don't install the move-echo verify gate on a tab already showing the
  changed-on-disk banner: it's autosave-suspended via externalMutation and
  verification skips a 'changed' tab, so the gate would strand forever.
- Content-verify reads each moved tab's own filePath. A cross-worktree/
  floating tab's relativePath is relative to its own root ('../…') and must
  not be joined onto the initiating worktree path (would read the wrong file
  and raise a false conflict banner on unsaved work).
- Coordinator settles the in-flight source suppression in a finally so a
  throw between rename and commit can't leak it, and only after the rollback
  rename so a late forward-rename delete stays suppressed.
- Migrate pendingEditorReveal.fileId across the rekey (matcher prefers it).

Co-authored-by: Orca <help@stably.ai>
2026-07-20 19:26:55 -07:00
NeilandOrca 17bd38c8a6 fix(editor): resolve the move verify gate proactively (review round 3)
The rekey gates every dirty moved tab pending a destination content check,
but verification only ran when a watcher event arrived for that path. If the
watcher was down, throttled, or the event coalesced away, the gate never
cleared and autosave stayed suspended for the tab.

The coordinator now drives verification for every tab it gated once the
rename has committed, so the gate resolves on its own. That makes the
destination-side event latch redundant, so the in-flight tracker is
source-only again.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 19:11:06 -07:00
NeilandOrca a3d029a75c fix(editor): review round 2 (mirror ordering, rollback error, diff sources)
- Close the host mirror tab only AFTER the rekey commits (capture pre-rekey
  resolution first): a rekey collision after a successful rename no longer
  desyncs the host by closing its authoritative tab (high).
- Surface a failed inverse rename instead of swallowing it: the thrown error
  now states the on-disk move may remain at the new path (high).
- Restrict diff-tab retargeting to staged/unstaged (purely path-derived ids);
  branch/commit diffs carry compare metadata and combined 'Changes' is
  worktree-rooted, so rebuilding them from path would produce a wrong id (med).

Co-authored-by: Orca <help@stably.ai>
2026-07-20 18:03:07 -07:00
NeilandOrca eb28a4a699 fix(editor): address review round 1 (4 majors)
- Coordinator now propagates the rekey result: a collision/stale AFTER a
  successful on-disk rename triggers an inverse rename + throws, instead of
  reporting success with the source tab stranded at a vanished path (#1).
- Cross-worktree: affected set spans all worktrees at the source path, sub-ops
  scoped per (worktree, owner), and the rekey partitions tab/group/tab-bar
  migration by each file's own worktree (was applied under one scope) (#2).
- Diff tabs: single-file unstaged diff tabs are now retargeted on a directory
  move (rebuild the diff id + relative path) instead of stranding (#3).
- Mirror close-notify moved to AFTER a successful rename so a failed rename
  can't desync the host by closing its authoritative tab (#4).

Adds tests: collision->inverse-rename, diff-tab retarget. 6698 tests green.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 17:56:09 -07:00
NeilandOrca 0964b84338 feat(editor): mirror-safe move — detach moved mirrored tab + close-notify host (stage 5)
The atomic rekey changes a tab's id, so a moved mirrored tab would be culled by
the host snapshot (losing its draft) or resurrect the old path. Ship the safe
minimum: the rekey detaches a moved tab from the host mirror
(mirroredFromRuntimeSession cleared) and the coordinator close-notifies the
host's old-path tab (close intent suppresses re-mirroring). Prevents the
data-loss/resurrection; the moved tab becomes companion-local. The full
host-rekey path-change protocol (preserving mirror ownership) is a documented
follow-up.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 17:42:10 -07:00
NeilandOrca a41117b126 test(editor): end-to-end coordinator move test (stage 4 done)
executeOpenEditorPathMove renames on disk, retargets the session in place with
draft/dirty/baseline preserved + gate/provenance installed, settles the in-flight
transaction; on rename failure the store is byte-identical and the transaction is
released.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 17:38:02 -07:00
NeilandOrca 42712ec4a9 refactor(editor): remove the dead TTL self-move registry (stage 4c)
The coordinator + op-scoped in-flight suppression + on-OpenFile provenance fully
replace the time-bounded self-move registry, so delete it and its two helper
modules (record-self-move-for-open-tabs, rename-open-editor-tabs-path). The
watcher source filter now uses only isActiveMoveSourcePath and the verification
trigger only the tab's pendingSelfMoveEcho. Rewrote the self-move test suite onto
the new primitives. 7225 tests green.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 17:37:08 -07:00
NeilandOrca fb2aa87796 feat(editor): move coordinator drives rename/drag/undo/redo/untitled (stage 4b)
executeOpenEditorPathMove is the single transaction for every in-app move:
quiesce affected saves -> op-scoped begin (per runtime owner) -> on-disk rename
-> atomic in-place rekey (installs the content-verify gate/provenance) -> settle
-> re-verify any destination echo latched before the rekey. On failure the store
is untouched. Verification now triggers off the on-OpenFile provenance (consumed
on resolve) and the watcher latches pre-rekey destination events. Wired into all
five call sites; old renameOpenTabsPathOnDisk + separate remap removed from them.
2751 tests green. (TTL self-move registry now dead; removed next.)

Co-authored-by: Orca <help@stably.ai>
2026-07-20 17:33:50 -07:00
NeilandOrca e45f4d4f31 refactor(editor): remap moves via atomic in-place rekey, not close+reopen (stage 4a)
remapOpenEditorTabsForPathChange now builds an owner-aware rekey plan (plain-path
id to the first owner, owner-qualified to the rest; previews resolve their source
to the moved edit's new id) and applies it via rekeyOpenFilesForPathChange in one
commit — preserving the full OpenFile + cursor/view/group/MRU state and closing
the close/reopen watcher-race window. Passes moveOperationId through so dirty
destinations get the content-verify gate. 4545 tests green.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 17:23:24 -07:00
NeilandOrca e82ed76ea5 feat(editor): move-echo provenance + autosave gate on OpenFile (stage 3 store)
Adds pendingSelfMoveEcho {operationId,targetPath} to OpenFile and has the
rekey action install it + pendingLiveDiskVerification on dirty autosave-capable
destinations (moveOperationId arg), atomically in the same commit that re-homes
the tab — so the verify gate survives the rekey and its op-id token supersedes a
stale in-flight verification. Replaces the module-scoped generation map (which
broke under rekey). Verification-reader wiring + coordinator follow.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 17:12:40 -07:00
NeilandOrca 2397566ffa feat(editor): op-scoped in-flight move registry + source integration (stage 2)
editor-path-move-inflight.ts tracks Orca-owned moves for the exact duration of
the rename+rekey (no TTL): source paths suppress the delete tombstone, target
paths latch a destination event seen before the rekey (never suppress). Wired
into the watcher's delete filter alongside the old TTL registry (OR fallback)
so suppression keeps working until the stage-4 coordinator drives every move
through beginEditorPathMove. Stage 2 of 5.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 00:06:23 -07:00
NeilandOrca fe337935ec feat(editor): atomic rekeyOpenFilesForPathChange store action (move restructure stage 1)
Foundation for treating an Orca-owned move as an in-place retarget of the
open edit session (not close+reopen), per the locked design. One commit-only
store update migrates every path-derived id + all id-keyed state
(openFiles full-spread, the 6 file-id maps, activeFileId(+byWorktree),
tabBarOrder, unified tabs/groups via the now editor-family-widened
migrateHydratedEditorTabsAndGroups, pendingEditorReveal, untitled consume).
Preflight fails closed on collision (never merges two live sessions) or stale
with zero mutation. Not yet wired to a coordinator (stage 4).

Stage 1 of 5; behind the shipped content-identity fix.

Co-authored-by: Orca <help@stably.ai>
2026-07-20 00:03:58 -07:00
NeilandOrca 78515c4a37 fix(editor): give live move-verification its own autosave gate
The live self-move echo verification reused pendingDiskBaselineVerification
as its autosave gate, but that field is also the always-mounted restored-tab
conflict scanner's work queue: the scanner scans any pending dirty tab,
launches its own read, and clears the flag without checking the live
generation — so it could lift the gate mid-read and let autosave overwrite a
genuine external write. Give live verification a dedicated
pendingLiveDiskVerification field (both suspend autosave; each cleared only by
its owner). Transient, not persisted, not carried across a re-home.

Co-authored-by: Orca <help@stably.ai>
2026-07-19 22:27:31 -07:00
NeilandOrca 86ef3b374a feat(editor): decide move echo vs external write by content identity
Replaces the time-bounded self-move suppression heuristic with a
correct-by-construction identity check, so a genuine external write to a
just-moved path is never swallowed and the move's own echo is never a
false conflict — regardless of watcher event-kind coalescing or timing.

- Remap now carries the edit-session identity (lastKnownDiskSignature,
  externalMutation, pendingDiskBaselineVerification) onto the re-homed
  tab. Previously the close+reopen dropped it, so a moved tab lost its
  disk baseline (and any pre-existing changed-on-disk conflict silently
  vanished on move).
- On a live self-move-target dirty event the watch hook now reads the
  destination and compares getDiskBaselineSignature(disk) to the tab's
  carried baseline: equal => move echo (suppress), differ/binary =>
  genuine write (banner). Autosave is suspended synchronously before the
  read so a write landing mid-read can't be overwritten; a generation
  token makes overlapping reads safe. Fails CLOSED (marks changed) on a
  missing baseline or read error — never blind-suppresses.
- The self-move registry now only scopes WHEN to verify. The source-side
  delete still can't be content-verified (nothing to read), so it stays a
  bounded, documented suppression.
- Trim the verbose Why-comments across these files to 1-2 lines.

Adds content-identity verification tests (echo/differ/no-baseline/read-
error/binary/autosave-gate) and a remap test for the carried identity;
splits the watch-hook suite to stay under the max-lines limit.

Co-authored-by: Orca <help@stably.ai>
2026-07-19 22:15:17 -07:00
NeilandOrca bccbe478ec docs(editor): document the failed-move suppression window as a bounded trade-off
A self-move stamp is placed before the rename and retracted if it fails,
so an event consumed during the pre-failure window is swallowed. Note
that this only matters for the rare unrelated-dirty-tab-at-destination
case and the recoverable missed-source-tombstone case, and that a move
has no bytes to echo-verify the way self-writes do.

Co-authored-by: Orca <help@stably.ai>
2026-07-19 21:32:10 -07:00
NeilandOrca e22ff9f430 test(editor): cover source-side self-move guard in the pre-remap ordering
Adds the case where the watcher's delete(old) arrives while the tab is
still at the old path (before remap re-homes it): with a live self-move
source stamp the tombstone must be suppressed. Pairs with the existing
naked-delete control (no stamp → deleted) to pin the guard's behavior.

Co-authored-by: Orca <help@stably.ai>
2026-07-19 21:29:21 -07:00
NeilandOrca 62e28ce784 fix(editor): give self-move stamps per-registration expiries + retract tokens
The refcount model used a single shared expiry scalar per role that only
grew via max() and reset at refs=0, so releasing the max-contributing
registration left survivors inheriting an over-extended window (and an
expired registration could be resurrected by a later stamp on a key the
opposite role kept resident). Both over-suppress genuine changes.

Model each stamp as an independent registration carrying its own expiry
(a list per role). recordSelfMove returns a ticket; clearSelfMove
retracts exactly that registration. A role is live while any of its
registrations is unexpired, so concurrent stamps, failed-move clears, and
expiry are all precise. Wrapper/helper thread the tickets through.

Adds regressions for the over-extension and resurrection cases.

Co-authored-by: Orca <help@stably.ai>
2026-07-19 21:17:36 -07:00
NeilandOrca 6681ec2fa0 fix(editor): refcount self-move roles so a failed move can't clear a live one
Two concurrent moves onto the same destination both stamp that path as a
target; if the second rename fails and clears, it must not erase the
first (successful) move's still-live target stamp. Reference count each
role's registrations and clear only the failed move's own contribution.

Adds a regression test for the shared-destination case.

Co-authored-by: Orca <help@stably.ai>
2026-07-19 21:06:26 -07:00
NeilandOrca 401518243d fix(editor): make move-echo suppression watcher- and TTL-robust
Round-3 hardening after adversarial review:

- Suppress the move's own watcher echo regardless of event kind. The
  main-process watcher coalesces a create+attr-change burst into a lone
  update, so a create-only gate let the echo through on some hosts and
  re-exposed the false banner. Suppression is now bounded by the
  self-move TTL; a genuine write to the exact path within that short
  window is the documented trade-off (draft is preserved regardless).

- Bracket the on-disk rename with the self-move stamp via a single
  renameOpenTabsPathOnDisk wrapper: stamp before (to beat the watcher),
  re-stamp on success (a slow SSH/runtime rename can outlive the TTL, so
  the fresh window must start when the file actually moved), and clear on
  failure (a rename that never happened must not suppress real events).
  All move entry points (explorer move, inline/tab rename incl.
  undo/redo, untitled rename) route through it.

- Treat a tab as remote for TTL purposes when it has a runtime owner OR
  an SSH worktree connection (an SSH tab can carry a null runtime owner).

- Registry tracks source/target roles independently per path so an
  immediate undo can't clobber the original move's in-flight stamp; cap
  raised above realistic bulk-move sizes.

Tests: registry role/clear/cap, the rename wrapper (success re-stamp +
failure clear), the call-site helper (dir move + clear), and watch-hook
coalescing-robust suppression + post-TTL surfacing.

Co-authored-by: Orca <help@stably.ai>
2026-07-19 20:57:53 -07:00
NeilandOrca 6216617e86 refactor(editor): harden move-echo suppression per adversarial review
Addresses review findings on the self-move suppression:

- Stamp the self-move at the call sites BEFORE the on-disk rename
  (recordSelfMoveForOpenTabs), not after the tab re-home, so the
  watcher echo can't win the race. This makes the source-side delete
  guard actually effective and removes a possible one-frame flash.
- Suppress only the move's own create echo, not update events, so a
  genuine external write to the moved path within the TTL still raises
  the changed-on-disk banner (closes an over-suppression gap).
- Track source/target roles independently per path so an immediate
  undo can't clobber the original move's still-in-flight stamp.
- Raise the registry cap above realistic bulk-move sizes so a large
  directory move never self-evicts its own not-yet-echoed stamps.

Updated + added tests: registry undo/role + cap, the new call-site
helper (incl. directory move), and a real-update-still-marks case.

Co-authored-by: Orca <help@stably.ai>
2026-07-19 20:40:37 -07:00
NeilandOrca c60669d278 fix(editor): don't flag editor-initiated moves as changed-on-disk
An in-app move/rename (explorer drag-drop, inline rename, tab rename)
re-homes the open tab to the new path and carries its unsaved draft
forward. The move also physically relocates the file, which the worktree
watcher reports as delete(old)+create(new) a few ms later. Because the
tab already lives at the new path by then, that create echo was treated
as an external write landing on a dirty tab and raised a spurious
"changed on disk" banner.

Add a short-lived self-move registry (the move analog of the existing
self-write registry) stamped at the single remap choke point, and have
the external-watch handler recognize the move's own watcher echo:
suppress the changed-on-disk mark on the re-homed dirty tab and the
tombstone on the source path. Genuine external edits are unaffected.

Covered by unit tests for the registry, the remap recorder, and the
watch-hook suppression (plus a no-over-suppression guard).

Co-authored-by: Orca <help@stably.ai>
2026-07-19 19:59:11 -07:00
Hugo SáezandJinjing e3721e8cf2 fix(pi): detect ask_user_question and surface as blocked state (#9457)
* fix(pi): detect ask_user_question and surface as blocked state

   Maps Pi tool_call/tool_execution_start events with ask_user_question
   to blocked state (was working), so worktrees surface in attention sort
   and trigger notifications — matching Claude/Codex/Grok behavior.

   Guards interactivePrompt derivation to Pi-only, OMP unchanged.
   5 new tests covering blocked transition, regression, malformed input,
   and OMP guard.

* fix(pi): gate ask_user_question blocked on raw tool_name and cover state exit

Address code-review findings on the Pi ask_user_question detection:

- Gate the Pi blocked classification on the event's own tool_name (matching the
  Claude/Grok normalizers) instead of the merged snapshot, so a partial
  follow-up event can't inherit a stale ask_user_question name from the tool
  cache and spuriously re-enter blocked. resolveToolState moves back after the
  state-name guard, so it no longer runs on discarded events.
- Make extractPiToolFields' agentKind parameter required; the sole call site
  always supplies it, and optional risked a future Pi caller silently falling
  back to OMP-safe (no interactivePrompt) behavior with no type error.
- Add coverage for the transition OUT of blocked (tool_execution_end -> working,
  agent_end -> done) and that a following regular Pi tool clears interactivePrompt.

* test(pty-connection): gate confirming null sample in idle-exit veto test

CI failed on a flaky call-count assertion: one timer advance can start
multiple getForegroundProcess reads, so the confirming null sample could
land before the replacement hook owner was installed. Hold 2nd+ null
samples until the veto owner is in place instead of requiring exactly
one extra call.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-19 17:42:41 -07:00
Jinjing 1def694e80 Add native chat skill picker with host-aware discovery (#9480)
* Add native chat skill and command picker with host-aware discovery

Adds a unified, keyboard-first skill and command picker to native chat that:

- Uses agent-native invocation syntax (slash for Claude/OpenClaude/Grok, dollar for Codex)
- Discovers skills only on the pane's execution host (local, WSL, SSH-unavailable, or runtime)
- Groups or separates commands and skills per agent configuration
- Deduplicates by canonical path but preserves visibility through all contributing roots
- Handles IME composition, loading states, and errors without claiming PTY-level control
- Records picker telemetry (open, item accepted, send classification, discovery outcomes)
- Extends shared agent profiles to define per-agent skill grammars and source ownership

* Remove obsolete reference and design documentation

Clean up stale design specs, implementation plans, and investigation notes from
docs/reference/. These documents predate the current implementation and are no
longer actively maintained or referenced by the codebase.

* Extract shared skill discovery utilities and add skill invocation envelo

- Move skill comparison and source classification to shared module for native/WSL reuse
- Extract display text sanitization to prevent control/zero-width character spoofing
- Add native-chat command envelope parser and surfacer for skill invocations
- Extend discovery timeout backstop to account for WSL metadata read sequence

* Localize skill picker UI for Spanish, Japanese, Korean, Chinese

Translate skill picker UI strings including commands, skills, loading
states, error messages, and scope labels for the new skill picker feature
across four language locales.

* Fix skill picker bugs and improve code robustness

- Fix i18n plural handling: rename `count` to `sourceCount` to prevent unintended plural-key resolution in localized strings
- Fix skill discovery array mutations: copy `root.providers` to prevent bugs during dedup merge
- Fix image attachments being silently dropped when message text starts with /skill or agent prefix
- Extract `quoteBashString` utility for WSL command code reuse across builders
- Add line-separator safety characters (0x2028/0x2029) to skill display filter
- Remove stale doc reference links and clarify inline comments

* Add reference docs for git compatibility and headless Linux server setup

Track previously untracked operational guides in `docs/reference/` that
explain Git binary compatibility requirements across host types and how to
run `orca serve` on headless Linux. Update AGENTS.md and README.md to link
to these references.
2026-07-19 17:31:34 -07:00
Jinjing a6df23c762 Tighten orchestration worktree isolation policy (#9482)
* docs(orchestration): require true isolation for worker worktrees

* docs(orchestration): deduplicate parallel worker guidance

* docs(orchestration): clarify isolation exceptions

* docs(orchestration): clarify checkout preference

* Tighten orchestration worktree isolation policy guidance

- Clarify that same-worktree workers remain orchestration children
  despite appearing as peers in the sidebar
- Define new-worktree creation as required only for explicit requests
  or concrete filesystem/checkout conflicts, not convenience
- Distinguish stacked worktrees from independent ones via --no-parent
- Rename agent-first guidance to reflect conditional requirement

* Release orchestration skill v25 with tightened worktree policy
2026-07-19 17:17:40 -07:00
Jinjing daf22f0720 Make Create PR handle sync by fast-forwarding behind-only branches (#9481)
- Create PR now fast-forwards behind-only branches before committing, using
  git pull --ff-only. This prevents the dirty-then-ahead+behind stall that
  occurred after commit without prior sync.
- Refactor runRemoteAction to return explicit status ('ok', 'failed',
  'superseded', 'skipped') instead of boolean ok + nullable error. Allows
  callers to distinguish real failures from action supersession or skips
  without stale-cache issues.
- Remove isCreatePrIntentSyncConflictError function and sync-conflict-specific
  copy since --ff-only fails cleanly if branch diverged; no merge conflicts
  to resolve.
- Extract isBehindOnlyUpstream predicate to shared module so eligibility
  checks and the one-click flow always agree.
2026-07-19 17:13:19 -07:00
OrcaWin b320bcb374 fix(gitlab): bound and coalesce auth probes (#9476) 2026-07-19 16:54:31 -07:00
Brennan Benson 245d79cc8c Preserve surviving workspace state when removing a runtime host (#9468)
* fix(runtime): preserve surviving workspace state on host removal

Avoid purging worktree-scoped tabs and editor state when an exact worktree id still exists on another host. Also retire legacy unhosted rows when the removed runtime was their repo's sole unambiguous owner.

* fix(runtime): purge rows when all owners are removed

* fix(runtime): include host setups in purge ownership

* fix(runtime): purge session-only state on host removal

* fix(runtime): respect restored session ownership on host purge

* fix(runtime): preserve surviving restored sessions on host purge
2026-07-19 16:36:00 -07:00
Brennan Benson 85ab72901b Persist the AI Vault session parse cache across restarts (#9474)
Cold launches re-parsed the entire agent-transcript corpus (measured
6.7 GB / 109 s upstream; 1.79 GB / 3.7 s locally) because the parse
cache was an in-memory Map. Persist the reusable portion (mtime+size
gated session entries, resume states dropped) to one JSON file under
the canonical userData dir: lazy load before the first scan, debounced
atomic save after scans that parsed anything. Restart scans now reuse
unchanged files (measured 328 ms / 3 MB, reused=1036).
2026-07-19 16:35:26 -07:00
Brennan Benson 098db33638 Keep lineage children with their parent when dragging to another status lane (#9472)
Dragging a parent worktree card to a different status lane in the sidebar
moved only the parent, orphaning its visible lineage children in the old
lane even though the drag preview showed them moving as one unit. All
status-lane and board drop commit paths (plus their hover previews) now
use the same lineage-expanded dragged set the reorder path already uses.

Fixes #9083
2026-07-19 16:35:12 -07:00
Jinjing d67ede1594 Implement confirm-only PR panel composer with classified error blocking (#9428)
* Clarify PR panel guidance: classify errors and confirm-only composer

Replace the ambiguous GitHub hosted-review boolean with a four-state evidence
model (found/positive_unresolved/not_found/unknown) so "No PR found" never
appears without an accepted lookup result. Classify GitHub refresh failures
into types (rate_limited, auth, network, permission, repo_unavailable,
gh_unavailable, unknown) for stable, honest copy. Confirmed-only composer:
preserve drafts across transient failures; hide Create during hard errors and
positive-unresolved evidence. Hard errors clear only when an eligibility
request starts after the error and returns an accepted outcome. Propagate
error types and unified retry schedule through the store. Sync mobile parity
with shouldOpenChecksPanelCreateComposer gating. Localize all new copy.

* Clarify PR panel guidance: classify errors and confirm-only composer

Add reviewLookupOutcome to hosted-review eligibility and thread it through
the panel so it never claims "No PR found" without accepted evidence. A
failed lookup is unavailable, not a settled no-PR. Fail closed on positive
unresolved evidence, hard refresh errors, and unavailable lookups. Add
structured GitHub refresh-error classification with Retry-After parsing.
Implement confirmed-only composer gating based on fresh, matching-context
eligibility with hard-error clearing. Mobile gates on reviewLookupOutcome
to prevent false Create claims. Surface throwOnFailure variants for each
provider so transport failures cross the RPC boundary instead of collapsing
to null. (Design success criteria 1–4; invariant 8.)

* Add exec-error helpers for subprocess error classification

Extracts stderr/stdout parsing and Retry-After detection into a
lightweight module that can be imported without pulling in the heavier
runner machinery. Supports PR-refresh error classification and proper
rate-limit handling for gh commands.

* test(mobile): include reviewLookupOutcome in create eligibility fixtures

Create / Push & Create now fails closed unless the lookup is not_found.
Update mobile test fixtures so accepted-no-PR cases can still proceed.

* Add OrThrow mock variants to forge-provider test mocks

forge-provider resolves branch reviews via the OrThrow variant so
lookup failures surface as unavailable instead of "no PR found".
2026-07-19 16:32:36 -07:00
JinjingandOrca c5d40565af Clarify Orca Mobile pairing connection paths (#9425)
* Restructure mobile pairing setup into a clear stepped layout

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

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

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

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

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

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

Address accepted findings from the mobile pairing UI rework:

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

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

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

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

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

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

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

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

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

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

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

Rename MobileRelayBetaAvailability → MobileRelayBetaNotice.

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

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-19 16:10:32 -07:00
Jinwoo Hong d7dc7e9f25 test: expect native context from local PTY 2026-07-19 16:04:05 -07:00
Jinwoo Hong ca7b9fb04e refactor: split PTY spawn result contract 2026-07-19 16:04:05 -07:00
Jinwoo Hong 1ffe14986e chore: keep provider types within lint limit 2026-07-19 16:04:05 -07:00
Jinwoo Hong bbb54065e8 fix: preserve native PTY context on reattach 2026-07-19 16:04:05 -07:00
Jinwoo Hong 8281038d60 fix: refresh CWD after late WSL context 2026-07-19 16:04:05 -07:00
Jinwoo Hong 01ce6db168 test: make WSL cwd fixture cross-platform 2026-07-19 16:04:05 -07:00
Jinwoo Hong 3efc930908 fix: preserve WSL CWD across daemon sleep wake 2026-07-19 16:04:05 -07:00
Jinjing 08f446190a Standardize per-repo Source Control AI save UX (#9427)
* 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.
2026-07-19 15:59:36 -07:00
Brennan Benson 624c8d4120 Make plain-text file:// links clickable in the terminal (#9467)
* 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
2026-07-19 15:21:06 -07:00
Brennan Benson 764cadafc7 fix(terminal): confirm the agent from ConPTY console presence to avoid false exits (#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
2026-07-19 15:20:54 -07:00
Brennan Benson 9d262c98f6 Bump mobile app.json to 0.0.32 (#9466) 2026-07-19 14:43:07 -07:00
Brennan Benson ca4003a9e3 Keep Agent Session History from freezing the app on huge OpenCode databases (#8924)
* fix(agent-history): keep the app responsive while scanning huge OpenCode databases (#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
2026-07-19 14:40:54 -07:00
Brennan Benson fbe7780566 Fix diff editor flashing "Loading…" on every save (#9424)
* 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
2026-07-19 14:30:59 -07:00
OrcaWinandBrennan Benson 37039cd095 fix(source-control): bound Create PR eligibility probe so it can't hang (#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>
2026-07-19 13:18:34 -07:00
Brennan Benson a833b5cba6 Clear a removed remote host's leftover repos and worktrees from the sidebar (#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 #8881
2026-07-19 13:15:14 -07:00