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>
- 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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
* 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>
* 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.
- 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.
* 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
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).
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
* 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".
* 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>
* 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.
* 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
* 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
* 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
* 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
* 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>
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