mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
ef55429f3d2ce3fbbcc542e4dcc8a6b36e464455
7532
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ef55429f3d | release: v1.4.162-rc.0 v1.4.162-rc.0 | ||
|
|
3240c26bce | fix(sidebar): contain nested agent metadata (#11336) | ||
|
|
3a80fbe162 |
Revert terminal rendering changes from #10692, #10794, #10871, and #10907 (#11338)
* Revert "fix(terminal): avoid flash while restoring parked terminals (#10871)" This reverts commit |
||
|
|
238d3a1ea1 |
fix(terminal): verify clipboard writes so TUI "Copied" never lies (#10827)
* fix(terminal): verify clipboard writes so TUI "Copied" never lies Windows/Electron can return from clipboard.writeText without updating the OS clipboard, so Claude Code / OpenCode OSC 52 copy and terminal selection copy looked successful while paste stayed empty (#8977, same root as #5611). Verify standard clipboard writes by reading back after write, surface OSC 52 host write failures with a toast, and route selection copy through a shared helper that only clears the selection after a confirmed write. * fix(terminal): harden clipboard write verification * fix(terminal): contain clipboard failure notifications * fix(terminal): isolate verified clipboard writes * fix(terminal): address greptile clipboard verify nits Drop the dead onWriteFailure pass-through from the coalesced OSC 52 handler so failure toasts stay owned by the microtask path. Cover multi-line / CRLF identity in write+verify tests, and export the verification-failed error constant for stable matching. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
fa449bc0ef |
fix(worktree-palette): stop blanked display names from crashing Cmd+J (#11323)
* fix(worktree-palette): stop blanked display names from crashing Cmd+J
Blanking the "Display Name" field made buildWorktreeMetaUpdates emit
`displayName: undefined` as a present key. The store's `{ ...worktree,
...updates }` spread then erased the live name, so the next palette
keystroke threw "Cannot read properties of undefined (reading
'toLowerCase')" in searchWorktrees (crash a1f81ea1, build 1.4.159).
Fixed at three layers so no single guard is load-bearing:
- Producer: persist the blanking intent as '' instead of undefined, and
let WorktreeSet accept '' so remote/SSH hosts stop dropping the clear.
- Store: applyWorktreeUpdates and applyDetectedWorktreeUpdates drop
present-but-undefined keys for fields Worktree declares required.
- Readers: resolveWorktreeDisplayName/resolveWorktreeBranchLabel mirror
the main-side mergeWorktree fallback (custom -> branch -> folder) for
all four Cmd+J searches, the checks/review index, and the render site.
Co-authored-by: Orca <help@stably.ai>
* test(worktree): assert omitted display name shape
---------
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
4517088c42 |
fix(gitlab): refresh self-hosted provider detection (#9909)
* fix(gitlab): refresh self-hosted provider detection * fix(gitlab): preserve auth refresh during host probe * fix(gitlab): merge refreshed auth hosts linearly --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
a7c8b8e071 |
fix(terminal): bound SSH & remote hidden-worktree terminal retention (C1) (#10625)
* fix(terminal): park SSH worktrees like local ones (C1 retention, slice A) SSH ptys were blanket-excluded from hidden-view parking, so a hidden SSH worktree retained every pane forever (C1: renderer heap climbs to the V8 ceiling). SSH bytes transit local main — fact-mode watchers already cover them, and main keeps a headless model served over pty:getMainBufferSnapshot that the SSH reattach path never consulted. - isParkRestorableTerminalPty: snapshot-backed OR (SSH + policy); threaded through both park verdicts, both selectors, watcher coverage, and the watcher start guard. Remote-runtime/fail-open/foreign/null unchanged. - Parked-SSH reveal paints from main's headless model (dimension-matched, ~5k rows) and degrades to the relay 100KiB replay unless the snapshot is a non-empty source==='headless' payload — never a blank/stale paint. - Kill switch: settings.terminalSshViewParking (default on). DESIGN.md records the approved plan and the H1 magnitude non-claim. Co-authored-by: Orca <help@stably.ai> * fix(terminal): bound hidden-worktree retention with a force-park budget (C1, slice B) Un-parkable worktrees (remote-runtime ptys, uncoverable tabs, SSH with the slice-A switch off) had unlimited retention: the parking cap/TTL only ever saw eligibility-passing worktrees, so one bad tab pinned a whole worktree's panes forever. Retention is now memory-bounded, not eligibility-bounded. - terminal-hidden-worktree-retention.ts: retention budget (12 hidden / 45min TTL, sized from the measured 2.5-19MB per-pane V8 cost, DESIGN.md §2) over hidden worktrees ordinary parking can never evict; reuses the hot-retain ranking so last-active exemption, deterministic ties, and deadline-driven rechecks hold. Fail-open/foreign-pty tabs are eviction-exempt (a remount would fresh-spawn and orphan the live shell). - Terminal.tsx: force-parked ids join the parked set AFTER the coverage veto (darkness for uncoverable tabs is the accepted cost); buffers captured via the sleep-flow registry before the unmount render; retention TTL added to the recheck deadlines for budget candidates only. - Verdict stays out of its own effect deps; policy test asserts idempotence and time-monotone membership (flip-loop dwell regression). - Kill switch: settings.terminalHiddenWorktreeRetentionBudget (default on). Co-authored-by: Orca <help@stably.ai> * fix(terminal): demote hidden scrollback for eviction-exempt worktrees (C1, slice C) The retention budget (slice B) must exempt worktrees holding fail-open or foreign-worktree ptys — a remount would fresh-spawn and orphan the live shell — which would leave that class unbounded again. Instead, past the same 45min retention TTL their hidden panes drop to the minimum scrollback tier (measured: ~19MB -> ~1.3MB V8 heap per 50k-row pane; trimmed history is gone by design, reveal restores the configured cap for future output). - terminal-hidden-scrollback-demotion.ts: module-state verdict registry (parked-watcher pattern) with content-equality notify damping; applied in the existing scrollback-rows effect in use-terminal-pane-lifecycle. - selectScrollbackDemotedTerminalWorktrees: pure, TTL-gated, time-monotone. - Retention TTL wakeups now also cover exempt worktrees so demotion fires. - Kill switch: settings.terminalHiddenScrollbackDemotion (default on). Co-authored-by: Orca <help@stably.ai> * fix(terminal): paint the SSH model snapshot inline, not via nested coordinator (C1 slice A fix) applyMainBufferSnapshot runs its own structuralReplayCoordinator.run; calling it from applyReattachPayload (already inside the coordinator when a relay replay exists) deadlocks on the coordinator's tail chain. The model paint now mirrors the daemon-snapshot branch inline (folded scrollback + rehydrate + screen, dimension-matched, escape tail last) and arms the restored-snapshot seq baseline so deferred/live chunks the snapshot covers dedupe instead of double-painting. Also falls through (no early return) so reattachPayloadApplied still latches. Adds the folder-workspace id parity unit case. Co-authored-by: Orca <help@stably.ai> * test(terminal): SSH park+reveal e2e round-trip + as-built design notes (C1) Docker-gated (ORCA_E2E_SSH_DOCKER=1) spec: SSH tab parks behind a decoy and reveal restores marker content at multi-viewport scrollback depth. DESIGN.md records the as-built deltas (inline paint, force-park shape, last-active floor) and the residuals so follow-ups aren't lost. Co-authored-by: Orca <help@stably.ai> * fix(terminal): paint SSH reveal from main's model even when the relay replay is empty (C1 review #1) A relay restart empties the replay buffer; the reveal previously painted nothing even when main's headless model held the session. The reattach now prefetches the model snapshot when no structural replay exists (SSH-shaped ptys only) and paints it inside the coordinator; emptiness is judged on the composed payload (scrollbackAnsi + data + pendingEscapeTailAnsi) so an alt-screen snapshot with an empty screen frame still paints. Co-authored-by: Orca <help@stably.ai> * fix(terminal): decouple scrollback demotion (slice C) from the retention-budget switch (C1 review #2) Per the approved contract each slice reverts behind its own switch: slice C now requires only the master terminalHiddenViewParking plus its own terminalHiddenScrollbackDemotion flag. The TTL wakeup timer fires for demotion candidates even with the budget switch off. No DEFAULT_SETTINGS entries exist for sibling flags (defaults are the '!== false' optional pattern), so no explicit defaults are added. Co-authored-by: Orca <help@stably.ai> * fix(terminal): scope eviction exemption to the tab, not the worktree (C1 review #3) One eviction-exempt tab (fail-open/foreign pty) previously vetoed force-park for its whole worktree, pinning co-located remote-runtime tabs forever. The worktree now force-parks while exempt tabs keep their mounted panes via a per-tab exclusion mirroring the Activity-portal pattern (legacy watcher sync, legacy render, and the overlay cold-parking hook). Ordinary parking is untouched — a worktree with an exempt tab still cannot ordinary-park. Slice C now also demotes exempt tabs' panes as soon as their worktree force-parks under the count budget (they are the only panes left mounted). Co-authored-by: Orca <help@stably.ai> * fix(terminal): demote un-parkable worktrees the force-park lever spared (C1 review #4) The last-active exemption means a single hidden un-parkable worktree never force-parks — and slice C previously only targeted exempt-tab worktrees, so its panes held full scrollback forever. Demotion now also covers un-parkable non-exempt worktrees past the retention TTL that are absent from the force-parked set (last-active spared, or slice B switched off). Membership stays time-monotone for fixed inputs; covered by new idempotence/monotone selector tests. Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep the hidden clock running through transient background-measure windows (C1 review #5) Whole-worktree background mounts (browser-automation bootstrap lease, mobile mounts, agent wakes) open a ~3s self-clearing measure window that previously deleted hiddenSince — every remount restarted the 30s hysteresis and the 45min retention TTL, so a periodically re-mounted force-parked worktree never re-parked. The measure window still pauses parking/eviction verdicts (all selectors skip measuring candidates); only the clock survives, so the prior verdict resumes as soon as the window closes. Visible and portal-holding worktrees still reset the clock. Co-authored-by: Orca <help@stably.ai> * test(terminal): make the SSH park+reveal depth assertion prove the model paint (C1 review #6a) Pad the session with ~180KB of output after the numbered markers so the earliest marker falls outside the relay's 100KiB rolling replay buffer while staying inside main's ~5k-row headless model; asserting marker_1 after reveal now proves the headless-model paint rather than passing under the relay fallback. Co-authored-by: Orca <help@stably.ai> * docs(terminal): rewrite DESIGN.md as the single as-built C1 contract (review #7) One contract matching the code: status IMPLEMENTED around force-park (not the unmount proposal), real kill-switch names with coupling + revert matrices, the true retention-floor formula with measured per-pane and demotion numbers, an explicit when-OOM-is-still-possible paragraph naming the H2 pendingSideEffects residual, the applyMainBufferSnapshot deadlock constraint inside the slice-A section, stable-signal phrasing instead of a capability latch, fail-open AND foreign-worktree exemption class, verified cites, and a planned/landed/follow-up test matrix. Co-authored-by: Orca <help@stably.ai> * fix(terminal): resolve the eviction exemption per pane, not per tab (C1 review #8) isEvictionExemptTerminalTab read only tab.ptyId — the FIRST leaf's pty — while the coverage veto that makes a worktree a retention candidate walks every pane. A split tab whose second leaf held an unrestorable pty therefore failed coverage (→ force-park target) yet looked exempt-free, so force-park unmounted it and orphaned the live shell. The exemption now resolves panes through the same resolveParkedTerminalPaneCandidates, keeping tab.ptyId in the union for the no-layout/no-capture case. Also from the same review round: - force-park's capture passes includeLocalBuffers:false like every other shutdownBufferCaptures caller; it was serializing up to 512KB/pane of scrollback into the store inside a fix meant to bound renderer heap. - Terminal.tsx unmount resets the scrollback-demotion registry — module state with no reset path, read by a pane effect that runs before the host effect that would clear it, so a stale verdict trimmed restore replays. - memoize watcher coverage per tab within the parking pass; the retention candidates re-asked it for every mounted worktree, not just the parked few. * docs(terminal): drop DESIGN.md — the as-built C1 contract moves to the PR body Co-authored-by: Orca <help@stably.ai> * fix(terminal): cap the deferred PTY side-effect queue (C1 residual H2) pendingSideEffects grew without bound under background timer throttling (~64 drained/s vs hundreds queued/s overnight). Cap at 512 entries with oldest-first eviction: titles drop (last-wins), a pending bell latches onto the next survivor, agent-status payloads collapse onto the survivor keeping the newest 16 (last-wins store state, KB-scale strings). Co-authored-by: Orca <help@stably.ai> * fix(terminal): carry command-lifecycle facts through parked watchers (C1 follow-up) Parked fact-mode watchers omitted onCommandFinished/onCommandCode*, so OSC 133;D and Command Code scrape signals went dark while parked. New parked-terminal-command-status.ts ports the store-level subset: git-UI nudge on every command finish, same-turn status-row drop for SSH PTYs (exact mounted-path parity — the foreground tracker refuses SSH ids), and the Command Code working seed / 1500ms done settle. Byte mode scans the same shared parsers for authority-off parity. Local-PTY status drops stay with the mounted pane: they need pty-connection's process-confirm ladder to tell a leaked nested-shell 133;D from a real agent exit. Co-authored-by: Orca <help@stably.ai> * test(terminal): retention-budget force-park e2e with a retentionLimit override (C1 6b) ORCA_E2E_TERMINAL_RETENTION_LIMIT flows preload → e2e-config → getTerminalParkingPolicyOverrides (exposeStore-gated, positive-integer only) so a spec can shrink the force-park budget to 1. The Docker-gated spec opens two remote worktrees on one relay target (second pre-seeded remote repo), disables terminalSshViewParking to make both un-parkable, hides both behind the local context, and proves the older one force-parks while the last-active exemption spares the newest; re-activating the evicted worktree restores the marker tail via relay replay. Co-authored-by: Orca <help@stably.ai> * test(terminal): retention-budget e2e via same-repo remote worktrees (passes docker lane) The first draft added a second remote repo mid-session, whose pane pty spawn misroutes to the local daemon with the remote cwd (pre-existing multi-repo issue, reproducible without any retention override — a seeded local repo plus one remote repo shows the same misroute). The spec now budgets across three worktrees of the ONE connected repo, created through the product createWorktree path (an external git-worktree-add only lands as a detected worktree needing adoption) and polled through the relay's transient post-connect reconnect window. Verified green on the local Docker lane in 20.8s. Co-authored-by: Orca <help@stably.ai> * fix(terminal): prevent remount thrashing during post-measure cool-down ( Implements the C1 retention contract: preserve worktree `hiddenSinceMs` through a background-measure window (so TTL/ranking stay honest), but re-park waits for a full `coldParkDelayMs` cool-down after the measure ends. Without the cool-down, every ~3s measure lease on a past-deadline worktree thrashes remount/reattach. Core changes: - Terminal.tsx: add measure clock (measuringTerminalWorktreeIdsRef) and post-measure cool-down tracking (terminalWorktreeParkCooldownUntilRef); gate parking candidates until cool-down expires. - Extract snapshot replay choreography to shared terminal-snapshot-replay-paint.ts (used by SSH reattach + daemon restore paths). - Add SSH model snapshot timeout (750ms) with fallback to relay replay. - Move cold-park recheck deadline logic to terminal-cold-park-recheck-deadlines.ts; add cool-down deadline to scheduling. - useTerminalTabColdParking: implement matching measure-clock contract with per-tab cool-down gate to keep tab deadlines synced with worktree retention clock. - Add resolveTerminalMountScrollbackRows() to demote new xterms under demoted worktrees (pane births during demotion must take the demoted tier at create). - Add kill switches: terminalSshViewParking, terminalHiddenWorktreeRetentionBudget, terminalHiddenScrollbackDemotion. * fix(terminal): detect Command Code completion in parked mid-turn panes Seed the byte watcher with in-flight turn state from agent status: the watcher is recreated per park cycle with no startup command to arm it, and the banner scrolled away before parking. Also memoize eviction-exempt checks and use SSH PTY ID builder in tests. * fix(terminal): flush pending command-code settles on reveal remount When a parked pane reveals mid-Command Code turn, the new detector cannot re-observe the already-passed idle composer. Cancelling the settle leaves the row stranded at 'working', so dispose now flushes the pending settle instead. Extract readInFlightCommandCodeTurn to shared space and seed detectors with in-flight turns so remounts complete mid-flight commands. Also memoize SSH model probes to prevent double timeouts on reattach. * fix(terminal): remove scrollback demotion (C1 slice C) The scrollback demotion feature for eviction-exempt hidden worktrees is no longer needed. Retention budget limits are now sufficient without this additional bound. Remove the terminal-hidden-scrollback-demotion module, the selectScrollbackDemotedTerminalWorktrees function, and related per-pane demotion logic. * test(terminal): assert bounded probe during stalled reveal Add assertion to verify that a stalled reveal operation makes exactly one `getMainBufferSnapshot` call, ensuring retry logic doesn't introduce redundant probes that would extend the timeout window before relay fallback. * fix(terminal): implement C1 retention budget for hidden parked worktrees Addresses OOM regressions in hidden parked terminals by force-evicting worktrees past a retention budget: at most 12 mounted while hidden, none past 45 minutes (absolute, not exempted by last-active). Eviction is least-recently-hidden-first. Exempt tabs (unrestorable local PTYs) keep their panes to avoid orphaning shells; worktrees are force-parked even if they contain exempts, and their buffers released elsewhere. SSH/remote worktrees serialize buffers pre-eviction for reveal; local worktrees keep daemon snapshots. Command Code's done-settle window is transferred across park/reveal boundaries so the row cannot strand at 'working'. Model probe on SSH reattach is scoped to park-reveal only, not ordinary reconnects. Includes new E2E suite proving the budget actually releases memory. * memoize eviction-exempt terminal tabs to avoid redundant store reads Each tab's exemption check re-reads the store and walks the layout tree. Introduce selectEvictionExemptTerminalTabIds() to resolve all exempt tabs for a worktree in a single pass, then memoize the result in Terminal.tsx and useTerminalTabColdParking. This prevents O(n) store reads when checking exemptions across multiple tabs and ensures the set remains stable across unrelated re-renders. * refactor: reformat hidden-worktree retention comments Reflow to 80-character lines and remove internal ticket references (C1, C1 slice C). * fix(lint): split overlay slot and eviction-exempt tabs under max-lines Static analysis failed because TerminalPaneOverlayLayer (401) and terminal-parked-tab-watchers (304) exceeded oxlint max-lines. Extract the slot component and eviction-exempt helpers into dedicated modules. * test(terminal): stabilize retention budget e2e control arm Stage un-parkable remote pty ids only after both worktrees are hidden, and keep re-staging during the control-arm poll so a late updateTabPtyId cannot flip the decoy back to park-restorable and ordinary-park it before budget engages. * test(terminal): pin retention e2e decoy to a mounted pane snapshot Use the active pane-identity snapshot for the decoy tab instead of all worktree tabs, and re-assert un-parkable ids after the control-arm hold so a deferred/empty tab id cannot fail the budget-off mounted-count check. * fix: memoize terminal eviction exemptions on layout leaf PTYs Splits add leaf panes to the layout store without changing the tabs array. A memo keyed only on tabs misses this change, leaving new panes unexempted for unmount. Include layout leaf PTYs in the exemption memo key so it recalculates when splits occur or PTYs are re-minted. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
e549dd3ef4 | fix(agents): make pane retention transfer-aware instead of suppressor-based (#11310) | ||
|
|
b4b3bcdb84 |
perf(vault): look up resume worktrees through the shared index (#11317)
Resolving a session's resume target scanned every worktree in every repo: two `Object.values(worktreesByRepo).flat().find(...)` calls, which also allocate a fresh 1124-element array each time, plus an equivalent `some()` walk. All three run per visible session row, so a panel render repeated them ~20 times. `getIndexedWorktreeMap` already exists for this and is WeakMap-cached on `worktreesByRepo`, so the index is built once per store snapshot rather than per call. `connection-owner-resolution.ts` already resolves worktrees this way. ~1.6ms -> ~0.002ms per render pass at 1124 worktrees. Net -4 lines. Behavior is unchanged: the map dedupes by id, which only diverges from `find()` when one id appears twice with different objects. Worktree ids are `repoId::path`, so a duplicate id within a repo array — the documented race the index was built for — refers to the same worktree. |
||
|
|
6cc579a48c |
perf(vault): dedupe scope paths by key instead of rescanning (#11314)
* perf(vault): dedupe scope paths by key instead of rescanning #11303 took the session maps off the workspace-switch path, but scope path derivation still follows the active worktree and stayed quadratic. addAiVaultWorkspaceScopePath deduped by re-normalizing every already accepted path on each insert, so accepting K paths cost O(K^2) normalize('NFC') calls — ~632k at 1124 workspaces. isAiVaultWorkspaceScopePathClaimed separately rescanned every live worktree per prior id, and runs twice per switch via activeWorktreePaths and scopePaths. - carry a Set of comparison keys alongside the paths, so each insert is one normalize plus one Set lookup - thread that accumulator from the workspace pass into the project pass rather than restarting deduplication against a plain array - build one comparison-path -> worktree id map for the claim check, keeping first-writer-wins to match the previous some() short-circuit deriveAiVaultScopeSessionPaths on a real 1124-workspace profile: 189.7ms -> 0.8ms. Output is unchanged, including ordering: verified against the previous implementation across 117 scenario/option combinations covering monorepo and mixed-repo layouts, priors both claimed and unclaimed, duplicate paths, NFD/NFC, WSL UNC, trailing and doubled separators, relative and blank paths, and four project-key shapes. Adds the first test file for this module: scope semantics (priors, claimed priors, cross-repo rejection, dedupe, NFD/NFC) plus a timing guard. Verified fail-first — the guard reports 220ms on the previous implementation. Path length is chosen deliberately, since normalize() cost scales with it and short synthetic paths understate the old shape. * fix(vault): make the claim check independent of worktree ordering Review catch on the first pass: keying claims by comparison path meant a duplicate path had to pick one owner, and picking the active worktree masked a real claimant later in the list. Concretely, with the active worktree also listed at its own prior path, the prior was reported unclaimed where the previous some() reported it claimed. Excludes the active worktree while building the set instead of comparing ids at read time, so any surviving entry is a claim by construction and ordering cannot decide the result. Adds a test over four orderings, verified fail-first against the previous commit. Also adds a timing guard for deriveAiVaultWorkspaceScopePaths, which the session-scope guard did not cover. Equivalence rerun against the pre-optimization implementation: 156 scenario/option combinations, identical paths and ordering. |
||
|
|
d07931c4c2 | fix(mobile): keep host action drawer close stable (#11306) | ||
|
|
c5102e1262 |
test(vault): guard the workspace-switch regression #11303 fixed (#11311)
#11303 removed the O(sessions x roots) path normalization from the session worktree map, but nothing fails if that shape comes back. Adds the two checks that were missing, plus the tool that would have caught it. - timing guard: 1200 worktrees x 400 sessions must build in <150ms. Verified fail-first — restoring the pre-#11303 per-session buildWorktreeCandidates call takes it to 238ms; it is ~15ms as merged. - path boundary: '/repo/alpha-sibling' must not be attributed to '/repo/alpha'. Hoisting the root normalization out of the loop must not degrade containment into a bare startsWith. Also adds tools/benchmarks/workspace-switch-paint-latency.mjs, which attaches over CDP and measures first-paint-after-click and max frame gap. The existing worktree-switch-responsiveness.spec.ts only times the synchronous click task, which stays ~1ms because the highlight is a direct DOM mutation — that is why a ~1s stall could ship without tripping a budget. On the affected build it read maxFrameGap p50=973ms. |
||
|
|
9cf31bdc00 |
perf(vault): stop rebuilding session maps on every worktree switch (#11303)
Switching worktrees rebuilt two ~500-entry maps in the Agent Session History panel because their memo deps included the active repo/worktree, which the maps never read; the worktree map also rebuilt ~530 path candidates (and re-normalized every root) per session, ~255k isPathInsideOrEqual calls per switch. - Drop activeRepo/activeWorktree from the sessionProjectById memo via buildAiVaultSessionProjectById, and activeWorktreeId from useAiVaultSessionWorktreeMap; 'current' is now stamped per row at read time (withAiVaultCurrentWorktreeStatus), so switches reuse both maps. - Hoist candidate building out of the per-session loop and precompute a normalized-root matcher per candidate, so map rebuilds on data changes are O(sessions + roots) normalizations instead of O(sessions x roots). NFC folding from #10841 is untouched; non-ASCII (NFD/CJK) matching is covered by new tests. Warm switch with the panel on All/500 drops 425ms -> 125ms on the full-scale rig (cold 491ms -> 193ms); panel-closed switches are unchanged. |
||
|
|
1df8aa5605 |
fix(dashboard): give the agent preview terminal a real pane's keyboard (#11015)
* fix(dashboard): give the agent preview terminal a real pane's keyboard The dashboard's preview terminal is a bare xterm, not a pane, so it never ran `resolveTerminalShortcutAction` — its only custom key handler covered copy/paste and IME. Ctrl+Backspace therefore fell through to xterm's default `\x08`, which readline binds to backward-delete-char: one character instead of a word. Route the preview's keys through the pane's own shortcut policy, so word and line kills, Option chords, modified Enter, and scrollback chords encode identically. Pane-scoped verdicts (splits, search, focus) are swallowed rather than passed to xterm, which would send e.g. Ctrl+Shift+D as a bare Ctrl+D. The policy needs three things the preview could not see: - kitty-protocol flags — mirrored locally from the same PTY output stream - the PTY's execution host — bytes follow the host, not the client OS, so a new `DashboardCard.terminalInput` profile is derived in the main renderer (the only one holding the store) and relayed to the pop-out - host terminal options — the ConPTY backend and the kitty withhold now apply Also brings the emulator itself up to a pane's: Orca's Unicode 11 width shim (replayed CJK/emoji/ZWJ laid out wrong without it), Windows Ctrl+Alt chord repair, user font/cursor/line-height/word-separator/sensitivity settings, ligatures, the TUI wheel multiplier, lazy Arabic shaping, clickable links, and the IME candidate anchor — extracted from pane-lifecycle so both surfaces share one implementation. The in-window drawer built its snapshot from a slice subset, which would have degraded the new profile to client-OS defaults there; it now reads the full store non-reactively. * fix(dashboard): enumerate pane-scoped chords instead of a default case The switch-exhaustiveness gate rejects a `default` over the shortcut-action union — it would let a newly added action be swallowed silently instead of forcing a decision at the preview's boundary. * fix(dashboard): preserve native shortcuts and PTY host routing * chore: keep merge scope limited to dashboard * perf(dashboard): avoid full-store copies for terminal profiles * fix(dashboard): validate terminal input profiles at IPC boundary * fix(dashboard): sync preview terminal refs on commit, not during render react-compiler rejects ref writes in the render pass; every reader is an event handler or a post-await continuation, so a commit-phase sync is equivalent. * test(dashboard): cover the three seams that relay the host-input profile Reverting any of them left every suite green: the dialog's terminalInput prop (the only reader of DashboardCard.terminalInput), the drawer's hand-threaded store slices, and the pop-out's republish triggers. Each new assertion was mutation-checked against its source line. * fix(dashboard): follow the WSL host for a preview terminal's byte routing The card resolver handed resolveTerminalInputHostPlatform a transport with no getLocalSessionMetadata, so a WSL pty on a Windows client resolved to win32 while its own pane resolves linux — Shift+Enter would then encode CSI-u where the pane sends alt-enter. Mirror the pane transport's own gate. * fix(dashboard): republish on every slice the host-input profile resolves from The compare set covered 4 of the ~11 slices that decide a card's execution host, so a change to the rest (folder workspaces, project groups, the runtime catalog, detected worktrees) never triggered a publish. On a quiet board there is no later publish to heal from, and the pop-out — which cannot re-derive the profile — keeps encoding bytes for the host the pty used to run on. * fix(dashboard): sync preview terminal refs on layout, not on a passive effect xterm's keydown is a native listener, so React never flushes a passive effect before it. A just-relayed host profile could therefore miss the next keystroke. * test(dashboard): pin the preview's replay-vs-live kitty scan The existing A/B passed either way: a lone CSI > u sets the same flags through scan and scanReplay. Redeliver the push across a snapshot and its replay so stack semantics would leave the TUI's single pop on a stale frame. * fix(dashboard): rebuild the drawer's snapshot on every host-input slice The in-window drawer read ~12 host slices through getState() while subscribing to none of them, on the premise that agent activity drives the next rebuild. A quiet board has no such rebuild: an SSH handshake completing with every agent idle leaves the preview terminal encoding bytes for the pre-connect host, and agentStatusEpoch only ticks on live status changes so it never heals. Watch the same set useDashboardPopoutBridge republishes on — each writer bails out when nothing changed, so the added deps are far quieter than agentStatusByPaneKey, which already rebuilds this memo on every status ping. The existing coverage mounted a second hook, which always recomputes; the new test re-renders the same hook after changing only sshConnectionStates. * fix(dashboard): key the preview by the user's terminal shortcut policy The preview passed 11 of the 12 inputs the pane's policy takes and let the 12th default to orca-first. Under terminal-first a remapped tab.close chord is meant to yield to the shell — Ctrl+W is a word-kill there — but the preview kept claiming it as a pane close and swallowed the bytes. * test(dashboard): pin the host-input profile to card snapshots only The count path main added in #11042 renders no cards, so it must not pay a per-pty host resolution on every agent-status tick. Both assertions run against a card that does have a live pty, so only the gate keeps the profile off. * refactor(dashboard): extract the board's client-host read The merge of main's label bounding pushed build-dashboard-snapshot.ts to 302 lines. The client's own platform facts are a distinct concept from the pty host each card keys against, so they move out rather than earn a max-lines bypass. |
||
|
|
aeeae53b1f |
fix(build): stop pnpm -r from crawling the mobile workspace (#11291)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
adc10cd21a |
fix(explorer): refresh tree on create/rename with case-tolerant cache keys (#10392)
* fix(explorer): refresh tree on create/rename with case-tolerant cache keys Windows watchers can emit paths whose casing differs from the worktree dirCache key, so create events never refreshed. Also apply rename events immediately by refreshing the parent listing instead of ignoring them. * fix(explorer): reconcile Windows update-only creates * fix(explorer): bound watcher update reconciliation * fix(explorer): index watcher cache paths * fix(explorer): avoid expanded directory rescan * fix(explorer): batch watcher subtree purges * fix(explorer): preserve Windows drive roots --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
afbd98d8a4 |
Support Windows drives in the remote host filesystem picker (#7439)
* Support Windows drives in the remote host filesystem picker
The remote picker was locked to the system drive on Windows hosts: the
breadcrumb root resolved to C:\ and typed drive paths (M:\dev) were
treated as filter text, so projects could only ever be created on C:.
- Server: answer host-root browses ('/') on win32 with the mounted
drives instead of resolving to C:\.
- Client: recognize drive-anchored input (M:\, M:/, m:) as path mode,
resolve segments from the normalized drive root, and make
joinPath/parentPath/breadcrumbs drive-aware. Up from a drive root
returns to the host root (the drive list).
Fixes #7438
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Document why joinDrivePath uses a literal backslash
Review feedback suggested path.win32.join, but the renderer bundle
imports no Node builtins anywhere and runs sandboxed, so path.win32 is
not available here. The backslash targets the remote Windows host
regardless of client OS; say so at the call site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Complete Windows drive browsing over SSH
* fix remote Windows drive browsing
* fix(ui): key remote breadcrumbs by path
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
|
||
|
|
3f53287554 |
fix(mobile): accept WebSocket pairing addresses (#9912)
* fix(mobile): accept websocket pairing addresses * fix(mobile): align manual pairing address validation * docs(mobile): correct custom address grammar comment * fix(mobile): enforce pairing endpoint size limit * fix(mobile): reject canonical IPv6 wildcard addresses * fix(mobile): handle unscannable pairing offers * fix(mobile): reset custom address dialog on close --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
6107789c97 |
Fix WebSocket fallback for reserved Windows ports (#7185)
* Fix WebSocket fallback for reserved ports * fix(runtime): narrow reserved-port fallback --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
af2972b3a9 |
fix(mobile): declare happy-dom so terminal-webview tests run standalone (#11238)
mobile/src/terminal/terminal-webview-{tap-routing,init-surface}.test.ts
request the happy-dom vitest environment, but happy-dom was only declared
at the repo root. The mobile suite resolved it by walking up into the root
node_modules, so `cd mobile && pnpm install && pnpm test` fails with
ERR_MODULE_NOT_FOUND and loses those 12 tests unless a root install
happens to be present.
|
||
|
|
76b6c137c6 |
fix(orchestration): sanitize legacy formatted JSON (#11263)
* fix(orchestration): sanitize legacy formatted JSON * fix(orchestration): harden legacy message formatting --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
d9fec8fd61 |
fix(updater): report releases still being published (#8914)
* fix(updater): distinguish releases still publishing (#8869) * fix(updater): preserve verified releases during probe outages (#8869) * test(updater): expect publishing copy for perf checks (#8869) * fix(updater): keep transport failures out of publishing copy (#8869) * fix(updater): preserve channel and feed fallback semantics (#8869) * test(updater): prove feed and asset failure boundaries (#8869) * test(updater): cover unavailable manifest probes (#8869) * fix(updater): fence publishing copy to stable releases (#8869) * fix(updater): preserve nudge deferral across release channels (#8869) * fix(updater): retain benign nudge handling for probe outages (#8869) * test(updater): preserve channel and transport proof fidelity (#8869) * test(updater): model asset HTTP status in feed fixtures (#8869) * fix(updater): preserve legacy prerelease probe handling (#8869) * test(updater): cover unavailable publishing-window nudge retention (#8869) * test(updater): prove publishing retry and channel cases (#8869) * fix(updater): preserve truthful readiness states * fix(updater): type release preflight failures * fix(updater): keep probe outages truthful * fix(updater): keep not-ready diagnostics neutral --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
3e4abef089 |
fix(settings): keep local WSL settings scoped to the desktop host (#9635)
* fix(settings): scope local WSL settings to the desktop host * fix(settings): verify local WSL capability ownership * fix(settings): respect capability host ownership * fix(settings): isolate paired host capabilities * fix(settings): key web capabilities to paired host --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
9150ac65cb |
Fix Windows setup sequencing wrapper quoting (#8806)
* fix(setup): correct Windows sequencing wrapper quoting * test(setup): preserve spaced Windows batch paths * refactor(setup): dedupe PowerShell encoder, clarify wrapCmd comment Route the Windows setup-sequencing and Hermes startup planners through the shared renderer-safe encodePowerShellCommand instead of two verbatim btoa copies, and make that shared encoder renderer-safe (Buffer is unavailable in the sandboxed renderer where both planners also run). Reword the wrapCmd comment so it describes the current single-outer-quote behavior instead of the old quote-doubling bug. * test(setup): cover Windows metacharacter paths * fix(setup): keep Windows runner paths out of cmd source * test(setup): preserve Windows setup failures * docs(setup): explain safe cmd path handoff --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
c6c6c71196 |
fix(opencode): use cross-platform data directory (#10362)
* fix(opencode): use cross-platform data directory * fix(opencode): honor in-memory database override * fix(opencode): harden database discovery coverage * test(opencode): reproduce Windows session discovery --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
c8dba6d72c | release: v1.4.160-rc.5 v1.4.160-rc.5 | ||
|
|
0660ad9d6e |
fix(orchestration): reject legacy mail acknowledgment (#11227)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
5c59c84c7a |
fix(plugins): close four trust-boundary holes in the plugin system (#11232)
* fix(plugins): close trust-boundary holes in the plugin system
Move five security decisions to their chokepoints rather than leaving them
enumerated at individual call sites.
- Kill-list revocation reaches content packs: PluginContentPackRegistry now
takes an isKilled predicate and intersects it with any caller-supplied
approval, so a killed plugin's VM recipes can no longer reach
spawn(..., { shell: true }) through either reconcile() call site.
- Bound kill-list generatedAt to a 24h future skew at the parse chokepoint.
A far-future timestamp previously made every genuine later list look
"older" and disabled revocation permanently, persisted across restarts.
- Protect the whole auto.components.settings.Plugin* translation subtree
instead of an enumerated prefix list, so language packs cannot forge the
consent provenance badge or rewrite install-error security copy.
- Resolve manifest panel icons by own-key only; "constructor"/"__proto__"
previously yielded non-component prototype members that crashed the
right sidebar to its error boundary.
- Give panel liveness frames a reserved control budget so a panel that
saturates its action budget can still answer the watchdog.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): keep the kill-list future bound off the cache read path
The schema-level generatedAt bound re-judged the on-disk cache against the
device clock at every launch, so a client whose clock ran behind the last
genuine publication discarded its whole cached kill list and started with
zero revocations. Move the bound to the two fetch chokepoints instead.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): remove the reserved-lane starvation window and the revocation TOCTOU
Review follow-ups on the trust-boundary fixes:
- The reserved liveness lane had a per-window count equal to the ping
interval, so a panel's own pong-shaped traffic could spend it and drop
the next genuine reply — reintroducing the starvation the lane exists to
prevent. The lane is now size-bounded only; rate stays bounded because
every pong is also charged to the data budget.
- Only schema-valid pongs take the lane now, so near-miss pong-shaped junk
cannot drain it. readPanelPongId replaces the zod parse on this
guest-controlled path (a rejected safeParse allocates an issue list, ~90x
the accepted-path cost) and is pinned to the schema by a parity test.
- Re-read the kill list inside approveAtomically: approvedKeys is snapshotted
before an awaited verification phase, so a plugin killed during that wait
could still publish VM recipes and language packs.
- Assert the curated icon resolves to FileText; the old equality also passed
when both sides fell back to Plug.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): match zod's safe-integer bound in the pong reader
readPanelPongId used Number.isInteger, but zod's .int() rejects anything
above 2**53-1, so pingIds like 1e100 took the reserved lane the schema
would have refused. The parity test never probed that boundary.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
3a67186623 |
fix: stop notification loss, credentialed cache reuse, and clipboard clobber (#11230)
* fix: stop notification loss, credentialed cache reuse, and clipboard clobber Mobile catch-up (#8591): fetchMissed swallowed the RPC failure while deliverLive kept advancing and persisting lastDeliveredSeq, so the next successful catch-up asked from above the abandoned range and the desktop cut it. Sessions are module-scope, so an unchanged epoch never resets it. Quarantine the watermark at the last contiguously-delivered seq and hold it there until some later catch-up actually drains — not just one retry. A batch cut short by a teardown quarantines at the last event it settled. Jira attachment cache: currentEpoch summed two independent counters, so a site at siteEpoch 1 read the same value before and after a global clear. The mid-flight guard passed and re-inserted credentialed image bytes that "disconnect all" had just purged — resident for the process lifetime since pruneExpired has no timer. One monotonic ticker, compared by max. Web copy fallback: the handler registered in the capture phase, so xterm's bubble-phase listener overwrote text/plain with the terminal selection afterwards; served was already true, so the copy reported success. Every Orca copy affordance over plain HTTP (Copy Pane ID, Copy Path, commit SHA, PR URL) pasted the terminal selection. Bubble phase with stopImmediatePropagation. Covers the secure-context retry branch too, which shares the same helper. * fix: roll back the persisted watermark on catch-up failure; cover stopImmediatePropagation Adversarial review of a98d7f4d5d found two gaps. 1. The quarantine clamped only writes made AFTER the failure. getMissedSince waits up to 30s, so a live event routinely persists a higher seq while the request is still outstanding; that value stayed on disk, and the next launch read it back and resumed past the abandoned range -- the original bug, reached through a restart. quarantineCatchUpWatermark now re-persists the clamped seq, so the stored value never outlives the gap it guards. 2. web-clipboard-copy-terminal-selection's second test registered its "late" document handler BEFORE the fallback's, so it lost on registration order alone and stopImmediatePropagation was never exercised -- the test passed with that line deleted. Bubbling reaches the document before the window, so a window-level listener is what actually requires it. * fix(mobile): mark a notification seen only once its show lands A pre-marked seen key made a rejected show unrecoverable: the next catch-up re-fetched the seq and the dedup guard dropped it, and the first later event to drain the batch lifted the quarantine past it. Also contains the rejection so it does not escape the un-awaited 'ready'/live handlers as an unhandled rejection. Co-authored-by: Orca <help@stably.ai> * test(web-clipboard): pin stopImmediatePropagation with a same-target handler Both existing cases passed with plain stopPropagation, and with the listener back in the capture phase — neither half of the fix was actually pinned. The window-level clobber is on a different target, so stopPropagation suppresses it too. Registering the clobber on the document, ordered after the fallback's own listener, is the only shape stopPropagation cannot cover. Addresses the review comment posted after the last commit. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
681c4ba458 |
fix(skills): stop calling the updater's own install a modified copy (#11249)
After a successful headless update the CLI installs source-repo HEAD,
which legitimately runs ahead of any shipped bundle. The scan classified
those bytes 'unrecognized', so the row went amber ('may be modified…
remove it') seconds after our own Update button ran, and the advice
looped: remove + reinstall lands the same newer content.
Scan half: a canonical/alias placement whose observed git tree sha
equals the updater lock's skillFolderHash is the CLI's own install, not
a user edit — reclassify it 'newer-known'. Display half: 'newer-known'
is recognized official content ahead of this build with nothing to fix,
so it no longer marks the copy blocked. Eligibility is deliberately
unchanged: ahead of the bundle means there is nothing this build can
update to, and offering one risks the provably-unperformable update
(#11110) when source HEAD still equals the lock.
Copies whose sha does not match the lock, copies with no lock entry,
same-name copies outside the placements the command writes, and
plugin-cache behavior all stay flagged exactly as before.
|
||
|
|
1d7e7656e3 |
fix(ui): preference sync, picker arming, zoom, chat status, and reverted locales (#11241)
* fix(ui): preference sync, picker arming, zoom, chat status, and reverted locales 7.1 ui.set rejected whole preference payloads on enum drift. The new AssertNoMissingKeys guard is key-only, so it could not see that LegacyWorktreeCardProperty omitted 'cli' (in DEFAULT_WORKTREE_CARD_PROPERTIES) or that rightSidebarTab omitted 'workspaces'/'pr-checks' and every plugin tab. UiUpdate is .strict(), so one bad value failed the entire batch and silently dropped sidebarWidth/groupBy/sortBy/filterRepoIds riding the same debounced write. Both enums now derive from the shared unions, AssertNoMissingValues catches value drift by name, and UiUpdate drops an unknown value instead of rejecting the batch around it. Unknown KEYS still reject. 7.2 The SSH shell-ready fallback moved from first-output to spawn, so a remote shell needing >1.5s to prompt got the bracketed-paste startup command before readline armed it, with no recovery afterward. The short deadline now applies only once output proves the shell is talking; a silent-since-spawn shell gets a longer budget and still delivers eventually. 7.3 The project picker armed in rank order but rendered in section order, so with a folder group present the BOTTOM row was armed on open and Enter created the workspace in the wrong place. Row keys now derive from the same sections that render. The folders bucket also gains the recent-exclusion guard the projects bucket has; that duplicate was unreachable, so this is symmetry, not a live bug fix. 7.4 setBrowserPageZoomLevel now compares before writing, so a pane reasserting a level the host already holds no longer emits a redundant host-wide HostZoomMap write. The user-applied level also moved to a module-level map keyed by page id: the guest webview outlives its React pane, so the pane-local ref re-seeded from the shared Settings default on every remount and let a later default retroactively hijack an already-zoomed tab. See PR notes on the part of this finding that could not be fixed as prescribed. 7.5 A non-null sessionId short-circuited the live-work escape hatch, forcing 'loading' over hook 'working' and rendering an idle pane mid-turn: Send instead of Stop, no typing indicator, no streaming preview. Status stays 'working'; the empty-transcript loading SURFACE moves to selectNativeChatViewState, which keeps 7.6 #10770 merged from a base predating #8549, reverting 182-187 translated strings per locale to English (es 182, ja/ko/zh 187) plus en.json's recipesHelp. Restored by script, only where the English source is unchanged between the two shas, so later legitimate edits are preserved: 0 keys added or removed, every value sourced from |
||
|
|
a721125d06 |
fix(perf): correct three 07-27 perf regressions (#11234)
* fix(perf): correct three 07-27 perf regressions Traversal capacity cap no longer scales with worker concurrency (#11026). retainWorkspaceSpaceScanEntry charged a traversal-wide entry counter, so N workers each holding a listing multiplied the live charge. At concurrency 48 a 48x2,100 tree (100,848 entries) hit the 100,000 cap while 100x1,500 (150,100 entries, 50% more) passed, and scanLocalWorktree treats the capacity error as terminal, reporting an intact worktree as "Unavailable" with sizeBytes 0. The cap is now per directory listing -- the only quantity fixed by directory shape -- restoring the invariant docs/workspace-space-scan-resource-bounds.md already states. Aggregate live retention stays bounded by the unchanged 64 MiB byte cap. Note: releasing each entry's charge at dispatch (the originally suggested fix) was measured and does not help; the peak is set at admission, before any entry is dispatched. Repo image icons are no longer fully base64-decoded on every snapshot publish (#11012). sanitizeRepoIcon reached decodeBase64Prefix, which sized its buffer to the whole payload to read a 24-byte header, running synchronously inside ipcMain.handle at a 250 ms throttle. Validation is now memoized on source+src in a BoundedMap. Measured for 10 icons x 256 KB: 37.34 ms -> 0.67 ms per publish. One over-long card label no longer discards the entire snapshot (#11012). isDashboardSnapshot was all-or-nothing and dashboard-popout returned early with no log while replaying lastSnapshot, so `orca terminal rename --title "<1025+ chars>"` froze the pop-out board on its last good paint with nothing surfaced. Labels are truncated at the producer, the validator drops only the offending card, and both the rejection and the drop are logged. The bound now lives in the shared snapshot contract so producer and validator cannot drift. Co-authored-by: Orca <help@stably.ai> * fix(perf): charge a scan listing's parent path once, not per entry The 4.1 fix made the entry cap per-listing but left the 64 MiB byte cap charging parentPath.length for every entry in a listing. Because a listing's entries all share one parent-path string, that multiplied the path by the directory's width, so the byte cap measured checkout depth rather than live heap. The reported symptom therefore still reproduced at the production default limits: 48 x 2,100 @ concurrency 48 raised a capacity error once the worktree path passed ~58 characters, while the same layout at concurrency 1 succeeded. The shipped regression test could not see this because it passes maxRetainedBytes: Number.MAX_SAFE_INTEGER, disabling the only cap still in play. Measured at a real 65-char worktree root, 3 of the report's 4 documented layouts still failed. The parent path is now charged once per listing, with its first entry, so an empty listing strands no charge. Per-entry overhead is unchanged at 512 B + name, which still dominates the estimate, so the OOM protection the original PR added is preserved. Adds a production-default-limits case covering the report's layouts under a deep root, plus an assertion that a short and a deep root reach the same verdict -- the path independence docs/workspace-space-scan-resource-bounds.md requires and which no existing test enforced. Co-authored-by: Orca <help@stably.ai> * fix(perf): prove the icon cache by decode count, not wall clock The caching test asserted a per-publish millisecond budget, which failed on CI at 5.64 ms against a 5 ms ceiling. Any threshold flakes on a loaded box, so count real sanitizeRepoIcon entries instead: 10 repos x 20 publishes is 200 icon checks against exactly 1 decode. Added cases pin the cache key (payload and source both re-decode; a cached image verdict never answers for an emoji) and that a rejection is cached too. Also drops budget.entries, which the per-listing cap left as a traversal-wide counter no check reads -- exactly the shape a future guard could reintroduce the concurrency bug from. Co-authored-by: Orca <help@stably.ai> * fix(dashboard): bound the project filter label the whole board rides on #11042 added snapshot-level filterOptions whose project labels are repo.displayName -- the same unbounded source this PR already bounds for card.repoName, but one level up where dropping a card cannot recover it. An over-long project name would fail isDashboardFilterOptions and take the entire snapshot with it, which is the exact frozen-board failure the per-card drop was added to end. Workspace-status labels are already capped at 32 by workspace-statuses.ts, so only projects needed this. Co-authored-by: Orca <help@stably.ai> * fix(dashboard): disambiguate the repo icon cache key The memoization key joined `source` and `src` with a space, but the sanitizer's base64 pattern admits whitespace inside a valid `src`. A rejected icon can therefore split the same concatenation differently and inherit an accepted icon's cached verdict, reaching the pop-out's `<img src>` without ever being sanitized. Length-prefix the source. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
2db02562b6 |
fix(skills): tell the user how to fix a skill the updater cannot converge (#11248)
Re-lands #11129, which was merged into #11128's branch rather than main and so never reached main. Content is identical to the reviewed and live-QA'd head |
||
|
|
bb9ae78136 | fix(macos): show TCC notice after first prompt (#11243) | ||
|
|
f790d9cbe8 |
fix(skills): stop the skill review dialog contradicting the badge that opens it (#11128)
* fix(skills): stop the skill review dialog contradicting the badge that opens it A skill whose only fault was an edited copy or one Orca could not read turned the setup-rail badge amber and offered Details — and Details opened a dialog headlined "All installed Orca skills are up to date." over an empty list. The badge says something is wrong, the dialog it points at says nothing is. The grouping only returned skills with an out-of-date copy, so those two states produced no row and the summary fell through to the all-clear headline. Include a skill when a copy needs attention as well, using one shared predicate so the badge and the dialog cannot disagree again. A plugin's own copy of a same-named skill stays out: that is the vendor's, not the user's drift. * test(skills): pin that a routine outdated copy raises no attention marker |
||
|
|
747b241145 |
feat(main): record main-thread hangs so we can measure them (#10256)
A deadlocked main thread never crashes, so it leaves no crash report and no artifact — incidence has been unmeasurable (n=1 confirmed, macOS 26.5.1, FB24004458 / electron#52437). This forks a plain-Node watchdog sibling under ELECTRON_RUN_AS_NODE that survives the deadlock, listens for a 2s heartbeat, and after 45s of silence writes a marker to userData. The next launch consumes it, records a durable crash breadcrumb, and emits a main_thread_hang_detected telemetry event carrying unresponsive_ms and self_recovered. Observes only — it never kills or relaunches the parent. A true positive recovers nothing force-quitting wouldn't, while a false positive would SIGKILL a live main thread mid-write. self_recovered counts exactly the stalls such a killer would have gotten wrong, so recovery can be built on evidence if the field numbers justify it. macOS-only, packaged-only (ORCA_HANG_WATCHDOG_FORCE=1 to test), with sleep-gap suppression and idempotent shutdown on will-quit. |
||
|
|
a6423d565b |
fix(macos): prevent stale UI surfaces after wake (#11226)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
638c3ca5d9 |
fix(agent-status): prevent ghost sidebar row on completed split-pane detach (#10698)
* fix(agent-status): prevent ghost sidebar row on completed split-pane detach Detaching a done-state split pane into its own tab migrated the agent paneKey from oldTab:leaf to newTab:leaf. useRetainedAgentsSync only saw the old key vanish and, finding no suppressor, resurrected it as an unclickable duplicate sidebar row (and inflated the worktree count). Plant a one-shot retention suppressor on the source key during transferAgentPaneAuthority, but only when the source actually held a live agent, so a suppressor is never leaked for a pane that had none. Fixes #10675 Co-Authored-By: Claude <noreply@anthropic.com> * fix(agent-status): annotate suppressor record type and condense retention comments Type the migrated retentionSuppressedPaneKeys as Record<string, true> so a computed-key `true` isn't widened to boolean, which broke the web typecheck. Also condense the retention rationale comments per review. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d3681f6306 |
fix(runtime): surface desktop RPC startup failures (#11037)
* fix(runtime): surface desktop RPC startup failures
* fix(runtime): isolate RPC failure telemetry
* fix(runtime): satisfy the changed-code quality gate and kill vacuous dialog tests
The `no-floating-promises` label span covers the whole `app.whenReady().then()`
callback, so adding lines inside it made a long-standing finding overlap changed
code. `void` is the linter's own suppression; no `.catch()` on purpose.
The startup-failure tests were vacuous: mutation runs showed the wait-for-show
deferral, the destroyed-window guard, the `closed` companion event, listener
cleanup, the cause walk, the cycle guard, and the truncation bound could all be
deleted with every test still green. The "not called yet" assertion ran before
any microtask, so it passed either way.
* test(runtime): de-brittle the desktop RPC-failure source assertions
Anchoring the slice on the full destructure and matching the whole dialog
call expression made an innocuous rename break the test with a cryptic
'expected -1'. Match the shape that is actually the contract instead.
* test(runtime): repair the silently-unbounded desktop startup slice
The desktopEnd anchor comment lost a word in
|
||
|
|
25da91d653 |
perf(dashboard): stop re-sending repo icon data URLs on every republish (#11089)
* perf(dashboard): stop re-sending repo icon data URLs on every republish #11012 put repo icons on the dashboard snapshot keyed by repoId. Image icons are data URLs capped at MAX_REPO_ICON_DATA_URL_LENGTH (400KB) and every repo contributing a card ships one, while the snapshot republishes up to 4x/sec (PUBLISH_THROTTLE_MS = 250) for as long as the pop-out is open. Icons change about never, so that structured-clones megabytes per second across the window boundary for bytes the pop-out already has. Publish the map only when it actually changed, comparing by reference since icons come off immutable store repo records. The two paths where the pop-out could be starting from nothing — it opened, or it mounted and asked — still force a full send, so the retained copy can never be the only one. The pop-out keeps the last map it was given when a republish omits the field. An explicitly empty map still clears, so removing an icon works. repoIconsByRepoId was already optional on DashboardSnapshot and isDashboardRepoIcons already returns true for undefined, so the main-process validator needed no change. * fix(dashboard): keep repo icons in the main-process snapshot cache The bridge now omits an unchanged repoIconsByRepoId from republishes, so the cached snapshot main replays to a mounting pop-out could be icon-less, blanking the board's repo glyphs until the forced publish landed. Carry the last map into the cache; the forwarded payload is unchanged. Also covers the forced full sends (open, reopen, snapshot request) that no test exercised. * test(dashboard): pin the icon omit on the throttled trailing republish * fix(dashboard): keep the popout bridge effect off the react-doctor gate The changed-code quality gate reports react-doctor findings that overlap added lines, and effect-needs-cleanup spans the whole publish effect — so this PR's edits inside it turned a pre-existing false positive into a red static-analysis check. Hoisting the store subscriber leaves the effect owning one disposable; behaviour is unchanged. * docs(dashboard): correct why watchSnapshotInputs sits outside the effect The effect owns four disposables (offOpenChanged, offRequested, the store unsubscribe, and the trailing timer), not one. State the real reason the subscribe is hoisted so nobody inlines it back and re-reds the gate. * test(dashboard): pin that the bridge subscribes only while the pop-out is open The lazy wiring exists so an enabled-but-closed pop-out costs nothing — a live subscriber would rebuild a cross-worktree snapshot on unrelated store writes. Nothing pinned the unsubscribe on close. |
||
|
|
13c193a00a |
feat(dashboard): add agent status search board (#11042)
* feat(dashboard): add agent status search board * fix(dashboard): keep idle controls reachable * chore: drop merge-only formatting drift * fix(dashboard): compare sparse subagent snapshots safely * fix(dashboard): satisfy settings handler lint * fix(dashboard): address review feedback * fix(dashboard): complete search and localized status copy * fix(dashboard): pad active filter row * fix(dashboard): keep idle control in board settings * fix(dashboard): source filters from workspace state * fix(dashboard): clarify PR and MR status filter * fix(dashboard): preserve review and board parity |
||
|
|
5753cf6c5c |
fix(updater): resume background checks after a local build session ends (#11223)
A local-build check (Option+click "Check for Updates" on macOS) pins activeUpdateSource to 'local' for the rest of the process. The 'update-available' success path never restores it, and runBackgroundUpdateCheck early-returns on it, so every wake-from-sleep check, window-focus daily check and nudge poll became a no-op once a local build reached 'available'. The one-shot automatic timer fired into that early return and nothing re-armed it, so the scheduling chain died too and lastUpdateCheckAt froze. Restoring the source when 'update-available' fires would break the flow the user just started — the pending download still needs the local feed and allowDowngrade. Instead the release source is restored when the user closes the offered card, which main previously never learned about, and only while status is exactly 'available': downloadUpdate() flips status to 'downloading' synchronously before it calls into electron-updater, so this cannot fire once a download is under way. The automatic timer now re-arms when a check is deferred rather than launched, so a deferral can no longer end automatic checks for the process lifetime. |
||
|
|
3c0cd6069f |
fix(release): stop packaging plugin authoring examples into app.asar (#11087)
* fix(release): stop packaging plugin authoring examples into app.asar electron-builder's `files` is an all-negation list, so its default `**/*` packs anything without an explicit `!` entry. examples/ arrived with the plugin system in #8549 and never got one, so 1.4.160-rc.3 shipped examples/plugins/hostile-panel/panel.html — the adversarial fixture the panel containment tests point at, complete with its fetch-exfiltration probe — plus hello-orca, inside every user's app.asar. Verified against the installed 1.4.160-rc.3 artifact, not just the config. The two orchestration design docs landed at the repo root in the same span and shipped the same way; fold them into the existing root-doc negation. Neither has a runtime consumer: bundled plugins ship via extraResources from resources/plugins/launch/, which is already excluded from the asar for exactly this reason. * test(release): assert the examples exclusion through the real file matcher The added case mapped each negation to a bare top-level token, so it passed under '!examples/README.md' — a pattern that still ships the whole tree. Drive app-builder-lib's FileMatcher instead so the assertion matches the test name, and pin the root anchoring so the negation cannot grow into '!**/examples'. |
||
|
|
a8126a0a92 |
fix(macos): explain the TCC prompts, and surface Full Disk Access only to users macOS is prompting (#9756) (#9910)
* fix(macos): add a Full Disk Access nudge to reduce recurring TCC prompts (#9756) macOS shows the "Orca wants to access other apps' data" (kTCCServiceSystemPolicyAppData) prompt and it can keep reappearing. The reappearing loop is not a fixable app bug: it is TCC identity churn — an unsigned local rebuild mints a new code identity each build, so macOS treats each as a new app — and Orca's other-app reads are already gated behind opt-in settings or explicit user actions. The durable remedy for the population we can help (release users) is Full Disk Access, a superset macOS grant that stops these prompts for a stable identity. Surface it with an ambient, dismissable sidebar card that reuses the existing developer-permissions IPC. macOS-only; probes FDA status at most once per renderer session (the probe itself reads protected data, so it must not repeat on focus/remount); "Open System Settings" opens the Full Disk Access pane; permanent localStorage dismissal. * fix(macos): stop the FDA nudge promising macOS will stop asking The card said Full Disk Access makes "macOS stop asking", but the grant covers this app while terminals are spawned by the detached PTY daemon (daemon-init.ts forks execPath with ELECTRON_RUN_AS_NODE + detached:true, reparented to launchd), which macOS treats as its own TCC identity. A user who followed the card would grant FDA and still be prompted from terminals. Scope the claim to reducing prompts and name the terminal caveat. * fix(macos): drop stale focus refreshes in the FDA nudge refreshFullDiskAccessStatus() applied whichever getStatus() round-trip resolved last. Rapid blur/focus puts several in flight, so an earlier pre-grant 'unknown' landing after a newer 'granted' un-hid the card and also wrote 'unknown' into the module-level session cache, re-nagging a user who already has Full Disk Access for the rest of the session. The adjacent FullDiskAccessSetupPrompt already guards this with a refresh sequence; mirror it here. Also unmount React roots in afterEach: clearing document.body left them mounted, leaking each test's window focus listener into later tests. * test(macos): unmount the StrictMode FDA nudge root between tests The afterEach unmount added in |
||
|
|
8b57e6e180 |
fix(ssh): resync after watcher terminal retry (#10691)
* fix(ssh): resync after watcher terminal retry * fix(ssh): resync after watcher terminal retry - Coalesce repeated recovery resyncs within 5s to reduce SSH refreshes during link flaps - Abort in-flight watcher installs when a replacement provider registers, preventing duplicate watchers from old and new transports - Clear resync state when removing watcher snapshots or on provider change to prevent stale retry timers * fix(ssh): resync after watcher terminal retry Avoid logging spurious warnings when a remote watcher is already closed or suspended. Move the console.warn call in handleRemoteWatcherTerminalError() to after the early-return checks. Refactor createSender() in tests to properly simulate the destroyed event for better coverage of retry-cancellation behavior. |
||
|
|
77ac0bd517 |
fix(codex): keep the stale-pane prompt when two accounts share a label (#11228)
The startup sweep asks main which panes are stale, and main answers by account id. The renderer then threw that away: it resolved both ids to labels and let the store's A -> B -> A collapse compare the strings. Two accounts can share a label — doAddAccount has no duplicate-email check, so one OpenAI login used in two ChatGPT workspaces gives both the same email, and a failed roster read collapses every account to 'Codex account'. Either way the notice was deleted for a pane that really is running under the account the user switched away from. The sweep then made it permanent: it marked every stale pane notified, including the ones whose notice had just been dropped, and a notified pane is suppressed for the rest of the session. Relaunching cleared the set but the deletion recurred, so the prompt never came back and the pane kept running on the other account's auth.json and quota, silently. Carry the account ids into the notice and decide on them, falling back to labels only for callers that have none; report which panes were left holding a notice so a dropped one cannot claim suppression. The prompt also names the ChatGPT workspace when that is what tells two same-email accounts apart, which is what the two duplicated getCodexAccountLabel copies now share. |
||
|
|
50f46889d9 |
fix(ai-vault): resume a bridged Codex session under the selected account's home (#11224)
* fix(ai-vault): resume a bridged Codex session under the selected account's home The account session bridge hardlinks every rollout into each per-account CODEX_HOME, and vault dedup keeps the lexicographically-smallest alias, so Resume could pin an inline CODEX_HOME naming a peer account — running the session under that account's auth.json and quota. At resume time the owning host now substitutes the selected account's home when it holds the same rollout at the same sessions-relative path, declining on any uncertainty so resume degrades to today's behavior instead of failing. * fix(ai-vault): repin dropped sessions without a cwd instead of resuming under the wrong account The drag payload only carried sessionCwd when session.cwd was truthy, so a null-cwd codex session dropped onto a pane silently fell back to the prebuilt command - which pins the wrong account's CODEX_HOME, the exact defect this PR eliminates on the other resume surfaces. - Serializer always sends sessionCwd (null when the session has no cwd), so absence now only means an older-serializer payload. - The repin rebuild accepts a null cwd (the builders already omit the cd prefix), matching the sidebar Resume/Copy paths which repin regardless of cwd. - An unrepinnable payload (absent sessionCwd) now fails loudly with guidance instead of silently resuming under the wrong account's home. |
||
|
|
ca5a821600 |
Stop relaunching creation-time agents on workspace activation (#10647)
* fix(activation): stop relaunching the creation-time agent on workspace activation Activating a workspace with zero renderable tabs launched the agent it was created with, unprompted and in approval-bypass mode. Navigation is not consent to start a process: the same fallback fired from post-delete focus handoff, the jump palette, keyboard cycling, CLI/relay activation, and notification clicks. The mechanism was superseded. #1814 added it when relaunching the created agent *was* the resume feature; #4706 later added real provider-session resume six lines above and left the fallback in place. What remained fired whenever a workspace had no renderable tabs -- including when nothing had ever slept -- and reported itself as `request_kind: 'resume'` while resuming nothing, discarding any resumable session a plain tab close had already purged. No caller depends on it. All seven intent-carrying callers pass an explicit `startup` on the branch where they intend a launch, and every no-startup branch either declined an agent, already has one running (host `didSpawnStartup`), or is this same defect arriving over IPC. Drops the now-orphaned imports, retargets the stale comment in launch-work-item-direct that cited reopen-relaunch as the reason to persist `createdWithAgent`, and moves the WSL default-args quoting assertion to launch-agent-in-new-tab, whose launch path still resolves those args. Regression tests are revert-sensitive -- all four fail if the fallback returns. * test(activation): name the relaunch regression tests after what they reach Three tests were named after scenarios they never invoked, which is the failure mode that lets a coverage gap read as closed. - The "host-originated" test's `notifyHostRuntime: false` is inert here: both gates resolve through `isWebRuntimeSessionActive`, false with no runtime environment seeded, so it was byte-identical to the plain reopen test. It no longer claims to cover the host `didSpawnStartup` leg, which lives in main and is unreachable from this layer. - The "post-delete focus handoff" test never deleted anything and never touched `prepareActiveWorktreeFocusAfterDelete`. That caller is asserted directly in active-worktree-focus-after-delete.test.ts, which locks out any opts. - The activate/close loop resets state instead of calling `closeTab`, so it does not exercise the sleeping-record purge its comment claimed. Also folds the primary reopen test onto `seedEmptyActivatableWorktree` — the fixture extracted for exactly that state, which its inline copy had drifted from by hardcoding a POSIX repo path. `preflight` is dropped from the launch-work-item-direct comment: the trust preflight reads the create-time argument (worktree-remote.ts), not the persisted meta. Removal safety and ownership do read the field and remain accurate. Renames the ported quoting test to what it pins. Under vitest's node environment `navigator.userAgent` carries no "Windows", so platform resolution bails before the WSL branch and the WSL preference is inert — the real coverage is single-quote escaping of user-configured agentDefaultArgs. * transfer large terminal history seeds across bounded protocol messages - Oversized cold-restore snapshots (>1MB) now upload via chunked startHistorySeedTransfer/appendHistorySeedTransfer protocol instead of inline, avoiding NDJSON line-size violations - Checkpoints automatically trim oldest rows to fit within configured byte limit (200MB) before commit - Protocol v30 required for chunked transfers; v29 daemons gracefully fall back to renderer-only recovery - NDJSON encodeNdjson() validates line size and rejects oversized payloads; notifications silently swallow encoding errors * fix(daemon): drop held output when teardown checkpoint fails to serializ When a final snapshot checkpoint fails to serialize (returns retryable), the pending output records must not be appended later—doing so would splice them over the seq gap left by the failed snapshot, defeating gap detection. Drop the records and retry the checkpoint instead. * Bump daemon protocol version to 30 * Bump daemon protocol version to 30 |
||
|
|
930ff96152 |
fix(skills): stop the scan issue budget evicting a read failure (#11221)
The per-scan issue budget kept an issue only when it explained a candidate or truncated the walk. Neither set intersects the attention set, so 'io-error' — the sole reason a plugin-cache scan can raise "Needs attention" — was droppable. Once 16 ordinary issues filled the budget (16 'outside-root' vendor symlinks is an install shape the scan itself documents as normal), a later read failure was evicted for a generic 'issue-limit' row that raises neither attention nor truncation, and the dialog headline read "All installed Orca skills are up to date" over a path that could be hiding a stale copy. Attention issues now outrank the budget, capped at a small reserve so an adversarial tree of unreadable folders cannot pin one issue per folder. |
||
|
|
a81f17c189 |
fix(skills): trust the updater's lock when a run installs content newer than the bundle (#11220)
skills update installs source-repo HEAD, which routinely runs ahead of the revisions a shipped build bundles. The post-run re-scan hashed that content 'unrecognized' (the registry has never seen it) and the verdict counted it as a failure — so a clean update reported "The update didn't finish / Updated 0 of N", and Retry repeated the false failure forever because the CLI now no-ops (lock == source). The 'newer-known' escape hatch never fires: the generator always points the manifest at the registry's newest snapshot, so no observed content can hash to a revision newer than the bundle. The verdict now computes the git tree sha of the observed bytes (a port of the generator's hashing, verified byte-for-byte against git write-tree and against every shipped skill's manifest gitTreeSha) and accepts an unrecognized placement when that sha equals the lock's skillFolderHash: the lock is the CLI's own record of what it installed, so disk matching lock means the command did its job — the bundled registry simply has not seen that revision yet. Half-written bundles (sha mismatch), unreadable copies, removed skills, degraded aliases, and outdated copies at the lock hash all still fail. |