mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
2411491c2fbe2b7b4cc460f44d078769d5800c19
5861
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69c76b5ec7 | perf: bound rich Markdown comment detection and preservation scans (#20287) | ||
|
|
7ae8a70c8d |
perf: avoid flattening tabs for smart-sort liveness check (#20279)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
1fe92719af |
perf(skills): index source ownership counts per discovery (#20259)
Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
efe5e296f6 |
perf: stop preparing over-limit palette queries (#20250)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
5de75cd6e9 |
perf: defer browser search row formatting until matched (#20248)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
80a22db9cb |
perf: stop heading lookup at the requested occurrence (#20246)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
f71f868308 |
perf: index pending terminal reconnect tab lookups (#20229)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
e9dabe9beb |
perf(diff): skip unused extension extraction during search (#20225)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
96f0c5101f |
perf(session): reuse persisted tab membership set (#20223)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
56fcb544e0 |
fix(browser): move cookie scoping off psl's stale suffix list (#20421)
* fix(browser): move cookie scoping off psl's stale suffix list psl@1.15.0 is its latest release and ships a Dec-2024 snapshot of the public suffix list. Measured against the current upstream list, it fails to recognise 600 of 10,030 suffixes; tldts misses 2. That gap is a cookie-isolation bug. psl does not know `api.br` is a suffix, so it falls back to the `br` rule and maps foo.api.br, bar.api.br and example.api.br all onto the single family `api.br`. Unrelated registrants then share a removal scope, and a replace-mode import for one clears the others' cookies. The same holds for seg.ar, co.az, gov.cz and ~597 more. tldts is called with allowPrivateDomains, without which the PSL's PRIVATE section is ignored and every *.github.io / *.s3.amazonaws.com / *.vercel.app tenant collapses into one family — 21 of 49 probed hosts changed family under the default. The new test pins that boundary. One deliberate behaviour change: hosts under `.local` (not in the PSL) were their own family under psl, which returned an all-null parse for them; they now resolve to the two-label boundary (app.orca.local -> orca.local), matching what Chromium treats as the registrable domain. * fix(build): bundle tldts into the main process like psl was psl sat in BUNDLED_MAIN_DEPENDENCIES, so it was inlined into the main bundle rather than externalized and copied into resources/node_modules. Swapping the dependency without moving that entry left a bare tldts import that afterPack's runtime-closure check rejects. * fix(build): point the output contract at tldts and drop the psl shim The contract test still asserted psl was in BUNDLED_MAIN_DEPENDENCIES, so it failed once the entry became tldts. src/types/psl.ts declared a module that no longer resolves; tldts ships its own types. * test(browser): pin the suffix boundaries the tldts swap moved Three semantic changes shipped untested: - `.local` is unlisted, and the libraries disagreed on what that means. psl returned an all-null parse so every `*.orca.local` host was its own family; tldts stops at `orca.local`. The consequence is wider than the family name — importDomainAncestors now yields the shared parent, so a replace-mode import of one host clears non-host-only cookies every sibling shares. - psl's snapshot had `compute.amazonaws.com` as a literal PRIVATE suffix; the current list only carries the wildcard, so the bare host is ICANN now. - The renderer's `psl.isValid` gate had no direct test at all — nothing imported the module from a test. Also drops comments that explained a boundary in terms of psl's internals. One was wrong under tldts: bracketed IPv6 does not reach an error branch, it parses with the brackets stripped and falls through the unlisted path. |
||
|
|
53eb639983 |
refactor(preload): drop the unused raw electron IPC bridge (#20419)
* refactor(preload): drop the unused raw electron IPC bridge `@electron-toolkit/preload` was used only to expose `window.electron`, which hands the renderer unrestricted `ipcRenderer` send/invoke/on for any channel — bypassing the typed per-domain bridges in `src/preload/api/`. Nothing consumed it. The only references were the assignment itself, the web client's empty fallback, and a test asserting that fallback has no keys — i.e. the web build already ran with it empty. * chore(build): drop the dangling @electron-toolkit/preload vite exclude The package is gone from package.json and source; leaving it in the preload externalizeDeps exclude list points at a package that no longer resolves. |
||
|
|
cf7ce05827 |
Make sidebar nesting easier while preserving animated reordering (#20412)
* Improve sidebar nesting while preserving animated reordering * Remove obsolete pointer ref from sidebar row context * Keep sidebar reorder previews tracking during autoscroll |
||
|
|
775137d70e |
refactor(renderer): improve IPC error handling with clamped and unclamped variants (#20340)
* refactor(renderer): give the IPC error reader a clamped and an unclamped shape * Simplify IPC error comments and clean up unused i18n Reduce multi-line comments to essential single-line context. Remove unused locale entries (entryDeleteFailed, entryFailedInWorkspace). |
||
|
|
403b62a8d8 |
ci: balance existing unit and E2E shards using recorded timings (#20367)
* ci: balance existing unit and E2E shards using recorded timings * ci: fix timing refresh units and deferred-menu test traversal * ci: preserve isolated E2E window launch policy * ci: keep diagnostic artifact outages from failing tests |
||
|
|
b357913654 | fix: keep landing footer menu above status bar (#20360) | ||
|
|
923858e098 | perf: defer closed menus to speed up cold worktree switching (#20311) | ||
|
|
ef6eeab26e |
fix(native-chat): shrink skill pill text (#20254)
Co-authored-by: Merge Sim <sim@local> |
||
|
|
556a7772ed |
fix(e2e): remove four real flake sources and one caret race (#20169)
Four E2E specs failed once each across six main runs. Each traces to a timing boundary the test could not control, not to product instability: - linear-url-workspace-entry: pasted before X selection ownership landed, delivering stale text. Gate on a clipboard read-back. - native-chat-first-flush-race: a bare 1_500ms sleep is exactly UNFLUSHED_SETTLE_MS, so it straddled the boundary deciding which of two hydration paths carried the test. Observe the not-yet-flushed read instead; a notFound is never cached, so this cannot perturb hydration. - orchestration-idle-mail-delivery: asserted that a PTY -> daemon -> main round trip beats a 500ms production heuristic. Use the existing ORCA_E2E_ORCHESTRATION_POINTER_ENTER_DELAY_MS knob. - tasks-page: the probe timeout was the one figure in the file not derived from GITHUB_TASK_SEARCH_IDLE_MS. worktree.spec.ts exposed a real product race rather than a test bug: the emoji caret-restore frame stayed armed through ordinary typing, so a late frame could yank the caret back mid-input. Cancel it on the non-emoji onChange path. Also repairs a stale assertion: #20025 changed remountTerminalTabForRecovery to return a result object and updated the sibling call site but missed this one, so the comparison to `true` could never pass. It is a deterministic break, not a flake. Co-authored-by: Merge Sim <sim@local> |
||
|
|
70a588c8bf |
fix(workspaces): complete a worktree create when a post-create step throws (#20175)
* fix(workspaces): complete a worktree create when a post-create step throws executeWorktreeCreation's try/catch ends once createWorktree resolves, and all three callers fire it with a bare void and no .catch. completeWorktreeCreation is the only thing that removes the pending creation, so a throw in that tail left pendingWorktreeCreations and activePendingCreationId set: the creation surface stayed up, workspaceChromeActive went false, and the finished workspace rendered no tab chrome while its panes mounted invisibly behind the panel. It only cleared when the user switched workspaces, because setActiveWorktree nulls the pointer. Silently -- no toast, no error state. activateAndRevealWorktree, ensureWorktreeHasInitialTerminal and ensureWebRuntimeWorktreeTerminalAfterWake are all synchronous with no internal guard; launchStructuredWorktreeSession guards only its awaited launch, and that catch's comment already names this stranding hazard. The worktree exists past that point, so each follow-up step is now guarded individually and falls back to the values the skip paths already used; control flow always reaches completion. The structured-launch cancelled/visibility returns keep their semantics, and a throw there is treated as a failed launch, matching what that module already returns for 'failed'. A .catch backstop on the three call sites turns anything that still escapes -- including prepareRequestForCreate, whose VM await has try/finally with no catch -- into a visible error state plus toast. Ablated: with the guards removed the new suite fails 4 of 5, the survivor being the no-throw control. * fix(workspaces): recover terminal after partial activation * fix(workspaces): preserve stamped launch tab on recovery * test(workspaces): cover recovered agent tab delivery * test(workspaces): name recovered agent delivery coverage --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
353a1c8038 |
fix(terminal): re-run deferred tab admission when a plan installs mid-activation (#20176)
planColdActivationTabDeferral can install an empty allowed set, deferring every tab so the pane filter renders none. The drain that undoes that, useActivationDeferredTabAdmission, depends only on backgroundMountRevision and renderedActiveWorktreeId while reading the deferred set from a mutable ref, and the install bumps neither: the only revision producers are the drain itself and the background-mount event path, which the activation plan never reaches. So this pass strands the workspace with zero panes: the worktree is already rendered-active while the startup gate is closed, which resets lastActivationWorktreeIdRef, then the gate opens on the same worktree and installs a plan. Nothing re-runs the drain until the user switches workspaces and back. The hook's own comment anticipates this launch shape and relies on re-reading on growth, but that re-read only happens on a dep change. applyTerminalColdActivation now returns activationDeferralPlanRevision, backed by a ref in the parking foundation and incremented only when the plan actually installs, which the admission effect takes as a dep. A ref rather than state because the pass runs during render, where a setState would be a render-phase update. The 4-tab admission cap is deliberately untouched: it reproduces the warm set an eager activation used to mount, so steady-state pane and WebGL-context population is unchanged. Ablated: with the change reverted the new suite's drain case fails on the deferred set surviving the timers; the precondition and the away-and-back control pass either way. Co-authored-by: Merge Sim <sim@local> |
||
|
|
ec9c3e0550 |
Fix remote hosted review browser routing (#20030)
* Fix remote hosted review browser routing * Fix remote review modifier hint * Address review feedback and fix routing test types * Avoid assuming active runtime owns workspace links * Align runtime routing regression expectation * Respect explicit local link ownership |
||
|
|
a0799d8f1c |
fix(terminal): move the recovery ledger onto the tab row and gate it on observed outcome (#20025)
* fix(terminal): move the recovery ledger onto the tab row and gate it on outcome The recovery budget lived in module-level Maps keyed by tabId. Anything keyed outside the row needs a release path, and that release fired on every remount-driven pane disposal, so each remount erased the budget it had just consumed (crash b5cfc6ca). Put the ledger on TerminalTab and write it in the same set() as the generation bump: reading the budget is now reading the tab, so releasing it independently has no expression. Counting was also the wrong control. Every remount mounts a pane that captures a FRESH recovery epoch, so the epoch check can never refuse its request — recovery re-requested the exact action that had just failed with no evidence anything changed. Gate on an observed outcome instead, reusing the direct-SSH pane retry vocabulary (success | failed | timed-out | superseded) and its settle call sites: an unsettled attempt blocks the next one, and a settled failure refuses the same reason until a new trigger arrives (generation move, or the user's Retry). The 3-per-5min cap stays as a breadcrumb-emitting backstop, not the control. viewMode now also lands on the row from the local toggles, mirroring how pin already does it, so the chat-ownership guard reads one index instead of OR-ing two. * fix(terminal): persist the row's viewMode and keep both chat-ownership reads The narrowed chat-ownership guard read a field the session schema strips: terminalTabSchema never declared viewMode, so the terminal row lost it on every load while the unified tab kept it. After a restart the row read undefined and recovery would remount a chat-owned tab's hidden surface — the race #19745's guard exists to prevent. Declare viewMode on terminalTabSchema so the row is durable, and keep the disjunction rather than replacing it. The schema cannot retroactively add the field to sessions already on disk, so the first load after upgrade still has it only on the unified tab; and for a safety check over two partly-redundant sources, a hole in either index should err toward declining a heal. Also cover three structural guards that no test was holding: both remote ledger-carry paths (terminal-build, remote-workspace-session-merge) and the only success settle in the state machine, including its placement past the failure branches. * fix(terminal): settle a fresh spawn's outcome and prove the ownership guard across a reload spawn-left-pane-unbound was the one recovery reason with no success settle: its remount heals by spawning, not reattaching, so it reached none of the reattach settle points and left the attempt 'pending' for the full 31s bound. A fresh spawn that binds a PTY now reports it, the dual of the unbound settle that already reported failure. Two tests outside src/ still called remountTerminalTabForRecovery by its old boolean contract and broke CI; both are updated to the admission result. Also strips the client-local recovery ledger at the remote-workspace projection boundary, in the type as well as the destructure, so a future producer cannot put another machine's Date.now() on the wire. * fix(terminal): resolve the pane's tab row once for both epochs after the main merge #20034 replaced connect-pane-pty's inline tab resolution with findTerminalTabForPane, and this branch had rewritten the line below it to read the recovery epoch off the row that block used to bind. The merge was textually clean and semantically broken: `terminalTab` no longer existed, so typecheck failed and every test that connects a pane threw ReferenceError. Resolve the row once through the new helper and feed both epochs from it, which keeps #20034's refactor and this branch's reason for reading the row here — a second lookup would put another tabsByWorktree scan on the connect path. captureTabRecoveryGeneration is narrowed to the one field it reads so the helper's record type can carry it. |
||
|
|
20c56249d5 |
fix(terminal): keep a deliberately slept workspace cold until it is woken (#20075)
* fix(terminal): keep a deliberately slept workspace cold until it is woken Sleeping a workspace kills its PTYs but keeps its panes mounted and keeps each tab's session id as a wake hint. Any later remount of those panes (recovery, parking, portals) reattached that dead id, and the daemon's create-or-attach spawned a fresh shell, so slept workspaces revived on their own (#10205). The existing sleep-intent marker now outlives teardown and gates the deferred connect itself, so both the reattach and fresh-spawn arms stay cold. It is released by activating the workspace, by any PTY binding to one of its tabs (CLI, automation, client wake), and by purge. A queued startup still connects. Reproduces the community root cause from gatsby74 in #13343; the regression e2e remounts a slept hidden pane and fails on main. Co-authored-by: gatsby74 <gatsby74@users.noreply.github.com> Co-authored-by: mmarabel <mmarabel@users.noreply.github.com> * fix(terminal): let a slept pane wait for its wake instead of latching cold A pane whose connect ran while its workspace was slept used to mark itself connected and stop; nothing re-armed it, so a wake that produced a live PTY before the user clicked (CLI create, background agent resume, split panes) left panes stranded. The connect now waits on the sleep marker and resumes when the marker clears, and a torn-down pane drops its listener. Tabs created with a live PTY clear the marker too, the sleep flow marks each workspace only when its own teardown starts, and purge forgets the marker without waking anything. * fix(terminal): wake a waiting pane once, in its remounted generation Activation clears the sleep marker after the set() that bumps dead tabs' generations, and the waiting pane only resumes its connect when its tab generation is still current. Otherwise the stale pane and its remounted successor both reattached the same session id on a deliberate wake. * fix(terminal): resolve the waiting pane's tab by either id and re-arm after wake The wake listener looked the tab up by the pane's render id, which can be a unified id whose terminal tab lives under entityId, so the generation check declined forever for those panes. Mount, fresh spawn, and the wake listener now share one live resolver. The wait flag resets when the listener fires so a second sleep can hold the pane again, listener dispatch is guarded, folder activation clears after its own set(), and the sleep flow re-asserts the marker after each teardown while releasing a workspace the user activated meanwhile. * fix(terminal): ignore PTY binds that land inside the sleep teardown window A spawn resolving while shutdown was still awaiting the host bound a PTY and cleared the marker, waking every waiting pane mid-sleep; re-marking afterwards could not un-connect them. The sleep flow now scopes each teardown so binds in that window are not wakes. The e2e asserts a deliberate wake yields exactly one PTY, and the dispose test proves the listener is gone. --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: mmarabel <mmarabel@users.noreply.github.com> |
||
|
|
2167cd2994 |
test(monaco): drive the real Monarch tokenizer instead of walking rule tables (#19981)
* test(monaco): drive the real Monarch tokenizer instead of walking rule tables
* fix(monaco): stop a truncated JSONL record poisoning every record after it
`@string` was pushed unconditionally, and Monarch state survives the line
break, so one truncated record left every later record inside the string state
— the whole rest of the file rendered as a single string. A truncated record is
a normal way for a .jsonl log to end.
Gate the push on a lookahead proving the closing quote is on this line, and
consume an unterminated remainder as `string.invalid` without pushing anything.
Measured against the real tokenizer over realistic JSONL. Two alternatives were
rejected: collapsing the string into one regex fixes the poisoning but loses
every `string.escape` token on well-formed records, and `includeLF` does not
fix the primary case at all (the `[^"\\]+` content rule swallows the newline
before an EOL rule can match). This candidate's token stream is byte-identical
to the previous grammar on every well-formed record — the existing inline
snapshot did not move — and is at or faster than it on 20k pathological lines.
* test(monaco): pin monaco's own mdx grammar as the embed-recursion regression
Upstream's shipped mdx grammar enters a `js` embed on every `{` and pops
on `}` with no budget, so it reproduces the unbounded embed-entry
recursion exactly — evidence the shape is monaco's, not something Orca's
grammars invented, and a tripwire for a monaco upgrade that changes it.
The same file proves an Orca grammar stays inside the budget under the
identical line. Measured here: 500 interpolations -> 500 frames, no error;
3000 chars -> RangeError at 945 frames. The ceiling is runtime-dependent,
so the test asserts the failure, not the number.
Adds a regression for astro's `^`-anchored frontmatter pop rule
(monaco-editor#1127) so an indented or trailing `---` cannot close the
fence early, de-duplicates the line-cap constant onto the budget module's
`MAX_TOKENIZATION_LINE_LENGTH`, and renames the recursion suite: embeds
cannot nest, so "embedded recursion depth" described the wrong thing.
|
||
|
|
5fa62feda7 |
perf(terminal): mount only the visible pane on a worktree switch (#20034)
* perf(terminal): mount only the visible pane on a worktree switch Activating a worktree mounted a TerminalPane for every tab it holds, not just the one on screen. Cold-activation deferral existed for this but engaged only past four deferrable hidden tabs, which exempted the 2-5 tab worktrees that make up almost every real switch. Deferral now engages for any deferrable hidden tab, and the siblings it skips are admitted one per idle frame after the reveal, capped at the population the old threshold would have mounted eagerly. Steady-state pane, WebGL-context and heap population are therefore unchanged; only the frame the mounts land on moved. * fix(terminal): judge admission eligibility on the largest deferred set seen Review found the launch worktree never warms up: it is restored active before hydration opens the startup gate, so admission read an empty deferred set, cached ineligible, and never recomputed once the real plan landed. Judge on the high-water mark instead - an over-cap worktree still stays ineligible as its set drains, but a later plan is seen. Also from review: the e2e WebGL counter read getPanes(), which returns a public projection with no webglAddon field, so it was always 0; read getRenderingDiagnostics() instead. Filler worktrees now clean up on failure (testRepoPath is worker-scoped), and the restore metric is named for what it measures rather than implying a pixel assertion. * test(e2e): wait for the reveal to restore, and scope the latency budget off CI CI failed with 'revealed terminal never restored its content': the harness sampled a fixed 4s window, which a shared runner can outlast, so a slow restore was recorded as no restore. Poll for the restore instead. Also stop asserting a latency budget on CI. Shared runners cannot hold a threshold; the structural invariants (one pane mounted by the switch, warm set restored) are exact and stay asserted everywhere. |
||
|
|
fab78c7669 |
fix(native-chat): show one live-turn indicator, and make Thinking mean reasoning (#19977)
* native-chat: render one indicator row for the live desktop turn The turn-timing row and the spinner+activity line were two rows saying "Working" at once. A settled turn keeps its own row; the live turn now has only the spinner row, labelled provider activity -> Thinking -> Working for N through the shared resolver. Reasoning is the turn's content, so it no longer becomes the activity label, and "Thinking" now means the turn is reasoning right now rather than that it has produced no output yet. * mobile: give the live turn row a spinner and the shared indicator label Mobile's per-turn row is already the only live indicator on the structured lane, but it pulsed a bare word and never showed what the provider said it was doing. It now renders a spinner beside the same resolved label desktop uses, and reads reasoning from the journal instead of inferring it from missing output. The bridge lane's four prompt/interrupt write seams move to one module so the controller stays under its line cap. * codex: mark streamed reasoning as reasoning too, and pin the provider markers The settled reasoning item carried the marker but the streaming one did not, so a live Codex turn - the only time the indicator is on screen - never read as reasoning. Both paths now stamp it; a plan document keeps its own presentation and must never read as reasoning. * fix(native-chat): tighten live turn reasoning state --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
3b82d8de64 |
fix(runtime): let connections own host status recovery (#20003)
* fix(runtime): let connections own host status recovery Verify runtime status after authenticated connection recovery and publish ordered snapshots to desktop and browser viewers. Consolidate failed-status retries in the connection owner and remove renderer retry/diagnostics merging. Adapt sidebar host-state derivation and regression coverage from Omar Shahine's original fix in https://github.com/stablyai/orca/pull/19163. Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com> * fix(runtime): show blocked hosts honestly and remove obsolete status options * fix(runtime): preserve timeout guidance and update IPC test fixtures * fix(runtime): preserve status evidence and address review gaps * test(sidebar): assert workspace host icons dimming and recovery tooltips * fix(palette): require available hosts before adding implicit badges * fix: retain disconnected host snapshots for new renderers --------- Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com> |
||
|
|
b6e4457552 |
fix(worktrees): close an idle structured chat on delete instead of refusing (#19762)
* fix(worktrees): close an idle structured chat on delete instead of refusing `worktree rm` refused whenever any structured chat session was attached to the workspace, so an idle Codex/Claude chat that had already answered was harder to delete than a terminal actively running the same agent. The PTY sweep stops every terminal it owns and refuses only for the ones whose exit it could not verify. The structured sweep refused on `live` alone and never attempted the close, which ran only under force. `live` is lease state — a provider child is attached — not work in flight, so it was never the right proxy for "you would lose something". Close first, refuse only on what did not settle. The refusal now means the same thing the unverified-PTY one does, so the toast takes that wording. * fix(worktrees): fence, bound and word the structured-session sweep Review follow-ups on the close-first structured sweep. The close-first direction is unchanged; four things it got wrong are not. Host fence. `listLiveStructuredSessionsForWorktree` matched on `location.workspaceId` alone, and a `repoId::path` id names a DIFFERENT workspace on every host (STA-4343). Once the sweep started closing rather than refusing, deleting a local workspace could close a live chat on an SSH or paired-runtime copy of the same id. It now takes the same two host fields the PTY sweeps already fence on, compared against the session's own `location.executionHostId`; neither field set means this machine. Shared budget. The close ran to completion before the first PTY sweep was constructed, and it is serial with a provider round trip per session — so a slow one spent the whole budget and the sweeps then rejected with a timeout for a stop they never attempted. It is now issued first but joined before the verdict, so the agent plane is still asked ahead of the terminal plane while the two share the clock. Timeout wording. The close raced the deadline fail-closed, and that sentinel carries the PTY timeout prefix, which the classifier reads first — so a wedged session close refused in terminal wording and refused identically again under the Force Delete meant to clear it (#11960). A close that ran out of time is now a session the removal could not confirm closed, which is what the refusal already words. Tracked, so a forced removal still waits out the abandoned-sweep grace before deleting files. Verdict fidelity. `closeStructuredAgentSessionChild` re-observes after the close, and that verdict was being discarded — so a session Orca watched stay attached and one it merely could not reach produced the same message, while the toast asserted "could not confirm" for both. `removal.ts` documents flattening those two as the thing not to do. The unclosed sessions now carry their post-close status, the detail uses the shared `still live:` marker, and the toast branches on it like the PTY pair above it. Also: the close takes the enumerated list instead of re-deriving it, so it no longer runs every liveness observation twice or names a session it never touched; and both teardown log lines count structured closes, since closing a chat is now an ordinary outcome of this verb. * fix(worktrees): keep a proven-exited session from refusing removal The structured sweep re-observes after a close that reported `stopped: false`, but folded a proven `exited` into `unverifiable` — so a close that threw past its own observation, or one whose death evidence landed a beat later, refused a delete over a child that is demonstrably gone. That is the defect this sweep exists to remove, and the PTY gate it mirrors never refuses on a proven exit. Take the proof, and run the tab retirement the close skipped when it gave up: a chat tab left behind re-attaches a released session pointing at a workspace that is about to be deleted. * fix(worktrees): name every unclosed structured session, not just the live ones The refusal named only the proven-live subset when any session was live, so a sweep that left one attached and two unconfirmed told the user "1 agent session (claude)" while three were about to be discarded — and dropped the providers of the ones it hid. The PTY sibling may drop everything outside its live list because a fresh inventory PROVED those exited; nothing proves that here, so both groups are counted. The `still live:` marker still leads, so the delete toast keeps showing the stronger warning. Also carries the structured close count through the forced-removal early return: that path skips the per-PTY verdict, not the sweep that already ended a user's chats, so the removal log claimed `structured=0` for chats it had just closed. * fix(worktrees): stop the forced-removal warn asserting a verdict it does not have The structured sweep splits its post-close verdict in two on purpose: "we watched it stay attached" and "we could not confirm it closed" are different things to waive, and `removal.ts` keeps a marker and a matcher together so the delete toast can tell them apart. The force-path warn then appended "still attached" to whichever verdict it got, so a removal forced over a close that merely ran out of time logged that Orca had seen the session running. That line is the only record a forced removal leaves of a child left pointing at a deleted `cwd`, so it is the one place the two must not be flattened. Carry the verdict verbatim, the way the unstopped-PTY warn above already does. * fix(worktrees): report the closes that landed when the sweep budget expires The structured close loop is serial, so the shared sweep budget can expire part-way through it. The timeout fallback was assembled by the caller and could only name the whole list: sessions this removal had already closed were reported as unclosed, named in the refusal the user reads, and logged as `structured=0`. The loop now records progress into a structure the timeout path reads, so both the refusal and the count say only what was observed. A session with no recorded outcome reports `unverifiable` — the same verdict as an attempted close that stayed unproven, because "never asked" and "asked, unconfirmed" are both exactly "not observed exited", and neither may claim `live`. The loop also checks the deadline before each close, so one slow provider round trip no longer starves every session behind it. It stops ISSUING closes; an in-flight one is left to finish, since nothing here can cancel a round trip. The structured host fence now reuses the PTY fence's own type instead of a look-alike that read `undefined` as local while the other read it as match-all, with both claiming the same precedence. `null` means this machine on both sides; ABSENT stays narrowed to local here, documented and pinned, because a single-host-id comparison cannot express match-all. Also pins a tradeoff that was accepted rather than wanted: the PTY sweeps run concurrently with the structured close, so a removal that refuses over a stuck session has already killed that workspace's terminals. * fix(worktrees): put the chat tab back when a structured close does not land `closeStructuredAgentSessionChild` hides the session's chat tab before it issues the close, so every failure past that point left a refused delete having still taken the tab out of the durable restore index. The conversation survived under `userData`, but nothing brought the tab back at the next launch. Both failure shapes now roll the hide back: `host.close` throwing, and the post-close observation coming back not-`exited`. The restore is gated on the visibility read taken BEFORE the hide, so it never publishes a tab for a session that was already hidden, and on a fresh observation, so it never resurrects one for a child a throwing close still took with it — which is what the worktree sweep reads when it counts such a session closed. It cannot throw out of the function, so the caller's original reason is still what the user is asked to act on. * fix(worktrees): keep the chat-tab rollback out of removals that delete the workspace The rollback added for a refused close ran on every unproven close, including the two shapes of removal that cannot refuse. Force Delete warns and deletes the checkout; a folder-workspace removal never refuses at all. Putting the tab back on those paths leaves a durable restore-index entry for a workspace that is then gone, and the chat republishes at the next launch pointing at it — the outcome this sweep exists to remove. The close now takes `restoreTabOnUnprovenClose`, on by default so `worker-stop` and `worker-release` keep the rollback, and the teardown sweep passes it only when the removal can still refuse. Second hole, same chain: `host.close` can return before the child's exit is recorded, so the close's own observation reads unverifiable and restores the tab, while the sweep's re-read one store write later proves the exit and counts the session closed. The two observations straddle that write and disagree. The sweep now re-drops the tab reference when it takes that proof, and the comment claiming the re-observation alone covers this is corrected. * fix(native-chat): stop a closing chat reading as a conversation that would not load Deleting a workspace now closes the structured chats inside it, and the chat pane outlives that close by a few frames. Every read it makes in that window — `agentSession.history` on refresh, `agentSession.subscribe` on reconnect — resolves through the host's `requireSession`, which refuses with `agent_session_ownership_unknown` for a session it no longer holds. The pane turned that into its terminal error surface, so an ordinary delete flashed `Could not load conversation` over the transcript before the tab retired. That code, raised by a READ, never means the transcript could not be read. It means this host has no session object by that id: one it has just closed, or one it has not attached yet, since the surface's hold is what attaches a session at all. Both windows end on their own. The genuinely latched lease — Orca cannot prove the previous owner exited — reaches the client through the acquisition path instead, so narrowing on the code costs a read no real diagnosis. So the read transport classifies before it reports: an unattached refusal stays on the reconnect loop it is already the subject of, and the pane keeps the transcript it has. It is a window, not a mute. A read still refusing that way past the grace is no longer transitional, and the pane is owed the failure rather than a spinner that never resolves. Every other failure still surfaces immediately, unchanged. The refusal code now has one definition, shared by the host that raises it and the client that narrows on it, so the two cannot drift into a red error nobody meant. Deliberately NOT changed: the order of teardown. The tab is retired after the close proves, not before it, because a close that does not settle has to put the user's chat tab back — the rollback this PR already establishes. Retiring the pane first would unmount it ahead of a close that may be refused, so the pane instead treats a session that has gone as a neutral terminal state. Mobile's structured chat reaches the same reducer but has no reconnect loop, and its hold refusal is what carries the diagnosis there, so the grace does not transfer; it keeps reporting as before. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
26db907895 |
fix(monaco): bound embedded-language recursion in svelte/astro/vue grammars (#19748)
* fix(monaco): bound embedded-language recursion in svelte/astro/vue grammars
Monarch's _nestedTokenize and _myTokenize tail-call each other on every
mid-line embed entry, and V8 has no TCO, so JS stack depth grew one level
per <script>/<style>/<!-- -->/{expr} on a line - bounded only by line
length. One 17,000-char line of '<script></script>' overflows svelte at
depth 997; 19,603 chars of '<!---->' overflows astro at 1748. Both are
under Monaco's own 20,000 maxTokenizationLineLength, so it was no defence.
The same recursion rescans the line remainder per level (quadratic),
matching the 38s synchronous stall before report 25d10fa1's
STATUS_STACK_OVERFLOW on a flat, healthy heap.
Add a shared embed-entry budget: enter an embed only while <=512 chars
remain. Each entry consumes a character, so depth is bounded by
construction; over-budget remainders continue on parallel non-embedded
states that keep tag-level colouring.
Also fix the zero-width nextEmbedded rules that dropped their embed
(token must be '@rematch'), the source of the "cannot pop embedded
language if not inside one" breadcrumbs, and rework vue's expression exit.
Fixing that drop without the budget would have armed the overflow in vue.
* fix(monaco): re-embed script and style bodies after an over-budget opening tag
`scriptBodyPlain` / `styleBodyPlain` were the only over-budget mirror states
without a re-entry rule, and they dropped `$S2` as well. A `<script>` or
`<style>` opening tag carrying more than 512 trailing characters therefore left
the whole block unhighlighted until its closing tag, however short the following
lines were. Carry the language through and re-enter the embed as soon as the
rest of the line fits, matching the markup and expression mirror states.
Also extends the recursion ramp so the densest embed shape (`{a}` / `{{a}}`) is
driven at Monaco's line cap (19_800 / 19_528 chars) instead of stopping at
7_500, and renames the inverted private `restOfLineTooLong` constant.
The budget stays at 512: an A/B of the real tokenizer at 512 vs 256 over
realistic SFCs differs on 8 lines, all of them 256 losing the html or
typescript embed on ordinary shapes such as a ~430-character Tailwind class
attribute.
* fix(monaco): pin the tokenization line cap and correct the budget's claims
Sets `maxTokenizationLineLength` explicitly instead of inheriting it. It is
an `IGlobalEditorOptions` value, so the one file-editor site pins it for
diff and Peek surfaces too. Defense-in-depth only — the comment says
plainly that it does NOT guard the embed recursion, which overflowed at
~17_000 chars, under this cap.
Corrects two overstatements in the budget module's own comments: Monarch
refuses to nest embeds, so the counts are sequential enter/exit
transitions (stack frames), not nesting depth; and the `RangeError` is
caught per line by Monaco's `safeTokenize`, so what is demonstrated is a
line that silently loses highlighting, not a dead renderer.
Notes monaco-editor#1127 at astro's `^`-anchored frontmatter pop rule: the
two-caret symptom is fixed in 0.55.1, but `^` in a pop rule is still
measured from where the embed was entered, not from line start.
---------
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <neil@stably.ai>
|
||
|
|
7d367b2aa4 |
fix(terminal): stop the recovery budget erasing itself during a remount (#19745)
* fix(terminal): stop the recovery budget erasing itself during a remount A recovery remount disposes the pane's xterm, and the disposal handler released the tab's recovery budget whenever getTab said the tab was gone. getTab reads unifiedTabsByWorktree while remountTerminalTabForRecovery reads and mutates tabsByWorktree; on the direct-SSH path the two indices diverge, so every successful remount deleted the timestamp it had just written. The cap never engaged and the pane remounted at render speed. Report b5cfc6ca (1.4.198, Windows): 8878 remounts across 8 tabs in 122s, all reason=reattach-unverifiable, against a cap of 3 per tab per 5 min, ending in a Skia bitmap allocation abort. Gate the release on the same index that governs remounting, via a shared locateTerminalTabForRecovery so the two cannot drift apart again. * refactor(terminal): resolve a recovery tab through one tabsByWorktree scan Collapse the recovery lookup onto a single primitive, locateTerminalTab, and express the already-exported isTerminalTabPresent in terms of it. The budget release now calls isTerminalTabPresent directly, so the store drops the hasTerminalTabForRecovery action added alongside it. The native-chat ownership guard also consults tabsByWorktree: the unified tab index owns viewMode, but it can transiently drop a row the remount index still holds, and that hole used to read as "not chat-owned" and remount a chat-owned tab. Drops the storm control case that passed with and without the fix, and pins the surviving drift case to the exact remount count the cooldown produces. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
8acce092ef |
Hide desktop theme imports from paired web clients (#20015)
* Hide Ghostty import from paired web clients - Ghostty import now respects showDesktopOnlySettings, matching Warp behavior - Consolidates desktop-only theme imports under a single showDesktopThemeImports flag - Adds showGhosttyImport option to control visibility across settings UI and search * Hide desktop theme imports from web clients Consolidate Warp and Ghostty import visibility behind a single `showDesktopThemeImports` flag. These theme import flows are desktop-only and should not appear on paired web clients. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
2ffac471bd |
fix(workspaces): seed shells only for blank selection (#19940)
* fix(workspaces): seed shells only for blank selection * fix(workspaces): create runtime-owned launch surfaces * fix(workspaces): report runtime surface failures --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
1798786d4e |
perf(native-chat): mount only the transcript rows near the viewport (#19869)
* refactor(native-chat): share one row-content derivation between row and list Windowing needs the list and the row to agree on which messages draw nothing: a row the list counts but the row declines to render would reserve estimated height for an empty slot. Extracts the block derivation out of NativeChatMessageRow into a module cached on the block array, so a streaming turn pays for it once per revision rather than once per consumer. * refactor(native-chat): keep an opened tool run open past its row's lifetime A tool run, tool line or diff card the reader opened is state they created, but it lives in the component's own `useState`. That is fine while every row is mounted forever. It stops being fine the moment rows can be unmounted: the run silently re-collapses behind the reader's back. Rows now read their disclosure from a transcript-level map when one is provided and fall back to their own state when they are rendered standalone. The controls that re-sync a run — the toolbar's expand-all, a turn's disclosure, a diff reveal — are folded into the key the choice is remembered under, so a control flip reads as "nothing recorded yet" and the new default stands without a mid-render write to a map an ancestor owns. `ToolLine` moves to its own file; the run was over the line cap with it. * perf(native-chat): mount only the transcript rows near the viewport A settled transcript mounts every row it has ever loaded, so the cost of opening a conversation grows with its length even though only a screenful is legible. Rows near the viewport are now the only ones in the document; the rest are reserved as estimated height and measured when they arrive. Four things had to change for that to be safe: - `zoom` moves from the transcript column onto the scroll container. Item measurements are in the zoomed content's pixels while `scrollTop` is not, so with the two split across the boundary the window's arithmetic was off by exactly the font scale — correct at the top of a transcript and blank deep inside it. The column's padding moves to a new inner element to keep the layout it had. This does mean the scrollbar itself zooms with the text. - The three siblings that made up a row — the message, the turn status, the turn's diff rollup — move into one wrapper that carries the spacing they used to take from the column. The spacing between rows is the window's `gap`, never the height estimate, which would otherwise be counted twice. - Messages that draw nothing no longer take a slot. Counted but undrawn, each one would reserve estimated height for a row that never appears. - Paging in older history is driven by scroll events alone. Every row that resolves its real height moves the content and re-fires the size observers, so the old "am I near the top?" test would have asked for another page once per measurement. It now also requires the view to have moved upwards and requires new items since the last request. Anchoring is the virtualizer's: `anchorTo: 'end'` re-resolves the row at the current offset across a count change, which replaces the hand-rolled prepend anchor, and `followOnAppend` keeps a reader at the bottom pinned there. The document-level bottom pin stays, because the typing indicator, the activity line and the column's end padding all live past the last row. Revealing a diff from a turn rollup can target a row that isn't mounted, so that row is pinned into the window and the card still reports its own position — a turn that touched four files lands on the one that was asked for. * fix(native-chat): let a pinned row reach the mounted window Two faults the windowing tests turned up, plus the handles they needed. The virtualizer memoizes its mounted index list on the range extractor's identity. Holding that identity stable — which is right for the measurement memo, and was the reason it was written that way — meant a row pinned after the fact was never picked up: revealing a diff in a row the window had left behind pointed at a row that stayed unmounted. The extractor now changes identity with the pinned set, which is not a dependency of the measurement memo, so nothing expensive is rebuilt. The offset a row sits at is read off the `offsetParent` chain, with a rect-based fallback for the case where there is none. Using that fallback for the window's own scroll margin was wrong in kind: with no layout to measure, it returns the scroll position itself, so the margin tracked the offset and the window sat at the top of the transcript wherever the reader scrolled. The margin now takes the offset chain or nothing; the fallback stays where it belongs, on the reveal. The scroll root and the window's spacer are named, so measurement can find the scroll root without depending on which utility class makes it scroll, and so a test can tell a window from a whole transcript. * test(native-chat): cover the windowed transcript, and prove the window engaged The integration harness stubs `offsetHeight` — on the scroll root and on every row — because that is what the virtualizer measures with, and a DOM without layout answers zero to all of it. Rows report the height their own estimate predicted, which keeps the reserved totals exact no matter which rows have been mounted long enough to be measured. Every case reads the window through one helper that refuses to pass when there is no window. Without that, raising the usability gate would send all of them down the whole-transcript path, where "fewer rows mounted than messages" is false but every other assertion still holds — and they would go on reporting green while covering nothing. Reserved height is asserted as an exact total rather than "greater than zero", which a degenerate empty window also satisfies, and the mounted range is asserted to bracket the offset rather than merely to be smaller than the transcript. Covered: the window mounts a subset and moves with the reader; the newest row and a reveal's target stay mounted from outside it; an opened tool run is still open when its row comes back; a message that draws nothing takes no slot; and the scroll root with no usable height still renders every row as a direct child of the transcript column. What the environment cannot show is stated where it matters rather than faked: its ResizeObserver never fires and a scroll assignment emits no event, so measurement settling, the bottom pin under a streaming turn, prepend anchoring and smooth scrolling are covered as pure decisions — height estimation, the pinned set, range extraction, and whether a position should page in older history — and left to a real renderer as behaviour. * docs(native-chat): say that one offset path does read rects * test(native-chat): pin the window against a row that grows in place Whole-message appends were covered; a row being replaced by a taller version of itself — what a streaming reply is — was not. The existing windowing harness gains two things it needs to see that: a scroll root with a real document (a height, a viewport, and a scrollTop that clamps), and a resize observer that delivers when a target's height actually changed, since happy-dom's never fires and nothing re-measures without it. Frame by frame, while one row grows from 24px to 6358px: the view stays 0px from the bottom, the row stays mounted, and the reserved total tracks the measurement rather than the estimate. A reader who scrolls up mid growth keeps the exact offset they chose for the rest of it. * test(native-chat): guard history prepend anchoring * test(native-chat): strengthen prepend anchor contract * fix(native-chat): preserve provider tool call identity * fix(native-chat): harden transcript windowing lifecycle * test(native-chat): install virtualizer viewport for turn timing * fix(native-chat): reject blank tool call identities --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
6c1580aeba |
i18n: Make file reveal labels translatable (#20010)
Convert hardcoded "Reveal in Finder", "Reveal in File Explorer", and "Open Containing Folder" labels to use i18n.translate() in three menu components. Add corresponding English locale entries so these platform-specific labels are now part of the translation system instead of untranslated strings. Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
3b99a59ea7 |
perf(terminal): stop spending reveal-restore frames on panes that replay nothing (#19972)
The hidden-output restore queue drains one entry per 16 ms frame. An entry whose pane has since gone hidden, been disposed, or had its restore superseded hits a guard and returns without replaying anything — but it still consumed the frame, pushing the next on-screen pane back 16 ms per dead entry. The scheduled callback now reports whether it started a replay, and the drain walks past entries that report false within the same tick. Order is unchanged (strict FIFO) and the one-real-replay-per-frame pacing is unchanged; only the no-op entries stop costing a frame. |
||
|
|
9b83f976f9 |
feat(native-chat): describe slash commands from the provider's own report (#19928)
* feat(native-chat): describe slash commands from the provider's own report The Claude session reports a description and argument hint for every command it can run, but the catalog kept only the name, so the `/` picker described the handful of commands our curated map covers and left the rest — `/goal` included — with a blank row. Carry `description`/`argumentHint` through the catalog and the session wire (both optional, so mixed-version hosts are unaffected), and let a reported description win over the curated one, which stays as the fallback for the name-only report shape. The curated maps are untouched, so structured dispatch still claims exactly the commands it claimed before. * feat(native-chat): show the reported argument hint in the slash picker `argumentHint` was carried to the renderer but nothing read it. Show it beside the command token — `/goal <objective>` over the description — so a row says how the command is invoked, not just what it does. It sits at the row's existing 11px muted tier, subordinate to the description, and truncates in a min-width-0 flex row; the picker also caps the hint at 80 characters, so a provider cannot swamp the row. * fix(native-chat): normalize slash command descriptors consistently --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
cdf41df37c |
perf(terminal): stop rebuilding per-workspace collections on a worktree switch (#19975)
Two allocations scale with the workspace count and are rebuilt on inputs that cannot change their result. `workspaceSurfaces` took `renderedActiveWorktreeId` as a memo dep, but `projectWorkspaceSurfaces` reads that id only behind a truthy `activeWorkspaceResolvedHostId` (the folder-collision tie-break), which is null unless the active workspace is itself a folder workspace. Every git-worktree switch therefore re-projected every surface and re-derived the id array to reach an identical answer. Gate the id on the host so the memo holds. The parked-watcher sync built a fresh empty `Set` for every workspace surface, even though only a mounted workspace can park a tab and the sync only reads the set. Share one empty instance for the rest. Both keep every effect firing on exactly the inputs it fired on before. |
||
|
|
ecd7b19ad4 |
fix(native-chat): pass agent-implemented slash commands through to the agent (#19929)
* fix(native-chat): pass agent-implemented slash commands through to the agent Claim what the host implements; pass through what the agent implements. Claude's harness expands a slash command out of the message text, so the host claimed catalog commands it had no way to run and answered "/init is not available in chat sessions" for commands Claude does run. Codex's app-server has no slash parser at all, so its catalog stays claimed — except /goal, which the model carries out through its own goal tools. * fix(native-chat): offer the agent-run commands in the structured picker Codex reports no command catalog, so its structured `/` menu is the host fallback -- which listed only the host's own commands and hid `/goal`, the one command the model itself acts on. The picker now appends the profile's text-driven commands, described from the curated catalog, so a command that passes through is discoverable and not merely typable. The menu invariant holds either way: a pick is answered by the host or run by the agent, never refused with "not available in chat sessions". * fix mobile structured command reconciliation * fix(mobile): keep native chat controller within lint budget * fix mobile controller lint budget --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
47b6c756f0 |
fix: prompt unexpectedly signed-out Cloud users once per version (#19966)
* fix: prompt unexpectedly signed-out Cloud users once per version * fix: align sign-in card English catalog with runtime defaults * fix: stack notification cards by their rendered height * fix: wait for fresh auth before showing signout card * fix: require verified auth before signout recovery * fix: retry transient auth readiness failures |
||
|
|
54216a7868 |
feat(cmd-j): compact palette location layout (#19939)
* feat(cmd-j): compact palette location layout * chore(i18n): sync palette runtime fallbacks * fix(cmd-j): preserve agent snippet context * fix(cmd-j): elide all file-backed tab paths |
||
|
|
ae729128b6 | refactor(renderer): share path head elision (#19938) | ||
|
|
fb9ba4b681 |
fix(editor): make markdown images inline so a paragraph stays schema-valid (#19746)
* fix(editor): make markdown images inline so a paragraph stays schema-valid Image was registered as a block node while paragraph is content:'inline*', but the markdown pipeline nests an inline image as a paragraph child. Schema.nodeFromJSON does not validate content, so the editor built a schema-invalid document that rendered fine and threw on the first step that reassembled the paragraph - i.e. on the user's next keystroke. Report 0e46c048 (1.4.198, macOS): RangeError "Invalid content for node paragraph" from checkContent via Node.replace, tearing down the editor.rich-markdown boundary. Register Image as inline and override paragraph's parseMarkdown so a lone image is not hoisted out of its paragraph. Also fixes the same crash class reachable through details/summary. Markdown output is byte-identical. * fix(editor): keep a fenced code block intact when an image is inserted into it Making the image node inline meant it could no longer be fitted into codeBlock (content:'text*', marks:''), so inserting one with the cursor inside a fence made ProseMirror close the block at the insertion point: the remaining code escaped as plain prose and the language attribute was lost, and autosave wrote that markdown to the user's file. The pre-fix block image split the fence into two intact blocks instead. Resolve the insert content against the target position: when an inline image cannot be fitted where the caret sits, wrap it in a paragraph so ProseMirror splits the block and both halves keep their ``` fencing and language. Prose insertion is unchanged. Every production insert path now shares that resolution - the toolbar picker, the slash command and the clipboard-screenshot paste through insertRichMarkdownImageFromPath, plus the GitHub/GitLab composer's image-URL insert - each with a regression test. Also guard the unchecked cast of Paragraph.config.parseMarkdown: a Tiptap upgrade that drops the field would otherwise turn every paragraph parse into a TypeError and take the whole editor down, instead of degrading to parseInline. Four of the new round-trip cases asserted only on getMarkdown(), which walks the document without running NodeType.checkContent and so emits byte-identical output from a schema-invalid document - they passed on the pre-fix code. roundTripMarkdown now runs doc.check(), the list-item and table-cell case performs a real edit, and the standalone-image case types beside the image. All twelve cases now fail on the merge-base. Adds an Electron e2e spec driving the real renderer: a paragraph image and a toggle-summary image each survive a keystroke, and Bold over a selection spanning the image keeps it. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
b0505f0418 |
fix(sidebar): let an agent row show the provider title (#19936)
* fix(sidebar): let an agent row show the provider's own session title The row resolver never read `aiVaultTitle`, so a terminal agent's tab showed the title from its transcript while its sidebar row showed the scraped live title — two names for one session. Placed at the same rank the tab strip uses. * fix(sidebar): scope provider titles to their sessions * fix(dashboard): refresh retained agent tab metadata --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
74cc9b5039 |
feat(desktop): native mobile push integration (2/3) (#19935)
* feat(desktop): integrate native mobile push delivery and lifecycle * fix(desktop): preserve notification replay policy and review invariants * fix(desktop): correct notification locale namespace and auto-ack tests |
||
|
|
027acb4efa |
fix(native-chat): settle a structured send on admission, not on the provider echo (#19863)
* fix(native-chat): settle a structured send on admission, not on the provider echo Sending a message in structured native chat raised "Message delivery is unconfirmed." with a Retry button on a message that had in fact been delivered. Measured across 14 days of local journals: 44 of 173 delivered sends (25.4%) tripped it. The dispatch path wrote the message to the provider, then waited a fixed 10s for the provider to echo the message's uuid back. That echo is emitted when the provider STARTS the turn, so a message queued behind a running turn cannot be echoed until that turn ends. Echo latency is bounded by the previous turn's duration, which is unbounded -- one send took 105 minutes. The 10s constant sat at the p75 of real echo latency, with the slowest clean send at 9.76s, a margin of 0.24s. No constant can work: the wait was measuring the wrong event. The false banner was not cosmetic. It invited a Retry, and Retry bypassed the operation ledger to redeliver. One message reached the model five times through that path. Dispatch now returns as soon as the transport write completes and writes no dispatch row; the submission stays `pending`, a neutral state, and the provider's echo settles it `accepted` through the late-settlement channel whenever the turn ahead of it ends. Delivery doubt is reachable only from process facts -- a refused write, a dead child, a dead host -- never from elapsed time. Retry re-delivers only where the recorded reason proves the message never reached the provider. The list is deliberately fail-closed: refusing a legitimate retry costs the user a re-type, while allowing an illegitimate one sends the model a second copy of their message. A refused entry now leaves the outbox with an explicit notice instead of parking at the head, where it would have wedged every message queued behind it. The send-response classification moves to a pure module beside the existing outbox reconciler, so both writers of an entry's state now live together and the decision is unit-testable rather than reachable only through the hook. Scope and known gaps: - Codex carries the same 10s stopwatch. It has no late-settlement channel, matches waiters by queue order rather than identity, and has no waiter lifecycle at all, so there was no safe subset to land here. A marker constant records the debt and deletes itself when that lands. - A message refused re-delivery loses its standing delivery notice and leaves only a transient error line. A passive "waiting to be accepted" affordance is the follow-up. - The restart reconciler that would decide a dead child or a dead host on evidence rather than refusing them is fully written and has never had a production caller. Wiring it is the next change, and it removes the re-type cost above. * fix(native-chat): harden structured dispatch settlement * fix(native-chat): preserve dispatch recovery evidence * fix(native-chat): preserve pending send compatibility * fix(native-chat): satisfy native import audit * fix(native-chat): bound legacy send settlement --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
a9338438c4 |
fix(native-chat): never adopt an unlisted model as the launch default (#19854)
* fix(native-chat): never adopt an unlisted model as the launch default `modelIsAdoptableAsLaunchDefault` is the sole gate on whether a model id may become the persisted `-m` launch flag for future native chats. For a catalog that does not set `discoveredModelsAreAuthoritative` — Claude and Codex — `!catalog.discoveredModelsAreAuthoritative` short-circuited the discovered branch to `true` for any id at all. So a raw launch flag (`worker-start --model claude-opus-5`) is seeded verbatim into the session record, and the first option write or model re-pick adopted it as the durable default. Every later native chat with that agent then launched `-m claude-opus-5` — an id neither the host CLI's list nor the catalog seed carries. Both branches now sit behind one precondition: the active model list or the catalog seed must carry the id. Ids that are carried keep their existing behaviour, including the authoritative-retirement and tracked-model rules. * docs(native-chat): state the launch-default precondition by id, not by vector A typed `/model` cannot introduce an unlisted id: matchNativeChatCatalogModelId returns only ids drawn from the list it is handed, so a never-seen id either collapses to a catalog id (claude `/model claude-opus-5` -> `opus`) or matches nothing (codex). It can only re-assert an id already in the record. The origins that do enter verbatim are the launch flag and an agent report -- applyNativeChatReportedSessionOptions writes `values.model` with no catalog matching. Say that instead, so the comment is true of the code as written and does not lean on the reconciled row that #19852 removes. Comment only; no behaviour change. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
33436c30d8 |
refactor(native-chat): unify agent session launch and open drafts in structured chat (#19681)
* wip(native-chat): first-pass draft routing into structured chat (to be reworked)
* refactor(native-chat): gather agent launch route inputs in one builder
Every launch entrypoint assembled the route resolver's inputs by hand and
they disagreed: only three of seven passed the project runtime blocker, so
a WSL-pinned project was refused structured chat from the tab bar but
admitted from the create dialogs. buildAgentLaunchRouteInput is now the
one place that gathers host, capabilities, workspace kind, project runtime
and TUI customization, and works for workspaces that do not exist yet.
Also deletes the dead draft-prompt blocker from the shared resolver; the
renderer stopped passing it and the main process never did.
* refactor(native-chat): share one structured launch settle loop
Five entrypoints copied the same loop around startStructuredAgentLaunch:
start, claim a refusal fallback, await, branch on refusal or unknown. The
copies drifted: direct work-item and full create reported an unexpected
launch error as success, and resume handled neither refusal nor unknown.
settleStructuredAgentLaunch now owns that loop and returns one settlement
(structured, refused-then-legacy, cancelled, visibility-unknown, failed).
Direct work-item, full create, folder workspace, both onboarding folder
paths and vault resume consume it; each keeps only its own legacy fallback.
Resume deliberately has no fallback. Unknown outcomes release the caller
uniformly so a stale fallback closure cannot fire on a later reconcile.
* refactor(native-chat): route the new-tab launcher through the shared settle loop
The new-tab launcher fired its refusal fallback and forgot it: nobody
learned whether the terminal fallback ran, and a visibility-unknown outcome
was never surfaced. Its structured branch now runs through
settleStructuredAgentLaunch with the terminal launch as the legacy fallback.
launchAgentInNewTab stays synchronous; the result gains a structuredSettlement
promise, and promptDeliveryResult keeps following the terminal fallback's
delivery on refusal as it did through the callers bridge before.
* refactor(native-chat): one legacy prompt delivery path and one trust preflight
The direct work-item flow kept its own seed-and-paste copy of the legacy
prompt delivery; it now uses deliverLaunchPromptToAgentTab with its own
timeout notice supplied as a callback. Three private copies of the trust
preflight (session continuation, worktree creation, folder workspace) fold
onto preflightAgentTrust. The direct work-item pre-launch mark keeps its own
entry because it differs in timing, not mechanism.
* refactor(native-chat): run quick create through the shared settle loop
Quick create was the last entrypoint driving the launch handle itself,
because its cancel lifecycle is real: when the creation is abandoned the
structured launch must be cancelled immediately so a staged prompt never
reaches the provider. The shared loop now takes a cancellation hook with an
eager subscription plus a post-await check; it cancels the launch once,
unsubscribes on settle, and reports cancelled without running the fallback.
Quick create keeps its two-branch legacy fallback and retire-on-late-cancel.
Also updates the surface-caller census for the onboarding launch module
that step 2 introduced.
* fix(native-chat): open editable drafts in structured chat for eligible local Codex launches
Route order asked the default-view-mode question first, and that decider
applies the terminal mirror gate (a TUI cannot clear more than forty lines
of prefilled draft), so a PR body over forty lines reached the plain
terminal before structured eligibility was checked. Structured eligibility
now comes first; the mirror gate applies only on the legacy branch.
The structured draft seed writes the launch-draft store directly with no
mirror gate, since a structured session has no terminal copy to fall back
on. Closing a settled structured tab clears an unadopted seed. The
structured session treats idle and loading as unsettled so the adoption
hook takes its baseline from the loaded transcript. Each caller passes one
delivery-mode value to both the route builder and the settle loop.
The structured session component test is split with a shared harness so
it stays under the test file line cap.
* test(native-chat): make the structured session test harness type-portable
* fix(native-chat): close review gaps in the shared launch settle loop
- Claim a refusal fallback only when the caller supplies one, so vault
resume no longer reports a terminal fallback it never opened.
- A failed or cancelled direct work-item launch returns no tab id, so the
caller never pastes the prompt into a setup shell.
- Terminal fork activates with providesInitialSurface for structured
launches and gates its toast on the settlement; the draft blocker
deletion made fork route structured too.
- A failed launch clears its draft seed. The failure toast moves to its own
module to keep the launch-state file under the line cap.
- Ratchet for settle-loop callers; cancel-during-fallback documented.
- Restore the local agent label lookup that the pane-agent identity
inventory expects instead of the inventoried helper.
* fix(native-chat): resolve the agent label through one module
* fix(terminal-pane): keep the fork dialog from reopening a created worktree
A failed or unknown structured settlement returned false after the fork
worktree already existed, so the dialog stayed open and a second click
created another worktree. Unknown now closes the dialog (the launch badge
already reports it); failed copies the context the way a null launch does.
* chore: restore pnpm-lock.yaml to main (local pnpm rewrite slipped into a commit)
* test(native-chat): stop asserting the deleted draft feasibility input
The routing-authority test expected the shared predicate to receive
isDraftPrompt; delivery mode is prompt metadata and never reaches
feasibility now, so assert its absence instead.
* refactor(native-chat): decide every agent launch route in one planner
The route was still resolved at seven callers, each also calling the settle
loop; two census tests only stopped an eighth. planAgentSessionLaunch is now
the one production caller of the resolver and its launch() the one caller of
the settle loop, and both censuses pin exactly that file.
The funnel is two-phase because three sites need the route before the
workspace exists and quick create persists its request for recovery: a plan
exposes route before creation and launches with the created worktree id;
a persisted quick-create request carries the verdict as data and re-enters
through adoptAgentSessionLaunchVerdict without re-resolving. Delivery mode
is fixed on the request once, so route and launch cannot disagree.
* test(native-chat): pin the two adopters of a planned launch verdict
* fix(native-chat): answer route readability from the repo when the worktree row is absent
The planner's transcript-readability input dropped the repo-level connection
fallback the direct work-item path still computes for its startup payload, so a
route planned in the window right after workspace creation saw `undefined` —
which reads as "not locally readable" — and downgraded grok/omp launches from
native chat to a raw terminal. Only `undefined` ("cannot determine the host")
now defers to the repo; a resolved `null` stays the local answer.
* refactor(native-chat): answer structured feasibility with a query, not a launch plan
Every rendered AI Vault row built a whole launch plan — execution-host lookup,
project-runtime resolution, capability read, plus a plan object and a launch
closure it threw away — to read one boolean off it. Feasibility and a launch
decision are different operations, so the planner now exports the predicate for
the first and keeps the plan for the second, and the census pins the query's
callers separately. Settings arrive by argument, which makes the AI Vault
callback's dependency on them real rather than a comment the linter contradicts.
The plan's `explicitStructured` branch had that gate as its only caller and goes
with it; the vault's launch already re-enters on an adopted verdict.
* refactor(terminal-pane): fold the fork's trust preflight onto the canonical one
`preflightForkAgentTrust` was a behavioural duplicate of `preflightAgentTrust`,
whose signature now accepts a nullable agent and workspace path and so is a
drop-in replacement. Its file is left holding only the launch-platform resolver
— which is not a duplicate, since it returns an override rather than a default —
so the file is renamed for what it now contains.
* refactor(native-chat): cancel a structured launch through an AbortSignal
The settle loop's launch cancellation re-derived the standard poll-plus-eager-
event primitive that `AbortSignal` already is, so it now takes one. The eager
semantics are unchanged: the loop still cancels on the abort event rather than
only polling after awaits, so a staged prompt is discarded before it reaches the
provider, and it drops its listener on settle instead of leaving the signal
holding the closure. Quick create owns the controller and bridges its store
subscription to it.
A cancel that lands after the refusal fallback already opened a terminal now
carries that surface on the settlement. It is the fallback's tab that exists, so
reporting the pre-launch one handed the caller a workspace with no agent in it.
* fix(native-chat): tighten quick create's structured launch settle path
Four things the launch path got wrong once the settle loop owned the flow:
- The abandoned-creation check now runs before the first-message rename flag is
written, so a creation being torn down is no longer marked for a rename that
will never happen (the order the pre-planner code had).
- A cancel that arrives after the refusal fallback opened its terminal reports
that terminal rather than the pre-launch tab.
- `plan.launch` is called outside the caller's try, and nothing awaits that
caller, so a throw there would strand the creation panel. It is now caught and
reported the way a failed launch already is.
- The launch route is a required argument instead of defaulting to
`terminal-tui`, which would have silently reported success with no surface
opened. Both callers already gate on the structured route.
* fix(native-chat): give one launch identity one prompt delivery mode
A caller joining a pending launch computed its outbox text from its own delivery
mode, so an auto-submit caller landing on a draft launch enqueued text the first
caller's seed was already showing in the composer: the user saw it and it was
sent. The mode is now fixed by the caller that opened the launch, and a joiner
delivers its text that way.
Seeding also moved to where the coalesce decision is made, so a launch whose
callers already settled as refused is not given a fresh draft — the refusal path
early-returns, so nothing would ever clear it and it would outlive every tab.
* fix(work-item): report a failed structured launch as a failed direct launch
`launchWorkItemDirect` returned true unconditionally, so a structured launch
that opened no surface still read as a started workspace. Callers hang
irreversible follow-up work off that boolean — the fix-checks dialog fires
`onLaunched` on it, which is documented as the home for host writes — so a
launch with no agent tab now reports false, matching what full create does.
The settle result says so explicitly rather than leaving callers to infer it
from a null tab id, which `notLaunched` also produces.
* test(session-tabs): pin the id a first structured publication is minted under
The launch draft seed is keyed on `structuredAgentSessionTabId(sessionId)`
before the tab exists, while the mirror mints ids with collision avoidance that
can append a `:history-N` suffix. The two agree today only because a fresh
session's base id is unique. Pin that where the id is actually minted, with the
collision arm alongside it so the divergence the seed depends on staying away is
visible rather than assumed.
* test(native-chat): pin the route connection fallback on the un-mocked resolver
The suite that covers the builder stages `getConnectionIdFromState`, so it can
characterize the fallback but cannot catch a defect that lives in owner
resolution itself. This one runs the real resolution over real store rows: two
repos publishing the same worktree id on different hosts, which is the
documented case where the owner cannot be named and `undefined` is returned.
Red with both fix files at the previous head, green with them.
Reverts the two caller pins added to the route census — the feasibility
predicate is exported from the planner, which the census already permits, so it
passes unedited and needs no permit clause.
* fix(native-chat): keep the structured launch's own agent eligibility check
Quick create's structured launch narrowed its guard to a bare `agent` presence
check, so a creation carrying an agent that cannot hold a structured session
reported itself cancelled once dismissed, where it previously reported that it
had done nothing. Unreachable through both callers today, but it is the last
local eligibility check in a module that otherwise trusts its callers for the
route, so it is restored rather than left to the required-route typing — which
says nothing about the agent.
Also corrects two comments that called the quick-create request "persisted".
It lives in renderer session memory and dies with the renderer; calling it
persisted made the plan/adopt split read as restart recovery, when what it
actually buys is a route decided before the worktree exists.
* fix(native-chat): keep the structured feasibility query typecheck-clean
The query threaded its narrow settings through the store, but the route
store's settings must satisfy the full GlobalSettings that two of its
resolvers require, so the narrow copy never fit. Ride the named settings
on the built input instead: the caller still names them, so a React memo
still depends on them, and no store-shaped object is needed.
Also give the launch state its delivery mode unconditionally; the key is
required, and a conditional spread makes it optional under
exactOptionalPropertyTypes.
* docs(native-chat): name the feasibility query's one remaining settings asymmetry
The builder reads launch customization off the store while the routing gate
reads the named settings, so one answer has two settings sources. It cannot
diverge with the single caller passing the object the store already holds, but a
PR about removing split sources should not leave that unstated.
* fix(native-chat): keep a coalesced joiner's draft unsent
joinLaunchDelivery stripped the joiner's delivery mode when the launch it
joined had established none, and an absent mode reads as submit. A joiner
that asked for a draft therefore had its text sent — the send-without-
consent this PR exists to prevent. Fall back to the joiner's own mode only
when nothing was established, so the first caller still wins otherwise.
* chore: re-trigger CI
GitHub created no workflow run for
|
||
|
|
2626e2eca4 |
Make the structured turn lifecycle row durable so completed durations survive (#19695)
* Make the structured turn lifecycle row durable so completed durations survive A structured-chat turn used to end by tombstoning its running lifecycle item, which threw away the only durable record of when the turn ended. Completed "Worked for" labels therefore depended on the renderer having observed the turn finish, and vanished on reopen. The lifecycle item is now revised in place, never tombstoned: - running, with startedAt, at the provider's turn start - completed or interrupted, with completedAt, at the provider's terminal frame, a user stop, or a child exit the host observed - unverifiable, with no end, when a cold acquire finds a running row from a generation whose exit nobody observed Both timestamps are the execution host's clock at receipt, captured before the deferred sink, so the completed value is identical on every client and needs no client clock. Codex history restore uses the provider's own second-granular endpoints for turns that predate this change. Desktop and mobile read settled durations off the journal through one shared selector, and anchor the live counter on the host start with the client's local receipt so a skewed client clock never leaks into the label. Locally observed durations remain the fallback for hosts that still tombstone. Timestamps live inside the existing turnLifecycle field, which old clients strip, and every working-state consumer keys on state === 'running', so no capability negotiation is needed. * native-chat: avoid stale working status on settled turns * test: align settled turn status expectations * Name settled lifecycle rows by their terminal state An interrupted or unverifiable turn must not read as completed for any consumer that renders status text raw. One shared helper builds the text for both providers from the lifecycle state. * test: deduplicate turn lifecycle suites Each behavior keeps one test; duplicated harnesses and restated cases go. * Key lifecycle rows to their user item and record the provider's measured duration A lifecycle row now names the user item that opened the turn by its provider key, so clients attribute timing explicitly and fall back to journal order only for rows from older hosts. A provider-initiated turn with no prompt can no longer claim the previous prompt's duration. When the provider measures the turn itself (Codex turn.durationMs, Claude result.duration_ms) the terminal row records it and clients prefer it over the host interval, so a turn shows the same number live and after a history restore. Host receipt times remain the live-counter anchor and the fallback. * Record a turn as a first-class journal item The turn record is now its own item kind rather than a status row carrying a lifecycle field: no text to misuse, and the fold matches the durable turn record other systems keep. Rows that carry it are stamped journal schema v3; every other row stays v2, so an older host keeps reading them and latches read-only at the first v3 row instead of truncating the epoch. Clients that predate the item would paint an unknown kind as a text bubble, so the host publishes the legacy status form to any client that does not advertise agent-session.turn-item.v1, through the same per-client seam background tasks use. The downgrade is transitional and goes once no supported release lacks the capability. The shared projection now renders unknown item kinds as nothing, so later kinds need no gate. One shared reader handles both forms for old journals and old hosts. * Preserve observed turn end across settlement retries * Retain turn attribution for loaded chat history * Preserve Codex exit receipt across close retries * Register completed turn duration reliability gate * Keep earlier turns through a Codex rewind and count a mid-turn attach from the real start Findings from an independent adversarial review of the typed turn record: - A Codex rewind adopted the provider's item list as the new epoch, and the provider never returns the host's own turn rows, so every duration before the rewind point vanished. The host's turn rows are now spliced back beside the item each followed, and recovery no longer expects the provider to prove rows it never owned. - The epoch row was stamped with the current schema version, so an older host latched read-only at row 1 of every new session, defeating the mixed version design. It carries no body and stays at v2; a stored-row test now reads SQLite directly, because the reader upcasts every row on read. - A send Codex folds into a running turn shares the opening prompt's provider key, and the alias map credited the duration to the later prompt. The earliest submission naming a key now wins. - The live counter anchored on first sight, so a client attaching mid-turn counted from zero. Published frames now carry the host's clock, the reducer keeps the last sample with its local receipt time, and both clients anchor on how long the host says the turn has run. * Correct turn duration gate assertion reference * Respect authoritative unknown native chat duration * Preserve unverifiable timing across older host upgrade * Record final completed turn duration reliability evidence * Fix the CI failures the merge left behind - A merged import list named the same module twice, which the native code quality plugin fails on. - A running turn is now reported by the host with no duration, so the settled map carries an explicit null for it; the hook test still expected the entry to be absent. - main gave the older-page action a cursor with a head-trim guard, so the retention test's epoch-only action no longer typechecks; it now passes an unbounded sequence, which is what the old shape meant. - The roster comparator moved into the extracted module, leaving its import unused in the reducer. * Split two files back under the line cap after the merge Merging main put both one effective line over 300, and the cap forbids a disable or a shave. The wire module's refusal vocabulary moves to its own file and is re-exported, so its consumers are untouched; the host's four thin mutation delegates move next to the functions they call. * Advertise the turn-item capability on every client transport Local IPC and mobile advertised it; the remote and web transports did not, so a desktop paired to a remote host, the CLI, and web silently ran on the legacy carrier forever and the canonical row was never exercised there. The renderer that paints it is the same build on every transport. * Update the web auth-frame expectation for the new capability --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
721a269289 |
test(native-chat): split structured question fixtures (#19924)
Co-authored-by: Merge Sim <sim@local> |
||
|
|
4f5a8275e8 |
Revert "test(native-chat): split structured question fixtures"
This reverts commit
|