Updated README to guide Windows users to the latest RC release,
which includes critical Windows-specific bug fixes not yet in the
stable build. Added prominent notices at the top of the download
section and in the direct-download links.
* fix(wsl): forward ORCA_ROOT_PATH/ORCA_WORKTREE_PATH and setup vars across the wsl.exe boundary (#9206)
Worktree setup scripts running under WSL saw empty ORCA_ROOT_PATH /
ORCA_WORKTREE_PATH ("cp: cannot stat /.env"): the vars were set on the
Windows-side spawn env, but wsl.exe only imports Windows env vars listed
in WSLENV, and the addOrcaWslInteropEnv allowlist omitted them.
Register ORCA_ROOT_PATH, ORCA_WORKTREE_PATH, and the CONDUCTOR/GHOSTX
compat aliases with a per-value flag (same pattern as
ORCA_AGENT_HOOK_ENDPOINT): /u when hooks.ts already Linux-translated the
value for a WSL worktree (a /p flag would double-translate and corrupt
it), /p when a wsl.exe terminal runs over a Windows worktree and the
value is still a C:\ path WSLENV must translate. ORCA_WORKSPACE_NAME is
a display name, never a path, so it is always /u.
* fix(wsl): populate WSLENV for runHook's direct wsl.exe invocations (#9206)
runHook spawns wsl.exe via execFile for archive hooks and for setup when
no renderer window exists (headless/CLI/RPC/mobile-created worktrees).
It set ORCA_ROOT_PATH etc. on the execFile env, but wsl.exe only imports
Windows env vars named in WSLENV, so the guest never saw them. Register
the setup vars in WSLENV via a helper factored out of the PTY path's
addOrcaWslInteropEnv, so the per-value /u-vs-/p flag decision stays in
one place. Also make the runHook WSL test assert on captured execFile
options after the promise resolves — expects thrown inside the mock were
swallowed by runHook's own error handling.
* fix(worktree): bound teardown RPCs so Windows workspace deletion can't hang
Native-Windows workspace deletion failed with "Timed out waiting for
physical PTY teardown". On win32 the daemon PTY adapter is the local
provider, and destructive teardown's kill/listSessions RPCs used the
DaemonClient 30s default — larger than the 10s sweep deadline — so a
slow/wedged daemon let the outer deadline fire with the confusing error
and blocked deletion.
Thread an optional `timeoutMs` through IPtyProvider.shutdown/listProcesses
and bound every RPC on the destructive-removal path (provider sweep,
registry sweep, and the runtime-graph sweep via stopAndWait) to the
remaining sweep budget minus a margin. The daemon adapter shares one
budget across ensureConnected + the RPC; the SSH provider forwards the
bound to the relay mux. stopAndWait splits the budget across its two
sequential RPCs and bounds the cold-start wait, failing closed.
Result: a wedged backend now fails fast with the accurate "Failed to
physically stop every PTY" (retry succeeds once the process is truly
gone), and the misleading deadline error no longer blocks deletion.
Fail-closed safety is preserved: a genuinely-live process still blocks
removal. Non-teardown callers pass no timeoutMs and keep the 30s default.
* refactor(worktree): thread an absolute teardown deadline instead of a relative timeout
Elegance pass on the teardown-RPC bounding. Instead of passing a relative
`timeoutMs` and reconstructing an absolute deadline + "remaining budget" at
three layers (stopAndWait, the daemon adapter's shutdown/listProcesses, and
per-RPC in the sweeps), thread one absolute `deadlineMs` (epoch ms) through
IPtyProvider.shutdown/listProcesses. Each RPC leaf converts to a relative
timeout exactly once, at the moment it issues (`max(1, deadlineMs - now)`).
Why this is cleaner:
- The threaded value's identity is a point in time, not a duration, matching
the codebase's existing deadline-based shared-budget idiom.
- Sequential RPCs (kill then liveness verify) share the budget structurally:
a later leaf converting the same deadline naturally gets less time, so the
explicit recompute-after-shutdown bookkeeping disappears.
- Removes the per-layer deadline re-anchoring that let the effective deadline
drift slightly later at each hop.
- The 500ms margin is now applied once (`teardownRpcDeadline`), and the dead
`: opts.timeoutMs` else-arms in the adapter are gone.
No behavior change: non-teardown callers still pass nothing and keep the
30s default + original connect behavior; fail-closed safety is intact.
Tests strengthened to pin the exact leaf-observed budgets and the margin.
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
The e2e terminal helpers typed `node -e ${JSON.stringify(script)}` into the
PTY. JSON.stringify emits POSIX-style \" escapes, which PowerShell does not
honor: it re-splits the program on `;` inside the payload, node throws
'Expected unicode escape' before emitting a single byte, and the OSC-title
assertions fail deterministically on Windows (default shell = PowerShell).
Stage the program in a unique temp .cjs file instead and send
`node "<forward-slash path>"` — no shell ever parses the program source, so
delivery is byte-identical on PowerShell, cmd, bash, and zsh (verified with
hexdumps: 07 1b 5d 30 3b ... 07 matches exactly across shells). Forward
slashes keep the quoted path valid in both POSIX shells and PowerShell; the
Codex startup marker moves from argv into the script body so no argument
quoting remains. macOS/Linux payload bytes are unchanged — only the delivery
mechanism differs. Test infrastructure only; no product code touched.
Capture the worktree and runtime route that produced each committed content-search result set, then reuse that owner for opens and retries. This prevents active-worktree and ambient-runtime changes from retargeting remote matches while preserving explicit local and SSH routing. Closes#9185.
* feat(mobile): add Quick Commands (terminal + agent-prompt presets)
Brings the desktop Terminal Quick Commands feature to mobile: saved
agent-prompt or terminal-command presets that launch a new terminal tab.
Entry point sits in the session tab strip next to the "+" new-terminal
button (with a divider) — quick commands spawn a tab, so they live with
tab creation, mirroring desktop's tab-bar split button.
- Launcher button + Quick Commands bottom sheet (search, This project /
Global groups, run/edit/delete rows, add row).
- Add/Edit sheet mirroring desktop TerminalQuickCommandDialog: Label,
Action toggle (Terminal Command | Agent Prompt), Agent select, Prompt /
Command Text, Advanced (Append Enter, Scope Global/Project), validation
and save-failure feedback.
- Launch reuses handleCreateTerminal (extended with enter + toast copy):
agent prompts launch the agent then deliver the prompt; terminal
commands run the (Enter-appended) command text.
- Expose terminalQuickCommands over the remote/mobile RPC surface
(getClientSettings/updateClientSettings allowlists, RuntimeStore type,
and the strict SettingsUpdate zod schema).
- Mirror the agent-prompt support predicate mobile-side (stdin-after-start
agents are unsupported) with a parity test guarding drift from desktop.
- Mock server: sample quick commands + settings.update handler for QA.
* fix(mobile): harden quick command execution
* fix(mobile): harden quick command persistence and launch
* test(mobile): preserve unexpected quick command errors
* fix(mobile): harden quick command launch performance
* fix(runtime): reject malformed quick command updates
* refactor(mobile): reuse shared quick-command logic instead of mirroring
The mobile quick-commands mirror was built on a false premise — that
runtime-importing src/shared/terminal-quick-commands breaks the RN bundle
/ Vitest. It doesn't: tui-agent-config → orca-cli-command-name is a pure
leaf with no module-load Node APIs (verified via probe + bundle-graph).
- Mobile now reuses the canonical desktop helpers (action/agent/scope/
matchesRepo/support/flatten) directly from src/shared; only genuinely
mobile-specific pieces (agent-branded labels, native row truncation,
the launch plan) stay local.
- Multiline runnable terminal commands now flatten via the shared
flattenTerminalQuickCommand (";"-join) — unity with desktop, so a
command saved on one runs identically on the other.
- Drop the MOBILE_TUI_AGENT_PROMPT_COMMAND_UNSUPPORTED mirror + its parity
test; use the shared supportsTerminalAgentQuickCommand predicate.
- Export the shared MAX_QUICK_COMMAND_* length caps for reuse.
* fix(mobile): protect quick command data boundaries
* fix(mobile): enforce quick command limits
* fix(mobile): make quick command updates atomic
* fix(mobile): keep quick command filters recoverable
* fix(mobile): use filled play icon for quick commands
* Revert "fix(mobile): use filled play icon for quick commands"
This reverts commit 169bf053b0.
* fix(mobile): gate quick commands on host capability
* perf(renderer): drive agent working-spinners from one shared clock
Per-element infinite CSS spin animations kept Chromium's frame pipeline
awake for the whole agent run — measured live (interleaved A/B/A/B,
renderer+GPU): 86.0/76.0 ms CPU/s with the CSS animation vs 61.0/61.0
with the clock, one working agent, and the CSS cost scales per element
while the clock is one flat timer for N spinners.
The clock ticks 12 steps/s at 30° — frame-for-frame identical to the
retired animation — writes style.transform on registered elements, and
stops on document hidden (native visibilitychange post-#9395), under
prefers-reduced-motion (static ring keeps the filled top border from
#9515), and when the last spinner unmounts. The stale-visibility latch
keeps it spinning when proven user input contradicts the occlusion
tracker.
Co-authored-by: Orca <help@stably.ai>
* fix(github): restore explicit space after Filter fallback in PRFilterSections
Pre-existing failure on main: i18n-jsx-spacing-guard requires {' '} after
the 'Filter' translate fallback, but the file had a bare JSX space (which
oxfmt collapses). Wrap the subject in a span so the explicit {' '} survives
formatting, matching the pattern in the other guarded files.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Under prefers-reduced-motion (Windows 'Animation effects' off), the agent
'working' spinner froze mid-rotation as a partial (3/4) ring, reading as a
broken spinner in the terminal tab and worktree card. Fill the top border so
it becomes a complete static ring, and apply the same treatment to
StatusIndicator so the sidebar dot honors reduced motion consistently instead
of stepping. Mirrors the existing feature-tour-preview-glyphs pattern.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(ssh): clear stamped agent status on disconnect
Batch transient cleanup by accepted SSH connection authority and use a monotonic cutoff so reconnect replay wins over delayed clears. Preserve pane launch, resume, acknowledgement, and retention metadata.
Caveat: legacy or renderer-owned rows without an accepted connection stamp are intentionally left to existing pane/PTY teardown; clearing them by host would be ambiguous.
* docs(ssh): explain stale status watermark
* fix(ssh): preserve status ordering after restart
* 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
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#7943