* fix(onboarding): run skill setup in the configured Windows runtime (#12103)
Onboarding was the one skill-setup surface that did not route its install
command through the resolved runtime. Settings, the feature-wall panels and
the Linear prompt all wrap theirs as `wsl.exe -d <distro> -- sh -c ...` and
pass a matching shell override; onboarding spawned a bare terminal and handed
it the raw `npx skills add ...`. With Node inside WSL, npx is not on the
Windows PATH, so the install failed.
The runtime resolver had a second gap behind that: it only consulted
per-project settings, and onboarding runs before any project exists. With no
project it returned undefined and fell through to the Windows host, ignoring
a global WSL default entirely. `getLocalAgentPreflightContext` already had a
no-project fallback for PATH detection; the skill-install path had none.
- extract that fallback as `getGlobalWindowsExecutionRuntimeContext` and
rewire the existing agent-preflight branch through it so the two cannot drift
- adopt it in `useActiveProjectSkillRuntime` when no project is active. WSL
only: a windows-host default already matches the old no-project behavior,
and resolving it would hand skill discovery a target where it had none,
re-triggering scans for every host-default user
- build the onboarding terminal's command for the runtime and pass its shell
override
- register the CLI in WSL rather than on the host, so `orca` lands on the PATH
the install actually runs on, and wrap the copied command to match
* fix(onboarding): keep skill setup runtime consistent
* test(onboarding): satisfy runtime settings contract
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(terminal): route remote-runtime link clicks to the system browser
Terminal link clicks classified ownership from the global
activeRuntimeEnvironmentId, which is null when runtimes are bound per
workspace, so a link clicked in a remote-hosted pane opened a local-only
Orca browser tab and never reached the host. Thread each pane's resolved
runtimeEnvironmentId into openHttpLink as sourceOwner across the OSC 8,
WebLinksAddon, and click-fallback paths.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): route link clicks based on pane ownership, not global sta
Clicking links on remote-hosted panes was routing based on global runtime state, causing unexpected reconnections. Now link routing decisions (where to open: Orca vs system browser) are based on the actual pane's owner — local, SSH connection, remote runtime, or unknown — regardless of whether any runtime is globally active. This ensures a local pane can route to Orca while another pane's remote runtime is active, and a remote pane always routes to the system browser.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(browser): bound retained webview guests across worktree switches
* fix(browser): merge duplicate imports flagged by import/no-duplicates
* fix(browser): evict guests without unmounting the worktree surface
Unmounting the surface disposed every parked terminal byte watcher for the
evicted worktree (bell/title/agent-completion facts dropped for good after
the 15s handoff TTL) and skipped the force-park scrollback capture remote
panes rely on. Destroy the guests only: hidden slots mount no BrowserPane,
so nothing resurrects a destroyed guest before the next visit, and no slot
ever unmount-detaches a live guest (STA-3228). Terminal-state vetoes and
the eviction re-render bump are no longer needed; the only veto left is a
guest an automation/mobile controller is actively driving.
* fix(browser): veto eviction for downloading pages; keep zoom; add kill switch
Main cancels a page's active downloads when its guest unregisters (tab-close
semantics), so eviction now vetoes worktrees with a page still writing a
download, tracked app-wide because download state was pane-local and hidden
panes are unmounted. Eviction is not a user close: re-remember explicit zoom
past the destroy-path forget so a revisit reasserts the user's zoom instead
of resetting same-host siblings through Chromium's partition-wide HostZoomMap.
browserGuestWorktreeRetentionBudget mirrors the terminal budget kill switch.
* feat(crash-reports): add byte attribution to renderer memory highwater breadcrumbs
Entry counts stay flat when a slice grows by value weight (97b9e86d leaked
~700MB while its biggest slice grew by 4 entries), so highwater breadcrumbs
now carry sampled per-slice KB estimates for the store plus a live pane
census (managers, panes, estimated scrollback KB) — the dominant heap cost
the store census cannot see.
* fix(crash-reports): bound renderer OOM profiling
* fix(crash-reports): total raw store estimate bytes
A mobile New Tab -> Codex create resolves the launch command and hands it
to the renderer, but when the renderer's startup queue is lost (the #7587
stall class) the pane spawns a plain shell and the create still settles
ready via PTY adoption - silently binding the phone to a bare terminal
forever, since the ready status also disables the #7837 activation-time
materialize recovery.
Record the resolved launch command on the pending create and, at every
renderer-backed settle point, deliver it to the adopted PTY when no spawn
command was recorded for it. Spawn commands are noted per PTY by both
spawn IPC handlers, so a missing record on the locally registered live
PTY proves the launch never ran; delivery types the command exactly like
the create would have, and the note prevents double delivery.
Fixes STA-3214
The parent-drift repair path (destroyPersistentWebview with
preserveViewport: true, introduced in #12137) tears down and rebuilds the
webview under the same browserTabId, but unconditionally forgot the tab's
explicit user zoom. BrowserPane then re-seeds from the Settings default on
the next mount, silently resetting per-tab zoom.
Only forget explicit zoom on a real close; a preserveViewport rebuild keeps
the same logical tab, so its zoom must survive.
* fix(updater): recover Linux .deb/.rpm installs that fail escalation
A `.deb` install fails with `No authentication agent found` when the session
has no polkit agent. Orca reported "Quit and reopen Orca, then try again" —
wrong advice — and its only action was Retry Download, discarding a verified
160 MB package that was still in the updater cache.
Keep the one-click install path, but make a failed root-package install
recoverable without downloading again:
- Retain the downloaded package and its expected SHA-512 from the
`update-downloaded` event, mirroring electron-updater's cache-name rule.
- Capture the child stderr that BaseUpdater logs but drops from the `error`
event, redact it (ANSI, control bytes, `<home>`, `<package>`, `<user>`,
1 KiB cap), and classify the failure. Classification reads the original
text — redaction can rewrite a matched phrase.
- Send a structured `linux-package-install` recovery status and render a
dedicated card: Copy Install Command / Try Automatic Install Again /
Show Package.
- Revalidate on every action: cache containment, lstat, streamed SHA-512,
timingSafeEqual. Concurrent requests coalesce into one hash pass.
- Build the command from fixed tokens plus one POSIX-single-quoted absolute
path, resolving sudo and the package manager only from /usr/bin, /bin,
/usr/sbin, /sbin. Orca never runs it.
- Disable `autoInstallOnAppQuit` for .deb/.rpm so an ordinary quit cannot
trigger the same failing escalation after the UI is gone.
Extracts the error-card presentation into UpdateErrorCardContent so
UpdateCard does not absorb another stateful surface.
Lifecycle breadcrumbs carry package type, reason, exit code and version —
never a path, command, username or raw child output.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Improve Linux package install recovery diagnostics
- Distinguish invalid-package-path errors from missing package manager
- Expand ANSI escape sequence stripping to handle OSC hyperlinks and DCS
- Prevent generic error logs from overwriting specific diagnostic verdicts
- Add error handling for shell.openUrl in update UI
- Fix test isolation with proper afterEach hooks
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Open SSH host add/edit form in modal dialog
Form moves from inline to a viewport-stable modal (STA-3067) so fields stay
accessible with long host lists. Includes sticky header/footer, dirty-state
protection against outside click, and session-aware Advanced state reset on
cancel/reopen.
* fix: add missing SshTargetForm localization keys
Sync en.json catalog for modal title/description strings so
verify:localization-catalog passes in static analysis.
* fix: translate SshTargetForm modal strings in es/ja/ko/zh
Add non-English catalog entries for the new modal title and
description keys so localized UIs match English.
* Prevent SSH form double-submit and fix dismissal detection
Adds a saving state to prevent concurrent saves when a user double-clicks
the submit button. Fixes outside-click dismissal by correctly tracking form
state across re-renders using refs. Extracts session termination logic to
a reusable module.
* fix: stop mutating formRef during render in SshTargetForm
React Doctor fails the static-analysis gate when refs are written during
render. Sync form into formRef in an effect so render stays pure.
* Keep your New Linear and Jira creation drafts after an accidental dismissal
* Avoid draft store writes while typing
* Update draft retention refs after commit
* fix(native-chat): stop clipping assistant text blocks at the tool-preview cap
Long assistant messages read over a paired connection (headless orca
serve viewed from desktop or mobile) were cut at 4,000 chars with a
'… (truncated)' marker and no way to read the rest. The mobile payload
diet in nativeChat RPC applied the tool-preview char cap to text blocks,
which are the fully rendered message body. Give text blocks their own
64k safety ceiling so real replies pass through whole while pathological
multi-hundred-KB blocks still can't freeze the phone.
Fixes STA-3230
* test(native-chat): cover long text stream frames
Destructive worktree removal proves every PTY is dead before touching the filesystem. When a stop
RPC failed, it re-listed the provider to check whether the PTY had already exited — but on the
same deadline the sweeps had just spent, so it timed out without ever asking and read "could not
verify" as "still live". The sweep spends that budget every run, making the refusal deterministic;
--force never reached the gate, so the workspace was unremovable forever.
- Verification gets its own budget instead of an exhausted remainder.
- Verdicts split into exited / live / unverifiable; the error names the blocking PTY ids and why.
- A reachable escape hatch: allowUnverifiedPtyStop, set only by genuine Force Delete affordances
and the CLI's --force — never by the force the ordinary delete confirmation already sets — with
an 'unstopped-pty' classifier reason so the desktop actually offers the button.
- Force also survives a sweep that cannot complete; the non-force path still fails fast.
Fixes#11960
* fix(codex): make quota probes credential-safe
Codex OAuth uses rotating refresh tokens, and Orca's quota probes spawned
real codex app-server processes inside live credential homes, hard-killed
them at a 10s deadline (cold starts run 10-25s), re-probed every inactive
account on each switch, and deselected accounts on torn auth.json reads.
- arm the RPC read deadline only after initialize responds (30s/40s boot
budget), and terminate probes via stdin EOF + SIGTERM with a bounded
drain before any hard kill; resolve only once the child exits
- serialize Orca-spawned codex processes per credential home (probe vs
probe, probe vs commit-message/PR-fields/branch-name/model-discovery)
- keep the inactive-probe debounce across account switches and stagger
inactive probes; the active account still refreshes immediately
- grade credential reads (present/missing/unreadable/no-credential) and
require absence to outlive a grace window before deselecting
* fix(codex): close remaining credential races
* fix(codex): keep failed probes under home lock
* fix(codex): observe probe pipe failures
* fix(codex): await Windows generation tree kills
* fix(codex): preserve incomplete shared credentials
* fix(codex): execute accepted account-switch restarts for unmounted panes
Accepting the Codex account-switch restart prompt queued every awaiting pane
but only mounted TerminalPane instances executed the queue, so background-tab
and parked panes stayed input-blocked on the old account with no prompt.
- Add a detached store-level driver (codex-detached-pane-restart) that
kill-and-respawns any queued pane no mounted transport claims, rebinding
tab/layout state so a later mount reattaches to the replacement PTY.
- Re-offer the prompt when a detached execution fails, and clear the notice
when the pane is gone, so input is never silently blocked.
- Sweep restored PTY ids at startup so stale panes in never-mounted tabs are
re-offered after an app restart.
- Carry launchAgent codex on the restart respawn so it waits for managed-auth
readiness and records the pane's launch account.
* fix(codex): fence detached restart ownership races
* fix(codex): unblock detached restart handoffs
* fix(codex): contain detached restart cleanup
* fix(codex): detach restart progress from cleanup
* fix(codex): avoid detached restart size wait
* fix(terminal): scope Codex restart prompt to pane
* fix(codex): contain detached restart sweep failures
* fix(browser): import __Host- cookies host-only so Chromium keeps them
The cookie file/JSON import path passed a Domain attribute for every
cookie. Chromium rejects any __Host--prefixed cookie that carries a
Domain (the prefix requires host-only, path=/, Secure), so file import
silently dropped session cookies like GitHub's __Host-user_session_same_site
and users stayed logged out after importing.
Mirror the browser-native import path, which already shapes __Host-
cookies host-only: omit domain and force path=/ when the name is
__Host--prefixed. Add a regression test covering both a __Host- cookie
and a normal domain cookie in one import.
* test(browser): cover __Host- cookie payload constraints
* test(browser): preserve ordinary cookie paths
* feat(editor): preserve PDF scroll position across tab switches
PDFs snapped back to page 1 on every tab switch — the one scrollable
editor viewer never wired into the shared scroll cache.
A raw scrollTop is the wrong unit here: PdfViewer resets zoom to
page-width on each mount, and page-width resolves to a different
absolute scale for a different container width, so a cached pixel
offset restores to the wrong place. Store the pdf.js location
({ pageNumber, top, left }) instead — PDF user space, scale
independent — and restore via scrollPageIntoView with the same XYZ
destination pdf.js uses for its own scale-change restore.
Scoped to the single-pane edit path: diff and conflict-review mount
several viewers on one path, so they pass no key and keep no memory.
Closes#12117
* fix(editor): defer PDF scroll recorder arm until the restore settles
pdf.js dispatches the init `updateviewarea` synchronously from `update()`
right after the `pagesinit` handler, so arming there recorded the restore's
own provisional landing. On a mixed-page-size document that landing uses
page-1 geometry and is wrong, and a tab switch before `pagesloaded` flushed
it over the good cached position.
Arm at the first of `pagesloaded` or first user input instead; arm
immediately only when nothing was cached. If neither ever happens the
recorder never arms and the cached position survives untouched, which is
correct because the reader never moved.
Co-authored-by: Orca <help@stably.ai>
* fix(editor): keep pdf.js location live after the PDF scroll re-apply
Two defects in the restore half, both from calling scrollPageIntoView
outside pdf.js's own scale-update path.
`#scrollIntoView` nulls `_location` when `currentScaleValue` is unset, and
it stays unset because 'page-width' resolves against a page list that is
still empty at load. At `pagesinit` that is harmless (`_location` is
already null and the init `update()` repopulates it), but the `pagesloaded`
re-apply runs with a live `_location` and nothing recomputes it — on a
uniform-page document the re-apply moves nothing, so no scroll event fires.
The next zoom then found no location, fell back to the page top, and the
recorder persisted that page-top position over the reader's offset. Call
`update()` after the re-apply to recompute it.
The user-moved guard also missed find: `PDFFindController` scrolls to a
match programmatically, and the find bar is not inside the watched
container, so neither the input listeners nor the arm gate saw a search.
Searching during the pagesinit-to-pagesloaded window yanked the reader back
to the cached page. Treat a 'find' dispatch as reader movement.
Co-authored-by: Orca <help@stably.ai>
* test(editor): pin the PDF scroll cache wiring and drop a dead seam
The key's journey from EditorContent through ImageViewer to PdfViewer had
no coverage, so dropping the prop anywhere along it would have shipped as
silently amnesiac scrolling with a green suite. Assert both directions:
forwarded when supplied, null when omitted. Verified the tests fail when
the forwarding is removed.
Also pin the trailing-from-first-record debounce, which every existing
test advanced past and so could be mutated into a restarting debounce
undetected, and delete the `timers` injection seam — it had no callers,
so only its two fallbacks ever ran.
Co-authored-by: Orca <help@stably.ai>
* fix(editor): do not arm the PDF recorder in a hidden worktree pane
Editors in background worktrees stay mounted under display:none so their
layouts survive worktree switches. There the container has no layout box,
so pdf.js bails out of scrollPageIntoView on a null offsetParent and
update() early-returns with zero visible pages, leaving its location null.
Arming anyway meant the reader's first zoom after switching to that
worktree scrolled to the page top and persisted it over the cached
position — turning a pane that merely failed to restore into one that
destroys the saved position.
Stay disarmed and keep the input watcher attached while the container has
no height, so the cached entry survives until the reader is actually
looking at the pane. A pane that gains layout between pagesinit and
pagesloaded still restores normally.
Co-authored-by: Orca <help@stably.ai>
* defer PDF scroll restore until hidden pane becomes visible
Why: a display:none pane can't scroll or update pdf.js's internal location, so
the reader's initial zoom persists over the cached position. Use ResizeObserver
to detect when the pane regains a layout box, then re-apply the saved scroll.
---------
Co-authored-by: Orca <help@stably.ai>
* Add first user prompt to AI Vault session history rows
Re-parse transcripts on demand to extract and display the untruncated first
user prompt for copy/reuse. List scans omit the body (payload/perf); UI loads
it when session details expand. Grok sessions extract the typed ask from
<user_query> envelope, skipping injected <user_info> bootstrap rows. Supports
Claude, Codex, Grok, and OpenCode agents.
* fix(ai-vault): split SessionTime out to pass max-lines lint
AiVaultSessionDetails exceeded the 400-line oxlint limit after adding
first-prompt UI; move SessionTime into its own module.
* fix(ai-vault): handle corrupt transcripts and fix OpenCode prompt captur
Corrupt transcripts now resolve null instead of rejecting the IPC call, matching behavior for other unavailable cases. OpenCode SQLite parsing now correctly captures all text parts from the earliest user message only, fixing truncation of large prompts and padding of small ones. Add stale-response guard in the UI to prevent late results from overwriting the current session when tabs switch. Consolidate text slicing via `sliceAtCodeUnitLimit` to avoid surrogate-pair splits across all callers.
* test(ai-vault): add first-user-prompt UTF-16 safety tests
Ensure truncation at safety limits doesn't split UTF-16 surrogate pairs,
preventing corruption of astral characters in captured prompts.
* fix(ai-vault): key first-prompt-card by session.id
Remounting the card on session switches prevents late responses from
a previous load from writing stale data into the component's refs.
Also improves conversation-turn key stability.
* fix(ai-vault): preserve first prompt after preview truncation
* refactor(ai-vault): improve first user prompt capture robustness and per
- Add 15s timeout to full-prompt load to prevent indefinite loading states
- Extract seedFullFirstUserPrompt helper for reuse across parsers
- Prevent AI-generated summaries from becoming the copyable first prompt
- Fix truncation detection in OpenCode SQLite by probing for N+1 rows
- Optimize text bounding to apply safety limit before toLowerCase
- Gate synthetic OpenCode path detection on agent type, not just # presence
- Add test coverage for remote execution host handling
* Fix FirstPromptCard loading state stranded by stale promise reuse
Clears loadPromiseRef during cleanup to prevent the dedupe handle from
causing StrictMode remounts to await stale in-flight requests. Stops loading
when session becomes non-loadable mid-request. Adds tests for StrictMode
double-invoke resolution and main-process timeout scenarios.
* refactor(ai-vault): split session parsers into modular files
Split secondary-parsers into individual files per agent type (copilot,
cursor, hermes, opencode) for improved modularity. Add test coverage
for first-user-prompt envelope handling: unwrap user_query tags and
reject bare user_info dumps.
* fix(ci): clear max-lines and flaky portal readiness check
Collapse an accidental multi-line regex wrap in ssh-connection-utils that
pushed counted lines to 301. Harden the latched-readiness test's ready
transition so CI load can re-observe attach after MutationObserver gaps.
* fix(ssh): extract proxy command helpers to pass max-lines
Move resolveEffectiveProxy/spawnProxyCommand out of ssh-connection-utils
so oxfmt line wrapping cannot push that file over the 300-line lint cap.
* capture first user prompt by ordering OpenCode messages by creation time
- Add `readOpenCodeMessagesInOrder` to rebuild transcript by timestamp, handling
corrupt/partial files gracefully instead of discarding sessions
- Extract SSH proxy command tests to dedicated file; add backpressure handling
and stderr draining to prevent proxy process stalls
- On Windows, reject unsafe characters in ProxyCommand values instead of
pretending to escape them; properly format cmd.exe invocation with verbatim
arguments
- Expand ProxyJump chains into -J plus final hop, mirroring OpenSSH behavior
- Decouple portal readiness reapply budget from flip-count budget via explicit
constant
pnpm dlx knip@5 downloads and runs whatever the newest 5.x is at
invocation time, outside lockfile integrity review — flagged P2 by the
v1.4.165-rc.0 release scan. Pin the exact version, matching the
react-doctor@0.9.1 pattern one line up.
Why dlx rather than a devDependency: knip 5.x peer-depends on
typescript@^5, and this repo is on typescript 7.0.2 — installed as a
devDependency, pnpm resolves knip against TS 7 and knip crashes at
module load (verified: same knip against a TS 5 peer runs clean). The
dlx sandbox auto-installs knip's own TS 5, which is the environment the
original #12077 sweep actually ran in.
Worker-start passed the Orca agent id straight to the shell as the worker terminal command, so `--agent cursor` ran `cursor` — which on Windows resolves to Cursor IDE's cursor.cmd and opened the desktop app, leaving a blank shell that timed out at agent_readiness. The same gap hit every agent whose CLI binary differs from its id (continue/aug/kiro/qwen-code/mistral-vibe/antigravity/trae/mimo-code/hermes/command-code/claude-agent-teams).
Adds TerminalCreateOptions.startupAgent so callers name the agent outright; createTerminal then builds the launch from the TUI agent config (command, agentCmdOverrides, default args/env, preflight trust) instead of sniffing the command string. Also covers repo-less folder workspaces, which previously skipped resolution entirely, and fails loudly instead of spawning a bare shell when an explicit agent cannot resolve.
Fixes#11926
* Honor configured shells during worktree setup
* Align setup launch paths with selected Windows shells
* Carry setup shell selection through deferred launches
* Prove Windows setup shell routing at its real adapters
* Ground remote PowerShell proof in the real writer
* Preserve Git Bash across deferred setup launches
* Harden Windows setup runner shell selection
- Resolve remote PowerShell binary without local pwsh probe: for SSH/remote
Windows worktrees, isPwshAvailable() reflects only the LOCAL host, so an
'auto' implementation could route the remote runner to a pwsh.exe the remote
lacks. Add resolveSetupRunnerShell(..., { probeLocalPwsh: false }) so remote
auto keeps the always-present powershell.exe; explicit pwsh.exe still honored.
- Preserve native exit codes in the PowerShell runner by checking
$LASTEXITCODE before $?, so a failing native command surfaces its real code
instead of a generic exit 1; $? still catches cmdlet soft-failures.
- Write the PowerShell runner with a UTF-8 BOM so Windows PowerShell 5.1 (the
new default powershell.exe) reads it as UTF-8 instead of ANSI, preventing
non-ASCII setup-script corruption.
- Add unit tests for the remote-probe behavior.
* Restore setup-shell scope narrowing over the rebase
The force-pushed rebase dropped five review-fix commits that were already
on this branch; this reapplies their combined effect on top of the new
base and the hardening commit:
- Keep SSH setup shell selection remote-owned (no local terminalWindowsShell
or pwsh routing for remote hosts; supersedes the probeLocalPwsh guard)
- Preserve cmd setup compatibility outside POSIX shells (no .ps1 runner
family, so the BOM/exit-code hardening is no longer applicable)
- Route WSL setup runners from the project runtime
- Avoid blocking PowerShell probes during setup creation
- Correct SSH and WSL background setup fixtures
* Satisfy the changed-code gates for the setup-shell runner
- createWorktreeRunnerScript took 7 positional parameters, tripping the
changed-code max-params gate; move it to a single options object.
- hooks-runner.test.ts deep-equals the createSetupRunnerScript result, so
assert the cmd shell now returned for native Windows worktrees.
* Carry the setup launch shell through observed and issue runners
- buildObservedSetupCommand takes the runner's launch shell so WSL-routed
Windows-drive setup replays use /mnt/c instead of Git Bash /c
- resolveSetupRunnerShell gates the posix runner on the same Git Bash
resolution the PTY uses, so a missing or non-MSYS bash keeps the cmd runner
- issue-command runners carry their launch shell, and the renderer passes it
when building the queued command
- treat a bare `bash` shell setting as POSIX like `bash.exe`
Co-authored-by: Orca <help@stably.ai>
* fix(worktrees): close counsel P1 gaps for Windows setup shells
Route windowless/headless creates through the shell-aware setup runner when a
PTY controller is available, existence-check explicit Git Bash paths before
committing to .sh runners, thread the resolved shell into issue-command
runners, and document the intentional Git Bash interpreter flip with a narrow
scope table.
* Convert setup env to MSYS form and harden the bare cmd runner launch
C3: a Git Bash setup runner now receives ORCA_*/CONDUCTOR_*/GHOSTX_* path
values in /c/... form, matching the runner path and the shell's own HOME/PWD.
C5: extension-less `bash` resolves to Git Bash everywhere, matching how
resolveWindowsShellStartupFamily already classifies it.
C7: runner paths carrying characters that cannot be quoted on a cmd command
line launch through a delayed-expansion PowerShell shim instead, and the batch
runner disables inherited delayed expansion so `!` in setup lines survives.
Co-authored-by: Orca <help@stably.ai>
* docs: note MSYS ORCA_* paths and bare bash Git Bash resolution
Keep the setup-shell release note aligned with C3 env conversion and C5 bare
bash resolution so the published claim matches runtime behavior.
* revert: drop windows-setup-shell doc allowlist and AGENTS link
Keep the counsel P1/P2 product fixes without expanding the docs allowlist
or AGENTS.md guidance surface.
* fix(plugins): contain Parcel unsubscribe rejections under Vitest
Dev plugin watchers fire-and-forget unsubscribe, and in-process Parcel
can reject when temp watch roots are already deleted. Catch those
rejections so they cannot fail the suite as unhandled errors.
* fix(plugins): keep in-process unsubscribe rejection surface
Swallowing Parcel unsubscribe errors broke mocked unsubscribe tests
that return non-Promises and expect rejections. Contain failures only
in PluginDevWatcher fire-and-forget paths.
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(gitlab): count diff lines whose content begins with -- or ++
countDiffLines skipped every line starting with ---/+++ as a file header,
but a removed line whose original text began with -- (SQL/Lua/Haskell
`-- comment`) becomes a diff line `---<content>`, colliding with the
`--- a/file` header — so its deletion was silently dropped from the
+N/-N shown in the GitLab MR dialog. Same collision for an added line
whose content began with ++ (+++ flag).
Track hunk state: ---/+++ are file headers only before the first @@;
inside a hunk every +/- is content, matching the unified-diff rule git
itself uses to disambiguate headers from content.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(gitlab): validate countDiffLines with actual diff format
GitLab's /diffs endpoint returns json_safe_diff starting at @@ without
file headers. Add comprehensive test coverage validating the collision
fix correctly handles this format: content lines beginning with -- or ++
are counted as additions/deletions.
Tests cover binary files, empty diffs, no-newline markers, and content
beginning with @@ or C-style ++. Clarify function contract: requires
hunk headers to distinguish headers from content lines.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* fix(workspace-cleanup): read reflog timestamps to avoid git maintenance
Workspace activity detection now reads the reflog to find the newest HEAD movement,
avoiding false activity signals from `git gc` and `git status` restamping logs/HEAD.
Extraction of git mtime probes to exclude files that maintenance rewrites (gitdir,
index, logs/HEAD), and instead read commit markers (COMMIT_EDITMSG, ORIG_HEAD) and
reflog entry timestamps. Expands the scan with a renderer-side activity estimate to
reconcile against the Resource Manager button's fast count. Adds deletion phase
tracking (queued vs deleting) and a mismatch notice when the two counts diverge.
* fix(workspace-cleanup): parse reflog timestamps with fewer digits and im
- Regex now accepts 1-11 digit timestamps (was 9-11); trailing timezone anchor makes digit-count floor unnecessary
- Add `removalInFlight` state to prevent duplicate removal batches; UI checks this flag alongside `removalProgress`
- Filter scan errors by selected repos; only show estimate-mismatch notice when scan is complete and error-free
- Mark candidate rows as non-selectable while deleting, even if `removing` flag is omitted
The leading-icon gutter used self-start with a hand-tuned pt-0.5 nudge,
leaving status dots ~2px and lucide icons ~1px above the 20px title line
box. Give the gutter h-5 to match the line box so icons center on the
first line for single- and two-line rows alike.
* feat(voice): allow selecting a microphone for dictation
Persist a preferred audioinput device in Voice settings and pass it into
getUserMedia, falling back to the system default when the device is gone.
* fix(voice): resolve mic preference by label and detect mid-capture loss
Drop Chromium's 'default'/'communications' aliases from the picker — pinning
one behaved exactly like system default and silently defeated the setting.
Resolve a stored preference against the live device list before capturing:
a unique label match heals an id that Chromium re-salted, a known-missing
device skips the doomed getUserMedia attempt that clipped the first words,
and an unreadable list no longer reads as "unplugged".
Surface the input ending mid-dictation instead of feeding silent zeros, add
a permission affordance so the picker is not empty before mic access, and
toast the fallback once per preference rather than once per utterance.
Co-authored-by: Orca <help@stably.ai>
* add e2e tests
* add e2e tests
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(browser): keep browser tabs rendering across worktree switches (STA-3228)
Switching away from a worktree that had a targeted background mount
unmounted BrowserPaneOverlayLayer, pulling the persistent <webview>
slots out of the DOM and killing their guests; the stale viewport cache
then kept rendering into the removed subtree, so the tab stayed blank
forever and reload threw. Keep the overlay mounted for hidden worktrees
(slots park their panes, so this stays cheap) and rebuild cached
viewports whose slot root remounted.
* fix(browser): retain live overlay slots without background churn
* fix(browser): latch overlay retention after commit
* fix(tasks): cap advertised GitHub pages at the search result window
GitHub's Search API rejects requests past its first-1000-results window
with HTTP 422, but totalPages was derived from the raw total_count, so
the pagination bar advertised pages that could never load and clicks on
them silently did nothing (#11485).
Cap per-repo advertised pages at floor(1000 / perRepoLimit), and when a
page load comes back empty, say so with a toast instead of ignoring the
click — clamping the advertised count only when no fetch threw, so
transient failures don't shrink the bar.
* fix(tasks): key pagination resets on repo selection, not array identity
The repos store installs a fresh array on every repos:changed event, so
the pagination-reset effect fired on background refreshes and bumped the
request generation, silently discarding any in-flight page navigation —
clicking an unloaded page did nothing whenever a repo refresh landed
during the fetch. Key the effect on the stable selection string instead.
* fix(tasks): distinguish end-of-data, window 422s, and failures on empty pages
Adversarial-review round 1 rework:
- fetchWorkItemsNextPage now returns issue-side envelope error types — the
channel the search-window 422 actually travels on (failedCount only
counts thrown repo calls).
- resolveEmptyPageOutcome (unit-tested) maps an empty page to
window-unreachable (clamp + toast), load-failed (toast only; may be
transient), or end-of-data (silently withdraw the speculative page the
count-fallback advertises).
- The work-items fetch effect is keyed on selectedReposKey too — its
unconditional page reset re-fired on every repos:changed array identity,
bouncing the user to page 1 mid-click. The key now includes the resolved
GitHub source context so identity changes still re-dispatch.
- Toasts carry stable ids so repeats replace instead of stack.
- Cap comment documents the conservative PR-scope tail loss; cap tests
pinned at shipped (36 → 27) and dividing (25 → 40) limits.
* fix(tasks): withdraw the speculative page when the failed count is zero
countedTotalPages of 0 comes from a swallowed count failure and routes
totalPages through the fallback, so the clamp must replace it like null.
* fix(tasks): tighten empty-page outcomes after round-2 review
- en.json's loadPageUnreachable carried the pre-reword text, and the
catalog beats the inline default — the two toasts were identical.
- end-of-data clamps only while the count is unknown/failed: the PR list
path swallows its own failures into clean-empty results, and clamping a
real count silently hid healthy pages (worse than the pre-fix no-op).
- A window 422 no longer clamps when a sibling repo's fetch threw.
- The generation effect mirrors every fetch-effect dep that resets page
state, so manual refresh/source switches invalidate in-flight clicks.
- selectedReposKey extracted as buildSelectedReposKey with stability
tests; envelope error types wire-tested through the store.
* fix(tasks): clamp against the committed count, not the click-time closure
Round-3 review: the count promise routinely resolves between click and
response, so deciding the end-of-data clamp from the closure value let a
stale null overwrite a real count. applyEmptyPageClamp now runs inside
the functional updater against the committed value, never raises an
earlier clamp, and a window 422 coinciding with a thrown sibling repo
resolves as load-failed so the toast and the clamp always agree.
* fix(tasks): only an all-window-422 empty page may clamp; harden count merges
Round-4 review: a sibling repo's envelope 403/404 arrives with
failedCount still 0, so the window branch now requires every error to be
the window 422 (non-window validation errors are demoted at the store);
the count resolution mins against an applied clamp instead of
re-advertising withdrawn pages; the generation effect mirrors
taskResumeApplied so its doc claim holds.
* fix(tasks): split the proven window limit from the count slot
Round-5 review: min-ing the count against an applied clamp pinned a
SPECULATIVE end-of-data withdrawal that raced ahead of the count,
permanently collapsing the bar for the generation. Proven window-422
limits now live in provenPageLimit (set once, only lowered, reset per
generation); the count overwrites its own slot unconditionally; and
deriveAdvertisedTotalPages (unit-tested for both arrival orders) caps
the count-or-fallback estimate with the proven limit, floored at the
loaded pages.
* fix(tasks): surface PR-side list failures so they can't read as end-of-data
Round-6 review: PartialWorkItemsResult had no PR error slot, so a
swallowed gh pr list failure reached the renderer as a clean empty page
— and with the count blocked (0) the speculative withdrawal deleted the
pagination bar with no toast and no recovery (a regression vs main's
silent no-op). PR-side errors now ride the envelope (errors.prs),
demoted so they can never join the issue-only window-422 signal;
errorTypes replaces issueErrorTypes; an empty page that a real count
said should exist now toasts instead of looking dead.
* test(tasks): cover the PR-error envelope end-to-end; neutral no-more-results toast
Round-7 review: the two literal gh-utils mocks lacked classifyListPrsError
(a PR-side rejection in those suites would TypeError instead of assert),
and the producer half of the errors.prs contract had no main-side test —
added both, plus a classifier contract test pinning the search-window
phrase the renderer keys on. The refused-clamp toast now reads the
committed count via a synchronous ref mirror instead of the click-time
closure, and says 'No more results' — nothing failed on that branch.
Both toast keys plus the new one are translated in es/ja/ko/zh.
* fix(tasks): preserve final reachable GitHub search page
* Extract GitHub search result window error pattern to shared constant
Extract the 1000-result window detection pattern to a single source of truth so
the classifier and consumer stay synchronized. The pattern is the only signal
separating a permanently unreachable page from a transient validation failure,
so drift or trimming silently demotes window 422s to generic failures and stops
capping the advertised page count (#11485).
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Allow clearing all agents from AI Vault session history filter
Add "Select all" / "Clear" buttons so users can quickly isolate one agent without unchecking each box individually. Previously, at least one agent had to remain enabled; now users can filter to zero agents and re-enable selectively.
* Address PR #12128 review feedback
- Make Select all / Clear real DropdownMenuItems so Radix roving focus reaches them by keyboard.
- Rename the zero-agent empty state to a neutral "No agents selected" now that zero agents is a valid filter.
- Use 모두 해제 for the Korean Clear label instead of 지우기 (erase).
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Both hosts carried token-identical copies of the GitHub work-item mutation
wrappers, PR diff mapping, presentation formatters, and four components.
Move them into src/renderer/src/components/github/ so there is one source.
Behavior-neutral: getStateTone, WorkItemStateBadge, PRReviewersPanel,
PRActionsPanel, CommentReplyForm, and the per-host work-item/PR-file caches
stay in place because they genuinely differ between the two hosts.
CommentCodeContext takes loadPRFileContents as a prop so each host keeps its
own private file-contents cache.
Also folds github-issue-comment-helpers.ts into github-user-avatar.tsx and
retargets the textual boundary tests at the new modules.
* refactor(usage): share the session/daily fold between Codex and OpenCode
The Codex and OpenCode scanners each carried their own byte-identical copy of
the ~325-line aggregation pipeline (createEmptySession, the three breakdown
folds, finalizeSessions, mergeSessions, mergeDailyAggregates). Two copies means
a token-accounting fix — a bucket that double-counts, a merge that drops a
breakdown row — lands in one provider and silently not the other. The copies had
already started to drift in comments only; the next drift would have been in
arithmetic.
The providers differ in exactly one dimension: the extra metric folded alongside
the token counters (Codex `hasInferredPricing`, OpenCode `estimatedCostUsd`).
That is now injected as an empty/fromEvent/fold triple, so the shared code stays
generic without collapsing the two record schemas into a nullable union. The
clone strategy stays per-provider (`cloneSessionForMerge` vs `structuredClone`)
rather than being unified on the assumption that the difference is accidental.
`usage-provider-contract.ts` is the seam a plugin-contributed usage source will
implement. It is deliberately generic over each provider's record types: Claude
bills per turn while Codex/OpenCode bill per event, and `cachedInput` is a subset
of `input` for the latter but a peer bucket for Claude, so a single normalized
record would push nullable handling onto every consumer.
No behavior change. Emitted objects are byte-identical, including key insertion
order — verified by diffing JSON.stringify of the scan output before and after
across mixed models, mixed locations, an inferred-pricing flip, and null vs
non-null cost. Persisted field names and schemaVersion are untouched, so caches
do not invalidate.
* refactor(usage): make the provider contract load-bearing and dedupe worktree refs
Follow-up to the aggregation extraction, addressing three review points.
`UsageProvider`/`UsageScanResult` were declaration-only, which is the same
speculative-interface problem #12077 just deleted 8,900 lines of. They are now
implemented by both real providers via `satisfies`, so the seam is typechecked
against actual scan functions rather than asserted. The blocker was that codex
returns `processedFiles` and opencode returns `processedDatabases`; rather than
rename persisted-adjacent fields, the source key is a type parameter, so each
provider keeps its own on-disk name and the contract still binds. Verified the
constraint bites: swapping the key to 'processedSources' fails typecheck.
`schemaVersion` is part of provider identity in the contract, so each provider's
SCHEMA_VERSION constant (with its cache-invalidation rationale) moves into the
provider module and the store imports it. Values are unchanged (codex 5,
opencode 2) and the stores compare them exactly as before, so no cache
invalidates. This also keeps store -> provider -> scanner acyclic.
`UsageWorktreeRef` collided with the existing export in usage-worktree-metadata
(3 fields, no repoId). Two different exported types under one name in src/main
is worse than the duplication being removed, so the scan-input type is now
`UsageScanWorktreeRef`; usage-worktree-metadata is untouched.
`createWorktreeRefs` was triplicated. Codex, OpenCode, and Claude copies are
byte-identical apart from the return type name (verified by diff), and all three
ref types have the same four fields, so one shared copy replaces all three. This
is the only change to claude-usage/.
No behavior change: same functions, same arguments, same call order. The store
tests' `./scanner` mock still intercepts scanning because the provider captures
the mocked binding; their now-inert `createWorktreeRefs` mock key is dropped so
it does not read as still mocking something.
findRemoteForUrlSsh, ensureUniqueRemoteNameSsh and
configureCreatedWorktreePushTargetSsh were byte-identical to
findRemoteForUrl, ensureUniqueRemoteName and
configureCreatedWorktreePushTargetWithExec in worktree-push-target-setup.ts,
differing only in calling provider.exec instead of the injected execGit.
528a887ab5 extracted the shared module out of worktree-remote.ts and left
the SSH twin behind, so this is an unfinished extraction rather than a
deliberate split.
Feed provider.exec through the existing GitRemoteExec seam instead — the
same adapter cleanupUnusedWorktreePushTargetRemoteSsh already uses. Drops
the now-unused parseGitHubOwnerRepo import.
prepareWorktreePushTargetSsh is intentionally left alone: it uses
provider.fetchRemoteTrackingRef (which forces --no-tags and pre-validates
refs) rather than a raw fetch refspec, and carries its own check-ref-format
preamble, so it is a real behavior difference and not a clone.
No behavior change: the substituted bodies are byte-identical.
* feat(updater): add an adhoc release channel for branch builds
Hourly covers main. This covers everything that is not main yet: a
dispatchable macOS build of an unlanded branch, published to
stablyai/orca-adhoc, so the team can run an experimental feature for a
few days instead of reasoning about it from a diff.
Adhoc sits at the bottom of the version order — 'adhoc' < 'hourly' <
'rc' < stable — so no routine check can walk anyone onto somebody's
branch; only an explicit pinned jump reaches one. It gets its own repo
rather than sharing orca-hourly's, because a branch build must not
appear in the list a developer riding main is looking at.
Signed and notarized exactly like hourly, for the same reason: macOS
anchors a notarized app's TCC grants on identifier + team, so an
unnotarized build reads as a new client and silently loses file access
under Documents/Desktop/Downloads.
Tags stamp to the second rather than the minute. Hourly runs under a
concurrency group and cannot overlap itself; adhoc builds are dispatched
on demand, so two people cutting from different branches inside one
minute is ordinary — and a minute-resolution tag would collide and fail
the second build after its whole pack-and-notarize run.
Channel-specific behaviour now derives from one DEDICATED_REPO_CHANNELS
list: repo mapping, macOS-only support, and UpdateSource. The RPC schema
that validates releaseChannelOverride was a hand-copied enum missing the
new channel, which would have rejected the override on its way to the
main process; it reads the predicate now.
* fix(updater): merge the duplicated shared/types import
Co-authored-by: Orca <help@stably.ai>
* fix(ci): default the adhoc build ref to the dispatch branch
The Actions UI puts its own "Use workflow from" branch picker directly
above the ref field, and picking a branch there is what most people read
as "build this". Making the field optional means the obvious action is
also the correct one; naming a branch explicitly still wins, so main's
copy of the workflow runs rather than a stale one on an old branch.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
mobile/src/constants/marine-creatures.ts was a hand-maintained copy of
src/shared/marine-creatures.ts, identical except for a comment header. The
copy existed because Metro only watched mobile/ and could not resolve
repo-root modules; mobile/metro.config.js:11 added src/shared to
watchFolders five weeks later, and ~195 mobile files already import from
src/shared. The renderer collapsed its copy to a re-export at the same time;
mobile was the leftover.
Point the one consumer at the shared corpus and delete the mirror, the
bespoke regex-scraping parity test that policed it, and the now-stale
max-lines baseline entry.
No behavior change: same exported symbol, byte-identical name list.
Recovery and broker open now send the optional reconnect hint so the
director admits already-assigned hosts through its bounded fast lane
(orca-cloud#212) instead of the placement queue that starved session
recovery during the 2026-08 incident. A rolled-back director that
rejects the hinted field gets one unhinted retry.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(editor): stop filing crash reports for expected lazy-chunk swaps
RichMarkdownErrorBoundary reported every caught error as a react-error-boundary
crash, including the LazyChunkLoadError sentinel that lazy-with-retry throws
after it has already exhausted its retries and its one guarded reload. That
sentinel means "the chunk hash changed under a running window" (an app update),
which is deliberate graceful degradation, not a crash.
RecoverableRenderErrorBoundary already skips reporting it (#6206); this boundary
was never updated. Crash b860def2 is exactly that path: a lazy_chunk_reload
breadcrumb ("Unexpected token ':'") fires first, then the post-reload attempt
surfaces LazyChunkLoadError and files a report.
The fallback UI is unchanged, so the pane stays usable and offers retry.
* fix(editor): prove the lazy-chunk reload landed before suppressing crash reports
- lazy-with-retry: reload guard stores the requesting document's identity, so a
vetoed reload() no longer reads as "recovery ran" (crash b860def2)
- lazy-with-retry: bound the post-reload suspension so a vetoed navigation
surfaces the real error instead of hanging the pane on a spinner
- RichMarkdownErrorBoundary: contain the LazyChunkLoadError sentinel without a
crash report, but record a lazy_chunk_boundary_degraded breadcrumb
- EditorContent: name the rich markdown chunk at the lazy call site
Co-authored-by: Orca <help@stably.ai>
* fix(editor): route lazy-chunk recovery reload through the intentional-restart path
Crash b860def2's recovery reload was requested and never landed: Terminal's
beforeunload handler preventDefault()s while any editor tab is dirty and Electron
cancels the navigation with no dialog, so chunk recovery could never run in the
common case. Take the updater's path instead — hot-exit backup, one synchronous
session checkpoint, restart latch — then reload.
- Reject on ORCA_RENDERER_UNLOAD_PREVENTED_EVENT instead of a blind, never-cleared
10s timer; keep the timer only as a backstop.
- Record a lazy_chunk_reload_vetoed breadcrumb in the same tick as the report it
now files, so the 30-entry ring cannot evict the evidence.
- Drop this document's own stale guard after a refused reload (capped in memory)
so saving the blocking tab does not forfeit recovery for the session.
- Carry reloadKey on LazyChunkLoadError and the degraded breadcrumb.
- Move renderer-restart-preparation to src/shared: it is now a renderer/preload
contract, and the composite web project cannot import preload runtime code.
Co-authored-by: Orca <help@stably.ai>
* fix(editor): clean up failed lazy chunk reload requests
* test(preload): exercise restart IPC registrations
---------
Co-authored-by: Orca <help@stably.ai>
* fix(i18n): localize status labels in settings and stats
Status pills and summaries in Settings and Stats were built from bare string
literals inside local helper functions, so they stayed English even when a
language pack was active. Neighbouring copy in the same components already went
through `translate()`, which made the panes look half-translated:
"Универсальный доступ GRANTED", "GitHub ... Connected", Russian orchestration
card titles above English summaries.
The coverage audit does not catch this: it inspects JSX attributes and object
properties, not values returned by helpers, so `verify:localization-coverage`
stays green while the strings ship untranslated.
Wrapped the remaining user-facing strings in `translate()` and let
`sync:localization-catalog` add the 31 new keys. `computerUseSummary.*` already
existed in `en.json` with identical copy but was never wired up, so those keys
are now connected instead of duplicated.
Moved the permission status helpers into `developer-permission-status.ts` to
keep `DeveloperPermissionsPane.tsx` under the 400-line gate. Agent prompts in
`orchestration-usage-examples.ts` are left in English on purpose: they are
payload sent to the agent, not UI copy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(i18n): use plural-aware message keys for settings labels
Replace template-based pluralization (using {{value1}} for "s") with
proper i18n plural forms following _one/_other suffixes. This enables
correct pluralization across languages with distinct rules.
Also extract duplicate integration status label translation logic
into a helper function.
* CodeRabbit's nitpick: the placeholder/plural assertions in settings-status-label-localization.test.ts only read en.json, so a translated catalog could ship a stale {{value1}} or a half-translated plural family undetected.
Added a second describe block over the four shipped locale catalogs (es, ja, ko, zh), keeping the exact English-value assertions untouched and separate:
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* fix(speech): coalesce model download progress churn
A model download emits one state update per HTTP chunk (thousands for a
500MB model). Each one costs the renderer an IPC round-trip and a forced
re-render of the speech-model menu, which stays open by design while a
download runs — the Radix portal/Presence tree all four page.settings
React #185 reports crashed inside.
Emit at whole-percent granularity (what the UI renders) and keep the
modelStates array identity stable when a refresh changed nothing, so a
no-op refresh no longer forces a commit.
Adds a speech_model_state_churn breadcrumb, registered in both
coalescing sets, because no bundle in the cluster carried any speech
telemetry to confirm a download was in flight.
* fix(speech): quantise polled model state so download progress stops churning the renderer
Adversarial round 1 found the renderer-side stabilisation was inert during a real
download. The progress fan-out already coalesces to whole percent, but the renderer
discards the event payload and re-polls getModelStates, and getModelState returned
the cached downloading state verbatim - raw sub-percent progress. So every chunk
produced a fresh object, resolveModelStates never matched, and all the real benefit
came from the main-side coalescing alone.
Quantise the polled reply to the precision the UI already renders
(Math.round(progress * 100)), keeping the stored cache exact.
Also from round 1:
- the whole-status dedup swallowed downloadModel's already-downloaded branch, whose
lone 'ready' is the only notification the requesting window ever gets - a dead
click and a permanently stale pane with two windows open. Gate it on
downloading -> downloading so every one-shot transition stays unconditional.
- rename the storm test off '.react185.': it counts renders and asserts nothing
about #185, and #185 could not be reproduced at IPC-realistic pacing.
* fix(speech): stop the churn breadcrumb firing on every healthy download
Adversarial round 2. CHURN_REFRESH_THRESHOLD was 60 per 5s window, but main
clamps download progress at 0.9 and emits on whole-percent change, so one
healthy download is capped at ~91 refreshes — and a 56MB model on a fast link
lands all of them inside a single window. The breadcrumb fired on every normal
download and burned a slot in the 30-entry ring it was coalesced into to
protect. Raise it to 250 so it only fires on the per-chunk shape it was added
to detect; noOpRefreshes in the payload still separates the two causes.
Round 2 also found that every assertion round 1 added was one-sided
(toBeLessThanOrEqual), so each of the three core behaviours could be mutated
into "do nothing" with the whole suite still green:
- suppress every downloading -> downloading event: progress bar frozen at 0%
- toWholePercentState returning 0: every poll reports 0%
- resolveModelStates never adopting a same-length change: the Voice pane never
updates and a finished model never shows as ready
The suite measured that the fix reduces work, never that it still does the
work. Convert the two ceilings to exact series, assert the storm test's render
floor as well as its ceiling, and add dictation-model-state-stabilisation.test.ts
covering adoption per changed field, a full whole-percent download, and both
sides of the churn threshold.
Ruled out and deliberately not fixed: round 1's request-sequencing MEDIUM on
refreshModelStates. 50 concurrent getModelStates() settle strictly FIFO over
ipcRenderer.invoke at constant microtask depth, and migrationReady is assigned
once in the constructor, so a monotonic request id would be dead code.
* test(crash-reporting): cover speech churn breadcrumb name-coalescing
The churn breadcrumb's registration in COALESCED_RENDERER_BREADCRUMB_NAMES and
NAME_ONLY_COALESCED_BREADCRUMB_NAMES had no test: the storm test only asserted
the constant equals its own literal. Removing either registration kept the whole
suite green.
It carries no message field, so without the name-only entry
rendererBreadcrumbCoalesceKey returns undefined and every firing takes its own
ring slot — the eviction this breadcrumb exists to avoid.
* test(dictation): pin the churn threshold as a rate, not a lifetime total
Both existing churn tests stopped at exactly 250 refreshes in one window, so two
mutations survived: deleting the per-window reset (the counter degrades into a
session total, and three healthy downloads at 91 refreshes each cry wolf), and
=== to >= (fires on every refresh past the threshold, flooding the ring).
Pins Date.now rather than using fake timers so the window rule is measured, not
the machine.
* test(dictation): pin the churn clock and the window's lower bound
The two 250-refresh churn tests measured real elapsed time against a 5s
window, so a loaded runner that took longer than one window to run the
loop would roll the window and go red. Pin Date.now in both.
Pinning alone leaves CHURN_WINDOW_MS unpinned below: shrinking it 5_000 ->
50 kept all 13 tests green, yet a 50ms window can never accumulate 250
refreshes and the detector would be dead. Add a storm spread across most
of one window so the constant has to be wide enough to hold a sustained
storm, not just an instantaneous burst.
Co-authored-by: Orca <help@stably.ai>
* refactor(speech): narrow progress churn fix
---------
Co-authored-by: Orca <help@stably.ai>
* fix(dev): split the confirmation dialog so Fast Refresh can accept it
`confirmation-dialog.tsx` exported both `ConfirmationDialogProvider` and
`useConfirmationDialog`, so React Fast Refresh could never treat it as a
boundary and Vite applied every edit to it in two passes under two `?t=`
stamps. When a second file in the same subtree changed in one watcher batch,
`createContext` ran twice and the provider published one context object while
the consumer read the other — `useContext` returned null and the hook threw.
Two field crash reports hit this at `ChecksPanel`, both dev-server sessions.
The context and hook move to a new component-free `confirmation-dialog-context.ts`;
`confirmation-dialog.tsx` keeps the provider and now exports only a component, so
the refresh runtime accepts it. Not one line of the provider body changes — the 16
hook importers just point at the new module, `vi.mock` targets included, and
`App.tsx` is untouched.
* test(dev): pin the confirmation dialog Fast Refresh boundary
The split that fixed the context-identity crash had no test behind it: no
test imported ConfirmationDialogProvider, and the six vi.mock call sites
replace the hook module wholesale, so they pass just as well with the
provider and hook back in one file. Assert the module shapes the refresh
transform actually keys on -- the context module registers no component,
so it never gets an HMR footer to invalidate through.
Co-authored-by: Orca <help@stably.ai>
* test(dev): assert the refresh boundary on the module namespace
The source-regex guard did not guard. Its patterns match only declaration
forms, so `export { useConfirmationDialog } from './confirmation-dialog-context'`
in the provider module -- which restores the crash, verified in a browser --
passed it 3/3. It also failed on a comment that merely contained the word
createContext, and would fail on React 19's `<Ctx value={...}>` shorthand.
Assert on the module namespace object instead, using react-refresh's own
component criterion, so re-exports and default exports are visible. The third
test renders the provider and resolves the hook through it, which is a real
behavioural check rather than a shape one.
Co-authored-by: Orca <help@stably.ai>
* test(dev): classify boundary exports with the refresh runtime's own predicate
The hand-rolled `^[A-Z]` name check called `export class Foo {}` a component;
the runtime rejects any class whose prototype carries extra members, so that
shape restored the two-pass split undetected. Use react-refresh's exported
`isLikelyComponentType` and mirror `isCompoundComponent` instead of a third
approximation. react-refresh was already resolvable only via shamefully-hoist,
so it is now an explicit devDependency.
* test(dev): tighten confirmation dialog boundary guard
---------
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): trust Pi CSI-u Shift+Enter on Windows (#9703)
Pi enables the Kitty keyboard protocol at startup and decodes CSI-u, but
TUI_AGENT_CONFIG['pi'] never set windowsShiftEnterEncoding, so on Windows
Pi could only get CSI-u via the flaky live-KKP-flag path
(isKittyKeyboardActivePane). After a tool ran a subprocess that emitted a
reset sequence, the KKP flags dropped to 0, Orca sent Esc+CR, and Pi read
it as plain Enter -> submit. It recovered on the next pane refocus.
Set windowsShiftEnterEncoding: 'csi-u' for pi, mirroring the Droid fix
(#7668), so the trusted CSI-u route covers Pi reliably independent of
KKP-flag churn from tool subprocesses.
* fix(terminal): complete Pi Windows CSI-u trust lifecycle
* test(terminal): name foreground retry timing
* test(git): accept bounded SSH remote probes
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>