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>
* test(runtime): prove mobile session publication rebuilds every worktree
buildMobileSessionTabSnapshots consults its per-worktree cache after building
the content, so a republish saves the fanout but none of the work. With 300
worktrees, an unchanged republish still does 601 units of per-worktree work,
and a single changed worktree does 602.
Publication is keyed on agentStatusByPaneKey/agentStatusEpoch, so this runs on
every agent status tick. On a multi-client runtime host with 381 worktrees this
allocated ~350 MB/min and rode the renderer into repeated 4 GB OOMs.
Tests are marked it.fails so the branch stays green; drop .fails when the build
loop skips worktrees whose inputs are unchanged.
* refactor(runtime): make mobile session snapshot inputs explicit per worktree
Every per-worktree builder in buildMobileSessionTabSnapshots took the whole
AppState, so a worktree's real input set was the transitive closure of seven
helpers and could not be memoized safely. Introduce MobileSessionWorktreeInputs
— built once per worktree — and thread it through the group projection and the
terminal/markdown/file/browser tab builders so the compiler proves the input
set. Tab- and pane-keyed slices are narrowed to this worktree's tab ids, file
ids, browser workspace/page ids, and pane keys; agent statuses are bucketed per
worktree once per publication via a tab-id index.
No behavior change. Dropping AppState from the projection path also removes the
second per-worktree read of browserTabsByWorktree, so the publication-cost
counter falls from 601 to 1 per publication and its two cases now pass.
* fix(runtime): skip unchanged worktrees before building mobile session content
buildMobileSessionTabSnapshots consulted its per-worktree cache only after
building that worktree's three Maps, group projection, and full tab array, so
the cache suppressed the fanout but none of the computation. Every agent-status
tick therefore rebuilt every worktree, which drove sustained 4 GB renderer
working sets on a host holding 381 worktrees.
Cache MobileSessionWorktreeInputs alongside each snapshot and reuse the snapshot
when every input field is reference-equal, before any intermediate structure is
allocated. Worktrees with a mounted TerminalPane always rebuild: their live
DOM/PaneManager state is invisible to store references. Absent per-worktree
slices now resolve to shared empty values so an empty worktree can compare equal
to its last publication. jsonContentEquals stays as the backstop on the rebuild
path for inputs that churn by reference without changing output.
With 300 worktrees, per-worktree content builds go from 300 to 0 on an unchanged
republish and from 300 to 1 when one worktree changes.
* test(runtime): cover agent status publication cost
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Bound exclusive host navigation to a generation-aware latest-wins
single-flight so bulk open and switch fan-out stay responsive on large
remote fleets. Add freeze repro harnesses and navigated settlement.
Backspacing away an entire Pinyin preedit ended the composition with
empty data, no textarea residue, and no input/keypress events — yet
_sendPendingComposition fell back to the last non-empty
compositionupdate data and typed its first character into the PTY.
Only trust that fallback when observed input evidence corroborates it;
a composition with no evidence in any channel was cancelled.
Fixes the macOS Pinyin regression from #11293 (stray letter left after
deleting a preedit); same fix covers IBus/fcitx Backspace cancellation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A single slot per code let a rapid second Enter press go unguarded: the
first release found a mismatched timeStamp, dropped the only entry, and
the second release then synthesized the Shift+Enter this guard exists to
prevent. Track one entry per press and drain exactly one per physical
release, so every press stays guarded until its own release. Auto-repeat
keydowns do not stack an entry, since the whole run ends in one release,
and the list is bounded so a press whose release never arrives cannot
grow it without end.
Refs #11878
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On Windows, the Enter-keyup synthesis path inferred the modified-Enter
chord from release-time modifier state. A plain committing Enter
(Process/229, no modifiers) followed by a rolled-over Shift for the next
doubled consonant made the keyup report shiftKey=true and synthesized a
Shift+Enter the user never chorded; a directly-sent Shift+Enter could
likewise send a second newline from its keyup once the next composition
started. Record observed Enter keydowns (code -> timeStamp) and let the
keyup synthesis run only for presses whose keydown the IME swallowed
entirely; a balancing keyup that copies the keydown timeStamp keeps the
evidence for the later physical release.
Refs #11878
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(terminal): expand variables in Windows PATH
* fix(terminal): preserve expanded Windows PATH at spawn
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(mobile): surface worktree catalog failures instead of showing 0 worktrees (STA-3123)
A connected host whose worktree.ps request fails now shows an explicit
catalog-failure state (with the RPC error code) on the host page, and
'Worktree list unavailable' on the home host card, instead of silently
rendering as a healthy host with zero workspaces.
* fix(mobile): mark cached worktree catalogs unavailable
* 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.