mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
60c7faf930e368fb4d10319e47571df87f2dfc1f
7430
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60c7faf930 |
fix(browser): restore configured zoom after page reload (#10800)
Reviewed with an independent reproduction. Rewrote the reload-zoom reassert to be per-pane instead of sharing the value zoom in/out writes, fixing Cmd/Ctrl+0 reset and cross-tab zoom leakage, with E2E coverage proven to fail on revert. |
||
|
|
872a9c3930 |
fix(terminal): make copy work in the HTTP web client (#10534)
Reviewed with an independent reproduction over a real plain-HTTP origin. Replaced the hidden-textarea fallback with a capture-phase copy event so clipboard text never enters the DOM, and added rejection handlers to the surfaces that had none. |
||
|
|
b601643960 |
fix(sidebar): stop worktree cycling from reopening collapsed groups (#10513)
Reviewed with an independent reproduction. Replaced a redundant per-keypress layout rebuild with cycling over rendered sidebar rows; verified the chord is not hardcoded to metaKey. |
||
|
|
f1d54c123b |
fix(file-explorer): commit inline rename on outside click and stop double-click rename flicker (#10867)
Reviewed with an independent reproduction. Both fixes verified; added a flush for a dropped directory toggle and an integration test connecting blur/Escape through to renameFileOnDisk. |
||
|
|
c6076a507c |
ci(release): detach non-blocking E2E (#11031)
* ci(release): detach non-blocking E2E * test(release): pin E2E dispatch retries |
||
|
|
b316174525 |
fix(panes): stop a stray mouse hijacking a touch divider drag (#11021)
The drag predicate recorded only the active pointerId, never its type, so during a touch-started drag any primary mouse or pen event failed the id match but passed the non-touch fallback and was accepted as the active pointer. Record the type at pointerdown and require both ends to be non-touch, keeping the WSLg mouse-press/pen-motion relay working. |
||
|
|
edc6cc007d |
fix(new-workspace): center agent selection in create dialog (#11020)
* fix(new-workspace): center agent selection in create dialog Pin the Agent combobox mark to 14px, drop residual button padding, and use a full-width min-w-0 trigger so icon, label, and chevron align with Project/Name in the new worktree dialog. * fix(new-workspace): optically align agent picker content * fix(new-workspace): center selected agent content |
||
|
|
dc45f79465 |
fix(runtime): drain the session-tabs coalescer when a listener subscribes (#11022)
multi-client-navigation-isolation.integration.test.ts fails intermittently on
clean main — measured at 4-8/24 with a flush-window sweep — and has been taxing
unrelated PRs across the repo.
Mechanism, confirmed by instrumented trace (schedule t=23412, listener REGISTER
23460/23461, coalescer fire 23462): a ~4ms race between the 50ms session-tabs
notify coalescer and listener registration in onMobileSessionTabsChanged. When
the pending timer fires after the listener registers but before
session.tabs.activate is handled, it emits a stale
{type:'updated', activeTabId:'host-tab'} that the test consumes.
flushAll() at the top of onMobileSessionTabsChanged, before the listener joins the
set — mirroring the flushAll() its own unsubscribe closure already performs.
DRAINING rather than cancelling is load-bearing: cancel/dispose has no emit, so it
would silence the stale frame but DELETE the pending update for subscribers
already registered, leaving them stale until an unrelated next schedule(). That
would trade a flaky test for a real lost-update bug. The newcomer cannot miss an
update by having it drained — listMobileSessionTabs and the coalesced emit read
the same mobileSessionTabsByWorktree map, and snapshot-read to listener-add is
macrotask-atomic.
Sweep: 4-5/24 before, 0/24 after. The regression test uses fake timers and is
two-sided — it fails if flushAll is removed (stale frame delivered) and fails if
flush is swapped for dispose (nothing drained), on different assertions, so it
pins the specific choice rather than merely the presence of a change.
Reviewed independently and returned clean with both mutations re-run by the
reviewer. A latent flushAll re-entrancy double-emit exists in principle — fire()
does not re-check pending.has the way flush() does — but is unreachable: both
production callers' listeners only invoke the dispatcher reply, a shape that has
shipped since #8141 on the symmetric unsubscribe flush.
Not verified: live paired-device mobile/relay behaviour (no device available); the
mobile conclusion rests on a code trace.
|
||
|
|
4340781c9f |
fix(codex): drop the resume argv when session provenance is unverifiable (#10805)
Closes #10793. When Orca could not verify the originating Codex session file it either threw — a red per-pane toast and a failed spawn, reported as constant spam on #10757 — or returned null. Returning null did NOT start a fresh session: the renderer had already baked ['codex','resume',<id>] into the command and pty.ts never rewrote it, so CODEX_HOME simply fell through to whichever account was selected. The resume argv is now dropped so a plain `codex` launches, with a banner telling the user. The invariant — never run `codex resume <id>` under an account that does not own that rollout — is now satisfied by construction rather than by refusing to spawn. A verified resume is unchanged and still pins CODEX_HOME to the originating home. Reviewed over two adversarial rounds; seven defects found and fixed, including a HIGH where local-provider (non-daemon) spawns still carried ORCA_SEQUENCED_STARTUP_COMMAND with `resume <id>` — the wrong account behind a banner claiming it started fresh. `env` is now declared after the strip so no point in the handler can reach the pre-strip value. Live-validated in a real Orca dev build: all five cases proven on the SPAWNED PROCESS, including a real rollout under an untrusted home (the only shape that discriminates) and the local-provider path forced by stopping the daemon. An earlier CI failure on multi-client-navigation-isolation.integration.test.ts was investigated and is a PRE-EXISTING flake — a ~4ms race in the session-tabs notify coalescer that fails 5-8/24 on clean main, more often than on this branch. Fixed separately in #11022. Not verified: no Windows execution — its POSIX-only tests skip there and the #10757 reporter is on Windows. SSH is partial: no spurious banner or drop observed against a real target, but headless spawn does not deliver startup commands so the remote argv could not be read. The relay/mobile notice channel deliberately has no banner; the argv drop does happen there, so the invariant holds. |
||
|
|
bd5a991ce7 | fix(e2e): launch plugins with real app identity (#11024) | ||
|
|
9c5d827d6a |
fix(codex): keep history, restarts, and account identity across an account switch (#10770)
Fixes #10757. Switching Codex accounts broke three ways, all rooted in the self-contained per-account CODEX_HOME from #9501. HISTORY DISAPPEARED. Codex's own /resume picker only lists rollouts under the launch CODEX_HOME, and nothing bridged history into a per-account home — only the AI Vault's discovery scan knew about the other homes. Every other Orca-visible home's rollouts are now hardlinked in, on selection and again at launch, so one physical log is listed everywhere. THE RESTART PANEL STUCK. A queued restart was only drained by a mounted TerminalPane, but the prompt covered every stale pane in the worktree including parked and cold-deferred tabs. Requesting a restart now answers the prompt immediately while the pane keeps its pending restart, and a pane drains it when its reconnected PTY binds. PANES STAYED ON THE OLD ACCOUNT. CODEX_HOME is fixed in a shell's environment at spawn and the daemon keeps those shells alive across app restarts, while the restart notices are renderer state and are discarded. Each PTY's launch account is now recorded on disk and compared against the current selection at startup. Also merged in: #10802 (a dismissed notice no longer kills the pane's keyboard), #10803 (the sweep arms on real PTY binds, and launcher Codex panes are no longer filtered out by Windows deepest-process reporting), #10804 (a resume-pinned pane now says which account it is on), #10870 (the restart card no longer parks focus on its destructive Restart button), #10853 (the retry ladder is widened past the Windows worst case). Six independent reviews found real defects in every original PR, several of them dead-keyboard bugs and three introduced by the fix for another defect in the same loop. Live QA on macOS covered every PR; Windows was validated three times. WINDOWS: pass 1 found two defects that made the stale-account fix a no-op there (the sweep fired before any PTY was bound and never retried; launcher panes were filtered out). Pass 3 at the merged head: the prompt appears on its own after a restart — warm ~3.7-4.2s, cold ~21s needing rung 4, so #10853's widening was load-bearing rather than precautionary; an ordinary sentence typed into a healthy pane while another pane's card is up reaches that pane and kills nothing; a pane running vim after exiting Codex gets no card, still none 45s later. auth.json byte-identical across every pass. KNOWN GAPS, stated rather than implied: #10804 is unverified on Windows (auto-resume could not be manufactured there); cross-volume Windows is untested and expected to yield no bridged history (EXDEV, and Codex ignores symlinked rollouts); a cold-parked pane never binds so the sweep never covers it; the subagent-deepest launcher shape could not be reproduced on Windows, so that branch is fixture-verified only; WSL passed isolation but the resume mechanism is host-lane only. A host-account switch also marks and mutes live SSH remote panes — confirmed pre-existing on main by two independent QA runs — tracked separately in #10992. Related pre-existing defect filed as #10863. |
||
|
|
0956d5ca3a |
feat(skills): run skill updates in the background without a terminal (#10843)
* feat(skills): run skill updates in the background without a terminal The Update skills dialog had no primary action at all — its footer was only Re-check and Close, and the real action was a pre-filled command in an embedded PTY that the user had to press Enter on. Orca already builds and validates that command, so it now runs it. - Add a headless runner for `npx --yes skills update <names> --global -y`. Both --yes flags are load-bearing: npx's skips the package-install prompt, and the skills CLI's takes its own non-interactive branch. stdin is ignored so `process.stdin.isTTY` stays falsy, which is the other half of that gate. - Own the run in main so closing the dialog backgrounds it instead of killing it, and surface it in the status bar: spinner while running, a green check on success that clears itself, and a failure that persists until acted on. - Derive per-skill outcomes by re-scanning the freshness inventory after exit rather than parsing stdout. `skills update` has no --json (that flag exists only on `list`) and reports progress per-source, not per-skill, so the run bar is deliberately indeterminate instead of faking a percentage. When the re-scan has a verdict it outranks the exit code. - Drop the version trail from the rows and surface the skill list and skip reasons directly instead of hiding them behind a disclosure. Also fixes a width bug the collapsed disclosure used to hide: deep plugin-cache paths set the dialog's width and pushed the footer actions off-screen. * refactor(skills): use one row component across every update state The ready and running views were separate components with different row shapes, so pressing Update swapped the dialog's body for a different layout. They are now the same `SkillUpdateRow` instances throughout — only the status slot's contents change — and a test asserts the row is literally the same DOM node from "update available" through pending to the result. - Collapse each skill's locations behind its own disclosure. A skill with several plugin-cache copies was dumping every path inline and burying the actions; the row now shows a location count and expands on demand. - Put status in a single slot between the name and the count rather than a leading icon column. A leading icon has nothing to show in the resting state and reserving its box just indented every name past an empty gap. - Pin the running/finished run's names in `groupSkillFreshness` so a successful update doesn't drop its own rows the instant the re-scan lands. `skill-freshness-group.tsx` becomes `skill-location-chip-copy.ts` — only its chip label/tooltip helpers survived, and it no longer holds JSX. * fix(skills): place the status glyph left of the skill name Review feedback on the row header: the badge belongs immediately right of the name so it reads as part of it, and the run's status circle/check belongs to the left of the name rather than sharing the badge's slot on the far right. Name, glyph and badge are now one left-aligned group; the location count and chevron stay right-aligned. `available` still has no leading glyph — an empty reserved box only indents the name past a gap with nothing in it. * fix(skills): correct the headless update run's verdict, cancel path, and stopping copy Review fixes for the headless skill-update runner. Main process: - Judge per-skill outcomes on a positive signal. "Absent from eligibleUpdateNames" is not success: a deleted, half-written, or unreadable skill also leaves that list, so a corrupt update reported a green check. skillUpdateFailedNames now requires every convergent placement to come back current or newer-known. - Retire a child's handlers with a per-run token. A failed spawn emits error *and* close, so the second settle clobbered the real spawn ENOENT; a cancelled child could also settle, or write output into, the run that replaced it. The token guards the rescan's finish closure too. - Hold the run `running` until the killed process tree is actually dead. Releasing on the synchronous path let an immediate re-Update spawn a second npx writing the same bundles, with a watchdog so a sweep that never settles cannot wedge the run. - Kill the tree, not just the npx wrapper, via killWithDescendantSweep. - Publish an error instead of a silent `started: false` when the cmd.exe rail rejects the resolved npx path, which a profile directory containing & or % is enough to trigger on Windows. - Coalesce captured output into one push per tick instead of structured-cloning the whole buffer to every window on each progress frame. Renderer: - Keep rows on screen while the settling re-scan runs. Refreshing the inventory nulls it synchronously, so every row vanished at the moment the result appeared. Rows render off the last good scan; eligibility stays on the live snapshot so nothing is authorized off stale bytes. - Retry the names that failed, not the eligibility list that same re-scan has just emptied. - Add Stop, restoring the escape hatch the embedded terminal used to provide, and say "stopping" on every surface rather than claiming the update keeps running in the background. - Show a skipped skill's reason outside the disclosure, so it no longer depends on a mount-time defaultOpen a later re-scan can never re-fire. - Drop the summary line telling users to open "Update details", a control this PR removes; it was translated into four languages. - Keep the success linger from retiring a result the open dialog is showing. - Delete skill-location-chip-copy.tsx: an unreferenced copy of the old row component, colliding on basename with the module that is actually imported. * fix(skills): divide update list from summary |
||
|
|
72875bda24 | fix(ci): stabilize flaky terminal and SFTP tests (#11018) | ||
|
|
3d98cda5b2 | fix(release): accept lowered telemetry declarations (#11019) | ||
|
|
c75c04eaae |
fix(runtime): reclaim orca-runtime.json when it stops describing this runtime (#10840)
* fix(runtime): reclaim orca-runtime.json when it stops describing this runtime On macOS the Chromium single-instance lock is silently defeated whenever `SingletonSocket`/`SingletonCookie` go missing from the profile — and the socket they point at lives under `$TMPDIR` (`/var/folders/.../T`), which macOS purges after 3 days (`com.apple.bsd.dirhelper`, CLEAN_FILES_OLDER_THAN_DAYS=3). A launch that slips past the lock runs a full startup, republishes `orca-runtime.json` with its own pid, and leaves the CLI on a dead pid once it exits: `orca status` reports `stale_bootstrap` and every terminal command fails `runtime_unavailable` while the original app keeps serving. The owner now watches its own discovery record and republishes once no live runtime is described. Reclaiming only a dead pid is deliberate: two live runtimes sharing a profile would otherwise fight over the file. Reproduced on macOS with two real Orca main processes on one profile: the second instance took the lock and clobbered the record, and killing it left `stale_bootstrap` against the still-healthy first instance. With this change the owner reclaimed the record in ~2s and the CLI returned to `ready`. Refs #7848 * test(runtime): assert stop() clears the metadata ownership timer The republish guard alone kept the shutdown test green, so the watch teardown was unasserted. Also drop the doc claim of startup/activation callers that do not exist. * test(runtime): stand in a real live pid for the sibling-runtime case Windows never assigns pid 1, so the hardcoded sibling read as dead there and the watch would reclaim the record. Own a synthetic pid instead and let process.pid play the live sibling. |
||
|
|
c25a130236 | release: v1.4.160-rc.2 v1.4.160-rc.2 | ||
|
|
0edc95fa35 |
perf(editor): cut per-keystroke work on two rich-markdown paths (#10862)
* perf(editor): cut per-keystroke work on two rich-markdown paths Doc links: both plugins walked every text node and ran matchAll on each — the auto-convert appendTransaction once per keystroke, the preview decorations once per keystroke and again per caret move. A link needs `[[`, so gate on a native substring check first. The two walks had duplicated their guard sequence; they now share one predicate. 3.1x-3.8x over the repo's own markdown. Annotations: resolving a comment's block re-serializes the whole document (every node, plus every adjacent pair), and both the highlight-range and comment-at-position paths did that once per comment — O(comments x document). Build the blocks once and pass them down. On a 12-node fixture with 8 comments that is 184 serializations down to 23. * test(editor): pin the one-build serialization baseline Review feedback, all four points: - The serialize-count assertions compared many-comments against one-comment, so they would have passed if BOTH built blocks twice. Pin the absolute count (23 = 12 nodes + 11 adjacent pairs) derived from the fixture size, so a regression to per-comment building fails instead of comparing equal. Verified by reverting the hoist: 2 tests fail. - Skip an empty benchmark corpus instead of evaluating `index % 0` and dereferencing undefined. - Build fixture paths with path.join. - Condense the benchmark header to purpose plus parity guarantee. Co-authored-by: Orca <help@stably.ai> * test(editor): harden doc-link performance evidence Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
d547e278f9 |
fix(mobile): deliver the notifications a reconnect missed, and never persist a watermark past them (#10816)
* fix(mobile): keep the reconnect watermark alive across the app's own teardown The catch-up added in #8690 could never run. app/index.tsx unsubscribes the notification stream on every non-'connected' state and builds a fresh subscription on reconnect, so the closure holding the ready-counter, the delivered watermark and the seen-set is destroyed exactly when a reconnect needs them. Every reconnect looked like a cold open, `reconnectReadyCount` was always 1, and notifications dispatched while the socket was down were never fetched. Move that state to a per-host module-scope session so it survives the teardown. Refs #8591 Co-authored-by: Orca <help@stably.ai> * fix(mobile): tag the notification watermark with a counter epoch so a desktop restart can't kill catch-up The desktop's notification `seq` is a per-process in-memory counter that starts at 0 on every launch. The mobile client's watermark is persisted in AsyncStorage and monotonic. After a desktop restart the two index different counters, so a client holding seq 57 meets a fresh counter at 2, `57 >= 2` cuts everything, and reconnect catch-up dies silently until the new process out-dispatches the old watermark — 57 notifications later. Users see nothing and get no error (#8591). Stamp every dispatched notification with an epoch identifying the counter lifetime, ride it on the `ready` frame and the getMissedSince response, and persist it beside the watermark. A watermark whose epoch doesn't match the live counter is void: the client resets to 0 and the desktop returns its retained buffer instead of nothing. The epoch param is optional on the wire in both directions, so a client or daemon that predates it degrades to today's seq-only cut rather than erroring. Also extracts the OS-permission helpers to notification-permissions.ts (re- exported, so no importer changes) to keep mobile-notifications.ts under its max-lines budget. Mutation-tested: 3 mutations applied to the epoch logic, 3 killed — including the storage-seed race guard, whose first mutant survived until the deferred-read test was added. * fix(mobile): make the notification watermark atomic and counter-scoped Round-1 review found four ways the epoch fix could still lose notifications. All four are addressed here. 1. Seen-set survived an epoch change. Seen-keys are seq-derived, and terminal bells carry no notificationId (they key on `seq:N` alone). After a restart the fresh counter re-issues low seqs, so a replayed post-restart bell was dropped as a duplicate of a bell from the previous counter. The dedup window belongs to one counter lifetime, so it is cleared on epoch change. 2. Legacy watermarks were trusted. Pre-upgrade installs stored a bare seq with no epoch. Adopting the first observed epoch as "nothing changed" left that unprovenanced seq cutting a counter it was never measured against — #8591 through the upgrade path. An epoch-less seq no longer survives adoption. 3. seq and epoch were separate storage keys. A process death between the two writes left epoch-B beside seq-57-from-A: a pair that looks internally valid on the next launch and is therefore trusted. They are now one JSON value, which cannot tear, with a read-only migration from the legacy key. 4. Sessions were never retired. They live at module scope so they survive the subscription teardown a reconnect performs, so host removal is the only thing that can drop them. Removal now retires the session and its watermark. Mutation-tested: 3 mutations, 3 killed. The first version of the bell test passed with the fix removed — it exercised the live path, which only adds to the seen-set; only the replay path consults it. Rewritten against the replay path, it fails with `expected 1 to be 2`: the literal lost notification. Mobile notifications + transport: 355 passed. Desktop replay: 11/11. * fix(mobile): catch up on the first connection after a cold open Catch-up hung off 'has this process connected before', which is false on the first ready of a fresh launch — exactly the post-upgrade / post-eviction case that loses everything between the stored watermark and the next live seq. Wait for the persisted read, then catch up whenever this device has delivered for the host before; a first-ever pairing still gets no replay. Co-authored-by: Orca <help@stably.ai> * fix(mobile): serialize live delivery behind the watermark seed, and key catch-up on the record Co-authored-by: Orca <help@stably.ai> * test(mobile): pin the two catch-up mechanisms mutation testing found unguarded Mutating each mechanism of the #8591 fix in turn showed two survived with the suite still green: the seed's epoch-provenance check, and the host session outliving the subscription teardown. Both are load-bearing, so pin them. - seen-set survives teardown: the desktop's retained buffer replays a notification already delivered live, and only the session-scoped seen-set stops a duplicate banner. - a seed resolving after a live epoch was adopted must not reinstate the dead watermark. Not reachable through subscribeToDesktopNotifications today ('ready' awaits the seed first), so it asserts on the exported pair and says so. Co-authored-by: Orca <help@stably.ai> * fix(mobile): serialize notification delivery per host so the watermark can't outrun what was shown Addresses two MAJOR findings from review of this branch. MAJOR #1 — the watermark could be persisted past a notification the user never saw. `deliverLive` advanced `lastDeliveredSeq` before awaiting the local show, and replay + live delivery ran concurrently, so a live seq 11 handled while catch-up was still showing seq 6 persisted 11. A process death before 7..10 were shown lost them permanently: the next launch asks the desktop for seq > 11. This predates the branch — `origin/main` advances the watermark at the same point — so it is a residual this fix closes, not a regression the branch introduced. It is fixed here because the branch is what makes the watermark load-bearing. Three changes: - the advance moves AFTER the show/dismiss await, so the watermark means "everything up to here reached the user" rather than "was dispatched" - a per-host `deliveryTail` promise chain (`enqueueHostDelivery`) serializes deliveries, so a monotonic advance is also an in-order one - the catch-up batch is ONE queue entry, not one per event. Awaiting per event returns to the event loop between replays and let a live event slot in between seq 6 and 7 — which is exactly the interleave being fixed. The RPC stays outside the queue: `sendRequest` waits up to 30s and holding the chain for that would stall live delivery on a slow link. MAJOR #2 — every delivery awaits the persisted read, so an AsyncStorage read that never settled disabled the host's notifications for the whole app lifetime, with no error and nothing to see. The seed is now bounded at 3s; a late seed still applies when it lands. Proceeding unseeded is strictly better: the watermark stays 0, so catch-up over-fetches and the seen-set de-duplicates. Serializing removed an overlap the duplicate-suppression relied on: `showLocalNotification` deduped two same-id events by observing the first still pending when the second arrived. With deliveries serialized the first completes first, so the second saw no pending state and scheduled a second banner for the same notification. The claim moves to enqueue time, where the overlap is still observable. Dismisses are deliberately not claimed — a dismiss for a shown id is what retires it. Evidence — each mechanism disabled individually against the unchanged suite: - batch-as-one-entry -> reverted to per-item enqueue: ordering test fails - watermark advance -> moved back before the await: ordering test fails - seed timeout -> removed: wedged-read test fails - live-path claim -> removed: concurrent-dedup test fails - replay-path claim -> removed: cross-path dedup test fails Each kills exactly one test, so no mechanism is unguarded and none is redundant. `mobile-notifications.test.ts`'s local `flushAsync` drained 10 microtask ticks. Deliveries are now several awaits deeper, so a fixed tick count under-drains; it yields to the macrotask queue instead. Verified with real timers that the behavior it asserts is unchanged — only the drain depth was wrong. Full mobile suite: 344 files, 2499 passed, 2 skipped. tsc clean, oxlint clean. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
2dac0741b4 |
fix(terminal): stop answering mode-2031 toggles that the same chunk withdrew (#10817)
* fix(terminal): stop answering DECSET 2031 subscriptions fish already withdrew fish enables and disables mode 2031 around every prompt (tty_handoff.rs), so a single PTY chunk routinely carries `?2031h ... ?2031l`. All three responders answered the sticky "an h appeared anywhere" flag, so each prompt cycle wrote `?997;1n` into a shell that had already handed the tty to a child — it lands as literal text, or as stdin for whatever is reading. pty-connection.ts's hidden-pane responder already had the right shape (`finalState !== 'subscribed'`); this brings the other three in line: - shared tracker: gate the '2031-subscribe' fact on the chunk-final state - parked-tab byte sidecar: same guard - visible-pane xterm CSI handler: xterm dispatches mid-parse, so there is no chunk-final state to read. Defer the reply to a microtask and re-check the subscription, letting a same-chunk `?2031l` cancel it. Refs #9993 Co-authored-by: Orca <help@stably.ai> * fix(terminal): decide 2031 replies per PTY chunk, not per xterm parse The previous commit deferred the visible-pane reply to a microtask so a same-chunk `?2031l` could cancel it. That cannot work: xterm's WriteBuffer parses every queued `terminal.write()` synchronously in one batch before any microtask runs, so the microtask sees the net state of N PTY chunks, not of the one that carried the subscribe. A TUI that subscribes in chunk N gets no reply when chunk N+1 happens to withdraw, and a fish prompt straddling two writes still gets answered. Move the decision to where chunk boundaries actually exist — pty-connection's dataCallback, which receives one PTY chunk per call. It scans raw bytes with `scanMode2031Sequences`, carrying a tail across chunks so a CSI split mid- sequence still resolves, and replies only when that chunk *ends* subscribed. Ownership stays single: gate-managed PTYs are answered by main's '2031-subscribe' fact, so the chunk scanner returns early for them, and the xterm CSI handler now observes only panes the scanner does not own. The tail is dropped on PTY replacement — a partial prefix belongs to the stream that produced it. Removes the microtask responder and the seed-reply retry path it needed. Mutation-tested: 6 mutations applied, 6 killed. * fix(terminal): carry DECSET 2031 withdrawals as a side-effect fact The previous commit moved 2031 reply decisions to the PTY chunk boundary and gave gate-managed panes a single owner: main's '2031-subscribe' fact. But the fact union is subscribe-only, and that left the withdrawal unobserved. For a gate-managed pane, main drops renderer-bound bytes after model ingestion, the chunk scanner early-returns, and xterm's CSI handler is disabled. So when a TUI emits `?2031l` while hidden, nothing retires the subscription: paneMode2031 stays set, and the next theme flip has maybePushMode2031Flip push `CSI ?997;2n` into the shell that replaced the TUI — #9993 again, through the theme-change door. Before this branch, skipHiddenRendererOutput observed those withheld bytes; consolidating ownership removed that observer without replacing it. No renderer-side observer can close this: the bytes are gone before the renderer sees them. The state protocol has to carry the withdrawal, so add a '2031-unsubscribe' fact alongside the subscribe across the three fact unions (shared, provider, daemon). It fires only on a real chunk-final withdrawal — a chunk with no 2031 bytes scans to null and stays silent. The renderer handler clears both maps and sends nothing: a withdrawal is not a query. Also closes two gaps an adversarial review found by mutation, both previously resting on comments rather than tests: the lifecycle parser-ownership predicate (extracted as isPaneParserOwnedMode2031Observer so it is directly testable) and the scan-before-reconciliation ordering that lets a chunk the snapshot drops as a duplicate still answer its query. Mutation-tested: 12 mutations applied, 12 killed (6 from the prior round re-run, 6 new covering this fix and the two survivors). * fix(daemon): refuse 2031 authority from a daemon that cannot retract it Round-2 review found a wire-compatibility hole in the original #9993 fix. Daemons survive app updates, so a new desktop can drive a daemon that was started by the previous build. Pre-v29 daemons emit '2031-subscribe' but have no '2031-unsubscribe' fact at all. For a gate-managed pane, main drops the renderer-bound bytes before the renderer sees them, so main's transient facts are the ONLY thing that can retire a subscription. Against such a daemon a TUI exiting while its pane is hidden leaves the subscription registered forever, and the next theme flip injects CSI 997 into whatever shell replaced it -- #9993 all over again, reached through the upgrade path. Gate it: bump PROTOCOL_VERSION 28 -> 29, add MODE_2031_UNSUBSCRIBE_FACT_PROTOCOL_VERSION with supportsMode2031UnsubscribeFact(), and drop '2031-subscribe' from any daemon below that floor. Trade-off: a gate-managed pane on a preserved v28 daemon keeps renderer-scanner authority instead of daemon-fact authority. That is exactly the pre-fact behaviour -- correct for visible panes, no worse than today for hidden ones -- and it resolves on the daemon's next restart. Non-2031 transient facts (bell, etc.) are unaffected at every version. Tests: two adapter regression tests (v28 drops subscribe, v29 forwards it), plus a version-pin test asserting the floor sits above every entry in PREVIOUS_DAEMON_PROTOCOL_VERSIONS -- so adding a new preserved version cannot silently re-open the hole. Mutation-verified in both directions: `false &&` (under-block) and `true` (over-block) each fail the new tests. * fix(daemon): gate background delegation, not just the fact stream A pre-v29 daemon can announce a 2031 subscribe but never retract it. Filtering that fact is not enough: while a pane is visible main's own scanner registers the subscription, and scan authority only moves to the daemon when the session is backgrounded. So the gate belongs on setPtyBackgrounded — decline to hand a non-retracting daemon authority at all, and main stays authoritative over the whole stream. Co-authored-by: Orca <help@stably.ai> * fix(daemon): clear a preserved pre-v29 background hint at attach, not just at background Co-authored-by: Orca <help@stably.ai> * fix(terminal): don't answer a 2031 subscribe whose withdrawal straddles a chunk Review found the chunk-final-state fix left one hole open. When the kernel cuts fish's toggle pair mid-withdrawal — chunk 1 ends "...?2031h prompt ESC[?20", chunk 2 is "31l" — chunk 1 genuinely ends subscribed, so it answers, and the reply lands as literal text at the prompt. Chunk 2 then recognizes the withdrawal but cannot recall bytes already written. The same byte stream is safe or corrupting purely by where the kernel split it. The scanner already retains an incomplete private-mode tail; it just didn't tell the caller whether that tail could still resolve to 2031. It now does, and a subscribe is held one chunk while the answer is still in doubt. Only subscribes defer — retiring a subscription writes nothing to the pty, so withdrawals stay eager. Deferral is narrow: a trailing "ESC[?25" (cursor hide) can never become 2031, so a subscribe already seen in that chunk is still answered immediately. This case predates the branch — the old sticky-flag policy replied here too — so it is a residual this fix now closes rather than a regression it introduced. Tests: three cases pinned (split withdrawal, non-2031 partial must not defer, split re-subscribe answers once). Removing the deferral fails only the first. * fix(terminal): preserve mode 2031 reply decisions * fix(build): record daemon protocol v29 compatibility --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a49d68f8c2 |
perf(git): overlap getBranchCompare's head-of-chain reads (#10895)
* perf(git): overlap getBranchCompare's head-of-chain reads
Four git spawns ran strictly in series before any compare work began:
branch --show-current, the base-ref probe, rev-parse HEAD, and rev-parse <base>.
Three are independent -- compareRef is display-only metadata and HEAD's oid does
not depend on the base ref -- so they now run concurrently. The fourth was
redundant outright: the probe already runs `rev-parse --verify --quiet
<ref>^{commit}` and discarded the oid it printed, which was then re-resolved by a
second spawn. resolveWorktreeBaseCommitOid returns that oid so it can be reused;
hasWorktreeBaseCommitRef now delegates to it, leaving its other 4 callers
untouched.
3.6-3.7x on a short remote base label (192ms -> 52ms), 1.44x on an
already-qualified refs/... base, which skips the probe by design.
Reuse is keyed by ref: resolveWorktreeAddBaseRef returns at its first successful
candidate, so only that ref's oid is ever read back. Peeling is safe because only
refs/heads and refs/remotes candidates reach the probe, where ^{commit} is a
no-op.
No new git features: this removes a spawn rather than adopting an option.
Co-authored-by: Orca <help@stably.ai>
* fix(git): preserve compare semantics across providers
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
6677b5f171 |
perf(cli): construct the runtime client only when a command needs it (#10919)
src/cli/index.ts was the only eager value-import of RuntimeClient, and five other eager modules imported just RuntimeClientError / RuntimeRpcFailureError from the runtime-client barrel -- dragging in client -> pairing -> zod -> ws -> e2ee on every invocation. Those error classes live in runtime/types.ts, which has zero children, so the five imports now point there and the client loads through the existing (already lazy by design) ctx.client getter. Eager modules 199 -> 46, with node_modules dropping 94 -> 0. `orca --help` 2.04x (59.6 -> 29.2 ms); the same for help, no-args, and both error paths, which return before constructing a client. Commands that DO construct one still gain 1.10-1.12x from not eagerly parsing the transport the local path never uses. Correction to an earlier note: websocket-transport alone is ~24 modules / ~8 ms, not the 107 / 28 ms once recorded -- that figure wrongly charged it for zod, which enters through shared/pairing on a different edge. Marginal cost, never isolated cost. Co-authored-by: Orca <help@stably.ai> |
||
|
|
077561f89a |
perf(terminal): measure stream byte length natively above a code-unit floor (#10916)
* perf(terminal): measure stream byte length natively above a code-unit floor The terminal RPC path counted UTF-8 bytes with a hand-rolled per-code-point scan that Buffer.byteLength does natively an order of magnitude faster. Routed through a small module rather than swapping the shared clipboard helper, which has ~50 renderer call sites and a partial-count contract on the over-limit path that must not change. 4.4x on an 8KiB batcher push, 4.2x on a 2MiB snapshot scan, 4.0x on the 48KiB chunk gate, and 1.26-1.33x on the adversarial early-trip shapes where the legacy scan bails after a third of the string. The floor is load-bearing, not defensive. Buffer.byteLength has a fixed ~14ns call cost against a scan iteration of ~1.5ns, so below the measured 8-12 code unit crossover the native call is a REGRESSION -- 4.2x slower at one code unit, which is keystroke echo, the most latency-sensitive PTY shape there is. Short inputs keep the scan verbatim; 16 leaves margin over the crossover so the worst sub-floor shape stays at parity. measureTerminalStreamByteLength takes the native count only when `length * 3 <= stopAfterBytes` proves the limit cannot trip, so the callers' truncated running total is never replaced by a full count. Co-authored-by: Orca <help@stably.ai> * test(terminal): benchmark production byte-length exports Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
025c242f0c |
feat(dashboard): tint agent cards by state, show the project as an icon, name the chat (#11012)
* feat(dashboard): tint agent cards by state, show the project as an icon, name the chat Three glanceability changes to the agent board: - The "Needs You" signal moves from the column border onto the cards themselves, and done agents get the same treatment in green. Idle cards that simply aren't running stay neutral, so a tint always means "this one wants you". - The repo is now its own icon with the name in a tooltip, instead of a mono label that truncated and competed with the worktree name. Icons ride the snapshot keyed by repoId — image icons are data URLs, and the snapshot republishes several times a second. - The user-message line is labelled with the tab's conversation name rather than "You", resolved through the same getAgentRowConversationName the sidebar's agent rows use. Status-only titles still fall back to "You". * refactor(dashboard): head the card with the session name, move the worktree beside the project The conversation name now sits next to the agent icon as the card's heading rather than prefixing the user-message line, and the worktree drops to the footer beside the project icon. The message line reads "You" again — the name moved up, so keeping it there said the same thing twice. Cards without a resolvable session name keep the worktree as the heading, and the footer omits it rather than repeating it. * fix(dashboard): thread settings into every snapshot builder caller Adding `settings` to DashboardSnapshotState left three callers constructing it without one. The in-window drawer's was a real defect, not just a type error: useLiveDashboardSnapshot derives its own snapshot rather than receiving the relayed one, so a dropped slice silently blanks generated conversation names in the drawer while the pop-out shows them. Bucket counts pass null deliberately — they never render a conversation name, so the sidebar stays unsubscribed from settings. Covers the drawer's wiring with a test, since `settings: null` type-checks and would blank names again without failing loudly. * test(dashboard): complete the terminal layout fixture TerminalLayoutSnapshot requires expandedLeafId; the neighbouring builder test hides this behind an `as unknown as` cast on the whole state object. |
||
|
|
a72068015f |
fix(panes): stop a stray touch hijacking a mouse divider drag (#11013)
Each pointer type has its own primary pointer, so a finger on a touchscreen arrives with isPrimary true while a mouse drag is in flight and satisfies the WSLg fallback in isActiveDragPointer. Exclude touch from that fallback; the pen path WSLg needs is unchanged and a touch-started drag still matches by pointerId. |
||
|
|
6943638053 |
perf(terminal): scan output frames by code unit, not per code point (#10915)
* perf(terminal): scan output frames by code unit, not per code point iterateTerminalOutputFrameChunks walked `for (const part of data)`, materializing a 1-2 character string per code point and calling terminalStreamByteLength on each, while accumulating the frame text with `chunk += part`. The accumulator was never needed: `chunk` only ever reconstructs the contiguous substring data[chunkStart..end), and `startSeq + chunkStartOffset + chunk.length` collapses algebraically to `startSeq + end`. Track two integer offsets and emit data.slice(chunkStart, end) instead, computing UTF-8 width inline from charCodeAt. Also short-circuits the cap gate on UTF-16 length before measuring UTF-8 bytes, which is sound because UTF-8 length is never below UTF-16 length. 2.4-6.1x across payload shapes, stable across reruns. This runs per terminal output batch and per snapshot chunk. Extracted to its own module along a real seam (the chunk-emission concern plus its two types and cap gate); methods/terminal.ts shrinks by 85 lines. No max-lines suppression added and the baseline is untouched. Co-authored-by: Orca <help@stably.ai> * fix(terminal): preserve chunk sequence rounding Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
f48cb78646 |
perf(store): index worktrees and tabs once per hydration loop (#10891)
* perf(store): index worktrees and tabs once per hydration loop Four sites in hydrateWorkspaceSession and reconnectPersistedTerminals rebuilt a flattened array inside a loop and linearly searched it: Object.values(worktreesByRepo).flat().find((e) => e.id === worktreeId) That is O(rows x ids) for O(rows + ids) distinct work. Build one first-wins index per loop instead. Neither loop sets state or awaits, so a single index over the store snapshot is valid for every iteration. 54.5x at a real 10-repo / 423-worktree session with 188 pending reconnects; 2.1x on a one-repo session. This runs synchronously on renderer cold start and gates workspaceSessionReady, which blocks terminal pane mounting. First-wins matters: Array.prototype.find returns the first match, so an index that overwrote on collision would resolve a different repo for a duplicated worktree id. Both the tests and the benchmark fixture carry a deliberate cross-repo duplicate so that difference is observable. Co-authored-by: Orca <help@stably.ai> * test(store): count the generated worktree rows instead of multiplying Review feedback: the table printed repoCount * worktreesPerRepo, which misses the duplicate id makeStore injects for multi-repo cases (that duplicate is what makes first-wins observable). Count the generated map, and say plainly that the fixtures are synthetic at real-world scale rather than a replay of a real session. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
1fd0f731fc |
fix(automations): bind agent terminal output before publishing, and launch SSH folder workspaces on their own host (#10818)
* fix(terminal): bind the agent PTY before the run tab is ever published Why: launchAgentBackgroundSession created the hidden run tab synchronously and only then awaited the agent spawn, so the store briefly held a tab with ptyId: null. Terminal.tsx re-renders on that write, and for an already-visited worktree the tab can neither cold-park nor defer — a TerminalPane mounts, finds no adopt candidate, and starts a fresh default shell. When the agent PTY finally resolves it is rebound in state but the mounted pane still holds the shell, so the user sees a bare prompt and the agent PTY is orphaned (#2989). Reserve the tab id before the spawn and create the tab already bound to the live PTY, with no await in between. * fix(terminal): resolve folder workspaces, and fail closed on a tab-id collision allWorktrees() reads only worktreesByRepo, so every folder workspace looked absent and its automation died at resolution. getKnownWorktreeById covers both. On a reserved-id collision, re-keying the tab could never work: ORCA_TAB_ID and ORCA_PANE_KEY are already baked into the spawned process, so routing and hook identity would permanently disagree. Retire the launch instead. Co-authored-by: Orca <help@stably.ai> * test(automations): split the background-session suite so it stays under the 800-line cap Co-authored-by: Orca <help@stably.ai> * fix(automations): route folder-workspace agent launches to their owning SSH host Review of the bind-before-publish fix surfaced a second defect on the path it newly makes reachable. A folder workspace has no repo row — its synthetic repoId is `folder-workspace:<groupId>` — so `repos.find(...)` returns null and every repo-derived launch input silently degraded to a local default: connectionId null, platform CLIENT_PLATFORM, isRemote false. The automation then spawned on the user's machine with a cwd that only exists on the SSH host. Before this branch that path threw before reaching the spawn, so the bug was latent; making folder-workspace automations work is what exposes it. Host resolution now goes through resolveAgentBackgroundLaunchHost, which falls back to the workspace scope (the same getFolderWorkspaceConnectionId that ordinary terminal creation uses) when there is no repo. Ambiguous scopes — mixed local/remote children — still resolve to a local launch rather than guessing a host. Extracted to its own module rather than inlined: the added branch pushed launch-agent-background-session.ts over the 300-line oxlint cap, and a max-lines disable is forbidden. Tests: an SSH folder workspace must spawn with its connectionId and remote cwd; a local one must stay local. Reverting the fallback fails the first and leaves the rest green. * fix(automations): close folder dispatch and adoption races Read live state before adopting a reserved tab so a collision that lands during the spawn is retired instead of re-keyed. Route persisted folder-workspace dispatch through its ambiguity-aware owner, including SSH auth, remote trust, quiet-shell fallback, and WSL shell selection. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
974447175f | feat(terminal): add manual parking developer action (#11016) | ||
|
|
79ec57d045 |
perf(relay): keep the PTY replay window as chunks, not a re-sliced string (#10900)
* perf(relay): keep the PTY replay window as chunks, not a re-sliced string appendReplayBuffer did `buffered += data` then `buffered.slice(-REPLAY_BUFFER_MAX)` once over the 100KB cap. It runs per raw node-pty emission -- before batching -- so once a PTY saturates the window (which a long-lived shell does almost immediately) every subsequent chunk copied the whole 100KB. Reuse RecentPtyOutputBuffer, which already solved this shape in the main process: keep chunks, drop from the head, defer the join to read(). The relay's three readers are attach, adopt, and revive only. 66-205x on the append path, per PTY, on the user's SSH host. RecentPtyOutputBuffer's limit is now configurable, because the relay retains 100KB where the main process retains 64KB. One arithmetic branch still used the hardcoded constant after that change and silently under-retained (100,800 of 102,400 code units); the equivalence tests caught it before it shipped, and the suite now pins the configured limit directly. Co-authored-by: Orca <help@stably.ai> * test(relay): exercise a real surrogate split; drop eval from the benchmark Review feedback, both valid: - The surrogate test never split a pair. The cap is even and a pair is two code units, so an emoji run alone always cuts on a pair boundary. A trailing single unit shifts the cut mid-pair, leaving a dangling low surrogate (0xDE00) -- asserted directly now, with the boundary-aligned case kept as its own test. - Parse REPLAY_BUFFER_MAX as a product instead of eval(). The regex already admits only digits, spaces and `*`, and eval tripped Biome's noGlobalEval regardless of the eslint suppression. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
7f3c95a585 |
fix(git-history): stop reading the option marker as the resolved ref name (#10906)
`rev-parse --verify` swallows --end-of-options, but --symbolic-full-name deliberately echoes it -- on every git version tested, 2.25 through 2.49: $ git rev-parse --symbolic-full-name --end-of-options feature --end-of-options refs/heads/feature resolveSymbolicFullName took the first non-empty line, so it returned the literal string "--end-of-options" instead of the ref. That value flows into gitHistoryRefFromFullName, matches none of the refs/heads, refs/remotes, or refs/tags prefixes, and every named branch and tag in git history was silently categorized as a plain commit with a garbage id. Skip the marker line. Version-independent bug; no test covered it. Also pins git's echo behavior in the real-binary compatibility suite, so if a future git stops emitting the marker the reason for the skip gets re-read rather than the assumption quietly rotting. Co-authored-by: Orca <help@stably.ai> |
||
|
+21 |
b31e66ed48 |
fix(browser): survive a transient Windows lock during cookie import (#10697)
* fix(browser): retry a transient Windows lock on the Chromium cookie snapshot copy The snapshot attempt loop only reacts to a `false` return, so a throwing copy escaped it entirely. On Windows, AV/EDR briefly opens a file literally named "Cookies" with FILE_SHARE_NONE, which turns an otherwise-fine copyFileSync into `EBUSY errno -4082 syscall=copyfile` and aborts the whole import (#9355). Route the main-database copy through the existing `copyFileWithWindowsRetry`, already used for the same AV window in #1507. It is a no-op off Windows (maxAttempts=1), so POSIX still fails fast and ENOENT is never retried on any platform. * fix(browser): degrade the cookie-import staging DB instead of aborting the import Staging exists only to back the cold-restart replay for cookies the in-memory path rejects, but three points in it were fatal to the whole import (#9355): - the staging copy from the live partition DB — also a file named "Cookies", so the same AV/EDR handle blocks it; - opening/PRAGMA-ing that staged file; - both were unguarded, so a throw escaped to the catch-all and returned `ok: false` even when every cookie could load in memory. Each is now non-fatal and diagnosed. Two invariants keep the degraded path honest: `imported++` moved out of the staging insert so the summary counts importable cookies rather than staged rows, and `setPendingCookieImport` is never called when staging is unavailable — registering a path that was never written would replay a missing or partial DB over the live partition on cold start. * fix(browser): stop a degraded cookie import from replaying a stale staged database Review round 1 found the staging-degradation path could leave an older pendingCookieImports entry registered while the import rewrote the live session, so the next cold start replayed the stale DB over fresh cookies. - add clearPendingCookieImport so a degraded import retires the old entry - degrade staging on BEGIN/insert/COMMIT failure instead of aborting the import - discard the staged cookie copy on every non-registering path - pin the stagingAvailable guard, which previously survived mutation Co-authored-by: Orca <help@stably.ai> * fix(browser): report a degraded cookie import honestly instead of as a clean success Making the staging failure non-fatal introduced a silent-loss path: the import clears the live jar before loading cookies, so when staging was unavailable AND Electron rejected cookies, the user lost their old jar, got none of the new cookies, and still saw "Imported N cookies". Adds an optional `warning` to the import summary, set only on that degraded branch, and routes every cookie-import toast through a shared emitter that raises a warning toast instead of an unqualified success. Also closes three test holes found in review: - clearPendingCookieImport had no direct tests; deleting the wrong partition key survived all 10 registry persistence tests. Now covered, mutation killed. - The staging-insert-failure test was vacuous (memoryFailed === 0 suppressed registration on its own). It now forces a memory failure. - No test pinned the success-path clear; removing it survived. Now covered. * fix(github): resolve owner/repo through SSH Host aliases (#10284) (#10361) * fix(github): resolve owner/repo through SSH Host aliases (#10284) Expand OpenSSH Host → HostName via ssh -G before classifying github.com identity so PR merge works when origin is git@alias:owner/repo.git. Transport URLs stay unchanged so IdentityFile selection is preserved. Do not long-negative-cache indeterminate ssh -G failures. * fix(github): harden SSH alias resolution * Update README downloads badge * fix(persistence): fsync state writes so a rename is actually durable (#10631) * fix(persistence): fsync state writes so a rename is actually durable `Store` wrote `orca-data.json` to a temp file and renamed it. rename() is atomic for readers but says nothing about durability: without an fsync the directory entry can reach disk before the data does. After power loss or a hard crash the file can come back holding the previous state or, worse, zero bytes — and `JSON.parse('')` throws, so an empty file takes the full corrupt-file path rather than degrading. This is the same empty-file symptom as #1158 from a different cause. That issue fixed a logic path that persisted empty state and added the .bak ring as a safety net; the ring also catches this, which is why it went unnoticed. Recovery costs up to an hour of tabs/layouts/session state (backups are throttled to >=1h spacing), and a user in their first hour has no backup slot yet, so they land on defaults indistinguishable from a fresh install. Both write paths now fsync the temp file *before* the rename, then fsync the containing directory. Directory fsync is best-effort by design: Windows cannot open a directory for fsync and some filesystems reject it, so it is swallowed. The file fsync is the load-bearing part and works everywhere. Measured cost on a 3 MB payload: ~0.2 ms per write, against a 1s debounce. The async path does not block the main thread. The syscall-order test mocks `node:fs` and counts fsync targets at the module boundary, asserting ['file', 'directory'] — proving the ordering rather than inferring it from reading the implementation, since a fsync after the rename would still pass every content assertion. * test(persistence): make the syscall proof platform-aware and actually prove the order Two problems, both found from CodeRabbit's Windows observation. The assertion hardcoded ['file', 'directory']. Directory fsync is deliberately best-effort — Windows cannot open a directory for fsync and some filesystems reject it — so on Windows the helper swallows the failure, only the file fsync is observed, and the test fails. The expectation now probes the real platform instead of assuming, keeping the guarantee tight where directory fsync works rather than dropping it everywhere. Worse, the test did not prove what its name claimed. Moving the fsync to *after* the rename still passes: the file is fsynced either way, and only fsyncs were recorded, so the correct and broken orders produced an identical log. Mutation-testing the "before rename" claim is what surfaced this — the mutation passed. The rename is now recorded in the same sequence, since it is the boundary the ordering is defined against. Re-running the same mutation fails, so the ordering claim is now backed by the test rather than asserted in a comment. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> * fix(window): stop burning macOS GPU on an invisible blur effect (#8482) (#10682) Co-authored-by: Orca <help@stably.ai> * feat(sidebar): distinguish and filter CLI-created workspaces (#10712) * perf(relay): stop snapshotting the whole pending-PTY map every drain tick (#10670) * perf(sidebar): share one worktree-keyed agent orchestration index (#10678) Co-authored-by: Orca <help@stably.ai> * fix(mobile): recover unreliable relay connections (#10709) * fix(mobile): recover unreliable relay connections * test(mobile): use valid raster preview fixtures --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> * release: v1.4.157-rc.0 * docs(relay): trim the pending-output drain comment (#10714) Co-authored-by: Orca <help@stably.ai> * fix(history): quarantine unreadable recovery generations. (#10713) * fix(history): quarantine unreadable recovery files * fix(history): preserve mixed recovery generations * fix(history): reanchor reconciled live sessions * fix(history): serialize final checkpoint queue * fix(history): drain sleep shutdowns before disconnect * fix(history): restore legacy wide sessions * fix(history): preserve malformed mixed logs * fix(history): preserve malformed log tails * refactor tests to reduce file size * refactor(history-recovery): extract freeze helper and improve test robus - Extract takeRecoveryFreeze to eliminate duplicated freeze-and-clear pattern across five call sites - Skip permission-mode tests on root CI containers (chmod 0o500 doesn't block root writes) - Replace fixed sleep with deterministic wait for queued exclusive checkpoints - Distinguish ENOENT (missing) from corrupt in history metadata reads - Add ceiling-dimension restore test and torn-tail exclusion assertion - Wrap chmod operations in try/finally to prevent leaked permissions from masking test failures - Add .catch() to checkpoint promise to prevent unhandled rejections from finally re-throws * test(history-recovery): consolidate checkpoint assertions Wait for both the checkpoint call and set clear atomically to avoid a timing race where the spy fires before the set is cleared. * fix(persistence): unbreak main by expecting the new 'cli' card property in fresh defaults (#10722) Co-authored-by: Orca <help@stably.ai> * fix(sidebar): stop worktree drag from spazzing when cards resize mid-drag (#10725) * fix(sidebar): make Cmd/Ctrl+1-9 match the rendered card order when the sidebar is closed (#10693) * fix(native-chat): wrap question text and option descriptions instead of truncating (#10025) * fix(mobile): pop to home when leaving a host so the back chevron animates backward (#9723) * fix(i18n/zh): correct technical literals and clear sense errors (#10048) * feat(speech): add Korean streaming zipformer STT model (#9893) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * feat(source-control): add copy relative path (#9018) * fix(gitlab): stop refresh button overlapping dialog close X (#9445) * fix(gitlab): stop refresh button overlapping dialog close X The GitLab item dialog's SheetContent renders its own close (X) at absolute right-4, but the header refresh button sat at the header's px-5 right padding and overlapped it. Reserve pr-10 on the header so the refresh button clears the close X, and lift it -mt-1.5 so its icon aligns with the close X on the same line. * fix(gitlab): integrate sheet controls into header --------- Co-authored-by: viniciussilva <vinicius.silva@plus10.de> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * fix(sidebar): raise selected workspace contrast in dark mode (#8321) * feat(editor): toggle Word Wrap from file tab actions and Alt+Z (#10086) * feat(editor): toggle Word Wrap from file tab actions and Alt+Z Long single-line and structured files wrap by default and misalign. Surface Word Wrap on the editor more-actions menu for normal file tabs (diff already had it) and add editor.toggleWordWrap (Alt+Z) so users can unwrap without opening Settings. Closes #9974 * fix(editor): toggle diffWordWrap for diff surfaces on Alt+Z CodeRabbit: Alt+Z previously always flipped editorWordWrap, leaving diff panes out of sync with the markdown actions menu. * test(editor): verify word wrap shortcut routing Cover editor/diff setting callbacks and the cross-platform Alt+Z binding. --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * fix(repo-icon): detect Tauri and WebP icons (#7942) Expand repository icon auto-detection to conventional Tauri and public/icon paths with PNG/WebP magic and dimension validation. Bound SSH probing while preserving candidate priority and PNG-only user uploads; SVG remains rejected. * Add bulk tab closing to mobile long-press sheets (Close Others / Left / Right) and complete the desktop tab context menus (#9323) * Add Close Tabs to the Left and complete Close Others across tab menus and mobile long-press sheets * Fold the per-sheet Close action into the bulk-close module (session route max-lines) * fix(mobile): preserve pinned tabs during bulk close --------- Co-authored-by: Tom de Bres <tomdebres@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * feat: implement Cmd+Enter as commit shortcut in Source Control (#9773) * feat: implement Cmd+Enter as commit shortcut in Source Control * test: add unit tests for commit shortcut and tooltip formatting * fix: address review feedback on modifier keys and test coverage * test: split mac and windows/linux shortcut and keydown tests --------- Co-authored-by: Andres Van Reepingen <andres.vanreepingen@datacamp.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * Add SenseVoice speech-to-text model (Korean/Japanese support) (#7436) * Add SenseVoice speech-to-text model (Korean/Japanese support) SenseVoice (zh/en/ja/ko/yue) is the only bundled local STT model with Korean and Japanese support. The existing local models cover only English and Chinese (Parakeet, Zipformer, Paraformer); Whisper Tiny is multilingual but trades accuracy for breadth. - Add 'senseVoice' to SpeechModelType - Register the sherpa-onnx SenseVoice archive in the model catalog (pinned SHA-256, single-file model.int8.onnx + tokens.txt layout) - Handle the senseVoice type in the STT worker via createOfflineRecognizer with the senseVoice model config (auto language detection + ITN) - Add model-catalog regression tests for the new entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(speech): use int8-only SenseVoice archive * fix(speech): refresh SenseVoice catalog metadata --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * fix(settings): show a way back to local accounts when a remote server owns provider-account scope (#8188) Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * feat(speech): add Parakeet TDT-CTC 0.6B JA voice model (#8207) * Add SenseVoice speech-to-text model (Korean/Japanese support) SenseVoice (zh/en/ja/ko/yue) is the only bundled local STT model with Korean and Japanese support. The existing local models cover only English and Chinese (Parakeet, Zipformer, Paraformer); Whisper Tiny is multilingual but trades accuracy for breadth. - Add 'senseVoice' to SpeechModelType - Register the sherpa-onnx SenseVoice archive in the model catalog (pinned SHA-256, single-file model.int8.onnx + tokens.txt layout) - Handle the senseVoice type in the STT worker via createOfflineRecognizer with the senseVoice model config (auto language detection + ITN) - Add model-catalog regression tests for the new entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(speech): add Parakeet TDT-CTC 0.6B JA to the speech model catalog * test(speech): cover stt-worker-model-config file resolution incl. single-file models * feat(speech): decode Parakeet TDT-CTC JA via sherpa-onnx nemoCtc offline recognizer * fix(speech): use int8-only SenseVoice archive * fix(speech): refresh SenseVoice catalog metadata --------- Co-authored-by: xsacdw <xsacdw@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * fix(terminal): stop switch bold flash and Windows lag (#10692) * fix(terminal): stop bold flash on worktree switch Worktree hide disposes WebGL and falls back to xterm's DOM renderer. On reveal, resume ran after paint and flushed backlog against DOM first, so the first frame used heavier CSS-AA glyphs before WebGL settled. Resume in useLayoutEffect and reattach WebGL before backlog flush so the first painted frame stays on the GPU path. No cold-park policy change. Co-authored-by: Orca <help@stably.ai> * fix(terminal): fit WebGL grid before backlog flush on resume Adversarial review: resume-before-flush alone wrote TUI backlog onto the transient DOM↔WebGL one-column-off metrics window. Order is now resume → fitAllRevealedPanes → flush on heavy reveal and window wake. Co-authored-by: Orca <help@stably.ai> * fix(terminal): latch viewport intent before WebGL wake resume Adversarial review: wake path synced intents after resume/fit, which can re-latch a pinned viewport as followOutput. Capture before reattach and drop the post-resume re-sync on heavy reveal (outer path already latched). Co-authored-by: Orca <help@stably.ai> * fix(terminal): complete visibility bookkeeping before PaneManager exists useLayoutEffect runs before the passive lifecycle creates PaneManager, so the mount-visible path never set hasCompletedVisibleResume. The first intra-worktree hide then wrongly suspended WebGL. Bookkeep completion even when managerRef is still null (extracted helper for max-lines). Co-authored-by: Orca <help@stably.ai> * fix(terminal): re-sync pin geometry after resume backlog flush Keep the pre-resume intent latch (reattach must not re-latch pins as followOutput), then re-sync after flush with preservePinnedAtBottom so scrollback trim updates absolute pin lines before enforce. Co-authored-by: Orca <help@stably.ai> * fix(terminal): drop same-tick post-flush intent re-sync flushTerminalOutput only queues terminal.write and returns before parse, so a same-tick re-sync read pre-parse resume/fit geometry and could overwrite pre-resume pins. Keep pre-resume latch + enforce only. Co-authored-by: Orca <help@stably.ai> * fix(test): expect default worktree card properties to include cli #10712 added 'cli' to DEFAULT_WORKTREE_CARD_PROPERTIES, but the fresh default-profile assertion still omitted it and fails verify. Co-authored-by: Orca <help@stably.ai> * perf(terminal): retain Windows WebGL across worktree hides * perf(terminal): bound retained WebGL contexts * fix(terminal): harden retained WebGL lifecycle * fix(terminal): preserve healthy WebGL on wake * fix(terminal): preserve reveal recovery ordering --------- Co-authored-by: Orca <help@stably.ai> * fix(mobile): clear native-chat composer optimistically at send time (#10226) * fix(mobile): clear native-chat composer optimistically at send time Over relay the send RPC round trip is visible and a lost ack (or a relay/direct cutover) could strand the sent prompt in the composer forever: the unconfirmed-send deadline dropped its tracking entry, so a late transcript echo could never clear the draft. Clear the draft at send time and restore it only on a definite rejection. holdUnconfirmedSend now only manages the delivery-unconfirmed notice; it no longer touches drafts. * fix(mobile): isolate question answers from composer drafts * fix(cli): bound orchestration ask timeouts (#10689) * fix(cli): bound orchestration ask timeouts * fix(cli): harden remote timeout boundaries * fix(crash-reporting): stop fit-retry bursts from erasing the pre-crash trail (#10729) * fix(crash-reporting): stop fit-retry bursts from erasing the pre-crash trail Windows renderer OOM F0BKR84AHEH (0xE0000008) arrived with a 30-entry breadcrumb ring in which two `terminal_safe_fit_retry_exhausted` bursts consumed 26-90% of the slots. Every hidden pane is `display:none` -> 0x0 -> unmeasurable, so one post-reload reattach wave exhausts the retry budget once per mounted pane inside ~60ms. The bursts were also uninterpretable: `pane.id` restarts at 1 per PaneManager and there is one manager per tab, so 34 identical `paneId: 1` crumbs cannot distinguish one pane looping from 34 panes firing once. Coalesce the crumb by name and carry the live-pane census on the payload instead, so the count survives without costing 34 ring slots. Same treatment for WebGL diagnostics, which were worse off: context-loss and atlas-reset crumbs only reached a DevTools-only ring (`window.n()`), so a renderer that dies takes them with it. That bundle had three GPU-process deaths in the 65s before the renderer OOM and zero WebGL evidence - absence of instrumentation, not absence of the event. Mirror them into the crash report, coalesced per kind so a routine atlas reset cannot mask a context loss. Evidence-only: no behavior, rendering, or lifecycle path changes. Co-authored-by: Orca <help@stably.ai> * perf(pane-manager): count panes without materializing public views The census runs on the crash path; getPanes() allocates a full ManagedPane projection per pane just to read .length. Co-authored-by: Orca <help@stably.ai> * test(crash-reporting): pin the fit-retry burst against the 30-entry ring Reproduces the F0BKR84AHEH ring loss directly: 10 pre-crash crumbs plus a 34-crumb per-pane burst. Uncoalesced, the burst takes all 30 slots and zero pre-crash crumbs survive; coalesced, it takes one slot, all 10 survive, and the pane count rides on the payload instead of on the crumb multiplicity. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): name the WebGL census the same as the fit-retry census The context-loss crumb spread getLivePaneCensus() raw, so one ring described one measurement two ways: managers/panes here, livePanes/livePaneManagers on the fit crumb. Spreading also meant renaming the census return keys would silently reshape the crumb. Name the fields at the call site and pin them. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): keep a hot coalesce key from being the first LRU eviction The suppression path returned before the delete-then-set that re-anchors recency, so a key hit continuously never moved from its original insertion slot and became the first eviction candidate — the inverse of the LRU's stated intent. `renderer_error` keys carry message+stack identity, so one noisy render loop mints unbounded distinct keys. Within a single 30s window that churn evicted the `terminal_safe_fit_retry_exhausted` key mid-burst, un-suppressing it and re-arming the exact ring flush the coalescing exists to prevent. Re-anchor position only; `recordedAt` is left alone so the suppression window still expires on schedule rather than renewing on every hit. Found while adversarially probing the LRU claim in #10729's own description, which asserted these keys "cannot evict live keys". * fix(crash-reporting): report the newest census of a coalesced burst The suppression path wrote nothing to the ring, so a coalesced burst froze its FIRST event. Panes mount progressively, so pane 1 exhausting alone legitimately measures livePanes: 1 -- and the 33 later crumbs, each carrying a truer census, were dropped. A 34-pane wave was recorded as `livePanes: 1` with no count: the exact "one pane looping" misread that coalescing by name was introduced to prevent. The existing burst test missed this because it fed a constant census on every crumb, making frozen-first and newest-wins indistinguishable. Stash the newest payload and fold it into the ring entry the key already owns: still one slot, now reading livePanes: 34 + suppressedSinceLast: 33. Resolution is deferred to snapshot time -- sanitizing per suppressed hit of a 1459/min crash loop measured 2194 ns/op vs 185 ns/op deferred. Two follow-on defects fixed alongside: an expiring key dropped its pending payload (it loses its only handle on the ring entry), and resolving the re-emitting key's own old slot double-counted a burst. --------- Co-authored-by: Orca <help@stably.ai> * fix(speech): download verified model artifacts directly (#10735) * perf(renderer): give owner-routed settings a stable identity (#10743) Co-authored-by: Orca <help@stably.ai> * perf(agent-status): validate hook payloads without the JSON round trip (#10752) * fix(gpu-fallback): make the crash window rolling, not launch-anchored (#10707) * fix(gpu-fallback): make the crash window rolling, not launch-anchored Software-rendering fallback only ever considered GPU child crashes in the first 30s after launch: `if (msSinceLaunch > this.windowMs) return`. Session 12e6ee64 crashed the GPU child 4 times (242s / 920s / 926s / 946s since launch). The last three span 26.0s — inside windowMs, exactly threshold — but every one was rejected because the burst began 920s in. The renderer died of process OOM (0xE0000008) 39s later. GPU work is demand-driven, so the first heavy compositing often happens minutes into a session. What distinguishes a broken driver from normal Chromium churn is that the crashes *cluster*, not when the cluster starts. Keep a sorted array of recent crash times pruned to windowMs behind the newest, and engage when the count reaches threshold. Measured against real field telemetry (341 distinct win32 launches with >=1 GPU crash, from process_gone_suppressed breadcrumb trails): the rolling window engages on 2/341 launches (0.59%), one of which is 12e6ee64. Max GPU crashes in any single launch is 4. The closest non-firing sequence ([0, 29531, 55136, 74178] — consecutive gaps that each fit the window but never put 3 inside it) is pinned as a regression test. Also destroy the Windows tray before app.exit(0) on this path, matching the app:relaunch IPC handler — app.exit skips before-quit, and this can now fire deep into a session rather than only in the first 30s. Mutation-tested: 6 mutants (launch-anchored gate, dropped pruning, dropped monotonic clamp, cutoff </<=, threshold >=/>, dropped engaged latch), all killed by the suite. Co-authored-by: Orca <help@stably.ai> * fix(gpu-fallback): ask before restarting --------- Co-authored-by: Orca <help@stably.ai> * feat(sidebar): add a filter to hide detached-HEAD workspaces (#10786) Adds "Hide detached HEAD" alongside the existing sidebar filters, wired through the same pipeline as Hide CLI-created: sidebar list, Cmd+J empty-query list, workspace board, active-filter badges, Clear/Reset Filters, and persisted UI state. The predicate reuses getWorktreeGitIdentityDisplay so the filter targets exactly what the card renders a Detached HEAD badge for. Requiring a real head (not just an empty branch) keeps folder workspaces and SSH-synthesized rows — which carry both empty — out of the filter. Activating a hidden detached workspace clears the filter, matching the existing reveal escape hatch for automation- and CLI-created workspaces. Splits the filter-state describes out of visible-worktrees.test.ts into sidebar-filter-state.test.ts to stay under the max-lines budget. Co-authored-by: Orca <help@stably.ai> * feat(daemon): add daemon_lifecycle replaced/retired telemetry event (#10058) * feat(daemon): add daemon_lifecycle replaced/retired telemetry event Implements STA-2376. Adds track('daemon_lifecycle', {transition, reason, live_session_count_bucket, version_skew?}) covering 'replaced' (unhealthy_resolver / stale_bundle / different_app_path / failed_health_check at daemon-init launcher sites) and 'retired' (died_respawn at the adapter respawn closures). Enum-only + .strict() + bucketed counts keep paths, versions, and raw counts off the wire; preserve-path transitions emit nothing. Cross-platform and SSH-safe; no-op in non-official builds. Test plan: affected vitest (158) green; typecheck/lint clean except pre-existing unrelated failures. * fix(daemon): prevent false lifecycle telemetry * test(daemon): restore once-ness on respawn reason assertions Keep STA-2376 reason checks without dropping concurrent-respawn coalescing coverage that prevents double died_respawn telemetry. * fix(daemon): emit replaced telemetry on runtime unhealthy_resolver respawn CodeRabbit: adapter-driven macOS resolver replacements forked a new daemon without a lifecycle event. Emit trackDaemonReplaced (not retired) so field diagnosis of #7936 covers the runtime path without mislabeling it as death. * fix(daemon): stop double-counting resolver replaces; drop redundant version_skew Three telemetry-correctness fixes to the STA-2376 daemon_lifecycle event. 1. The runtime macOS resolver respawn double-counted. doRespawn() disconnects but never kills the daemon, so the ensureRunning() that follows re-enters createOutOfProcessLauncher, which re-detects healthy + resolver-unhealthy + 0 sessions and emits the replace itself. The closure emitted a second one. It also emitted before the outcome was known, so a resolver that recovered mid-flight (or a session appearing) left a 'replaced' on the wire for a daemon the launcher went on to preserve. The launcher's emit is gated on a confirmed kill, so it is the correct sole emitter; this reverts the emit added in |
||
|
|
10ca89ac8b |
feat(updater): switch to validated local mac builds (#10889)
* feat(updater): switch to validated local mac builds * test(updater): cover local build recovery actions * fix(types): keep local build contract in project sources |
||
|
|
97cb32c1cc |
fix(terminal): release an abandoned synchronized-output frame on reveal (STA-2694) (#10907)
* fix(terminal): release an abandoned synchronized-output frame on reveal Alt-screen agent TUIs (OpenCode/OpenTUI, Codex, grok) bracket every repaint in `?2026h … ?2026l`. Hiding a pane mid-bracket — which a worktree switch or cold-park lands on routinely, since these brackets are written many times a second — leaves xterm's `decPrivateModes.synchronizedOutput` latched. RenderService.refreshRows checks that latch *before* rendering, so while it holds, every repaint Orca owns is a no-op: the forced render-pause repaint, the plain `refresh()` fallback, and the shared glyph-atlas rebuild all render zero rows while the xterm buffer is perfectly correct. Release the latch at the two reveal repaint entry points so those repaints actually paint. Also adds an OpenCode-shaped alt-screen e2e fixture and spec. The existing inline-TUI convergence spec covers the normal-buffer shape (live block glued to the bottom, history scrolling into scrollback); this covers the full-screen alternate-buffer shape, where nothing scrolls and so no row ever self-heals through the scroll path. Scope note: xterm arms a 1s watchdog that clears this latch on its own, so this closes a bounded window rather than the whole STA-2694 report. The e2e spec passes with and without the production change for that reason; the unit tests are what pin the behavior. Refs STA-2694. * fix(terminal): clear the render model on the plain-refocus repaint path `schedulePaneRevealPresent` — the atlas-preserving path a plain window refocus takes — only called `terminal.refresh()`. xterm's renderers are diff-based: `_updateModel` early-continues on any cell whose code/fg/bg/ext still match the cached model, so a refresh repaints nothing for a pane whose buffer never changed. When an occluded window loses its canvas contents while that model stays populated, the refresh skips exactly the cells that went stale and the pane keeps compositing pre-hide pixels — until a window resize reallocates the model, which is the repair users find by hand. Clear the model first (`RenderService.clear()` → renderer `clear()` → `_clearModel(true)`) so the refresh becomes a guaranteed full repaint. That drops cached cells and glyph vertices but NOT the texture atlas, which is shared by every same-config terminal and whose mid-stream wipe re-arms xterm's page-merge garble race (xterm.js #4480) — the reason this path is atlas-preserving in the first place. Also covers the DOM-renderer fallback in `resetWebglTextureAtlas`: `clearTextureAtlas()` is what invalidated the model on the WebGL path, so a pane without an addon had nothing invalidate it and hit the same skip. Scope note: the e2e spec guards buffer/geometry convergence across the hide/reveal boundaries and adds idle-agent and headful desktop-hide cases, but it cannot observe a stale canvas — both oracles built for that (canvas-vs-buffer ink sampling, screenshot-vs-forced-repaint) were proven blind by injecting the defect, and the spec header documents why. The unit tests pin the ordering and the atlas-preservation invariant. Refs STA-2694. Co-authored-by: Orca <help@stably.ai> * docs(terminal): hand off the STA-2694 reveal-artifact investigation Records both fixed defects with their xterm mechanisms, the reveal/wake call graph, why every e2e oracle for a stale canvas was proven blind, how to arm the in-app render-desync sentinel on real hardware, and the one unverified lead (dimension staleness) that would explain why a window resize specifically is the repair users find. Refs STA-2694. Co-authored-by: Orca <help@stably.ai> * Revert "fix(terminal): clear the render model on the plain-refocus repaint path" This reverts commit |
||
|
|
cef52c68be |
fix(runtime): stop rejecting ui.set when Linear or Jira resume fields are present (#10715)
Reviewed with an independent reproduction. Extended the fix to the UiUpdate parity gaps one level up, and added a typecheck-level assertion so future field drift fails the build instead of silently rejecting paired-client payloads. |
||
|
|
abcdc04f6b |
fix(ci): mirror missing lint steps in PR workflow (#10601) (#10623)
Reviewed with an independent reproduction. Added the allowlist entry that unblocked verify:localization-coverage on main, the 4th drifted step, and a parity gate that fails when pnpm lint's chain contains a script absent from pr.yml. |
||
|
|
3baffb49ff |
fix(runtime): refuse SSH hosts in project setup instead of acting locally (#10799)
* fix(runtime): refuse SSH hosts in project setup instead of acting locally projectHostSetup.clone and .setupExistingFolder threaded executionHostId all the way down but never used it for routing: cloneRepo runs a local mkdir plus a local gitSpawn, and addRepo probes the path with existsSync/statSync. An `ssh:` host therefore cloned and validated on the *local* machine and then registered the result as living on the SSH host. It only failed loudly here because the remote path did not exist locally. With a plausible destination the clone succeeds and writes a setup record pointing at the wrong machine. Nothing legitimate sends `ssh:` to these RPCs: the renderer maps every ssh host (including ephemeral-VM `ssh:runtime-ssh-*`) to the desktop IPC path, which dispatches to addRemoteRepoFromPath/cloneRemoteRepo, and the IPC handler symmetrically rejects `runtime:`. Only the CLI can reach here with `ssh:`. Fail closed until the RPC learns to route through the SSH providers. * test(runtime): make the SSH guard test observe the corruption it names The test asserted `gitSpawn` was never called and no repo was registered, but neither assertion could fail. `/home/brennan` is unwritable on macOS, so the pre-guard clone died at `mkdir` before reaching `gitSpawn`, and `/home/brennan/orca` failed `isGitRepo` before reaching `addRepo` — the exact side effects under test were unreachable either way. `rejects.toThrow` also aborted the test before those lines ran. Use a real temp destination and a real temp git repo, await both calls via `.catch`, and assert the side effects before the wording. With the guard disabled the test now fails on `gitSpawn` being called once with a real `git clone`, and on a repo registered stamped `executionHostId: 'ssh:openclaw'` — the silent local-clone-recorded-as-remote defect itself. `gitSpawn` is stubbed so a regression records the call instead of hitting the network. Also document the SSH restriction on `project setup-existing-folder`, which the guard now rejects. `setup-clone` already carried that note; its sibling did not. |
||
|
|
1fcbf8e5fe |
fix(dashboard): stop the agent icon shrinking on long card titles (#11001)
A bare <svg> flex item shrinks with its row, so kanban cards and the terminal dialog rendered a squashed ~9px agent icon whenever the worktree name overflowed. Wrap both in the shrink-0 span every other surface already uses. |
||
|
|
3716a7bb49 | fix(markdown): render task continuations as text (#11008) | ||
|
|
0ab5f499cb | fix(cmd-j): restore focus when issue match routing declines (#11010) | ||
|
|
a065db154c |
fix(sidebar): spin the worktree dot while Claude Code is thinking (#9040) (#10684)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
2a640abfbe |
fix(pty): strip inherited Claude child-session stamps at spawn (#9961)
An Orca GUI or daemon launched from inside a Claude Code session inherits CLAUDE_CODE_CHILD_SESSION / CLAUDE_CODE_SESSION_ID / CLAUDE_CODE_BRIDGE_SESSION_ID. Every spawn path spreads the host's process.env, so each terminal Orca opens is marked a nested Claude child and Claude silently disables transcript persistence — real sessions stop writing on-disk history with no visible error. Older-protocol daemons are deliberately preserved across upgrades and the auto-updater relaunch inherits the previous app's env, so one contaminated launch propagates through subsequent updates. Orca never sets these variables, so an inherited value is always poison. Add a deny constant plus an inherited-only filter merged into envToDelete at both pty spawn call sites, keeping a stamp explicitly passed in args.env. Unlike the agent-hook keys this is not gated on isDaemonHostSpawn, because the local provider and the relay host spread their own process.env too. Review fixes: mergePtyEnvDeletions is now variadic (the nested form passed a `string[] | undefined` intermediate into a `readonly string[]` parameter and did not typecheck); coverage extends to the runtime-controller spawn path, the local provider, and the SSH route, whose exact spawn-options assertion had to be updated because envToDelete is no longer ever undefined. |
||
|
|
ab60045371 |
fix(shortcuts): gate Cmd/Ctrl+N folder-workspace jumps on path status (#10748)
Reviewed with an independent reproduction, including a live-app check. Replaced tests that mocked the module containing the fix, and reused the existing workspace activator instead of adding a 4th copy of the dispatch. |
||
|
|
39a200d900 |
fix(release): restore the Windows inner-binary signature gate (#6487) (#10719)
* fix(release): restore the Windows inner-binary signature gate
electron-builder 26.9+ dropped the bundled 7zip-bin package, so the gate's
hardcoded node_modules/7zip-bin path stopped resolving in
|
||
|
|
c0734f039d |
fix(terminal): disarm stale TUI modes when a pane confirms return to shell (#9608)
A TUI killed hard (SIGKILL, OOM, crash) never restores the modes it armed. When its parent shell survives, the emulator keeps mouse tracking, focus reporting and Kitty keyboard flags on: every pointer move over the pane lands as typed SGR motion reports at the prompt, and the doomed process burns CPU parsing the motion firehose while it lives. Orca's existing mode cleanups all hang off dead-PTY paths (hibernation kill, daemon reattach), so an agent dying under a live shell crossed none of them. Fire POST_REPLAY_REATTACH_RESET at the pane-foreground-agent tracker's confirmed return-to-shell transition, next to the sibling stale-title cleanup. That transition is gated on a real foreground-process read rather than the bare OSC 133;D, because a full-screen agent's nested command shells leak their own D onto the main PTY. The write goes through the replay guard so xterm's auto replies cannot leak to the shell as input. The reused constant already excludes ?2004l, so the bracketed-paste protection the live shell re-arms at its prompt is preserved. |
||
|
|
17fc40eae6 |
fix(hooks): drain POSIX hook stdin without PATH (#10885)
Reviewed with an independent reproduction on macOS and Ubuntu 20.04. Fixed five test files that asserted the old literal strings, and switched the reader to `command -p cat` so it also survives hosts without /bin/cat. |
||
|
|
cdd5ceb72b |
fix(jira): render issue description/comment images with lightbox (#8938)
* fix(jira): render issue images and open them in a lightbox Jira ADF media nodes were dropped when converting descriptions/comments to Markdown, so screenshots never appeared in the Tasks drawer. Download image attachments with authenticated Jira API access, embed them as data URLs on issue/comment detail loads, and add a viewport-centered lightbox. Closing with X/Esc only dismisses the preview, not the issue sheet. * fix(jira): open comment images in the same lightbox as description Jira issue comments still used compact markdown, so screenshots rendered but could not expand. Use the document renderer for comment bodies, add a regression test for the expand control, and sync MarkdownImageLightbox locale keys. * fix(jira): harden inline image handling * fix(jira): harden inline image discovery, escaping, and downloads Address PR review findings: correct media-attachment pairing, Server/DC attachment lookup base path, markdown-safe external URLs, wider HTML discovery with gated alt fallback, concurrent downloads outside the API semaphore, and a main-process attachment data-URL cache with lower caps. * fix(jira): Option A multi-same-name attachments and post-map media warns Fix discovery so repeated alts (image.png) get distinct attachment ids, flush resolution warns after ADF mapping using attachment-only stats, clear attachment cache on clearToken with epoch-guarded singleflight, and add Server comment path plus release-before-binary regression tests. * fix(jira): simplify comment media request skip condition Only needingCount determines whether to skip the attachment metadata request — htmlIds alone cannot produce a download without needing media. Add type annotation for mediaAttrs for clarity. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
2bb3276a35 |
fix(cmd-j): focus the destination workspace's own terminal after a jump (#10695)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
24706ccff0 | fix(terminals): negotiate explicit close intent for paired runtimes (#10129) | ||
|
|
0f91af821d |
ci: parallelize PR checks and accelerate Vite builds (#10989)
* ci: parallelize and accelerate PR checks * fix(ci): make accelerated checks runtime-safe * fix(ci): address review findings * fix(ci): retry transient Electron downloads * test(ci): cover Electron download retry limits |
||
|
|
a1ad4714e9 |
fix(gitlab): render item descriptions and comments with document markdown variant (#9161)
Co-authored-by: viniciussilva <vinicius.silva@plus10.de> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |