createLaunchEnv adds a team (with nested panes map) per agent-team leader,
keyed by a fresh teamId. The only eviction (removeTeamForLeaderHandle) ran
solely from closeTerminal (explicit user-close). The natural-exit paths
onPtyExit and dropDisconnectedPtyRecord tore down every other per-pty map
but never the team, so a leader exiting on its own leaked the team + panes
map permanently.
Evict via the leader handle (handleByPtyId.get(ptyId)) in both onPtyExit
and dropDisconnectedPtyRecord, mirroring the closeTerminal call. Add
getActiveTeamCount for assertability.
Regression test drives the real onPtyExit/dropDisconnectedPtyRecord: fails
before the fix (teams leak) and passes after.
Co-authored-by: Orca <help@stably.ai>
dispatchCompletion writes a per-paneKey entry into the module-scoped
lastCompletionIdentityByPaneKey on every completion. paneKey is a
never-reused ${tabId}:${leafUUID}. The map is module-scoped so it survives
a live-stream remount (dispose-then-recreate, isLive() still true), but
dispose never evicted it, so genuine teardown (PTY gone) leaked one entry
per closed pane.
Evict in dispose only when !options.isLive() — the same discriminator the
cross-remount dedup tests rely on (remount disposes with isLive() true).
Regression test fails before the fix (identity survives teardown, 200 leak)
and passes after; the existing cross-remount dedup suite still passes.
Co-authored-by: Orca <help@stably.ai>
onData creates+sets a pty record before the stopped-state guard, and onExit
deletes the record instead of leaving a tombstone. A data chunk arriving
after onExit (the exit-then-data race in pty.ts shutdown) resurrected a
fresh record that nothing ever deleted, and double-counted the session.
ptyId is a fresh per-spawn UUID, so the three ptyId-keyed maps grew
unbounded.
Track exited ids in a bounded FIFO and refuse onData resurrection for them.
Regression test fails before the fix (record resurrected, 200 leaked) and
passes after.
Co-authored-by: Orca <help@stably.ai>
recordCoalescedCrashBreadcrumb set entries keyed by agent:${agentType}:${state}
with no prune and no cap. agentType is an open string (length-trimmed only,
never enum-checked) over the relay/SSH/OSC ingest paths, so the map grew
unbounded over a long multi-agent session — the sibling breadcrumbs array
is capped at 30, but this map was not.
Prune entries past their suppression window and LRU-cap to 128, mirroring
ProcessGoneDedupe's prune/maxKeys idiom.
Regression test fails before the fix (1000 keys) and passes after.
Co-authored-by: Orca <help@stably.ai>
handleWriteError adds a sessionId to disabledSessions on any best-effort
history write failure. closeSession (the normal PTY-exit teardown) deleted
the writer but never the disabled flag; the only other deletes are on id
reuse (never happens) and explicit user-kill. A session poisoned by a
transient mid-life write error then exiting normally leaked its id in
disabledSessions forever.
Delete the flag in closeSession after writers.delete. Add isSessionDisabled
and disabledSessionCount test accessors.
Regression test fails before the fix (50 ids leak) and passes after.
Co-authored-by: Orca <help@stably.ai>
removeProject hand-deleted only a handful of per-worktree maps and never
called the canonical purgeWorktreeTerminalState/buildWorktreePurgeState,
so ~30 worktree-scoped maps (unified tabs, groups, layout, git status,
browser, everActivatedWorktreeIds, …) retained an entry per worktree of
every removed repo with no reaper to recover them.
Route removeProject through purgeWorktreeTerminalState before its
repo-scoped set() (which still handles repo-level keys). The purge is a
pure state reducer with no PTY side effects.
Regression test fails before the fix (entries survive) and passes after.
Co-authored-by: Orca <help@stably.ai>
Three worktree-keyed maps (remoteStatusesByWorktree,
recentlyClosedEditorTabsByWorktree, defaultTerminalTabsAppliedByWorktreeId)
were re-keyed on rename but purged by neither removal path. Five
page/workspace-keyed browser maps (browserAnnotationsByPageId,
remoteBrowserPageHandlesByPageId, pendingAddressBarFocusByPageId/ByTabId,
recentlyClosedBrowserPagesByWorkspace) were cleaned only on the single
removeWorktree path; the bulk authoritative-scan reconcile missed them.
worktreeId / workspace id / page id are unbounded ephemeral key spaces,
so every removed worktree leaked entries for the session.
buildWorktreePurgeState now collects doomed page ids and omits all eight
maps; the single removeWorktree reducer deletes the three worktree-keyed
ones. A stale comment claiming purge already dropped them is corrected.
Regression test fails before the fix (entries survive removal) and passes
after.
Co-authored-by: Orca <help@stably.ai>
* fix(store): cap prRefreshStates the same way prRefreshSequences is capped
prRefreshStates is keyed by the same unbounded PR cache key
(execution-host/repo/branch) as prRefreshSequences, but was committed
uncapped one line below the capped sequences map. Status-only events
(paused/skipped) and upstream-error outcomes add entries that no prune
path ever removes, so it grew monotonically over a long session.
Bound it by insertion order via a shared capRecordByInsertionOrder helper
(capPrRefreshSequences now delegates to it), and delete-then-set the
touched key in the status writer so only idle keys are evicted.
Regression test fails before the fix (map grows past the cap) and passes
after.
Co-authored-by: Orca <help@stably.ai>
* test(store): fix prRefreshStates leak-test types for tsc/tsgo
The leak test ran under vitest (esbuild strips types) but failed tsgo:
the status-event helper widened literals and the seeded entries typed
reason as string. Annotate the helper as GitHubPRRefreshEvent (in-flight
variant) and type seeded values' reason as GitHubPRRefreshReason.
Co-authored-by: Orca <help@stably.ai>
* fix(store): make prRefreshStates eviction status-aware to prevent UI regression
prRefreshStates backs visible status pills, so a plain insertion-order cap
could drop an in-progress (in-flight/queued/paused) indicator under a large
working set. Raise the bound to 2000 (well above any realistic tracked-branch
count) and evict settled statuses (error/skipped) first; active entries are
dropped only as a last-resort hard bound. Evicted entries self-heal on the
next refresh event.
Test asserts settled-before-active eviction and that the bound holds; still
fails on the un-capped (main) behavior.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
The "Open any file, URL, agent, ..." new-tab entry had two gaps:
1. Agents were not really searchable. findMatchingTabAgentLaunchOptions only
matched on exact alias equality, so a partial name ("gemin", "clau") surfaced
no agent. It now scores prefix/token matches (exact alias wins; otherwise every
query token must prefix an alias token; mid-string and single-char hits are
rejected so agents — which rank above file results — don't noisily hijack the
list). The tokenizer is shared with the create-menu matcher via
query-token-match.ts.
2. Arrow keys didn't navigate the list. Navigation only ran when there were >1
result rows, and the just-opened (empty-query) state trapped focus in the input
so the static create/agent list below was unreachable. Arrows now move the
highlight for any result count, and with no result rows focus routes into the
enclosing Radix menu; ArrowUp on the first item returns to the search box so
the trip isn't one-way.
Also adds combobox/listbox + aria-activedescendant semantics so the highlight is
exposed to assistive tech, and splits the row presentation and option types into
focused modules to keep the component under the line cap.
* Allow deleting downloaded speech models on desktop and mobile
Implement safe deletion of local speech models across desktop and
mobile platforms.
- Add a shared helper to coordinate STT service teardown, file deletion,
and voice settings cleanup.
- Prevent deleting speech models that are currently in use or starting.
- Update desktop and mobile settings UIs to expose the delete action.
* Track multiple pending model deletions and update action disabling
- Replace the single pending delete ID string state with a Set of IDs on desktop settings to track multiple concurrently deleting models.
- Switch the desktop delete action to use the standard themed Button component.
- Disable all row interactions on the mobile voice model list when any model action is busy.
- Add unit test coverage verifying that individual row delete button disabled states behave correctly.
The Mobile Emulator intro callout in the new-tab ("+") menu closed the whole
dropdown when Keep/Hide/Dismiss was clicked, and the "Mobile Emulator hidden"
toast used duration: Infinity so it never went away.
- Drop the onAction/runAndNotify menu-close wiring so the callout actions leave
the dropdown open; removing the prop makes reintroducing the close a type error.
- Auto-dismiss the toast after 30s (still dismissible).
- Make the new-tab dropdown modal={false} so the toast's "Settings > Mobile
Emulator" re-enable link stays clickable while the menu remains open.
Tests: callout button wiring + pointer-down guard + compile-time no-prop guard;
toast duration unit test; updated intro-actions test asserts the 30s duration.
Lazy chunk import() rejections (stale/corrupt chunk -> native SyntaxError, e.g. "Unexpected token ']'", re-thrown by React.lazy) now self-heal via lazyWithRetry: retry with backoff, one sessionStorage-guarded reload, then fall through to the error boundary (loop-safe). Adopted at every renderer lazy site. Fixes the recurring terminal.workbench / right-sidebar / page.mobile react-error-boundary crash.
* feat(worktree): reuse an existing branch when creating a worktree (#5181)
Adds an explicit "Reuse this branch" checkbox to the new-worktree composer.
When an existing local branch is selected in the smart name field, the
checkbox (default on) checks that branch out instead of creating a new
branch from it — and the choice now survives renaming the worktree folder.
Previously, reusing an existing branch only worked implicitly: it required
keeping the auto-filled worktree name equal to the branch, and editing the
name silently reverted to "create a new branch". There was no discoverable,
durable control — the gap the issue reports.
The fix is renderer-only: picking an existing local branch already sets
baseBranch === branchNameOverride, so the existing backend reuse path
(`git worktree add <path> <branch>`, preserveBranchOnDelete) checks the
branch out correctly across local/SSH/runtime. The checkbox pins that
override via the existing branchNameOverridePreservesNameEdits flag so the
worktree folder can be named independently while the branch is reused;
unchecking creates a fresh branch from the selected ref as base.
- New pure helper resolveComposerBranchReuse (local-vs-remote + default).
- Checkbox hidden for remote-only refs and non-branch sources.
- Tests: pure helper, card render/toggle, and a backend integration test
proving a renamed folder still reuses the exact branch.
* fix(worktree): refine reuse-branch checkbox (placement, label, eligibility)
Address review feedback on the "Reuse branch" control:
- Move the checkbox directly under the branch (smart) selection instead of the
Advanced "Name" field — the worktree folder name can legitimately differ from
the branch, so the choice belongs next to the branch pick and is now visible
without expanding Advanced.
- Rename the label "Reuse this branch" -> "Reuse branch".
- Make eligibility dynamic: reuse is impossible when the branch is already
checked out in another worktree (git allows a branch in only one worktree), so
the checkbox is now hidden in that case (it was already hidden for remote-only
refs). When a busy branch is picked, the override is no longer pinned to it, so
creation cleanly falls back to a new branch from that ref as base instead of a
silently-suffixed branch.
Adds isBranchCheckedOutInWorktrees (pure, unit-tested) and threads the repo's
worktree branch list from the store into the selection logic.
* feat(worktree): animate the reuse-branch row's show/hide
Keep the "Reuse branch" row mounted and collapse it with a grid-rows
transition (same pattern as the Advanced drawer) instead of conditionally
rendering it, so the create-worktree dialog grows and shrinks smoothly when the
option appears/disappears as the selected branch changes. The checkbox is taken
out of the tab order while collapsed.
* refactor(worktree): address review nits on reuse-branch control
From a multi-dimensional review of the branch (all findings low severity):
- Reset reuse state (reuseEligibleBranch, reuseSelectedBranch,
branchNameOverridePreservesNameEdits) on repo and project switches, matching
the other reset paths — avoids carrying stale branch-scoped state.
- Disable the reuse checkbox while collapsed so no focusable control lives
inside the aria-hidden row.
- Extract the busy-branch override decision into a pure resolveComposerReuseOverride
helper and unit-test it (busy local branch drops the override; remote-only ref
keeps it) — pins the "no suffixed-branch collision" guarantee.
- Note the worktreesByRepo visibility limitation near the busy-branch check
(a branch busy only in a hidden external worktree is caught by the backend).
- Strengthen tests: reuse-checkbox toggle in both directions, empty worktree
list case.
* fix(daemon): reap dead terminal sessions and clear stale checkpoint flags
TerminalHost.sessions never removed exited sessions: sessionIds are minted
fresh per pane and never reused, so each dead Session pinned a @xterm/headless
emulator (~5000 rows of scrollback) for the lifetime of the long-lived daemon
process. Nothing reads a dead session's emulator (getSnapshot/takePendingOutput/
listSessions all skip !isAlive), so it was pure retained memory.
Wire a Session onExit hook to TerminalHost.reapSession, which disposes the
emulator and drops the entry from the map. Fires on natural exit and the
kill-timeout force-dispose path; the immediate-kill path reaps inline. This is
the 'TerminalHost dead-session cleanup' the handleSubprocessExit comment already
anticipated.
Also clear DaemonPtyAdapter.sessionsNeedingFullCheckpoint on session exit and
non-keepHistory shutdown — a cold-restored session that exited before its first
checkpoint stranded a permanent Set entry.
Regression tests assert the emulator is disposed / the flag cleared on exit
(both fail before the fix). Full daemon suite (557 tests) stays green.
Co-authored-by: Orca <help@stably.ai>
* test(daemon): cover forceDispose reaping; fix stale comments
Review follow-up:
- Update the 'already-exited session' test: with reaping, natural exit disposes
+ drops the session (never force-killed) at exit time via session.dispose, so
host.dispose only sees live sessions. Comments now match; assert the exited
session is gone from listSessions.
- Add a forceDispose (graceful-kill-timeout) test: a stubborn child that ignores
kill is force-disposed after KILL_TIMEOUT_MS, disposing its emulator and reaping.
- Clarify the shutdown() comment that the unconditional checkpoint-flag delete is
a harmless no-op under keepHistory.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(store): bound prRefreshSequences with an LRU cap
prRefreshSequences (Record<cacheKey, number>) is keyed by PR cache key
(execution-host/repo/branch). applyGitHubPRRefreshEvent only ever wrote entries
and never removed them, so the map grew monotonically with the number of distinct
(host, repo, branch) tuples observed over a session — branches are ephemeral and
unbounded. The sibling prRefreshStates is pruned on settle, but prRefreshSequences
was not (its retained entries back the out-of-order sequence guard, so delete-on-
settle is unsafe).
Cap it to MAX_CACHE_ENTRIES, evicting the oldest-touched keys; the writer moves
each touched key to the most-recent position so active branches aren't evicted.
An evicted long-idle branch simply restarts sequence comparison from 0.
Regression test fails before the fix (map grows past the cap) and passes after.
Co-authored-by: Orca <help@stably.ai>
* test(store): cover the move-to-end behavior of the prRefreshSequences cap
Review follow-up: the prior tests passed even if the delete-then-set move-to-end
were removed. Add a test that refreshes the oldest key and asserts it survives
capping (evicting the next-oldest instead) — fails without the move-to-end.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
The runtime-client-events subscription sync had an overwrite-orphan race: a
subscribe is async, so if an environment id is removed from the desired set
while its subscribe promise is in flight (and another live subscription keeps
the generation from bumping), then re-added before the original resolves, the
de-dupe guard sees no live/pending entry and starts a SECOND subscribe. Both
resolve and the second set() overwrote the first's unsubscribe in the map,
leaking the first subscription's preload handle for the renderer's lifetime.
Extract the sync logic from the 5k-line useIpcEvents hook into a unit-testable
createRuntimeClientEventsSync module (previously had zero coverage), and guard
the resolution: if a subscription for the env already exists, unsubscribe the
duplicate instead of overwriting. A deterministic regression test reproduces the
race and fails before the guard (orphan never unsubscribed).
Co-authored-by: Orca <help@stably.ai>
* fix(store): evict runtime detected-agent caches when environments are removed
runtimeDetectedAgentIds and isDetectingRuntimeAgents are keyed by runtime
environmentId and gain an entry whenever an environment's tab-bar launch menu
probes for agents. The only removal action (clearRuntimeDetectedAgents) had no
production caller, so removed environments leaked their entries for the renderer
session.
Add a retainRuntimeDetectedAgents(keepIds) action (mirroring the slice's existing
retainRuntimeEnvironmentStatuses) and call it from setRuntimeEnvironments
(optional-chained, since minimal unit-test stores omit the detected-agents slice).
Also guard the in-flight detect resolve so an environment removed mid-detect is
not re-added. Regression test fails before the fix (stale entry retained).
Co-authored-by: Orca <help@stably.ai>
* fix(store): guard the detect .catch() path against re-leaking pruned entries
Review follow-up: the in-flight guard was applied to the detect .then() but not
.catch(). An environment removed mid-detect whose probe then rejects would re-add
its isDetectingRuntimeAgents entry (and clobber a re-added env's spinner). Apply
the same runtimeDetectPromises === pending guard to .catch(), and add in-flight
resolve/reject regression tests (the reject test fails before this fix).
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(shortcuts): make Select Tab/Workspace 1-9 remappable (#5741)
Expose Select Tab 1-9 and Select Workspace 1-9 as single remappable rows in
Settings -> Shortcuts. Previously both chords were hardcoded in
window-shortcut-policy.ts and absent from the Shortcuts UI.
Each is modeled as one ranged action whose stored chord is a representative
(digit canonicalized to 1) that fires for any of 1-9, mirroring the cmux
single-row UX. Defaults reproduce existing behavior (mac: tab Ctrl+1-9,
workspace Cmd+1-9; win/linux: tab Alt+1-9, workspace Ctrl+1-9). Swapping the
two modifiers - the headline use case - works without a false conflict.
Co-authored-by: Orca <help@stably.ai>
* refactor(shortcuts): tighten digit-index review nits
- Canonicalize digit-index overrides in getEffectiveKeybindingsForAction so
display/conflict detection stay consistent regardless of stored digit.
- Use a shared DIGIT_INDEX_KEY_PATTERN regex instead of string comparison.
- Document that extra modifiers (e.g. Shift) are allowed on digit-index chords
and that the matcher honors terminal-context gating.
- Clarify the workspace-first precedence comment.
- Add tests: Shift modifier, physical-code fallback matching, both ranges disabled.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Add Cmd-J tab session search
References docs/cmd-j-tab-session-search.md for the design.
* Support searching and activating all editor-family tab types
- Index and search editor, diff, conflict-review, and check-details tabs when their backing files are open.
- Deduplicate tab activation logic to ensure exact unified tab IDs are targeted correctly.
- Update palette placeholder texts and localized labels to use "tab title" and "agent prompt" instead of "page title" and "emulator".
- Localize missing tab error strings in Spanish, Japanese, Korean, and Chinese locales.
- Ensure cross-platform path compatibility in search tests using path.join.
* fix(window): remove double title bar on Linux
On Linux the BrowserWindow titleBarStyle fell through to undefined, so
the native WM title bar stayed and stacked on top of Orca's own
renderer-drawn titlebar — two title bars. macOS (hiddenInset) and
Windows (hidden) already suppress the native one.
Drop the native frame on Linux (frame: false) and extend the renderer's
Windows-only custom-titlebar branches (logo/menu anchor, app-name
hiding, window-controls spacer + CSS vars, and the WindowControls
min/max/close cluster) to Linux via a shared `hasCustomTitleBar` flag,
so Linux gets one title bar with working window controls — matching the
Windows custom-titlebar path.
Fixes#5724
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* review: harden Linux custom titlebar gating
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* fix: dismiss stuck browser address bar autocomplete on webview click
Electron webview guests never dispatch outside pointer events to the
renderer, so the address bar history dropdown could stay open after
clicking the page. Close it on guest focus, window blur, and Escape.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: cover browser address bar dismiss paths
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Speed up Windows CLI launch by delivering short startup commands through shell args, falling back to stdin for oversized commands, and moving Codex historical session bridging into an incremental background task. Includes review fixes for Windows symlink and WSL test reliability.