mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
b79206533ea983620ea20378b6b1116ce2cd5bde
8652
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ef39f32d4f |
test(native-chat): split the windowing test harness out of the suite (#20773)
#20719 grew NativeChatMessageList.windowing.test.tsx to 897 effective lines, past the 800 ceiling for test files, so oxlint fails on main. Moves the shared layout/ResizeObserver stubs into native-chat-windowing-test-harness.tsx. No test was changed, split or dropped: still 5 describes and 23 it() blocks, 29 assertions passing. The stubs' mutable knobs become one exported `layout` object because an imported binding cannot be reassigned across modules. AGENTS.md forbids a max-lines disable, so extraction is the fix. |
||
|
|
99062ed80b |
fix(worktrees): preserve unverifiable disk witness (#20713)
* fix(worktrees): preserve unverifiable disk witness * fix(worktrees): follow gitdir/commondir markers in disk witness The disk witness validates created worktrees by reading the repo's common directory from disk. Previously it only checked for a direct .git directory and returned a status object that conflated different failure modes. Now it properly follows .gitdir and commondir pointer files to locate the true common directory, fixing detection on repos with linked git directories (worktrees, submodules) and WSL scenarios. Error handling is simplified: definitive absence returns undefined, other read failures throw with proper cause chains, eliminating the ambiguous "unverifiable" state that would mask real errors. * fix: validate gitdir marker targets are directories When a .git marker points to a missing or non-directory path, that's unverifiable—not the same as an absent .git file (bare repo). Validate accessibility before reading commondir to catch these errors clearly. |
||
|
|
438603f9e7 |
feat(native-chat): add a message rail for jumping between your prompts (#20719)
* feat(native-chat): add a message rail for jumping between your prompts A vertical rail down the right edge of the transcript, one bar per user message, with the bar for the turn you are reading highlighted once scrolling settles. Hovering the rail opens a panel that previews every prompt and jumps to it on click. Bars are capped at 20 and sampled evenly across the thread, always keeping both ends and the active bar, so the rail stays readable at a glance on a long conversation. The active bar is resolved from virtualizer offsets rather than by scanning rendered rows: the transcript is windowed, so an off-window row has no element to measure. The row at the scroll fold resolves to its owning prompt through turnKey, which is what keeps your own message lit while you read a long reply instead of going dark. Jumps reuse the existing reveal/pin path and scrollMessageToTop, which releases the bottom pin. Scrolling through the virtualizer directly would leave a reader snapped back down by the next streamed token. Ticks cover loaded history only; older prompts gain a bar once "Load earlier messages" pages them in. * fix(native-chat): service a rail jump once and give its pin back The rail borrowed the diff reveal's pin to reach a row the window had left behind, but copied only its state shape, not its consumption. The request was never cleared and the effect depended on `slots`, which is rebuilt on every render, so three things went wrong at once: - every later render re-scrolled to the jumped message, dragging a reader back there for the rest of the pane's life, and forcing the bottom pin off each time; - the standing request outranked `revealedDiff` in the shared pin, so revealing a diff outside the window silently stopped mounting its row; - the pinned row stayed mounted and measured indefinitely. The request now carries a monotonic id, is serviced once, and is released as soon as the scroll is issued, which hands the pin back. The rail's scroll listener had the same churn: it listed `items` in its deps, so a streaming turn tore the listener down and cancelled the pending idle timer on every frame and the highlight never settled. It now subscribes once and re-reads on a key built from the prompt ids. Also: the hover trigger is a real button, because `asChild` discards the primitive's focusable trigger and the panel is the only way to reach these messages; the wheel forwarder honours line and page delta modes rather than treating every delta as pixels; and the e2e panel assertion is exact, since a loose bound passed at 20 rows against 20 ticks. * fix(native-chat): make prompt rail accessible and reuse previews * fix(native-chat): supersede prior navigation when selecting a prompt |
||
|
|
ff5b1a5a05 |
fix(native-chat): preserve detached transcript position during growth (#20710)
* fix(native-chat): stop the transcript following an end it measured short The virtualizer compensates a row's measured size change by moving scrollTop whenever it believes the view was already at the end. It decides that from the spacer's own height minus a container-absolute offset, so the distance it computes is short by everything in the document outside the spacer: the transcript's top gutter, the "load earlier" block while older history is still pageable, and the trailing chrome. A reader sitting ~100px above the bottom therefore measured as "at the end", and every row that settled below them dragged them down to it. Measured in the windowing harness with a 92px gutter and 24px of trailing chrome: a reader parked 96px above the end is pulled to the end on the first growth frame, scrollTop 9261 to 9357. The same option gates following an append, but that path measures the true document distance, so it was never wrong, only redundant. The transcript already decides whether to follow the end from the scroll container's real geometry, and it re-pins once the growth is in the document rather than before it, where the library's own write is clamped. Both library end behaviours are retired by a threshold no finite distance can meet; the prepend anchoring that shares the option is kept. overflow-anchor:none is restated as structural: the engine's anchoring writes never pass through the scrollToFn adapter that attributes this pane's own scrolls, so they would arrive unmarked and read as the reader leaving. * fix(native-chat): preserve visible rows on first measurement |
||
|
|
b61a2347b9 |
feat(design-system): gate renderer UI with @shadcn/lint (#20731)
* feat(design-system): gate renderer UI with @shadcn/lint Wires shadcn-ui/lint's Oxlint plugin into the two places this repo already ratchets: the changed-lines PR gate for rules the renderer can't satisfy today, and `pnpm lint` for the one that is already at zero. - config/oxlint-design-system.json: no-restyle (layout allowed), no-raw-colors, require-static-classes -- scoped to src/renderer/**/*.tsx, run over added lines only. Measured at 10 findings across the last 60 commits (771 changed files), so it holds the line without a migration. - config/oxlint-dead-classes.json: no-unknown-classes repo-wide, with the renderer's plain-CSS hook namespaces allow-listed. Now at zero. - no-inline-styles and no-arbitrary-values stay off; STYLEGUIDE says why. Fixes the three live bugs the linter found: - `--editor-surface` never reached `@theme inline`, so `bg-editor-surface` generated no CSS -- 12 editor/artifact/notebook panes fell through to the page background instead of #1e1e1e in dark mode. - `scrollbar-none` is not a Tailwind utility and was declared nowhere, so the remote file browser breadcrumbs showed the scrollbar they meant to hide. Declared as a real `@utility`. - Notebook markdown cells used `markdown-preview-body`, which no stylesheet defines; the styled class is `markdown-body`. They rendered unstyled. * ci: run the dead-class gate in PR CI `pnpm lint` gained check:dead-classes, and pr-workflow-lint-parity requires every `pnpm lint` step to have a matching step in pr.yml. * fix(notebook): keep markdown theme selectors working |
||
|
|
db09a7bd50 |
fix(native-chat): let a reader park just above the latest message (#20709)
* fix(native-chat): let a reader park just above the latest message A reader who scrolled up by less than the bottom threshold was still classified as being at the end, so follow stayed armed and the next chunk of stream carried them back down. One constant was answering two different questions: how close to the end still counts as pinned, and whether a reader's own scroll meant to stay there. The first wants slack, because a streaming last message jitters in height by tens of pixels. The second wants almost none, because it is a statement of intent. Give it its own, far stricter band, and move the choice of band into the decision rather than leaving it to the call site, which is where the two got conflated. Re-arming follow now requires the reader to be within 4px of the end: enough for fractional-pixel and zoom rounding, well inside one line of prose. The pin and the jump-to-latest affordance keep their 48px band. * fix(native-chat): make transcript intent own end following |
||
|
|
ffc331212c |
Fix PTY child process verdict to preserve unverifiable state (#20729)
* fix(pty): preserve unverifiable local child reads * fix(pty): make child-process inspection synchronous Separate foreground and child-process sampling. Sample child processes synchronously after confirming foreground availability, returning unverifiable verdicts when pty reads fail. Handle both transport loss and local read failures uniformly in the completion coordinator. |
||
|
|
4bcdc67369 |
Distinguish pane load failures from empty states (#20735)
* refactor(renderer): give the IPC error reader a clamped and an unclamped shape * fix(settings): tell a failed load apart from a genuinely empty pane * refactor: consolidate import types and simplify failure handling - Move filesystem import types to shared for renderer use - Add compactIpcErrorMessage for single-line error display - Consolidate entry failure toasts to single global slot - Simplify account tracking and discard retry logic * fix type * fix: clear stale state when pane loads fail Credential reads, account fetches, and skill scans can fail, leaving stale data on screen. This change clears previous state when a load fails, distinguishing load failures from genuinely empty results, and prevents stale controls from appearing after failed re-checks. Use readIpcErrorMessage for consistent error handling and track runtime targets to invalidate results from old targets. * fix(settings): show credential action when bitbucket status read fails When the credential-read operation fails, allow users to retry by showing "Add or replace credentials" button. Initialize the credentials dialog with the current (confirmed) connection state instead of stale data from a failed read, preventing outdated information from pre-populating the form. |
||
|
|
6cb5643241 |
fix(deps): migrate Tiptap security updates with Markdown compatibility guards (#19376)
* chore(deps): evaluate coordinated Tiptap security migration * fix(editor): adapt link ranking and initialization for Tiptap 3.31 * fix(editor): preserve literal Markdown through Tiptap serialization * test(editor): cover literal saves in local folder and paired workspaces * test(editor): reselect folder after closing its final tab * perf(editor): avoid repeated inline source-marker lookahead scans * refactor(editor): inline redundant HTML match wrapper * test(chat): await Tiptap React skill-pill rendering --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
b8554f1c59 |
fix(composer): clarify failed attachment drops (#20704)
* refactor(renderer): give the IPC error reader a clamped and an unclamped shape * fix(composer): name the attachments a drop could not add, in one toast * fix(composer, source-control): use one stable failure toast slot - Replace per-worktree toast IDs with single slot that replaces on each failure - Remove destructive retry actions; discard must confirm in dialog - Consolidate filesystem import types to shared location - Add compactIpcErrorMessage for string error handling * refactor: centralize filesystem import types and clarify failure naming Move import result types from main/ipc to shared layer so they're available across preload and renderer. Rename uniformFailure → commonFailure and skippedOrFailed → failureCount for clarity. Simplify preload/API type definitions by reusing shared types directly instead of duplicating inlined union shapes. * Reuse single toast slot for composer drop failures Multiple drop failures now replace the previous toast instead of stacking, preventing notification clutter. Uses a dedicated toast ID separate from Source Control's stage/discard notifications. |
||
|
|
767b7c14f1 |
fix(ai-vault): expand nested OMP session history (#20663)
Expand saved OMP descendants lazily while preserving exact child targets for Resume and View Log. Retain expanded branches across virtual scrolling and reject late responses/cycles. Includes the independently reviewed child-workspace correction from #20629. 61 combined target/map/nesting tests and actual OMP child/grandchild storage/CLI smoke pass. Earlier hidden Electron proof covers eight generations and narrow sidebar layout. Folder-only unresolved child targets remain disabled. No live delegation or full terminal-launch proof claimed. Addresses #12885 Scope 2. |
||
|
|
742a7ad842 |
fix(omp): resume independent child sessions from history (#20629)
Add Resume to eligible local OMP child history rows. Resolve lazy child targets from their own cwd and host, never an unrelated active workspace. Unresolved folder-only targets stay disabled; copy-command remains available. Verified production map/resume resolver regression before/after; 50 focused tests and independent 40-test review, web types and code quality passed. Actual OMP storage/CLI smoke confirms distinct child/grandchild sessions. No native Windows or live SSH launch claim. Addresses #12885 Scope 1. |
||
|
|
f55b7ba680 |
fix(native-chat): cancel pending prompts precisely (#20601)
* fix(native-chat): hide activity while awaiting input * fix(native-chat): keep approval turns cancellable * test(native-chat): satisfy split PR quality gate * fix(native-chat): catalog approval cancellation label * fix(native-chat): include approval cancellation runtime label * fix(codex): settle prompts when cancelled turns complete * fix(codex): settle prompt registry fallbacks * test(native-chat): cover pending interaction fallbacks * test(native-chat): split prompt state coverage * test(native-chat): keep prompt state isolated * fix(native-chat): bound prompt turn backfill * refactor(codex): centralize prompt registry bounds * fix(native-chat): cancel pending prompts precisely * fix(native-chat): consolidate capability imports * fix(native-chat): harden precise prompt cancellation * fix claude cancellation teardown races * retry claude prompt lifecycle admission * bound claude prompt cancellation retry work * fix(codex): bound prompt turn identity on registration * fix(native-chat): route rejected late dispatch settlements * fix(codex): retain exact cancellable prompt turn ids --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
dd85e5fc81 |
fix: keep OMP terminals when folder workspaces become Git repos (#20653)
Preserve the original folder locator through Git upgrade and subsequent listing, persistence, and removal decisions after proving it still names the same checkout. Independently reviewed with 60 focused persistence/listing/removal tests and six native Windows real-Git/NTFS cases covering case/slashes, junction retention and retargeting, remote-host isolation and unrelated checkout preservation. Prior source-connected native OMP proof confirms process survival. Full PR CI passed; no rebuilt full-app after-proof claimed. |
||
|
|
41e42beab4 |
fix(worktrees): safely remove prunable git-file registrations (#20617)
Preserve checkout files and the named branch when removing a positively attested malformed Git-file registration. Reject file/symlink targets in deferred directory deletion. Verified exact head with 75 focused tests including actual Git malformation, preserved marker/file bytes and branch HEAD. Independent review and complete product CI passed. WSL routing is covered by unit tests; direct SSH fails safely without local recovery. Fixes #17316 |
||
|
|
bac96b212e |
fix(hooks): actually terminate a timed-out hook's process tree (#20576)
Repairs #20559, whose termination was a no-op: `detached` is a spawn-only option and `exec` ignored it, so the shell never became a group leader. Verified against real processes. Refs #19334 |
||
|
|
4a027626e9 |
fix(agent-session): honour the backup-recovery fence floor on surface release (#20708)
* Fix surface release fence recovery floor * fix(agents): advance backup recovery floor past lost mint |
||
|
|
49fba59925 |
fix(runtime): apply the tui-idle evidence ranking to mailbox delivery (#20578)
fix(runtime): retry a delivery that the idle gate refused Gates delivery at the two points where each implementation commits to typing into the pane, rather than at each caller, and parks-and-re-offers a refusal so late idle evidence cannot strand a queued message. Refs #6011 |
||
|
|
46eb5959fa |
fix(ui): contain idle caret paint so agent panes stop burning CPU (#10554)
An idle agent pane kept ~40% of a core busy just by being frontmost. The xterm cursor and the native chat caret blink with no paint-containment boundary, so Chromium treated each blink as damage to the whole pane ancestry and re-rasterized it twice a second. - `.xterm-container` and the native composer's input shell get `contain: paint`, bounding blink damage to the surface that blinks. - The mention hint gains `z-20` to match the slash picker: a contained element becomes a stacking context and paints at z-index 0 in tree order, which would otherwise cover the hint's drop shadow. Also records that DECSCUSR pins `decPrivateModes.cursorBlink`, which wins over the option in `_updateCursorBlink` — so parking `cursorBlink` does not reliably stop a hidden pane blinking. Pre-existing, documented only. Co-authored-by: Wooseong Kim <innocarpe@users.noreply.github.com> |
||
|
|
389d672dab |
fix(omp): preserve saved conversation names in session history (#20636)
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
68f0b2e835 |
feat(runtime): stream file uploads instead of buffering whole files (#16106)
* feat(runtime): stream file uploads instead of buffering whole files Staging read each dropped file whole with readFile(), base64-encoded it (a 4/3 expansion), and passed the string through IPC to the renderer, which re-chunked it. Peak memory was ~2.3x the file size before a byte moved, so a 25 MB per-file cap existed to protect the heap. Staging now records identity only. The byte pump moves into main, where the file handle and the runtime socket both live: 384 KiB slices (512 KiB once base64-encoded, matching the chunk size the renderer used) appended through the existing files.writeBase64Chunk RPC. Peak memory is one slice regardless of file size, so the ceilings become user-safety limits on an unattended transfer — 2 GB per file, 8 GB per drop — and over-limit errors name both the size and the limit. Because staging and streaming are separate calls, the staged entry carries size, inode, device and mtime, and the streamer re-checks all four against the pre-open lstat and against the handle it actually reads. A source replaced or rewritten at the same size between the two calls is refused rather than uploaded under the original name. The post-read check compares mtime as well as size, so an in-place rewrite mid-transfer aborts before commitUpload renames anything into place. O_NOFOLLOW, realpath containment and stat identity are preserved, and the pairing revision plus the runtime id ride every chunk, so a re-pair or a replacement runtime aborts instead of appending the rest of the file to a different host. No wire change: files.writeBase64Chunk and its params are untouched, so old and new hosts behave identically. The SSH import path is separate and unchanged. The web client has no local filesystem to stream from and says so instead of failing obscurely. * fix(runtime): close the empty-upload and per-drop budget holes Two gaps the first pass left open. A zero-byte source returned before the post-transfer identity check, so a file that gained content during the empty write's round trip committed as an empty file at the user's chosen name. The empty chunk now falls through to the same final check the slice loop uses. Each staged source also started its own byte counter, so the 8 GB ceiling capped one source rather than the drop: five 2 GB files staged cleanly at 10 GB total. The IPC handler now carries one budget across sourcePaths and adds only what each source actually staged. The per-file ceiling is still re-enforced where the bytes move; the drop total holds at staging because identity enforcement means each file streams exactly the bytes measured. * docs(runtime): name the invariants the upload helpers carry * fix(runtime): name the source in errors and stop uploads with their window Three problems an independent review turned up. A dropped file's relative path is '', so the over-limit error read "'' is 3 GB, over the 2 GB per-file remote import limit" — the message this change exists to fix, naming nothing. Errors now fall back to the file's own name; the staged entry keeps '' so the destination path is unaffected. The streamer had the same shape, falling back to the hidden .orca-upload-<nonce> temp destination, a path the user never chose. The byte loop used to live in the renderer and died with it. Moving it into main meant closing or reloading the window left the rest of a multi-GB transfer running, with the renderer's temp cleanup never reaching its finally. An AbortSignal now rides the caller's lifetime and every chunk, is re-checked per slice, and main sweeps the abandoned temp path itself when the renderer is no longer there to do it. Upload failures also reached the import result wrapped in Electron's "Error invoking remote method '...'" prefix, because the throw crossed IPC instead of happening in-renderer; extractIpcErrorMessage unwraps it. An existing staging test asserted the empty-name message, so it encoded the bug rather than catching it; it now asserts the file name. * test(runtime): cover the containment check and the per-chunk host guards The "escapes the dropped root" test only reached the lstat symlink guard, so assertEntryInsideRoot had no coverage at all. The shape that actually needs it is a regular file under a symlinked intermediate directory: lstat sees a plain file, and realpath containment is the only thing that refuses it. Disabling the guard now fails this test and nothing else. Nothing asserted that the SSH target, connection generation and execution host reach the writeBase64Chunk params either — the renderer tests stop at the IPC boundary, so the streamer's half of that contract was untested. * fix(runtime): survive a straggling append when sweeping an aborted upload Aborting rejects the in-flight chunk locally, but the host may still apply that append, and appends open with flag 'a' — which recreates the file the sweep just deleted. The delete and the straggler also race: they are separate calls on a queue that is not ordered between them. Slices are strictly sequential, so at most one append can be outstanding. A second pass after it has had time to land is therefore sufficient, not merely a heuristic. The sweep moves out of filesystem-mutations.ts into its own module so the behaviour is testable directly. Found by an independent review pass, which also pointed out that the "escapes the dropped root" test only reached the lstat symlink guard. * fix(runtime): abort uploads only when the document commits, and honour manual disconnect per chunk did-start-navigation fires before will-navigate blocks an external link or a stray file drop, and the renderer survives those (verified against Electron 43 with a hidden window). Aborting there killed a healthy upload with a misleading 'window went away' error. did-navigate fires only once a new document has replaced the caller. The renderer's per-chunk calls used to go through the IPC handler that refuses a manually disconnected environment; the loop in main made no such check, so a disconnect mid-upload kept pushing the rest of the file. The handler now resolves the selector to an environment id and the streamer checks it per slice. Adds slice-boundary coverage against the real chunk schema and host write flags, staging-to-stream on a real filesystem, and handler-level lifetime tests. --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
f21f81dcfc |
fix(agents): find OMP by its full project name (#20647)
* fix(agents): find OMP by its full project name * test(agents): make picker baseline proof omit OMP aliases * style(test): brace picker baseline condition |
||
|
|
8d93505958 |
fix(terminal): retain renames before renderer pane hydration (#20619)
* fix(terminal): retain renames before renderer pane hydration * test(terminal): keep late renames from recreating closed tabs |
||
|
|
ee1a0a4e2d |
fix(git): avoid Windows tree kills after the command has exited (#20606)
Validated and independently reviewed OMP integration fix. |
||
|
|
fc4519cda4 |
fix(omp): preserve zsh startup with global aliases (#20621)
Validated and independently reviewed OMP integration fix. |
||
|
|
3632311d0b |
fix(omp): preserve status after terminal title owner rewrite (#20610)
Validated and independently reviewed OMP integration fix. Co-authored-by: shahidbeig-a11y <258701601+shahidbeig-a11y@users.noreply.github.com> |
||
|
|
955051ded0 |
fix(codex): settle a structured send on admission, and stop minting a colliding identity (#20138)
* fix(codex): settle a structured send on admission, and stop minting a colliding identity Two sends could be written into the journal under one durable identity. Codex coalesces a mid-turn `turn/start` into the running turn rather than refusing it -- measured against real `codex app-server` builds 0.147.0, 0.150.1 and 0.153.4, none of which refuse and none of which fire a second `turn/started`. The dispatch path read the turn id from the turn/start response and stamped every accepted send `ordinal: 0`. Since a coalesced send gets the running turn's id back, two submissions persisted the same `providerItemId`. That string is durable, and it is the key a restore uses to match a submission against provider history, so the second message's real history row matched nothing and rendered as an extra bubble on replay. On 0.147.0 it is worse than a collision: the coalesced response returns a turn id that never starts and never completes, so the persisted key named a turn absent from history and NEITHER message could match. Identity is now minted from the echoed user message at `identityFor` -- the single point that mints the journal row's own identity -- so the settled key is by construction the one replay computes, rather than a parallel calculation that can drift. Dispatch returns `admitted` when the transport write completes; identity settles on the echo through a channel that did not previously exist for Codex. Waiters are keyed by client message id instead of being shifted off the front of an array by arrival order, and they are cleared on session close and child exit -- previously a timeout was the only thing that ever ended one. `TURN_ID_WAIT_MS` is deleted. It was never reachable on any build measured: `readCodexTurnId` returns non-null on all three, so the 10s wait never fired. The comment justifying it claimed older builds acknowledge before the id exists, which no tested build does. Three comments asserting Codex answers a mid-turn send with `turn already running` are corrected. Their only backing was a test fixture inventing that error string. The correction is factual only -- every changed line in `src/main/runtime/orchestration/` is a comment, and mid-turn delivery is still refused for both providers. Whether that policy is right is a separate question; it was resting on a false premise. Known gap, stated rather than implied: this prevents new collisions and does not repair journals already written with a colliding or phantom key. Those conversations keep duplicating on restore. Repairing them means re-matching persisted submissions against provider history and rewriting `providerItemId` -- which is what `journal-submission-reconciler.ts` is written for, and it still has no production caller. * test(codex): drop the synchronous-accept contract and the colliding `:0` from the integration fakes Three tests in the structured-session integration suites encoded the dispatch contract this branch replaces, and two of them pinned the defect it fixes. They asserted `agentSession.send` answers `dispatchState: 'accepted'` carrying `providerItemId: codex:<thread>:<turn>:0` at send time. That ordinal was never observed; it was stamped on every accepted send, which is exactly the collision this branch removes -- a send coalesced into a running turn is answered with the running turn's id, so two submissions persisted one durable key. The visible failure was a 30s timeout rather than a failed assertion. The fake client advertised no `agent-session.pending-send-result.v1`, and without it the host holds the reply until the send settles: a shim for clients too old to render a pending bubble. The fake provider then echoed the user message with no `clientId`, so nothing could correlate that echo back to the submission, and the wait ran to its own 30s ceiling. Real Codex sends `clientId` on that echo, and the fake now does too, which is what makes it a model of the provider rather than a sketch of one. The identity assertion is kept rather than dropped. Each send now asserts `pending` with no identity at admission, then asserts the submission settles `accepted` at `codex:<thread>:<turn>:0` once the echo lands. Same ordinal, but earned from `identityFor` on the echo -- the key a replay recomputes -- instead of guessed from the turn/start response. Ablated: removing `clientId` from the two echoes leaves both submissions `pending` and fails both assertions, so the assertion is load-bearing and not satisfied by something incidental. Both suites' client fixtures now advertise the capability set the desktop renderer sends in `src/main/ipc/runtime.ts`, which is what these suites mean by a client. The older-client settlement wait keeps its own coverage in `src/main/runtime/rpc/methods/structured-agent-session.test.ts`. `structured-agent-session-runtime-exit.test.ts` asserts `pending` for the same reason; it drives the host directly, so it never took the compatibility path, and what proves delivery there is still the turn the reacquired provider starts. The replay suite's "without dispatching it twice" property is untouched: one `turn/start` call, one replayed ledger row. * fix(codex): preserve unsettled dispatch correlations * test(codex): type the dispatch fixtures instead of asserting over them main's new casting gate (#20367 base) flags type assertions on changed lines. Replace them with checked types: the recording sink already satisfies its interface, both CodexSession fixtures are now annotated and carry real collaborators, the settlement assertion compares whole identities, and the integration helper reads submissions through the host's public journalSnapshot instead of its private session map. * fix(test): merge the duplicate doubt-reasons import the merge left behind Both sides added an import from journal-dispatch-doubt-reasons and the merge kept both statements, which the whole-repo native plugin gate refuses under --deny-warnings. * test(codex): a Fast mode turn is admitted, not accepted #20506 landed its Fast mode tests against the dispatch contract this branch replaces: a Codex send now returns admitted and settles its identity on the provider echo. The tier assertions the test exists for are untouched. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
dede24df46 | fix(store): preserve state identity for no-op updater branches (#20703) | ||
|
|
c3372aeadc | fix(ai-vault): bound streamed remote JSONL records (#20700) | ||
|
|
2186a885dd |
fix(store): stop two no-op writes from re-running every selector in the app (#20641)
* fix(store): stop two no-op writes from re-running every selector in the app
zustand bails out of a `set` only when `Object.is(next, state)`. Two updaters that
mean "nothing changed" hand it a fresh reference instead:
- `setWorkspacePortScanRefreshing` wrote unconditionally — the one action in its
file that did; its four siblings all early-return `state`.
- `applyGitHubPRRefreshEvent` ended its no-op branch with `: {}`, and
`Object.assign({}, state, {})` reproduces every field unchanged while still
notifying. ~20 sibling sites in the same store already use `return state`.
Both rebuild the root and wake every subscribed selector (~2.2k per the listener
census). Renders are unaffected — the selection is unchanged — so the cost is
wasted selector evaluation, not commit pressure. The resulting state looks
identical either way, which is why it goes unnoticed; both tests therefore count
subscriber notifications rather than asserting state.
* test(store): use checked initial state in notification regression
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
|
||
|
|
6c70801f72 |
fix(source-control): prevent text wrapping in section headers and action buttons (#20046)
* fix(source-control): prevent text wrapping in section headers and action Use flex layout constraints (flex-1, shrink-0) and text truncation instead of wrapping to keep section labels and action buttons on a single line in the right sidebar. * test(source-control): add section action button alignment tests Ensure View all button stays on single line with icon actions in crowded section headers. Pin layout constraints (shrink-0, flex-wrap, whitespace-nowrap) to prevent regression. * Rely on Button base styles for action label wrapping Remove redundant shrink-0 and whitespace-nowrap utilities from section action buttons. These should be supplied by the Button component's base variant, not duplicated at each usage site. |
||
|
|
d5be0d69e7 |
Add copy button to code blocks (#20357)
* feat(native-chat): add copy button to code blocks Enable users to copy code snippets directly from chat messages via a dedicated copy button on fenced code blocks. Supports language detection and integrates with markdown rendering via a `renderCodeBlock` prop. * i18n: add English copy code button label * refactor: use React.isValidElement type parameters for type narrowing - Specify props types as type parameters to React.isValidElement instead of casting after the fact - Allows TypeScript to narrow element.props type automatically - Eliminates manual type assertions in extractCodeText and extractCodeFenceLanguage |
||
|
|
2acd2f4c88 |
Surface stage, unstage and discard failures with retry capability (#20423)
* fix(source-control): surface stage, unstage and discard failures * fix(source-control): use single slot for entry failure toasts - Consolidate entry failures to one stable slot instead of per-worktree - Handle stale retries inline at click time rather than via a cleanup hook - Remove retry button from discard failures to prevent destructive accidents * test: improve type safety and mock patterns in source-control tests - Add proper type definitions for toast options and test data instead of using `as never` - Replace `mock.calls.at(-1)` with safer `mock.lastCall` pattern - Create `entry()` helper to construct typed test entries - Add explicit type annotations to mocked functions for better IDE support * test: extract shared toast options type for source control tests Consolidate duplicate `ToastOptions` type definitions across three test files into a single `SourceControlToastTestOptions` type, reducing duplication and improving consistency. * fix(source-control): separate refresh failures from mutation failures Post-mutation refresh failures are logged separately, not surfaced as toasts (mutation already succeeded). Use preventDefault() on retry to prevent sonner's auto-dismiss from swallowing re-raised failures. Consolidate stage/unstage into a shared handler to reduce duplication. * fix(source-control): only dismiss entry failures from the owning worktre Track which worktree owns the shared entry-failure toast slot. When a mutation completes, only dismiss the slot if the completing worktree is the one that raised the failure — a slow retry in one worktree should not erase a failure another worktree has since raised into the slot. * Remove entry mutation status refresh helper Inlined into the caller during consolidation of failure handling and tracking in the source-control entry mutations flow. * Simplify entry mutation refresh without wrapper Call refreshActiveGitStatusAfterMutation directly instead of through the refreshEntryMutationStatus helper. This ensures refresh failures propagate directly from the callback without being caught as mutation failures. Remove tests that validated the wrapper's error handling. |
||
|
|
d8b6151e8c |
fix(native-chat): keep a resumed transcript pinned to its end (#20651)
* fix(native-chat): keep a resumed transcript pinned to its end Follow state was recomputed from distance on every scroll event, and a pin writes scrollTop itself, so the browser reports that write back as a scroll event a frame later. Once a resumed session's later history pages and settling row heights had moved the end away from it, that echoed event read as the reader leaving and the pin was dropped for good, stranding them mid-transcript. Measured in Chromium: a pin followed by same-task growth delivers a scroll event reading 2000px from the bottom, indistinguishable from a reader scrolling up. Pins now go through the virtualizer instead of writing scrollTop directly, so both parties resolve the end through the same maximum rather than holding rival definitions of it. Whether the reader left is now a question of provenance rather than distance: an offset this transcript wrote is never a departure. The end test reads live geometry, because the virtualizer's own isAtEnd subtracts a cached offset from a live maximum and this handler runs before that cache is refreshed. overflow-anchor:none stops the engine moving scrollTop under a settling row, which would otherwise look like the reader. This does not make ownership singular. The virtualizer still writes autonomously from several paths and those writes stay unattributed; what this removes is the rival definition of the end, not the second writer. * fix(native-chat): cancel stale end reconciliation * fix(native-chat): attribute scroll ownership centrally |
||
|
|
b4d435806f |
fix(native-chat): bound a dispatch reason before it reaches the journal row (#20654)
`AgentJournalSubmission.reason` was the only unbounded field written by Orca's own code. `dispatchSafely` sets it from the adapter's raw `error.message` and `journalDispatchRowBuilder` stored it verbatim, so a provider error carrying a multi-megabyte body -- a stringified HTTP error payload, say -- reached the row at whatever length the provider sent, and stayed on disk at that size for the life of the journal. It now goes through `boundInlineText` with the journal's existing inline limit, the same idiom already applied to arbitrary text on the Claude and Codex translation paths. The bound must stay head-preserving. `dispatchRejectionWasTransportWriteFailure` prefix-matches the value, and `dispatchRejectionReasonIsInternal` builds on it, so a bound that kept the tail instead would stop classifying a clipped transport failure and render raw provider text to the user as an ordinary rejection notice. A test pins that, and clipping stays marked rather than silent so a truncated reason is never presented as the provider's complete explanation. Rows written before this keep their full text, so readers can still meet an unbounded reason. |
||
|
|
c6a7216984 |
fix(native-chat): hide activity while awaiting input (#20496)
* fix(native-chat): hide activity while awaiting input * fix(native-chat): keep approval turns cancellable * test(native-chat): satisfy split PR quality gate * fix(native-chat): catalog approval cancellation label * fix(native-chat): include approval cancellation runtime label * fix(codex): settle prompts when cancelled turns complete * fix(codex): settle prompt registry fallbacks * test(native-chat): cover pending interaction fallbacks * test(native-chat): split prompt state coverage * test(native-chat): keep prompt state isolated * fix(native-chat): bound prompt turn backfill * refactor(codex): centralize prompt registry bounds --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
eba56f2f69 |
feat(ai-vault-search): construct the session search indexer in the scanner service behind a setting (#20516)
* feat(ai-vault-search): persist agent-session search consent and retention Two booleans and nothing else: `enabled` and `historyDays`, off by default because building the index reads every transcript on the machine. No `paused` -- the PR 3 indexer is immutable, so every change is close-and-construct. The settings IPC normalizes a write like every other field and hands the change to the index; there is no UI for it until PR 8. * feat(ai-vault-search): hold one indexer and engine pair per host The object that owns a host's live index and the three recipes that change it. The indexer is immutable, so a settings change is close-and-construct, disabling is close with no replacement, and clearing is close, remove the database, construct. The new instance's first sweep purges a narrowed window and admits a widened one, so neither needs a code path. The database sits beside the scanner's parse cache, one file per host. A runtime with no node:sqlite can hold no index at all, which the Node 18 floor on orcad and the relay makes a real case rather than a hypothetical one. * feat(ai-vault): let the scanner child own the session search index The transcript reader runs in that child, so the index consumer has to as well: one read serves both the session list and the index. Three request operations (search, status, reconcile) and one fire-and-forget settings message carry everything a parent needs; main never opens the database file. The init frame becomes a factory because it is read at every spawn, so a respawned child sees current consent rather than the first frame's. A child holding a running index is never idle from the parent's side, so idle retirement is suppressed while the index is on -- retiring it would stop the reconcile loop until some later scan happened to respawn one. Both files this lands in were already at the max-lines ceiling, so three collaborators move to where they belong rather than being disabled around: the invalidation deadline into the class that owns invalidations, call cancellation and the start requeue into the call-state module, and orcad's flag parsing into its own file. * feat(ai-vault-search): register a search service on every host that answers Without a registered service a host answers no-service, which means "this host does not have the feature" rather than "the index is off". All three hosts now answer the second thing. The desktop forwards to the scanner child. orcad and the SSH relay daemon have no such child -- orcad ships only the watcher and daemon entries, and the relay's AI Vault sidecar runs the remote scanner, which publishes nothing to the transcript channel -- so on those two the index lives in the process that would drive its reads, gated on a runtime that has node:sqlite at all. The relay registers with consent off and no way to turn it on: nothing carries a setting to a remote host yet. That is the honest state, and it is still worth registering, because it is what tells a client the difference between off and too old. * test(ai-vault-search): price a warm pass over five thousand transcripts The number the reconcile interval will be revisited against, measured rather than argued: a warm sweep stats every file under every root, a warm cycle stats the newest N per agent, and neither reads what the index already holds. It does not tune the interval. * fix(ai-vault-search): answer the casting gate without assertions main's new type-assertion rule reaches every file this branch touches. All nine sites drop the cast rather than carry a SAFETY: rationale: the operation guard narrows with `in`, the sqlite probe narrows the builtin it loads, the child test keeps the discriminated reply instead of widening it, and the settings resolver takes `unknown` -- which is what it really reads, since a persisted profile can hold a value no version of this code wrote. * fix(ai-vault-search): let a refreshed scan root reach the live index The parent re-resolves scan roots before every policy push, precisely so a WSL distro or extra Codex home that appeared since the child spawned enters the window. The child forwarded only the settings to a live instance and used the roots solely in its `??=` initializer, so those roots were dropped for the child's lifetime. The indexer stays immutable: a structurally different root set closes the pair and constructs a new one, the same way a changed databasePath already does. Compare via `sameSessionSearchRoots` rather than a plain JSON compare, because nothing fixes the key order two producers write; lists are sorted too, since the indexer walks every root and a re-enumeration that reorders is not a change. An unchanged set still never restarts a running index. The orcad and relay in-process hosts resolve roots once at install and never re-apply, so they have no such seam. * fix(ai-vault): restart the scanner child the index is holding Three review items. The hold keeps a child alive for the index, but only a queued call ever started one: `pump()` skipped a hold with an empty queue, so an idle indexing child that crashed, or an `ensureChild()` that failed at start, left indexing stopped until an unrelated request happened to arrive. `pump()` now starts the child the hold requires, which is also the restart callback the fault policy already schedules, so the existing delay and circuit bound the retry exactly as they bound a queued call's start. `updateSessionSearch` goes through the same seam instead of its own `ensureChild` call. A search registers no AbortController, so a cancel sent for a search id was added to the `cancelled` set and never consumed. Nothing can reach that today -- no caller passes a signal and the child answers in milliseconds -- so this is only a leak of ids: consume it when the search settles. The orcad argument doc claimed a `--`-prefixed value stays a flag. The parser takes the next token regardless, and orcad-launch-contract.test.ts pins that, so the doc is what was wrong. Behaviour is unchanged. * fix(ai-vault): recover search indexing and refresh scan roots * fix(ai-vault): defer search refresh policy reads * fix(session-search): stabilize paging and host enablement * fix(session-search): refresh host roots within full sweeps * docs(session-search): clarify initial root fallback |
||
|
|
a4c11f1889 |
fix(native-chat): stop a bounded tail read from moving the chat cursor past unapplied rows (#20581)
* fix(native-chat): stop a bounded tail read from moving the chat cursor past unapplied rows
A structured chat pane could latch "Working for N" forever after the agent had
finished, showing the send arrow rather than Stop, while the sidebar and
`worktree ps` correctly read idle.
The client replica has one position (`state.cursor`) and one body. Two
operations keep those consistent: replace (both from one host snapshot) and
append (rows contiguous with the cursor). The `tail-page` branch was a third
thing: it took the cursor from the journal head, the items from a bounded page
(200 items, byte-capped), then merged retained client submissions over the
page's. Under continuous journal writes the client is always slightly behind,
so the branch ran on every window focus and on every pane re-activation. When
more than a page of rows had landed since a send, that send's user item fell
off the page, its submission was not carried, the retained `pending` survived,
and the cursor jumped past the dispatch-acceptance row. Nothing re-sends it: a
batch carries only touched items and that submission is never touched again.
Delete the third operation rather than guard it. A live subscription is now the
only thing that moves the cursor, and `subscribe({ cursor })` already replays
exactly the missed rows.
- remove the window `focus` listener and the owner/transport `refresh` contract
- skip warm hydration: a retained owner subscribes at its applied cursor
- cold hydration keeps its history read, applied as the existing `snapshot`
(replace) event rather than `tail-page`
- delete the `tail-page` action and its reducer branch
- delete `resumeCursor` and `shouldAdvanceStructuredResumeCursor`; two cursors
with two advancement rules were how position and body drifted apart
`older-page`/`loadOlder`, the unattached-refusal grace, generation guards and
the coalescer are unchanged. No host, wire or schema change.
Also fixes a second cost of the same branch: focus during a busy turn discarded
paged-in older items, shrinking the transcript to one bounded page mid-turn.
* fix(native-chat): preserve unavailable mixed-version session fences
|
||
|
|
8e26d516d8 | perf(browser): dispatch coordinate pointer input in process instead of one subprocess per event (#20593) | ||
|
|
01f8aa8d96 |
fix(grok): stop SessionStart orca-status hook hanging for 10s (#20090)
* fix(grok): stop SessionStart orca-status hook hanging for 10s Grok writes one JSON hook payload and waits for the process to exit without closing stdin. The POSIX hook used `cat`, which waits for EOF, so SessionStart deadlocked until Grok's 10s timeout: session_start hook (global/orca-status) failed, ignored: timed out after 10000ms Read one JSON object with raw_decode instead; return as soon as the object is complete. Fall back to cat when Python is missing. * fix(grok): decode the hook payload incrementally instead of per chunk The JSON stdin reader strict-decoded the whole accumulated buffer after every read and caught only json.JSONDecodeError. A multi-byte character split across two os.read calls therefore raised UnicodeDecodeError, which is a ValueError but not a JSONDecodeError, so the interpreter died — after consuming stdin. The `python3 || python || cat` chain then handed the next reader a truncated stream, and `cat` blocked on a pipe the caller never closes, reinstating the exact 10s SessionStart timeout this reader exists to avoid. Measured: a CJK+emoji payload written byte by byte hung until it was killed; a 200KB payload split mid-character arrived as 4 bytes. Hold the decoder across reads, treat any ValueError as "not complete yet", and guard the whole program so a non-zero exit implies stdin was never read — only then is the `||` fallback safe. Emit the object's own text rather than re-serialising it, which was rewriting non-ASCII as \uXXXX. Skip leading whitespace, which raw_decode does not. Separate the first-byte wait (5s) from the idle wait (1.5s) so a writer that is merely late is no longer dropped. Also unset a HOME that does not exist before spawning the interpreter: macOS resolves /usr/bin/python3 through an Xcode stub that re-runs its whole tool lookup without a reachable cache, costing 6.6s per spawn and overrunning Grok's budget on its own. This is what made the existing large-payload and empty-PATH lifecycle cases fail. The Python program now lives in a shell variable instead of being inlined twice, which halves the generated script and keeps it readable. --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
170dbdb874 |
fix(ai-vault): ignore non-absolute env overrides for agent scan roots (#13118)
Six scan roots took a directory from an environment variable and used it verbatim. A relative value is resolved by whichever Orca process reads it — main sits at `/` when Finder-launched, the terminal daemon chdirs itself to the user data dir, the AI Vault service inherits main's cwd — so one value names a different directory in each, and walkSessionFiles walks it with no depth cap, no entry cap and no time budget, about once a minute per the session-list cache TTL. The agent CLIs do accept a relative home (verified against real Grok 1.0.30: `GROK_HOME=myhome grok du` creates `<grok-cwd>/myhome`), but they resolve it against their own per-terminal cwd, which no Orca reader shares. Falling back to the default home is therefore not a lost configuration — it replaces an unbounded walk of an arbitrary tree with a bounded read of a known one, and matches what readGrokHomeEnvelope, skill-provider normalizedRoot and absoluteConfiguredDir already do with the same values. Add resolveAbsoluteDirOverride and apply it to CODEX_HOME, COPILOT_HOME, OPENCLAW_STATE_DIR, DEVIN_HOME, KIMI_CODE_HOME and GROK_HOME. It takes an explicit platform so the Windows shapes are provable from a POSIX CI box: `C:\...`, `C:/...` and UNC roots are kept, while the drive-relative `C:foo` and bare `C:` fall back. Tilde expansion stays out of it — Grok creates a literal `~` directory rather than expanding one — so absoluteConfiguredDir keeps its own Pi/Prime-specific expansion and delegates the absolute check. isAbsolute is syntactic only, so `/..` still collapses to `/`. That is fine for read-only discovery; these roots never gate renderer-supplied paths. Tests assert at the call sites, not just on the helper: the four session-scanner-agent-sources roots are module-level consts evaluated at import time, so they are exercised through AI_VAULT_AGENT_SOURCES with vi.stubEnv plus vi.resetModules. Reverting any one of the six call sites fails them (11-33 cases each). Closes #13082 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2ed89b8781 |
fix(github): name an unfiltered empty project view instead of blaming a filter (#20588)
* fix(github): skip Projects search index for unfiltered views Empty query still used items(query:\$q), which routes through GitHub's Projects search index and can return totalCount 0 while the board is full during index lag. Omit the query argument when the view filter is empty. Fixes #12648. * docs(github): drop the false stable-shape claim for empty project filters Unfiltered item fetches omit items(query:) so boards skip search-index lag. The View.filter field is still '' when GitHub returns null. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(github): name an unfiltered empty project view instead of blaming a filter The search-index workaround in this branch was a no-op. Live introspection of ProjectV2.items shows `query` is declared `String = ""`, so omitting the argument and sending `$q = ""` coerce to the identical resolver input; GitHub applies declared defaults for omitted args (verified against its own endpoint). There is no non-search item field on ProjectV2 and ProjectV2View has no `items` at all, so no request shape can dodge the index. Revert the branching query construction and the module it added. What the user actually reported in #12648 is the copy: a view with no filter rendered "No items match this view's filter", which reads as data loss when a freshly populated board momentarily comes back empty. Word the empty state from the view's own filter — the filter message only when there is a filter, and an honest "no items yet" plus a transience hint when there is not — and share the one implementation between the table and roadmap surfaces. Refs #12648. --------- Co-authored-by: bbingz <zzb@gxsmjx.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
539d4d1f32 |
fix(native-chat): resume structured chats cleanly after restart (#20509)
* fix(native-chat): retire provider ownership on restart * fix(native-chat): stop showing a restart eviction as a provider death Restarting Orca turned a resumable structured chat into a user-visible `Provider exited: recorded pid absent on host`. Quit never released the durable lease, so restart probed the recorded pid, adjudicated the session evicted, and wrote a synthetic status row against a chat that was perfectly resumable. The fix is the missing teardown phase plus the missing fence check: quit now evicts every provider child this host owns — stopping it, settling its journal and handing the lease back — and the release compare-and-swaps on the fence it expected. Restart then finds a released lease and reopens the chat silently. What the user sees is decided by the typed death evidence rather than the shape of a settlement id: only an `exit-observed` death writes copy, and that copy now carries its cause so an auth failure and an OOM kill do not read alike. The reassuring wording stays. Historical synthetic rows are filtered out of the render projection, which needs no schema change and leaves every real provider-exit row alone. Also: - Bound the new eviction phase well below the quit deadline; a quit that dies mid-eviction leaves the lease unreleased, which is the original bug. - Scope the interruption verdict to work that was mid-response. A provider that died while waiting on an approval interrupted nothing. - Keep host bookkeeping in step with the adapter: the provider-child flag clears when the child is proven stopped, not seven steps later. - Drop the router's duplicate shutdown gate and acquisition drain — both adapters already own theirs — and latch the router closed so a late acquire cannot fan a session back out to closed adapters. - Attach the real cause to the settlement failure a quit reports, and remove a recovery-ticket field that was hardcoded at its only construction site. * fix(native-chat): scope the legacy status filter to the copy it retires The read-time filter hid every status row carrying a `restart-eviction:` identity. That identity is still minted, so a genuine provider death settled under it would have been dropped from every rendered page. Match the retired `Provider exited` copy as well, so only the legacy rows are hidden. Three smaller corrections alongside it: - The settlement retry path now applies the same unfinished-work check the live exit path uses, so a provider that died waiting on an approval no longer gets told a response was in progress. - Bound the exit reason before composing the outcome copy, so a stderr dump in the reason cannot push the "you can continue" sentence past the row's byte cap. - Correct the teardown comment: tail rows are protected by eviction's own per-session ordering, and `closeAll` is a backstop for children eviction never took, including one whose eviction was refused. * fix(native-chat): retire legacy status rows at the projection source The read-time filter that hides the retired `Provider exited …` rows ran on the way OUT of the page builder, after the paging math had already measured the unfiltered timeline. A backward window landing entirely on those rows returned an empty page that still reported `hasOlder: true` with a null `window.oldest`, so the renderer's backfill loop re-asked from the same anchor forever. Its only no-progress guard compares `window.oldest?.sequence` to the anchor, and `undefined === n` never breaks. The live subscription opens behind that loop, so the transcript never finished loading either. Filter where items ENTER the page pipeline instead: the reduced snapshot gets one renderable timeline, the forward path gets one renderable batch, and the window bound, effective limit, `hasOlder`, `window.oldest` and `nextCursor` are all computed over that single array. A window with nothing left behind it now reports end-of-history. Also restore the eviction retry contract. Clearing `hasProviderChild` as soon as the adapter proves the child gone is honest, but it is a different fact from the wind-down this host still owes. A retry after a step aborted between the two was reading "no child here" and skipping both the dead-generation settlement and the lease release the aborted attempt had promised to repeat. The obligation is now tracked separately and cleared only by a release that actually landed. And rename the filter to the copy it retires: it drops only rows carrying the retired `Provider exited` text, not restart-eviction status rows in general. * fix(native-chat): read the wind-down a close owes from the live child An eviction recorded "nothing owed" whenever it ran over a session with no provider child of its own, and the retry then read that record in preference to the child in front of it. A session suspended to an agent terminal is exactly that shape, and the trip back to native re-acquires into the SAME session object rather than replacing it, so the next close skipped both the dead-generation settlement and the lease release — leaving the record claiming a live owner this host had just stopped, and a pending send unsettled. The obligation is now derived the way the quit sweep already derived it, from one shared predicate: a live child always owes a wind-down, and a remembered `false` only carries the obligation forward, never cancels it. Also drops a memoization in the history page that could never hit. Its key was the snapshot's items array, which the reducer rebuilds on every `snapshot()` call, so each backward page allocated a fresh key; the one reader that does share a snapshot across pages reads forward and never calls it. The comment claimed a multi-page read filtered once, which was not true of either path. Tests: the handoff round trip that strands the lease, and the quit sweep picking up an eviction whose close retry never came. * chore(native-chat): scope three helpers to their file and pin the teardown order retryUnexpectedExitSettlement, hasUnfinishedStructuredAgentSessionWork and isRetiredProviderExitStatusItem each have no consumer outside the file that defines them, so they no longer advertise an external contract. The quit-path phase list documents its order as load-bearing, but nothing asserted it. Pin the phase names so evict-owned-sessions cannot drift out of its slot between drain-attaches and flush-event-sinks. * fix(native-chat): stop the router reporting a stop it never observed `closeAll` cleared the route table and set one boolean, after which that boolean was the only surviving evidence about any session. Two call sites then spent it: `releaseAcquisition` and the stop path each turned a route-lookup MISS into reported success. Eviction reads a `true` from the stop path as proof the provider child is gone and releases the durable lease on it, so a session the router never routed could have its lease handed back on the strength of "I have no record, but everything is closed." Loss of contact is not evidence of process death. The fix keeps the evidence instead of the inference: adapter shutdown only resolves once every child is proven stopped, so `closeAll` now marks each routed session `stopped` rather than forgetting it. A routed session still answers `true` from its own retained proof; a session with no route answers `false`, which leaves it indexed for a real retry. `releaseAcquisition` drops its short-circuit and asks the adapters, which answer from their own session maps. The acquire-side latch is unchanged: once closed, the router stays closed and refuses new work. Behaviour that changed: a post-`closeAll` stop for a session the router never routed, or one the host already acknowledged as released, now reports unproven instead of proven. That matches what the same call already answered before `closeAll`, and no real flow reaches it — quit evicts every owned session before `closeAll` runs, and eviction only asks the adapter for sessions whose provider child this host acquired through the router. * test(native-chat): ratchet the retired provider-exit copy out of production The retirement filter hides a status row on two facts: a restart-eviction item id and copy that opens with the retired prefix. The identity half is still minted today, so the filter cannot tell a new producer's row from the legacy row it exists to hide — any future writer of that copy would be dropped from every transcript with no trace. Until now that safety property lived only in a doc comment. Scan the shipped tree for a string literal that OPENS with the retired prefix, which is exactly what the filter's `startsWith` reads. Comments are stripped first, so prose about the retirement is not a producer, and the filter's own constant is exempt. Tests are excluded: writing the copy is how the filter is exercised. * revert(native-chat): drop the read-time retired provider-exit filter Fix forward instead. The lifecycle change in this branch stops any new `Provider exited: <reason>` row from being written; rows a previous build already persisted stay in those transcripts and age out with them. A permanent read-time filter for a cosmetic, shrinking set was not worth its maintenance cost, and its paging seam was the only place a backward window could land entirely on hidden rows. Removes the filter module and its test, restores agent-session-history-page.ts to its pre-branch form, and drops the tests that only existed to prove the filter did not over-match or wedge the backfill loop. The copy ratchet stays and now carries the whole guarantee: with no filter in front of it, any production writer that resurrects the retired prefix reaches the user's transcript directly. |
||
|
|
4634d2c03b |
fix(native-chat): let the provider reopen a Claude turn it resumed itself (#20518)
* fix(native-chat): let the provider reopen a Claude turn it resumed itself A Claude turn could only be opened by Orca's own send echo, while any `result` frame closed it. The provider resumes work on its own — a background task reports in and wakes the agent after `result` settled the turn — and nothing Orca sent ever arrives to reopen one, so the session projected `idle` for the rest of the work. The model's own output is the evidence a turn is running, the way Codex's `turn/start` is, so it opens one; whichever opened it, the next `result` settles it. Subagent frames still open nothing: children outlive the turn that spawned them, and their work is their parent turn's, never a turn of its own. * fix(native-chat): bracket a resumed turn around the output that opened it A resumed turn was published after the frame's own rows, so its first tool call sat above the turn record. Every reader that scans back to the turn record and stops — the active-tool reader behind the sidebar's tool line, and the turn-window activity selector — looked straight past it, and the row showed working with no tool until a second call landed. The turn now opens before its frame is journaled. A send's turn keeps its existing order: the user echo is that turn's anchor and is written first. Prompt journaling moves to its own module, verbatim, to keep the translator clear of the line cap. * refactor(native-chat): declare the turn open at each content site The resumed-turn rule was a predicate that re-derived whether a frame had produced anything, duplicating work the frame handler had already done. The content sites know: each one now calls an idempotent ensureTurnOpen before it journals, and the guard against reopening a live turn lives in that one place. Behaviour is unchanged; claude-turn-opening.ts is left owning only the send echo, which is the one opener that anchors its turn to a user row. * fix(native-chat): gate both turn edges on root-ness The reopen path already refused nested output; the guard now reads before the already-running check so both edges state root-ness first. The close path had no nesting check at all, so a child's result would have ended the turn that spawned it. The two edges read parent_tool_use_id differently on purpose, and both fail towards not over-claiming: opening needs proof of root-ness, so an absent field opens nothing; closing needs proof of nesting, so an absent field still closes. No real Claude stream has been observed carrying a nested result — the session that prompted this work has none in any subagent stream — so the close-side guard is symmetry, not a demonstrated fix. * fix(native-chat): stop provider output reopening a turn nothing can close Self-audit found two paths the reopen rule opened where no event could ever settle the turn it created, leaving the row working for the life of the session. Both now suppress reopening until an accepted send lifts it: - a frame arriving after the session ended, when no event will settle anything - a turn the provider failed, or the user stopped, where the next thing the provider says is not a resumption Each has a one-lever ablation: removing the suppression read alone fails exactly those two tests, and both fail as working-instead-of-idle. * fix(native-chat): open a resumed turn from its first streamed delta Streamed deltas short-circuit before the frame handler, so a resumed turn whose first output is streamed text — the common case, since partial messages are a pinned launch contract — kept reading idle while its partial text was already journaled and visible. The streamed path now opens the turn too. Also from the same audit: - a nested result no longer swallows its own failure diagnostic; the turn gate now guards only settlement, and the provider-fallback row is written either way - root-ness treats an absent parent_tool_use_id as root, so a build that omits the field cannot silently stop opening turns - the suppression latch only ever sets on a failed result; a later clean result cannot lift it, and only an accepted send does The opener moves into claude-turn-opening.ts so both entry points share one root-then-suppression-then-idempotency order. * fix(native-chat): preserve resumed-turn lifecycle semantics |
||
|
|
0c812843ef |
feat(native-chat): show a drop target on the whole chat pane (#20561)
* feat(native-chat): show a drop target on the whole chat pane Dropping a file into a chat only worked if you hit the input box, and nothing on screen said so. A drag aimed at the transcript fell through to the terminal behind the chat, which pastes the paths into the hidden TUI. The chat pane shell is now the drop surface. While a drag carrying files is over it, the pane dims behind a card naming what the drop will do; the composer's existing attach logic — workspace, execution-host and SSH-owner checks included — runs unchanged from the wider element. Two routes reach the composer and they are widened differently: - an in-app drag is claimed in the renderer, so the pane calls the composer's handlers through a claim the composer publishes to the surface around it. - an OS drag is delivered by the preload drop route, which consumes the event at `document` before React sees it. The pane widens that route by publishing the composer's scope key as the nearest drop marker, and learns the drag ended from a document-level listener rather than a React drop. The surface lives in the chat portal because that is the one place wrapping both the bridge and structured panes, so neither pane root grows a second copy of this wiring. No composer mounted (a question card owns the input region) means no marker and no overlay, so that drag stays the terminal's. A guarded composer still answers for the drag — that refusal is what keeps it out of the terminal — but the pane does not invite a drop it will refuse. * Fix native chat drop ownership across session surfaces * Keep native drop completion listeners ready before hover renders |
||
|
|
06c83e24c0 |
fix(workspaces): recover a real terminal when activation throws for a planless agent create (#20190)
* fix(workspaces): recover a real terminal when activation throws for a planless agent create #20175's activation-failure recovery re-seeds the workspace surface, but passed callerProvidesSurface: true whenever an agent was selected. Activation is the caller that would have provided that surface, and it just threw. With the flag set and no startup plan, worktree-initial-terminal-seeding takes its zero-tab pre-seed branch: it queues setup/issue commands and returns null instead of creating a terminal. The recovery then recovers nothing and the freshly created workspace opens with no tabs. Dropping the flag from the recovery call is the whole fix. The create-time call keeps it: there, the agent pane really is the surface the caller provides. The enclosing object literal becomes a ternary because oxlint's unicorn/no-useless-spread rejects a lone conditional spread. * test(workspaces): reuse activation recovery coverage * test(workspaces): keep recovery assertion focused --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
ad49aae600 |
fix(workspaces): re-seed a gate-reported empty workspace after an agent selection (#20182)
* fix(workspaces): re-seed a gate-reported empty workspace after an agent selection #19940 routed two different questions through one predicate. An agent selection justifies skipping the *pre-emptive* shell at create time, but it was also suppressing the async activation gate's fail-closed re-seed. The gate returns `empty` only after adoption, structured inventory and resume all produced nothing, so it is positive evidence the agent surface never arrived (dead PTY, unreadable census, null startup plan) — exactly when the re-seed is needed. Suppressing it there left the workspace with zero tabs and no recovery: ensureWebRuntimeWorktreeTerminalAfterWake returns early for a plain local workspace, so nothing else seeds one. `gatedEmptyOutcomeReseedSuppressed` now answers the re-seed question on its own, honouring only an explicit `providesInitialSurface` caller promise — restoring v1.4.200's behaviour for that path. The create-time skip keeps using the agent-inclusive `activationProvidesInitialSurface` unchanged. * refactor(workspaces): make gated reseed policy explicit * fix(workspaces): harden gated empty reseeding * chore(workspaces): split duplicate-folder routing fix * refactor(workspaces): keep reseed helper outside caller census --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
243f443155 |
fix(session-search): read oversized numeric file IDs on Windows (#20551)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
55b3392018 |
fix(terminal): drop the agent gutter from copied selections (#19770) (#20545)
* fix(terminal): drop the agent gutter from copied selections (#19770) xterm selections are screen cells, not logical text. Agent CLIs paint their messages behind a fixed left gutter, so every copied line carried that gutter into the clipboard and pasted replies came out indented. Terminal clipboard writes now drop the run of spaces that *every* selected line shares, so relative indentation (nested bullets, fenced code, YAML) survives and only the gutter is lost. A selection that starts mid-line, or that includes any column-0 line, has a shared run of zero and is copied verbatim. Applied at every terminal clipboard seam: the Cmd/Ctrl+C shortcut, the pane context menu's Copy, right-click-to-copy, the app menu's Copy, copy-on-select, the X11 primary selection, the dashboard popout's preview terminal, and mobile's selection Copy button. New "Trim Gutter on Copy" terminal setting (default on) restores the old verbatim-cell behaviour. * fix(terminal): honour the gutter-trim setting on mobile copy Mobile stripped the gutter unconditionally, so turning "Trim Gutter on Copy" off left one surface still rewriting the clipboard. Mobile now mirrors the desktop preference through the existing settings.get RPC — a host predating the setting sends no key, which reads as on, matching the desktop default. Also folds the single-use gutter helpers into their callers so the shared module exposes one function. * refactor(terminal): parse each selection line once in the gutter rule Also locks the Windows subtlety with a test: a blank CRLF row is '\r', which reads as a zero-indent content row and would cancel the gutter unless the CR is split off first. * fix(terminal): publish the gutter-trim setting to paired clients settings.get is an explicit allowlist projection, not the whole settings object, so terminalCopyTrimsGutter never reached mobile: the client read the key as absent, which means "older host", which means on. Mobile therefore always trimmed and the desktop opt-out was inert. Adds the field to the projection and a test that fails if it is ever dropped again — absence is indistinguishable on the client from an old host, so a silent regression here has no other signal. * chore: drop unrelated formatter drift from this branch A repo-wide `pnpm format` swept a quote-style change in pnpm-workspace.yaml and a blank line in source-tree-walk.test.ts into this branch; neither is related to the gutter fix. * fix(terminal): trim the gutter on native copy events too xterm binds its own DOM `copy` listener that writes raw screen cells (CoreBrowserTerminal `_initGlobal`). Orca's own chords never reach it — they preventDefault in keydown — but Ctrl+Insert is a Chromium copy accelerator on Windows/Linux and is not in `terminal.copySelection`'s bindings, so it still copied the gutter. Orca binds Shift+Insert for paste on those platforms, which makes the asymmetry worse. A capture-phase listener on the xterm element now writes the trimmed text, closing the class rather than the one chord: any native copy event — assistive tech, execCommand — lands on the same path. Installed for both terminal panes and the dashboard popout's preview terminal. |
||
|
|
3763103084 |
fix(hooks): report a timed-out hook as unverifiable and terminate its process tree (#20559)
## In plain terms Orca lets a project define scripts that run at certain moments — one when a workspace is set up, one just before it is deleted. Those scripts get a time limit. When the limit ran out, Orca asked the script to stop and then believed whatever the script said on its way out — so a script written to shut down politely could be cut off halfway through its work and still report that it had finished. Anything relying on that answer was relying on a guess. Now the verdict comes from the clock, not from the script: if it ran out of time, that is what is reported, whatever exit code it managed on the way out. Orca also stops the script's *children* rather than just the script, so a background process it started can no longer outlive it. Split out of #20153 so the gate that consumes this answer is reviewed separately. `Refs #19334` rather than `Fixes`, because it does not close the issue on its own. ## The bug `exec({ timeout })` sends SIGTERM and then reports what the child did. A hook that traps SIGTERM and exits 0 therefore comes back with a **null error** — success — despite having been cut off. ```js exec("trap 'exit 0' TERM; sleep 5", { timeout: 200 }, (err) => …) // err === null ``` That is not an `exited` vs `unverifiable` nicety: it is a failed hook reported as a passing one. Realistic triggers are ordinary — a Node wrapper with a graceful `process.on('SIGTERM')`, an rsync wrapper that cleans up on signal. ## What changed **`runHook` owns the deadline.** The verdict comes from running out of time rather than from the corpse's exit code, and it is settled *at* the deadline rather than whenever the child gets around to dying — a hook that traps the signal and keeps running must not hold its caller open. **A timeout withholds the exit code.** So does a spawn failure, where `exec` reports a *string* code (`ENOENT`); the `typeof code === 'number'` guard is what keeps a hook that never ran out of the "exited" verdict. Callers that distinguish "exited N" from "outcome never observed" can now trust that distinction: | failure mode | `error.code` | signal | verdict | | --- | --- | --- | --- | | non-zero exit | `23` | — | `exited 23` | | command not found | `127` | — | `exited 127` | | killed | `null` | SIGKILL | outcome not observed | | deadline expired | *(the deadline, not the exit)* | SIGTERM→SIGKILL | outcome not observed | | deadline expired, hook traps SIGTERM and exits 0 | `0` | — | outcome not observed | | spawn failure | `"ENOENT"` *(string)* | — | outcome not observed | **Termination reaches the process group.** The script is a shell and the work is its children, so signalling only the shell leaves a `sleep` or an `rsync` alive holding the pipes open. SIGTERM first, then SIGKILL after a grace. **One `classifyHookProcessResult`** now serves the native and WSL branches, which had been mapping a finished process to a hook verdict by hand, identically. That duplication predates this change. ## Terminating the tree, and a test that could not fail The escalation went wrong once in review, in a way worth recording. A first attempt skipped the SIGKILL when the *direct child* had already exited — a dead child needs no signal. That is correct about the child and wrong about the group: a hook that backgrounds a server typically loses its shell leader to the first SIGTERM while the server keeps running, so the skip fired in exactly the case the escalation exists for. The escalation now probes the **group** with signal 0: `ESRCH` means nothing is left to kill, anything else gets the signal. **The residual trade-off, stated rather than implied.** Signalling by negative pid names whatever group owns that pid *now*. Once the leader is reaped its pid can be recycled, and a probe cannot distinguish a surviving descendant from a stranger that inherited the number. Killing a runaway hook is both the likelier event and the one the deadline promises, so the group is signalled whenever it answers; the remaining window is pid wraparound inside the grace. **A test that cannot fail is worse than no test.** The first regression test drove `runHook` with `process.kill` intercepted — and passed against *both* the broken and the fixed version, because with signals intercepted nothing dies, so the child never reached the exited state the bad guard keyed on. It was false assurance, not coverage. `terminateHookTree` is therefore exported and the regression pinned directly against it: it fails on the old version (`expected [] to deeply equal [[-4242, 'SIGKILL']]`) and passes on this one. ## Behaviour change for `setup` hooks Both hook kinds share `runHook`, so this is not confined to archive hooks. **A setup hook that backgrounds a long-running server now has that server SIGTERM'd — then SIGKILL'd — with the rest of its process group when the deadline expires, where previously it was orphaned and survived.** Arguably the better behaviour, since an orphaned server is a leak, but it is a real change and should be a decision rather than a discovery. ## Evidence Against real shells and real signals, because this bug is invisible to a mock (`hook-archive-timeout-observation.test.ts`, through `runHook` itself rather than an extracted helper): ``` ✓ fails a hook that traps SIGTERM and exits zero, despite its zero exit ✓ settles at the deadline even when the hook refuses to die ✓ passes a hook that finishes inside its deadline ✓ reports an observed non-zero exit as the exit it is ``` Plus `hooks-archive-exit-observation.test.ts` for the wiring — including the string-`ENOENT` case — and `hook-archive-termination-safety.test.ts` for the escalation branching. ## Checks `pnpm tc` · `oxlint src` · `oxfmt --check` · 113 tests across `src/main/hooks*`. The classification table above is measured against real `exec`, not reasoned. |