"Hide sleeping" swept each project's main workspace out of the sidebar as soon as
it had no live PTY, browser tab or agent — even with "Hide default branch" off.
For a project whose only row is that workspace (a folder workspace, a fresh
clone, a detached-HEAD main), the entire project vanished with no in-place way
back.
Adds a shared `isSleepingSweepExemptWorkspace` predicate keyed on
`isMainWorktree` rather than the branch name, so folder workspaces (no branch),
detached-HEAD mains, and SSH rows whose head/branch are blanked while a provider
is disconnected all stay put. Wired into `computeVisibleWorktreeIds` (sidebar,
Cmd+1-9, workspace board), the jump palette's duplicate inline pass, and mobile's
`filterWorktrees`.
Ships default-on with an escape hatch: a persisted
`alwaysShowDefaultBranchWorkspace` setting surfaced as "Except default branch"
under "Hide sleeping". Explicit "Hide default branch" still wins, since it
filters before the sleeping sweep.
Mobile reads the setting but never writes it back, so a desktop opt-out can't be
clobbered by a filter tap before the ui.get roundtrip lands.
Combines the two PRs open against #8873. #8966's exempt set is a strict subset of
this one, so its production diff was subsumed rather than ported; its jump-palette
render harness and e2e spec were carried over, and are the only such coverage here.
Fixes#8873Closes#8966
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* test(repro): demonstrate #7732 GitLab pipeline job details never load in Checks panel
Co-authored-by: Orca <help@stably.ai>
* fix(gitlab): load pipeline job traces in the Checks side panel (#7732)
Expanding a GitLab pipeline job in the Checks panel always showed
"No inline details are available for this check.": the mapper dropped the
numeric job id, `PRCheckDetail` had nowhere to carry it, and every consumer
called the GitHub check-runs API, which returns null for a GitLab job.
- carry `gitlabJobId` on `PRCheckDetail` and add the `gitlab-job:` branch to
all three identity ladders (panel rows, editor tabs, fix-prompt keys) so
same-stage jobs with no web_url stop colliding
- add a runtime-routed trace client so SSH/remote workspaces work, not just
local IPC, and thread the MR's `projectRef` for fork pipelines
- bound the trace in main via the existing `sliceCheckLogTail` (now shared,
not GitHub-only) so a multi-megabyte CI log never crosses the 1 MB
transport frame cap; strip ANSI/section markers up to the CR only, which
keeps each section's visible header and command echo
- render the excerpt inline instead of "Log tail available in full details."
- feed GitLab traces to "Fix with AI", which previously sent bare check names
- skip the fetch for jobs that cannot have a trace (created/manual/skipped)
so GitLab's 404 does not replace the benign empty state, and re-arm a
failed load when the job's state changes since the panel has no retry
Co-authored-by: Orca <help@stably.ai>
* fix(gitlab): treat a missing job log as an empty log, not an error (#7732)
Round-1 review follow-up.
- a job canceled before it started (or whose log was erased/expired) is
`completed`/`cancelled`, so the panel fetched its trace, GitLab answered 404,
and `classifyGlabError`'s issue-edit copy ("Issue not found — it may have been
deleted.") landed verbatim on the auto-expanded check row; main now maps that
404 to an empty trace so the row keeps its benign empty state
- keep a missing project a real error (GitLab masks unauthorized projects as
404) and add `classifyJobLogError` so 403/unknown failures stop borrowing
issue-edit wording on a job-log read
- broaden the empty-log copy in all five catalogs: it now covers erased and
expired logs, not only jobs that never ran
- e2e: derive the repro screenshot dir from `process.cwd()` (or an env
override) instead of a hardcoded POSIX path to a throwaway worktree
- bound the raw trace before the ANSI/section passes so a multi-megabyte log
is not scanned in full on the main-process event loop
- drop the redundant `if (repo)` in `handleFixChecksWithAI` and the now-dead
"Log tail available in full details." catalog entry
Co-authored-by: Orca <help@stably.ai>
* fix(gitlab): address review — project ref on reload, retry re-arm, IPC timeout
- Carry the MR's GitLab project ref on the check-details tab so reloading a
fork/cross-project job tab fetches the trace from the pipeline's own project.
- Re-arm the sidebar retry when a details load resolves to null, not only when
it throws; a detail-less row otherwise never retried after the job moved on.
- Bound the local `gl.jobTrace` IPC call with the same 30s timeout the runtime
RPC path uses — glab runs without a subprocess timeout in main.
- Document that the trace 404 -> empty-log mapping is deliberately broad.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* test(repro): demonstrate #10142 tab X close bypasses running-process confirmation
Unit repro: closeTerminalTab (the X-button/middle-click entry) never consults inspectRuntimeTerminalProcess and drops a tab with a live child.
E2E repro: Cmd+W shows 'Stop running command?' for a tab running sleep 300; cancelling then clicking the tab X closes it silently.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): confirm running-process close on every tab close path (#10142)
The tab-strip X button, middle-click and the tab context menu closed a
terminal with a live child process without asking, while Cmd+W raised
"Stop running command?" for the same tab. The probe lived only in
TerminalPane's pane-level close handler; every mouse entry point reaches
closeTerminalTab(), which guarded pinned tabs and nothing else.
Move the decision into closeTerminalTab, above the web-runtime branch so
paired/remote host-backed tabs are covered too, and give the last-pane
keyboard close back to it instead of probing twice:
- running-terminal-close-guard.ts probes every live PTY of the tab and
fails open on a rejected probe or a stale remote handle, matching what
Cmd+W already did. No live PTY ids => fully synchronous close, so idle,
parked and hibernated tabs keep today's behavior.
- shouldConfirmRunningTerminalClose keeps lifecycle echoes, bulk closes,
CLI/RPC closes and the post-confirmation re-entry off the modal path.
- A standalone confirm store drives RunningTerminalCloseDialog, which
reuses the existing CloseTerminalDialog (no new user-visible strings).
The request carries the tab label because a tab-strip close can target
a tab the user is not looking at, and dedupes by tab id.
- TerminalPane.handleRequestClosePane now delegates the last pane to
closeTerminalTab. Its transport ptyId is nullable by design, so the old
path silently skipped the prompt mid-reattach; the pane keeps its own
probe only for closing one pane of a split.
- Agent panes win the dialog copy when a split has both an agent and a
plain command busy, instead of depending on PTY spawn order.
- Tab-group closeItem ran leaveWorktreeIfEmpty synchronously after a close
that can now defer; it moves to onClosed and still honors skipEmptyCheck.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): close the running-process confirmation gaps on every path (#10142)
Follow-up hardening on the tab-close confirmation, from review of the first
pass:
- A pinned tab with `confirmClosePinnedTab` off never got the running-process
prompt on any path, including Cmd+W, which is a regression against the old
pane-level behavior: the pinned branch short-circuited on pinned-ness alone
and re-entered with `force`, which the running guard excludes. The pin prompt
now supersedes only when it will actually appear; with the setting off the
close falls through to the running guard.
- The probe chain had no `.catch`, so a throw in the decision (a copy-kind
lookup on a tab id makePaneKey rejects, a store subscriber) left the tab
silently unclosed with no user feedback. It now fails open, as the pane path
it replaced did.
- A wedged remote inspect RPC could leave the X button looking dead for its
full 15s timeout. The probe is now bounded; every close path shares the
bound, so keyboard and mouse still behave identically.
- The agent-vs-command copy had two resolvers on exactly the keyboard/mouse
seam this issue is about. terminal-close-copy-kind.ts is now the single
policy; TerminalPane and the tab-strip guard both call it.
- The running queue is async while the pinned queue is synchronous, so both
could be pending at once and stack two modal overlays. The running dialog now
waits for a visible pinned confirmation.
- Deduping a repeat close request dropped the second caller's callbacks; it now
folds them in, so both closes resolve from one prompt. Ticking "don't ask
again" also drains queued prompts instead of showing one the user just opted
out of, and a queued prompt no longer inherits the previous tab's tick.
closeTerminalTab drops its private pinned predicate for the shared
isUnifiedTabPinned, whose only consumer the previous commit had removed.
* test(e2e): wait for `sleep` to own the terminal before closing it (#10142)
The running-process close specs polled `hasChildProcesses` to decide the tab
was busy, but macOS starts the shell under `login`, so an initialising terminal
already reports a child before `sleep 300` runs. Both specs could therefore
press close against a shell that never started the command: the probe correctly
saw an idle terminal and closed without asking, and the adjudicated repro failed
against a correct fix.
Wait for `foregroundProcess === 'sleep'` instead. Assertions are unchanged, and
the repro still fails at the pre-fix baseline (a92d8e0b0d).
* fix(terminal): ask instead of closing when the close probe times out (#10142)
Round-1 review follow-ups.
- The 4s probe bound closed the tab outright, but `inspectRuntimeTerminalProcess`
gives remote runtimes a 15s RPC timeout: any probe taking 4-15s silently killed
a running remote command that Cmd+W used to prompt about, and the pane path now
delegates its last-pane close to this guard. An unanswered probe is unknown, not
idle, so the timeout raises the confirmation with every pty treated as a
candidate. Failing open still applies to an *answered* probe (rejection, stale
remote handle), which is the pre-existing pane behavior.
- The split-pane Cmd+W probe had no bound at all; it now shares the same one, so
the two paths give the same answer to the same question.
- The renamed regression spec dropped its repro scaffolding: the hardcoded
/tmp screenshot directory (also a cross-platform path violation) and the
title/docblock that still described the bug as open.
* fix(terminal): adopt the double-activation guard and layout pty lookup from #10167 (#10142)
Cross-referenced against @innocarpe's #10167, which solved the same issue.
Two things it got right that this branch did not:
- Queue actions now hold off for 350ms after a queued request replaces the
visible one, matching the sibling pinned-tab confirmation. Without it the
second click of a double-click aimed at one tab lands on the next tab's
prompt and kills a running process the user never saw asked about — the
exact bug class this PR exists to close.
- The pty lookup unions the layout bindings with ptyIdsByTabId. A mounting
pane is bound into the layout before the liveness map catches up, and the
store's own teardown collector unions both for that reason, so reading only
the map let a close slip through that window with no prompt.
Also replaces the render-time ref write that failed React Doctor's
"Ref mutated during render" rule: the queued-request checkbox reset now goes
through CloseTerminalDialog's existing subject-change reset instead of
remounting via key, which keeps the exit animation on one element.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): keep relay runtime recovery alive without direct connectivity
A phone paired over the relay whose direct LAN endpoint is unreachable
(e.g. a Tailscale IP with Tailscale off) could lose the runtime channel
permanently: the reconnect controller's recovery gates parked with no
timer and no logs, the supervisor snapshotted relay credentials once at
start (dying silently if the read failed and dialing stale tokens after
rotation), and the only path that cleared a rejected-credential gate
required a working direct connection. Field symptom: home card shows
"Connected - Orca Relay" (or "Can't connect - check Tailscale") while
the host page sits at zero worktrees forever.
- gates (fresh-credential, external-signal) now arm a slow 60s reprobe
instead of parking; each gated attempt re-reads the durable credential
bundle and adopts it when its version is fresher than the rejected one
- supervisor start no longer dies for the process lifetime when the
initial Keychain read fails or the bundle is expired
- every recovery decision now reaches logcat and the in-app connection
log ([relay] lines); previously the whole relay dial path was silent
- direct-return probing extracted to mobile-direct-return-probe.ts,
credential selection to mobile-relay-credential-selection.ts
Regression suite mirrors the field failure (rejected outer credential,
unreadable bundle at start, expired bundle, E2EE rejection without a UI
nudge) plus real-rpc-client failover integration tests; the four
deterministic scenarios fail on the previous code.
* fix(mobile): adopt durable relay credentials by outcome, not version
Adversarial review caught two blockers in the version-comparison rule:
renewals extend expiresAt without bumping current.version, and a re-pair
restarts the version counter — both left the durable bundle unadopted
and reproduced the original outage. Selection now adopts the disk bundle
exactly when it yields a dialable (unexpired, non-rejected) credential
while memory does not, which also keeps revoked versions unresurrectable.
Also from review: the gate reprobe cadence now escalates 60s -> 15min
ceiling with 0.75-1.25x jitter (no fleet phase-alignment, no permanent
one-minute beacon); clearing a gate drops its timer, pending tick, and
cadence so an orphaned reprobe cannot swallow the next fast backoff; the
reprobe tick token is only minted while its gate still holds; and a
merely missing/expired bundle uses a plain cooldown instead of the
fresh-credential gate so it cannot force rotations on direct reconnects.
New regression tests (all red on the previous code): renewal without a
version bump, re-pair with a restarted counter, orphaned-timer backoff
swallowing, escalating gated cadence, and background/foreground recovery
after an E2EE rejection.
* fix(mobile): reset gated relay cadence on app resume
Review round 2: an escalated fresh-credential gate kept its cadence
across background/foreground, so reopening the app could wait out a
15-minute tick (measured 11.25min to first attempt after a 2h
background) — indistinguishable from the outage itself. A resume now
resets the streak even when it cannot lift the credential gate, and a
successful direct connection does the same in resetForDirectConnection.
Also: the streak now advances once per fired tick instead of once per
armed-delay computation (three arms per cycle escalated 60s -> ceiling
in ~7 minutes instead of the documented eight steps); delay computation
is a pure read.
* fix(mobile): rotate relay sessions on resume expiry, not attach deadline
Live phone verification of the failover fix exposed a second defect the
old latch had been masking: the relay-hello's leaseExpiresAt is the
cell's attach-reservation deadline (now + 10s for resumes,
credential-store.ts:213 server-side), but the supervisor scheduled
proactive rotation from it with a 30s margin clamped to 1s — so every
relay runtime session force-replaced itself ~1s after connecting
(measured every ~2.5s on device, 253 dials per 5 simulated minutes in
the red test). Any RPC slower than the cycle could never complete,
which is the "Worktree list unavailable" symptom.
The session now captures resumeExpiresAt from the hello (updated by the
resume confirmation) and rotation keys off it. Test fakes previously
used a 120s lease, which is why no suite ever reproduced the loop; they
now mirror the production 10s attach deadline, and a churn regression
holds one session across 5 minutes with direct unreachable.
* fix(mobile): clamp lease rotation delay on both ends
Adversarial review of the resume-expiry rotation fix caught an int32
setTimeout overflow: production resumeTtlMs is 30 days, and
30d - 30s = 2,591,970,000ms exceeds INT32_MAX, so Node (and vitest's
fake timers) clamp the timer to 1ms — 3001 relay dials and credential
writes in 3 simulated seconds, ~2500x worse than the churn being fixed.
The delay is now clamped to [60s, 6h]: the ceiling makes overflow
unreachable regardless of server TTL (a harmless re-resume every 6h on
long sessions), and the floor bounds any bad deadline to one forced
rotation per minute instead of a sub-second loop — which also disarms
the Math.max(1000, ...) landmine for return-unchanged-grace resumes
whose stored expiry can be arbitrarily near.
Also from review: getLeaseExpiresAt is renamed getAttachDeadlineAt (it
had zero production callers left; the plausible name is how the churn
bug happened), the expired-vs-missing bundle cases now log distinct
strings, and both test fakes use production constants (10s attach
deadline, 30-day resume TTL) — fictional fake values hid all three
defects in this subsystem. The four forced-rotation lease tests are
retimed to the 60s floor with direct pinned unreachable so return
probes cannot race their windows.
* style(mobile): merge duplicate imports in relay failover test
* style(mobile): use T[] array syntax in credential selection
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(terminal): reset stale mouse tracking on cold restore (#12101); stop OSC color-reply echo leak in POSIX agent panes (#12112)
#12101: a force-killed TUI never emits its DECRST reset, so its armed mouse
mode is latched into the on-disk checkpoint and re-derived into the
replacement process's emulator via the cold-restore history seed -- through
both rehydrateSequences and SerializeAddon's own mode trailer. The revived
bare shell then echoed SGR motion reports at the prompt. Seed a
RESET_MOUSE_REPORTING segment after the snapshot (before the torn escape
tail), only when there is real recovered content so the empty-array
"nothing to recover" sentinel survives.
#12112: agent panes arm a main-side PtyStartupIngress that answered opencode's
startup OSC 10/11 queries synchronously inside node-pty's onData, while the
POSIX tty still had ECHO on. The line discipline echoed Orca's own reply back
out as visible text. Echo suppression existed but was gated on windows-conpty.
Add PtyStartupReplyDelivery: POSIX defers the write off the query's turn and
recognizes its own echo anywhere in a span (bounded, non-destructive); ConPTY
keeps its synchronous write; windows-wsl is byte-identical to before.
Fixes#12101
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): read the slave's ECHO bit before answering a color query
The startup color reply was written into a PTY still in cooked mode, so the
line discipline echoed it back as visible junk (#12112). Whether that will
happen is readable state on the slave rather than something to infer from
returning bytes, so the reply now waits until the ECHO bit is observably
clear instead of guessing at echo shapes.
Two echo sources exist and only one is readable. A `quiet` verdict proves
the kernel will not echo, so it retires the caret projection; readline
echoes a master write in software with the tty already raw, so that
projection stays armed on every path. Scoping `quiet` narrowly is the whole
correctness argument here: reading it as "no suppression needed"
reintroduces the bug at a plain shell prompt.
Polling is bounded by a wall-clock budget rather than an attempt count,
because each probe is a subprocess and a multi-pane restore serializes them
on fork. Withholding measures flat at ~210ms from 1 to 100 panes.
Also resets a cold-restored pane's mouse reporting (#12101). The armed mode
is re-derived from the dead process's own persisted bytes through two
channels, so the daemon seeds a reset into recovered history and the
renderer stops trusting a persisted "live agent" signal after a cold
restore. The reset literals move to one shared profile module.
Fixes#12101Fixes#12112
Co-authored-by: Orca <help@stably.ai>
* test(terminal): pin the cold-restore reset on the spawn-adopted reattach path
A spawn can be answered with an adopted session, which reaches the reattach
handler by a door that skips the restored-session path. Pin that the cold-restore
signal survives it, so #12101's junk cannot come back through it.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): note why the adopted-reattach snapshot leaves the cursor visible
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): harden startup reply delivery
---------
Co-authored-by: Orca <help@stably.ai>
* test(e2e): start the board marquee off the board's padding cliff
`selects the full lane across a single large marquee scroll jump` failed the
changed-e2e-specs job with `Received: 0` — no card ever previewed. The CI
trace shows the drag started at (284, 195) and the failure screenshot shows a
native text selection with no marquee overlay, so the board never accepted
the pointerdown.
An element scan across that row shows why: the marquee may only start on
empty board space, and the usable strip is only x 280-298 — the board's own
left edge on one side, the first lane's cards on the other. `selectionBox.x +
4` aims 4px inside that strip, so a layout that rounds a few pixels
differently lands outside the board entirely, where the pointerdown never
reaches the handler and the browser text-selects instead.
Aim at the middle of the measured strip, assert the start point really is
empty board space before pressing, and assert the selection rect appears so a
rejected gesture fails immediately instead of surfacing 15s later as "0 cards
previewed".
Also carried over from the first attempt at this deflake: the lane scroll now
jumps until the virtualizer stops moving the bottom, because a measured card
is much taller than the row estimate and a fixed pass budget commits the
marquee short of the last cards; and the final assertion checks the badge's
text so a short selection reports its count.
* test(e2e): pick the marquee start point from live geometry, not a precomputed one
Round 2 fired the new guard: `marquee start point (290, 195) must be empty
board space` with `onSurface: false` AND `onIgnoredTarget: false` — so
elementFromPoint returned something outside the board entirely, even though
(290, 195) sits inside the measured 280..299 strip. Aiming at the middle of
the strip is still aiming at a point computed before the probe runs, and the
sheet, sidebar and lane fill keep resizing that strip afterwards.
Stop precomputing the point. Read the geometry and scan for an empty point in
the same DOM turn, walking a grid across the strip between the board's left
edge and the first card, over the lane's top rows only — the marquee anchors
its range in content space, so a start below the first card would drop it from
the 102. Take the first point the board itself reports as empty, and require
two consecutive probes to agree so a frame mid-relayout cannot win.
Keep the precondition guard, now reporting the live geometry and the elements
that blocked each rejected point. If no empty point exists at all, skip with
that reason instead of failing on a layout the test cannot drive.
* test(e2e): start marquee from an explicit empty lane
* test(e2e): find visible board space for marquee start
* test(e2e): quarantine unstable marquee hit test
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment
The packaged Windows `orca.exe` launcher read
`ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the
case-sensitive process block into a case-insensitive dictionary via `.Add`.
An inherited block carrying both `PATH` and `Path` threw
`ArgumentException: Item has already been added. Key in dictionary: 'PATH'`,
so every `orca` invocation exited 1 before Electron started
(native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67).
The launcher now mutates its own environment with
`Environment.SetEnvironmentVariable` and never touches either
`ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment
block and the child inherits the live one verbatim.
Orca was also minting the duplicate itself. `applyTerminalAttributionEnv`
read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows
PTY that inherited `Path` got a second spelling; which one the child resolved
was non-deterministic. `createLaunchEnv` did the same and, because its read
always missed on Windows, shipped Agent Teams terminals a `PATH` containing
only the tmux shim dir.
`resolvePathEnvKey` (extracted from the existing precedent in
windows-environment-path.ts) now drives every PATH read and write in the PTY
env pipeline, and attribution collapses Windows onto the single OS-resolved
spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior
is unchanged and a case-sensitive POSIX `Path` variable is never touched.
Closes#12046
* test(windows): track the launcher's own-environment marker
The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND
off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the
old dictionary writes and failed.
Co-authored-by: Orca <help@stably.ai>
* fix(windows): follow the host block's PATH spelling on sparse daemon env patches
Resolving a path-less Windows env to `Path` handed the daemon's own
`{...process.env, ...opts.env}` merge both spellings when the host block spelt
`PATH`. Fall back to the host block's own key, and collapse again inside the
daemon since that merge happens after attribution.
Co-authored-by: Orca <help@stably.ai>
* fix(windows): resolve the live PATH spelling by block order, not casing
Win32 resolves a duplicated variable by taking the first case-insensitive
match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first
preference targeted the shadowed spelling on the reporter's own
`["PATH","Path"]` block. Drop the attribution-side collapse with it: it
deleted the other spelling's value, and deleting the live key promotes
the shadowed one, so an env that stripped down to empty lost both.
* chore: drop unrelated merge formatting
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Attribute destructive runtime and daemon close diagnostics to the requesting client and exact target, record outcomes only after completion, and add reliability-gated attribution regressions.
* fix(mobile): keep main-buffer TUI footer above the iOS keyboard
The iOS keyboard-avoidance lift anchored on the terminal cursor row. Pi's
TUI renders in the main screen buffer (not the alternate screen) with its
footer/status rows below the input caret, so the altScreen full-lift branch
was skipped and those rows stayed under the raised dock / keyboard.
Anchor the lift on the bottom-most non-blank viewport row instead of just
the cursor: the WebView now emits contentBottomRow, and the lift uses
max(cursorY, contentBottomRow). This generalizes the alt-screen case,
keeps short output at the top put, and matches prior behavior for a
scrolled shell prompt.
Extracted the lift into a pure, unit-tested function
(terminal-keyboard-avoidance-lift.ts) and moved metrics parsing into a
tested helper on the contract.
* fix(mobile): preserve keyboard metrics through notification dispatch
* fix(mobile): harden terminal keyboard metrics
* fix(mobile): ignore unstyled terminal whitespace
* fix(mobile): preserve decorated terminal whitespace
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(mobile): render Mermaid diagrams in MobileMarkdown (#11141)
Co-Authored-By: Grok Companion <noreply@x.ai>
* fix(mobile): keep streaming mermaid fences as raw code until the fence closes
* perf(mobile): memoize MermaidDiagram and add a CDN load watchdog
* fix(mobile): escape mermaid source before embedding in WebView script
JSON.stringify leaves </script>, &, and U+2028/U+2029 raw, so a diagram
source containing </script> broke out of the inline script and ran
arbitrary WebView JS. Diagram source is untrusted (agent output, PR/chat
content), and this component now renders from chat and markdown preview,
not just the PR sidebar. Escape those chars to \uXXXX; the literal still
parses back to the exact source. Adds an adversarial buildHtml test.
* fix(mobile): embed the mermaid engine instead of fetching it from a CDN
The diagram WebView loaded mermaid from jsdelivr at runtime: offline and
constrained-network renders always fell back, the stalled-load watchdog
existed only to paper over that, and an unpinned floating-major CDN script
with no integrity check ran inside the WebView. Embed the lockfile-pinned
package's prebuilt bundle via a postinstall generator (same mechanism as
the terminal WebView engine) so the document loads nothing external; the
watchdog is removed as obsolete and a no-external-URL gate pins it.
* chore(deps): align mermaid at 11.16.0 across desktop and mobile
Desktop floated ^11.15.0 while the mobile embedded engine resolved 11.16.0.
Raise the desktop floor so both lockfiles resolve the same version, and pin
mobile exact: the generated WebView engine embeds the package bytes, so an
implicit range bump would silently change what ships.
* fix(mobile): block Mermaid diagram network requests
Mermaid image-node URLs can initiate subresource requests even with the engine embedded. Keep the WebView offline by restricting resource types through its document CSP.
* style(mobile): format Mermaid routing test
* fix(mobile): use stable keys for Mermaid diagrams
* fix(mobile): keep duplicate Mermaid keys distinct
Combine each diagram source with its sibling occurrence so identical diagrams remain unique while source edits still remount the WebView and later streaming prose does not.
* fix(mobile): keep Mermaid transitive within release-age policy
---------
Co-authored-by: Grok Companion <noreply@x.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
GitLab's project-ref cache stored `null` forever and returned any cached
value straight from the map, so a repo probed before `origin` was configured
— or before `glab auth login` ran for its self-hosted host — kept
hosted-review provider detection stale until app restart. The negative-TTL
work that shipped for Azure DevOps / Bitbucket / Gitea skipped it.
Mirror `createRemoteRefProbeCache`'s semantics: negatives expire on the
shared interval, positives stay, the SSH provider generation joins the cache
signature so a reconnect re-asks, and a probe abandoned as stale can no
longer publish over its successor. Transient git/SSH failures stay uncached.
Expiring negatives would otherwise turn `glab auth status --hostname` into
one spawn per repo per interval on the hosted-review poll, since a non-GitLab
remote reaches it too, so remember the unauthenticated answer per host — not
per repo — on the same clock.
* fix(ui): always show Onboarding in the Help menu
Users could not find "Show onboarding again" because it required holding Alt. Surface it on the default Help menu so replay is discoverable without reinstalling.
* fix(ui): always show Restart Orca in the Help menu
Surface Restart next to Check for Updates by default and drop the Alt-only admin gate, which no longer hid anything else.
The per-worktree rebuild memo from #12207 refused to skip any worktree
with a registered TerminalPane, because builders read live PaneManager/DOM
state the memo could not witness. Hidden worktrees stay mounted (8 parked
+ 12 retained by default, unbounded with terminalHiddenViewParking off),
so that set rebuilt on every publication — on the always-on hosts the memo
targeted, ~20 worktrees paid the full content build plus the new inputs
build and deep compare every tick.
Capture the live reads instead: snapshot pane leaf ids, the live active
leaf, the serialized pane tree, and per-pane numeric/pty bindings once per
publication into MountedTerminalSurfaceCapture, compare captures by value
in the memo, and have the builders read only the capture. Live state
outside the capture is now unrepresentable in a builder, so the skip stays
provably complete — and a mounted worktree whose panes did not change
reuses its snapshot like any other.
The sync before-unload checkpoint staged renderer state and then queued
store.flushPendingAsync() fire-and-forget, so reload/restart/update paths
navigated while the staged session, scrollback and UI state were still
only in memory. Quit is covered by the will-quit flush barrier; those
paths were not.
Keep staging synchronous (no sync durable writes), but record the flush
outcome and expose it on app:await-before-unload-checkpoint. Restart,
updater install and lazy-chunk recovery reload now join that write before
navigating and abort the attempt when it fails or outlives a 20s deadline.
Destructive worktree removal swept PTYs by worktree id alone. Worktree ids are
`repoId::path` and the store keeps one per host, so deleting an SSH worktree
could stop a same-id local (or other-connection) workspace's terminals — or fail
outright with `selector_ambiguous` when two hosts owned the id.
Every destructive teardown now names its owner (resolvedWorktreeId plus the
connection/runtime environment), matching the already-hardened forget-local path:
- IPC `worktrees:remove` (git + folder workspaces)
- runtime `removeManagedWorktree` (CLI/mobile `worktree.rm`, git + folder)
- missing-worktree terminal reconciliation, including its no-provider fallback
The #11960 allowUnverifiedStop force-delete gate is untouched.
* perf(agent-status): coalesce live status bursts into one render pass (STA-3328)
Each live agent-status event arrives as its own IPC task, so a multi-agent
burst paid one full React render pass per event (200-488ms main-thread tasks
under heavy orchestration). Same-task store commits batch to a single render
pass, so buffer a burst for one 33ms window and apply it in one task. The
leading event still applies synchronously (zero added latency for single
events), only an applied event arms the window (dropped/pending events stay
immediate), and both clear paths purge queued sets so a coalesced event
cannot resurrect a removed status.
* fix(agent-status): preserve queued completion on teardown
Two robustness follow-ups from the #12194 safety-review loop (pass 3),
staged by the reviewer before its session ended; both fail toward extra
retention only:
- recency bookkeeping now runs while the retention kill switch is off, so
re-enabling evicts in true LRU order instead of worktree-list order
- interrupted downloads release the eviction veto instead of pinning the
worktree as downloading forever
The adhoc workflow checked out any requested ref and ran its scripts and
electron-builder config with MAC_CERTS, the notary password, and the adhoc
publisher token in reach — including refs/pull/* fork code a maintainer
could dispatch in one innocuous-looking click.
Vet the ref before checkout: PR refs are refused, branches/tags resolve in
a bare tree:0 scratch fetch, raw SHAs must be reachable from a repo branch
or tag (a partial clone lazily serves PR-only commits by SHA, so name
resolution alone is not a trust test), and checkout pins the vetted SHA so
a race push cannot swap the commit. Also reference an adhoc-mac-build
environment so the secrets can later be fenced off from stale workflow
copies via repo settings.
* perf(runtime): withhold unchanged mobile snapshots from the graph payload
Every graph sync structured-cloned all 222 worktree snapshots to main even when
none had changed: 374 KB and ~5 ms per clone, paid twice because Electron clones
on serialize and again on deserialize. That transport cost — not the renderer
rebuild — is the bulk of a publication.
The renderer now sends only the snapshots main has not acknowledged and names
the rest in unchangedMobileSessionWorktrees. Detection is object identity, not a
deep compare: an unchanged worktree already returns its cached snapshot object.
Main seeds nextWorktrees from that list so its prune keeps withheld worktrees
live instead of removing them.
The call itself is unconditional. syncWindowGraph is not a one-way publish — its
return value is the only channel carrying agentOrchestrationByPaneKey to the
renderer, and the handler adopts pre-allocated handles, merges detached leaves,
refreshes writable flags, and drains graph-sync callbacks on every sync. Skipping
it would starve all of that.
Two failure modes are closed explicitly. The memo advances only after main
acknowledges, so a publication that throws is resent in full rather than
silently withheld forever. And a worktree main dropped on its own — worktree
metadata removal — comes back in mobileSessionResyncWorktrees, which also clears
the accepted-revision record so the republish is not rejected as a no-op.
Unchanged republish at 222 worktrees / 787 tabs: 374 KB to 3.4 KB, 5.08 ms to
0.02 ms per clone. One changed worktree: 5.3 KB.
* fix(runtime): resync stale withheld mobile snapshots
* fix(runtime): align accepted mobile snapshot membership
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
main's version only moves on `release:` commits, and stable patches are cut
from release branches that never merge back. On 2026-08-03 main read
1.4.165-rc.0 for twenty hours while 1.4.165, 1.4.166 and 1.4.167 all shipped,
so every hourly built in that window was stamped 1.4.165-hourly.* while
carrying code newer than 1.4.167 — and sorted below the stable its user was
already running.
Resolve the base from the main repo's published tags instead, taking the patch
above the highest shipped stable. package.json stays a floor for the case where
main leads the tags.
Co-authored-by: Orca <help@stably.ai>
gh repo create leaves the repo empty, and publishing a release creates a
git tag that needs a commit to point at. The adhoc build got all the way
through notarization and upload before failing on "Repository is empty".
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): bound terminal viewport resubscribe loop with backoff (STA-3337)
An empty scrollback frame with absent host dims was coerced to 80x24, which
never equals a phone viewport, arming a zero-delay unsubscribe/resubscribe
loop (~25/s) that broke long-press gestures and drained battery.
- Absent host dims now hold the stream instead of resubscribing.
- Fit resubscribes are budgeted per handle (3 attempts, escalating backoff)
with an absence-gated refill mirroring the chat-side rearm bound; on
exhaustion the view degrades visibly via toast instead of hot-looping.
- A fresh post-measure match counts as convergence instead of resubscribing.
- setTerminalModes keeps the Map identity when the mode is unchanged, so
same-mode frames no longer re-render the session route.
- Host emits the subscriber viewport as scrollback dims when the snapshot
and PTY size are both unavailable, so current hosts converge immediately.
* fix(mobile): cancel stale viewport retries after convergence
* feat(ssh): add SSH config host picker for add-host form
Users can now click 'Fill from ~/.ssh/config…' to browse available SSH
config hosts in a picker, select one, and have the form automatically
prefill with resolved connection details (hostname, port, username, auth).
Previously, an 'import' button provided bulk sync on this form—confusing
and unhelpful when everything was already synced. That action is now
available as a secondary 'Add all' option in the picker.
* fix(ssh): import filter preservation and label fallback
- Reuse search loader on import completion to preserve active filter inside generation guard
- Fall back to hostname when manual host has no label, not empty string
- Make alias duplicate detection case-insensitive to match config picker behavior
- Validate host availability when restoring project group selection
- Add aria-selected attribute to picker options for accessibility
* fix(ssh): harden config picker import, alias folding, and host targeting
Review findings on the ~/.ssh/config picker + bulk add:
- Guard config-host resolution with a generation counter so a late resolve
cannot overwrite a later pick or a form the user backed out of; freeze the
other rows while a pick resolves.
- Stop "Add all N" from re-adopting deleted hosts — it now imports without
reAdopt, matching the new-host count it advertises. Settings → Import keeps
the explicit re-adopt path.
- Fold SSH aliases through a shared normalizeSshConfigAlias for import
ownership, delete tombstones, reclaim, picker search, and the save-time
duplicate check, which now occupies configHost *and* label like the picker.
- Persist GSSAPIAuthentication only when a parsed Host entry asks for it, not
when `ssh -G` merely echoes the /etc/ssh system default.
- Fail closed with unavailable/setup-not-found when an explicit
projectHostSetupId names a non-actionable host instead of silently creating
the workspace on a sibling host.
- Cache the parsed config for the picker session (refresh on open/retry) so
filter keystrokes no longer reparse and Include-expand the file, keep the
filter usable during loads, add a Retry on load errors, explain an empty
Identity file after a config fill, and drop the always-false aria-selected.
* refactor(ssh): centralize host result limit and extract folder group val
Move SSH_CONFIG_HOST_RESULT_LIMIT to shared types so the renderer's limit message
cannot drift from the host's query limit. Extract findActionableFolderProjectGroup
to avoid repeating the folder-host-availability check across the composer hook.
* fix(ssh): pass -F to ssh -G when HOME differs from passwd home
In E2E tests and sandboxes, isolated HOME can differ from the system
passwd home. OpenSSH resolves the default config via getpwuid (passwd),
while Node's loadUserSshConfig uses os.homedir() (HOME-aware). Pass -F
to explicitly specify the config path when they diverge, so ssh -G and
the picker resolve the same file.
* fix(ssh): verify config host exists before resolving with ssh -G
When a user edits ~/.ssh/config and removes a host, the import picker
should not fall back to ssh -G's echoed response (which treats any alias
as valid). Check the reloaded config file before resolving.
- Force reload config on each resolve to catch user edits post-open
- Reject aliases not in the current config before calling ssh -G
- Add test for deleted alias edge case
- Fix workspace-target fallback to honor explicit host selection
* fix(ssh): let tombstoned aliases be re-picked in the config picker
Allow users to reclaim a deleted SSH host by re-picking it from ~/.ssh/config. Tombstoned aliases now appear in the picker with a "Removed from Orca" badge and remain pickable, but don't count toward "Add all" operations — ensuring passive import never resurrects a deleted alias while still giving the user a recovery path.
* fix(agent-status): restore hydrated nonterminal statuses as unconfirmed
A hook transition that fires while Electron is down has no receiver and is
discarded, so last-status.json can restore a stale 'working' as confirmed
truth for up to the 7-day hydrate TTL. Stamp hydrated nonterminal rows with
restoredUnconfirmed, carry it through both IPC paths, and treat such rows as
never-fresh in the shared and renderer freshness gates so the sidebar,
worktree.ps, and the raw snapshot all present the same degraded semantics.
Terminal states restore as-is; any accepted live event clears the flag; the
flag itself is never persisted. Interrupt/question inference refuses to
fabricate transitions onto unconfirmed rows.
* fix(agent-status): shed unconfirmed marker when the liveness sweep verifies done
The restored-subagent reaper's reconciled entry spread carried
restoredUnconfirmed onto a process-probe-verified 'done', making freshness
gates suppress a legitimate completion. Keep the marker only while the
reconciled state stays nonterminal.
* fix(agent-status): let live evidence replace hydrated rows
* fix(agent-status): keep restored rows degraded
Sort accepted live evidence after hydrated rows even across wall-clock rollback. Let unconfirmed rows own their preserved pane titles without asserting live state, while retaining independently live sibling evidence.
* fix(agent-status): suppress unmapped restored titles
Treat a single runtime title as covered by the single restored hook row while layout identity is unavailable. Preserve ordinary age-stale fallback and mapped sibling-pane evidence.
* fix(renderer): drive agent working spinner on the compositor (STA-3328)
The shared 12Hz JS clock wrote el.style.transform on every mounted spinner
(41 live = ~490 main-thread style writes/s), keeping style perpetually dirty
and delaying keystroke dispatch (measured typing inputDelay p90 363ms; 19ms
with the writes stopped). Replace it with a steps(12) CSS transform animation
so rotation runs on the compositor; one mount-time animation-delay write
preserves the shared-epoch phase sync.
* fix(renderer): lock agent spinner phases to document time
* fix(renderer): tolerate missing spinner animation API
* fix(ai-vault): support session scanning in SSH worktrees
Add relay-native aiVault.listSessions scanning that discovers agent
sessions on SSH hosts. Includes fallback to filesystem crawl for
legacy relays, full cancellation support, result validation, and
scan coalescing to reduce redundant work.
* fix(ai-vault): scan sessions in SSH worktrees with coordinated cancellat
- Extract batching logic to `mapRemoteScanBatches` for reuse and proper cancellation checkpoints
- Move `AiVaultScanCoordinator` from relay to main to handle concurrent same-key requests with individual cancellation signals
- Report scope path truncation consistently across relay and SSH fallback paths
- Gracefully degrade relay handler on unsupported platforms instead of aborting startup
- Refactor issue display to separate blocking errors, scope notices, and skipped transcript counts
* fix(ai-vault): stabilize SSH session scan CI
Swallow async WSL relay stdin EPIPE so the live hook-relay shard no longer
fails after all tests pass. Merge main, resolve scan/relay conflicts, and
align cancellation/host-issue reporting with IPC expectations.
* fix(ai-vault): harden session scan cancellation, relay timeouts, and preemption
Thread the abort signal through every scan and parse path so superseded or
cancelled scans stop promptly instead of parsing every remaining transcript
for a caller that already left. Replace the fragile message-text relay
timeout check with a typed error code so unrelated errors carrying the
phrase "timed out after" no longer suppress the filesystem fallback. Fix
scan coordinator preemption so a forced Refresh in one window no longer
re-enters as a spurious cancellation in another. Add a host-leg cache for
the all-hosts view and cap filesystem concurrency so a single slow remote
home cannot stall the whole merge.
Co-authored-by: Orca <help@stably.ai>
* fix(ai-vault): use stable React keys for scan issue banners
Drop array-index keys so react-doctor/no-array-index-as-key passes.
Uniqueness comes from host, kind, agent, path, and message.
* fix(ai-vault): SSH session scanning with configurable depth limits
Implement depth-aware caching and proper scan boundaries to make SSH session
scanning reliable in worktrees. Users can now select between faster (250
sessions) and comprehensive (unlimited) history scans. The scanner:
- Deduplicates scans across relay, host leg, runtime, and renderer layers
- Reuses larger scans to serve smaller depth requests
- Properly bounds in-scope discovery per-limit
- Fixes timeout enforcement when SSH providers ignore abort signals
* Move sessionLimit ref update to useLayoutEffect
Keep render pure for React Doctor by deferring ref updates to
a layout effect, which still executes before render-dependent
effects that consume the ref.
* fix(adhoc): stamp version prefix from main, not the feature branch
Adhoc builds check out arbitrary refs whose package.json often lags
version bumps (e.g. 1.4.165-rc.0 while main is 1.4.168-rc.1). Hourly
always builds main so it already tracks the product line; adhoc now
resolves the base version from origin/main (or ORCA_ADHOC_BASE_VERSION)
so branch builds share that prefix.
* Revert "fix(adhoc): stamp version prefix from main, not the feature branch"
This reverts commit a26a18eb3fd83f7e7d2db9a6a7c3e02e0f79089a.
* fix(ai-vault): fix scoped backfill and coordinator race conditions
Resolve race where the last waiter leaving could abort an already-settled scan (add `settled` flag). Redesign scoped session backfill to keep searching through newer files until the scope reaches its requested session quota instead of stopping at the candidate limit; out-of-scope files no longer consume the scope budget. Centralize scan limit normalization and fix error classification for cancelled scans using the proper helper instead of checking Error.name. Disambiguate cache keys using JSON and add cancellation check after scope discovery phase.
---------
Co-authored-by: Orca <help@stably.ai>
* Post fixing replies after launching PR comment resolution agents
Orca now automatically replies to selected comments after launching an
agent to resolve them. Review-thread comments get nested replies; review
summaries and issue comments get top-level @-replies. Payloads are
snapshotted at queue time and posted only after prompt delivery
succeeds. Bounded concurrency (4) prevents SSH slowdown.
* Batch conversation replies instead of posting per-comment
Combine N unresolvable comments into a single PR conversation reply to avoid timeline spam. Review threads still get one nested reply each. Refactor helper functions to pr-comment-fixing-reply-body.ts and change replyAsConversation(comment, body) → replyAsConversation(body).
* Handle bot account mentions in PR conversation replies
Use buildPRCommentConversationReplyBody to properly handle GitHub App
bot accounts, which don't resolve as @-mentions. Enforce delivery semantics
with type-safe PendingPRCommentAiAck payloads and improve error handling
around agent launch to prevent stranded state.
* Fix React Doctor ref-mutated-during-render errors in ChecksPanel
Move comment-resolution payload ref clears and latest-callback mirrors
into effects so render stays pure for the static analysis gate.
* Optimize worktree parent picker: conditional mount, virtualization, lazy
- Mount WorktreeParentPickerPopover only when open to avoid hundreds of
unmounted instances subscribing to lineage and worktree store updates.
- Virtualize the candidate list and resolve activity statuses only for
visible rows, eliminating redundant status subscriptions.
- Extract filtering, placement calculations, and row rendering into
separate modules for testability and clarity.
* Memoize worktree parent picker search handler
Wrap search state update in useCallback to stabilize the handler
across re-renders. Reduces unnecessary effect runs and enables better
memoization of child components.
* Optimize worktree parent picker: defer unmount, memoize IDs
- Defer unmount until exit animation completes (200ms) to prevent premature teardown
- Memoize visibleWorktreeIds to prevent status hook from rebuilding its selector on every render
createExpandCollapseActions minted five fresh closures per TerminalPane
render; four are deps of useTerminalKeyboardShortcuts, so its seven
window listeners tore down and re-registered on every render and all
effect-owned IME chord/modifier state silently reset. Memoize the
actions via useExpandCollapseActions and pin registration stability
with a render-churn regression test.
Fixes#12269
The cell stamps invite expiry at exactly now+10min from its own clock while
the desktop rejected anything past now+10min from the local clock with zero
tolerance, so any cell clock ahead of the machine by more than network
transit made every Relay pairing code fail with an opaque toast. Same
defect class as the host-proof freshness incident; same 30s leeway.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
The runtime RPC WebSocket listener bound to 0.0.0.0:6769 at startup, so a
desktop with no paired device was reachable from the whole LAN before the
user opted in. Default the bind to 127.0.0.1 and widen to all interfaces
only on an explicit opt-in:
- createMobilePairingOffer / getRuntimePairingUrl widen (ensureNetworkExposure)
before advertising a LAN endpoint; the rebind reuses the resolved port so an
already-issued offer stays valid, and concurrent offers share one rebind.
- orca serve and E2E set exposeNetworkByDefault to bind wide at startup.
- A previously-connected device (lastSeenAt > 0) rebinds wide at startup so
reconnect after restart keeps working; a pending/never-connected offer does
not persist exposure across a restart.
The advertised pairing endpoint still resolves to a concrete interface address,
never the 0.0.0.0 bind host.
* fix(sidebar): route local folder adds to the intended host, not the global runtime
Adding a local folder while connected to a remote runtime failed with
"<path> was checked on <host>, but that host did not report a usable folder"
because addRepoPath decides local-vs-remote purely from the global
settings.activeRuntimeEnvironmentId when no explicit host is passed.
Two local-add flows relied on that global fallback and got misrouted:
- useAddRepoLocalFolderFlow (native picker / drag-drop): the Add Project
host selector can display "Local" (selectedRuntimeEnvironmentId = null, so
the guard passes) while the global still points at an unavailable runtime.
Native-picked/dropped paths are always local, so force local routing.
- AddProjectFromFolderDialog ("Add folder as project" on a subfolder): a
subfolder lives on the active repo's host, so carry that host through the
modal data and route by it — local for local projects, the owning runtime
for runtime projects — instead of the globally-active runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sidebar): route runtime server-path adds by selected runtime, not global
The Add Project "server path" step (reached only when a runtime host is
selected) called addRepoPath(path, kind) with no explicit host, so it
inherited the global settings.activeRuntimeEnvironmentId. When that global
diverged from the dialog's selected runtime, the add was misrouted off the
host the user picked — the same root cause as #9541, opposite direction.
Route the server-path add by the dialog's selected runtime explicitly.
Co-locate selectedRuntimeEnvironmentId in useAddRepoHostSelection next to
selectedSshTargetId so the dialog reads it from one place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(sidebar): route the pre-add git server-path scan by the selected runtime
For kind === 'git', scanNestedRepos runs before addRepoPath and can
early-exit the flow into the nested-repo review, but it still routed by the
global active runtime — so it could scan the wrong host even after the add
itself was correctly routed to the selected runtime (CodeRabbit).
- scanNestedRepos accepts an optional runtimeEnvironmentId in its controls;
when present it routes by that host, else falls back to the global
(existing callers unchanged).
- useAddRepoServerPathFlow passes the selected runtime into the scan and
derives runtimeKind/streaming support from it instead of the global-reading
getNestedRepoRuntimeKind(null), so telemetry and the nested review target
the same host as the add.
Adds renderer- and store-level regression tests covering scan routing to the
selected runtime, the null-override local case, and the nested-review handoff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(sidebar): make nested-scan cancellation route by the scan's owning host
Follow-up to CodeRabbit review of the scan-routing change:
1. scanNestedRepos treated `{ runtimeEnvironmentId: undefined }` as an explicit
local override via the `in` check. Only null or a string is now explicit;
undefined falls back to the global (matches getAddRepoPathRouteSettings).
2. scanNestedRepos gained a routing override but cancelNestedRepoScan still
routed by the global — an asymmetric contract where an override-routed scan
could be un-cancellable if the global diverged mid-scan. cancelNestedRepoScan
now takes the same override, and useAddRepoNestedReviewState remembers each
scan's owning host by scanId (set when the scan is registered) so both stop
and reset cancel on the host the scan actually ran on. The local folder flow
routes its scan explicitly local so scan, cancel, and add all agree.
Adds store-level regression tests (explicit override wins, undefined falls back
to global, cancel routes by override) and a new useAddRepoNestedReviewState test
covering cancel-by-owning-runtime for stop and reset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(sidebar): keep subfolder adds on their owning host
* fix(onboarding): keep completion on captured host
* chore(review): drop unreachable onboarding recovery
* fix(sidebar): preserve paired runtime checkout ownership
* fix(runtime): index paired worktrees by logical owner
* fix: fail closed on worktree owner alias collisions
* docs(sidebar): clarify host-routing intent flagged in review
Two Greptile P2 notes, addressed as comments (no behavior change):
- project-added-default-checkout.ts: the runtime branch's `hostId === executionHostId`
is NOT unreachable — a colliding repo id can carry a runtime-qualified hostId with
no runtimeOwnerEnvironmentId (see the "repo IDs collide" test). Documented why the
comparison is reachable and load-bearing rather than replacing it.
- AddProjectFromFolderDialog.tsx: note that omitting the runtimeEnvironmentId spread
intentionally signals local (NonGitFolderDialog coerces absence to null).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(activity): control portal readiness observer delivery
---------
Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>