* feat(new-workspace): branch-tab create row, scoped empty hints, name under Advanced, focus polish
- Branch tab autocomplete now offers "Create new branch <name>" instead of a
separate + button + dialog (replaces the original UX from #1408 / #1400).
Suppressed when the typed query exactly matches an existing branch.
- "Use <X> as workspace name" row is restricted to Smart mode; on dedicated
source tabs (GitHub/Linear/Branch) it was off-topic noise.
- Per-mode empty-state hints ("search GitHub PRs and issues", "find a branch
or create a new one", etc.) replace the generic message.
- When a source (PR/issue/Linear/branch) is selected, the auto-derived
workspace name is exposed as a "Name" input under Advanced where it can be
reviewed/overridden. With an explicitly typed name the smart input itself
is the name field, so no duplicated control.
- Forward Tab from the Repo combobox now skips the Smart/GitHub/Branch/Linear
segmented control and lands directly in the search input. Shift-Tab from
the input still focuses the active tab trigger so the segmented control is
reachable in reverse. Implemented via a focusCapture interceptor on
TabsList that distinguishes outside-entry from intra-list moves and from
the input's own shift-tab.
- Trigger checkLinearConnection() on mount so the "Connect Linear in
Settings" hint isn't shown stalely when the composer is the first
Linear-aware surface to render in the session; gate the disconnected
message on linearStatusChecked.
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): Enter commits typed text while results are stale
When the user types faster than the 200ms search debounce, rows and
commandValue still reflect the previous query, so Enter would silently
pick a stale source row (e.g. a PR from the prior letters). Treat
results as stale while loading or while debouncedQuery hasn't caught up
to the input value, and commit the typed text via onValueChange instead.
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): suppress stale source rows in autocomplete while typing
Source rows (GitHub items / branches / Linear issues) are driven by
debouncedQuery, so they're stale until the user pauses for the 200ms
debounce. The previous attempt only short-circuited Enter in the
input's onKeyDown, but cmdk's Command component has its own Enter
handler that still fires onSelect on the highlighted (stale) row.
Filter source rows out of the rows list itself when debouncedQuery
hasn't caught up to value, leaving only the typed-text row (use-name
in Smart, create-branch in Branches). With no stale rows present,
neither cmdk nor our own handler can pick a wrong source on Enter,
and the popover gives visual feedback (collapses to the typed-text
row) while results catch up.
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): keep stale results visible, force highlight to typed-text row
Previous attempt removed source rows while debouncedQuery hadn't caught
up to value, which caused funky disappear/reappear flicker as the user
typed (rows present briefly while fresh, gone after the next keystroke).
Keep all source rows visible at all times to preserve continuity, but
control which row is highlighted while stale:
- Smart/Branches: force highlight onto the typed-text row (use-name /
create-branch) so cmdk's Enter handler commits the typed text instead
of a stale issue/PR/branch.
- GitHub/Linear: no typed-text fallback row exists, so clear the
highlight; the input's onKeyDown falls through to onPlainEnter rather
than picking a stale source.
Also fixed the input's Enter handler to fall through to onPlainEnter
when no row matches the (possibly empty) commandValue, so the keypress
isn't inert in the cleared-highlight case.
Co-authored-by: Orca <help@stably.ai>
* feat(new-workspace): auto-highlight matching source for #NNN, GH URLs, Linear IDs
When the typed value is unambiguously a source reference, snap the
autocomplete highlight onto the matching source row once it appears
instead of leaving it on the typed-text fallback. Enter then picks the
intended source.
Recognized patterns:
- GitHub shorthand: #1234
- GitHub URL: https://github.com/<owner>/<repo>/issues/123 or /pull/123
- Linear identifier: STA-123 (case-insensitive [A-Z][A-Z0-9_]*-\d+)
Stale-results behavior is unchanged: while debouncedQuery hasn't caught
up to value, the typed-text row stays highlighted (Smart/Branches) or
the highlight is cleared (GitHub/Linear) regardless of intent.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Wrapped FirstLaunchBanner handlers' bodies in try/finally so inFlight resets even if fetchSettings rejects, preventing the banner from going permanently inert.
Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Fixed two TaskPage resume-state regressions: the debounced GitHub search now persists even when activeTaskPreset is non-null (drops the guard, persists current preset+query), and re-added the mid-sess
Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
* fix: address pr-bug-scan findings from #1444
Threaded linkedPRNumber through all ChecksPanel force-refresh paths; gated useComposerState's PR-URL-from-name recovery on selected repo slug match; wrapped main-process linkedPR fallback gh pr view i
* fix: satisfy react-hooks/exhaustive-deps in selectedRepoSlug effect
Extract selectedRepo?.path into a const so the effect's only dep is
the path string, removing the missing-dependency warning.
---------
Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Neil <neil@nousresearch.com>
When a worktree is created from a PR via the source picker, the new
local branch differs from the PR's head ref, so the branch-keyed PR
lookup misses and the worktree card shows no PR strip. Pass the
worktree's linkedPR number through the IPC call and fall back to a
number-based lookup in the main process. Also recover linkedPR from
a PR URL pasted into the workspace name when the user skips the
source picker. PR strip now wraps the whole row as the PR link.
Co-authored-by: Orca <help@stably.ai>
Two related top-band issues:
1. Non-workspace view: Tasks page used pt-3 above its X/source-icons row,
placing the cluster's center 6px below the sidebar "Tasks" nav row's
center. Drop to pt-1.5 so both rows center on the same baseline.
2. Workspace view: the strip TaskPage renders to replace the full-width
titlebar was 42px, but PR #1356 standardized titlebar/titlebar-left/
right-sidebar header at 36px. Match that with h-9 so the band is
uniform across columns when a worktree is active.
Co-authored-by: Orca <help@stably.ai>
Persist transient Tasks page position (GitHub mode, active preset/query,
Linear preset/query) in PersistedUIState so reopening Tasks restores the
user's working context instead of falling back to defaults. Source, repo
selection, team selection, and active project keep using their existing
settings paths.
Co-authored-by: Orca <help@stably.ai>
* fix(layout): align right sidebar to top in non-workspace views
Wrap the left + center columns in a flex-col that owns the conditional
titlebar, and lift <RightSidebar /> to be a sibling of that wrapper.
The titlebar now spans only the left+center area, so the right sidebar's
own header anchors at the top of the window in landing/settings/tasks
views — matching the workspace-view alignment.
Also gate the titlebar's right-sidebar toggle on !rightSidebarOpen so
it no longer double-renders alongside the close button inside the
right sidebar's own header when the sidebar is open.
Closes#1354
* fix(titlebar): shrink non-workspace .titlebar to 36px
PR #1381 shrank .titlebar-left to 36px but left .titlebar at 42px.
Before the right-sidebar top-alignment fix, the 6px mismatch was
hidden because the right sidebar was pushed below the titlebar in
non-workspace views. With top-alignment, the 36px right-sidebar
header no longer lines up with the 42px titlebar — visible as a
6px seam on the Landing page when the right sidebar is open.
Match all three (titlebar, titlebar-left, right-sidebar header) at
36px so the top strip is uniform across views.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* Fix new workspace composer focus restore
* Unify new workspace source selection
* WIP: selected source pill in smart workspace name field
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): truncate source pill so it doesn't expand the dialog
Co-authored-by: Orca <help@stably.ai>
* feat(new-workspace): add open-in-browser button to source pill, fix vertical alignment
Co-authored-by: Orca <help@stably.ai>
* refactor(new-workspace): drop redundant kind suffix, distinct PR/issue icons, tooltips on pill actions
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): type linked URL into agent input without auto-submit
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): use bracketed-paste for draft URL injection so it actually appears in the agent input
Co-authored-by: Orca <help@stably.ai>
* feat(agents): per-agent draft injection strategy (codex slow paste, pi/opencode type-chars)
Co-authored-by: Orca <help@stably.ai>
* fix(agents): smarter TUI-ready heuristic + bracketed paste for codex/pi/opencode
Replaces per-agent strategy guesswork with a measured readiness check:
title-idle / non-shell-foreground stable for 1.5s / 2.5s minimum floor.
Verified against codex, pi, opencode, claude in a node-pty + xterm-headless
test rig — bracketed paste lands in the input buffer for all four.
Co-authored-by: Orca <help@stably.ai>
* refactor(agents): drop unused per-agent draft strategy abstraction
The TUI-ready heuristic in agent-paste-draft.ts works for every tested
agent (claude/codex/pi/opencode), so the AgentDraftInjectionStrategy
field, type-chars + bracketed-paste-slow code paths, and per-agent
overrides are dead. Keep the `agent` arg on pasteDraftWhenAgentReady
for future per-agent escape hatches without touching every call site.
Co-authored-by: Orca <help@stably.ai>
* feat(agents): skip draft URL injection for copilot + cursor-agent
Both TUIs open with a 'Do you trust this folder?' menu on first launch
that consumes keystrokes as menu input — pasting a URL there either
selects an arbitrary option or quits the session. Mark them with
skipDraftUrlInjection so the workspace still opens cleanly; the user
types/pastes the URL themselves once past the trust menu.
Co-authored-by: Orca <help@stably.ai>
* feat(agents): native --prefill for claude, trust pre-write for cursor/copilot
Replaces the empirical TUI-ready waits with two deterministic mechanisms:
1) `claude --prefill <text>` flag — Claude launches with the URL already in
its input box, no submit. Eliminates the readiness/paste race entirely
for the most common agent.
2) DECSET 2004 (`\x1b[?2004h`) detection on the PTY data stream for every
other agent. That escape is the protocol-level "input layer ready,
accepting bracketed paste" handshake — emitted by claude/codex/pi/
opencode/gemini/cursor-agent/copilot the moment the input box mounts.
We tap it via a sidecar subscription on pty-dispatcher (no interference
with the primary xterm handler) and paste as soon as it lands. The
8s budget is now an upper bound, not a target.
Cursor-agent and Copilot's "Do you trust this folder?" menus are bypassed
by writing the same trust artifacts the CLIs themselves write after the
user accepts:
- Cursor: `~/.cursor/projects/<slug>/.workspace-trusted` (slug = abs path
with leading `/` stripped, remaining `/` → `-`).
- Copilot: append cwd to `trustedFolders` in `~/.copilot/config.json`
(the same array the bundled `addTrustedFolder` writes).
Verified against the cursor-agent CLI bundle (versions/2026.04.17-787b533/
index.js: `_=".workspace-trusted"`) and the @github/copilot 1.0.32 bundle
(`isFolderTrusted` / `addTrustedFolder` both read/write `trustedFolders`).
Both check via realpath() before string-comparing, so the trust preset
canonicalizes too.
skipDraftUrlInjection is dropped — both agents now get the draft URL
paste once the trust menu is pre-resolved.
Tests: 24 passing across tui-agent-startup, agent-trust-presets,
pty-dispatcher routing.
Co-authored-by: Orca <help@stably.ai>
* fix(agents): wait for post-?2004h render burst to settle before paste
OpenCode emits DECSET 2004 at ~500ms during alt-screen setup, then runs
a 1.3s splash render with NO bytes on the PTY, then paints the actual
input box at ~1.85s. Pasting on the bare ?2004h signal lands during the
silent gap and the bytes are dropped.
The fix: take ?2004h as the necessary precondition, then wait for the
TUI's render burst to finish — defined as 1500ms of stream silence
after the most recent post-?2004h byte. This captures both the fast
TUIs (claude/pi/codex emit setup escapes in one burst then go quiet)
and the slow ones (opencode emits, sleeps for the splash, emits again,
then goes quiet).
Verified against opencode/claude/pi in a node-pty rig: paste lands on
the first try with the new strategy. The hard 8s timeout still caps
the wait when an agent fails to launch.
Co-authored-by: Orca <help@stably.ai>
* fix(agents): guard agentTrust IPC so stale preload doesn't crash launch
If the preload bundle is older than the renderer (a real situation in
electron-vite dev because preload changes only apply on full restart,
not HMR), `window.api.agentTrust` is undefined and the launch crashes
with "Cannot read properties of undefined (reading 'markTrusted')"
before the worktree even opens.
Guard the call sites in launch-work-item-direct and useComposerState
to skip the trust pre-write when the IPC isn't exposed, and wrap the
invoke in try/catch so an IPC error never blocks the launch — the user
just sees the trust menu and accepts it manually, same as before this
feature shipped.
Co-authored-by: Orca <help@stably.ai>
* feat(tasks): route 'Use' through the New Workspace dialog instead of yolo-create
The Use CTA on the Tasks page used to create+activate a worktree
synchronously, which surprised users — the worktree appeared in the
sidebar before they had a chance to confirm name / agent / setup. The
unified New Workspace dialog landed in this branch already supports
opening with a linked work item pre-filled (see openComposerForItem /
openComposerForLinearItem), so just route Use through it.
The launchWorkItemDirect helper stays exported for ProjectViewWrapper,
which has its own UX where the immediate-create flow is the right call.
Co-authored-by: Orca <help@stably.ai>
* test(agents): include `agent` field in autohand startup-plan assertion
Merging main brought in the Autohand Code agent test (PR #1382), which
predated this branch's addition of `agent` to AgentStartupPlan.
Aligning the assertion fixes the lone CI test failure on this PR.
Co-authored-by: Orca <help@stably.ai>
* refactor(agents): drop unused expectedProcess arg + snapshot sidecar set
Two minor follow-ups from self-review:
1. `pasteDraftWhenAgentReady` no longer reads `expectedProcess` — readiness
is gated on DECSET 2004 alone now, not on PTY foreground process. Drop
it from the signature and from the two callers (launch-work-item-direct,
new-workspace).
2. The pty-dispatcher's sidecar fan-out iterates the live Set, which is
safe against deleting the current element but not against a watcher
that synchronously subscribes a sibling. Snapshot via Array.from
before the loop. Cheap (Set is tiny) and removes the latent footgun.
No behavior change.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): match the unified smart-name input's new placeholder
The CreateFromTab refactor in this branch replaced the separate "Workspace
name" Input with a single SmartWorkspaceNameField whose default-mode
placeholder is "Type a name, #1234, branch, GitHub or Linear URL". The
worktree-create e2e test was still anchoring on the old "Workspace name"
text and could not find the input.
Update the placeholder regex to match the new copy. Free-form text typed
into smart mode is treated as a workspace name by submitQuick — same
contract the test used before.
Verified locally: targeted e2e passes in 2.2s.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Add support for custom desktop notification sounds
* perf(notifications): cache custom sound + restart-on-play
Avoids re-reading the configured audio file (up to 10MB) from disk and
re-transferring it over IPC on every notification. Adds a path-only
resolver so repeated dispatches with an unchanged sound skip the heavy
load entirely.
For burst handling, follows the VS Code AccessibilitySignalService /
GNOME canberra pattern: one shared HTMLAudioElement per sound, restarted
from t=0 on each play, with an in-flight guard that drops new plays
while the sound is still ringing. This self-dedupes by the sound's own
duration without any magic time constant — distinct sounds remain free
to overlap. The Test button passes force: true so an explicit user
action always plays through.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* Add an Appearance setting for the app/workbench UI font family. It will be independent from terminal typography and will not change Monaco, diffs, markdown editor text, or terminal panes.
* Update IDE font copy
* feat(cli): add browser tab profile controls
* feat(cli): add tab profile automation primitives
* refactor(cli): narrow tab profile automation scope
* chore: retrigger PR checks
* review: harden tab profile automation CLI
- Wait for tab re-registration after browser.tabSetProfile so a follow-up tab list --show-profile reads the new sessionProfileId from BrowserManager instead of the stale one from the previous webview
- Wait for tab registration after browser.tabProfileClone, matching browser.tabCreate, so the cloned browserPageId is operable when the CLI returns
- Short-circuit browser.tabSetProfile when the tab is already on the requested profile so we do not tear down and remount the webview for a no-op switch
- Switch TabShow.worktree from OptionalPlainString to OptionalString to match every other tab schema; empty --worktree should fall back to the active worktree, not pass through as the empty string
- Add max-lines disable to browser.test.ts (file grew past 300 lines after adding the new tab-profile and tab-show tests)
* review: fix useIpcEvents test setup for tab profile API
CI failure: useIpcEvents.test.ts threw at module load with TypeError: window.addEventListener is not a function. The chain: the rebased useIpcEvents.ts imports destroyPersistentWebview from webview-registry, which calls window.addEventListener at module load. The test stubs window via vi.stubGlobal as a plain object without addEventListener, so the typeof window check passes but the call throws.
- webview-registry.ts: tighten the module-load guard to also check that window.addEventListener is callable, so importing this module from a non-DOM-ish test env (vitest node env with stubbed window) does not throw at module load
- useIpcEvents.test.ts: add the new onRequestTabSetProfile and replyTabSetProfile stubs to all 8 window.api.ui mocks so the new IPC subscription registered by useIpcEvents resolves
* review: restore profile CRUD lost during rebase onto 1397-merged main
The rebase brought commit 3242aa27 (refactor: narrow tab profile automation scope) onto a main that already had the lifecycle CRUD from 1397. The refactor commit removes BrowserProfileList/Create/Delete types, runtime methods, RPC registrations and schemas, plus the help/specs entries, because those were the precursor versions in commit 1 of this branch. Post-rebase those removals land on the hardened versions inherited from main, breaking 1397.
Restore:
- runtime-types.ts: BrowserSessionProfile import; ProfileList/Create/Delete result types
- orca-runtime.ts: ProfileList/Create/Delete result type imports; browserProfileList/Create/Delete methods
- browser-core.ts: ProfileCreate, ProfileDelete schema imports; browser.profileList/profileCreate/profileDelete RPC registrations
- browser-schemas.ts: ProfileCreate, ProfileDelete zod schemas
- help.ts: list/create/delete subcommand lines under Browser Automation
- specs/browser-basic.ts: list/create/delete spec entries
---------
Co-authored-by: Nikolatesla-lj <Nikolatesla-lj@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Fixed symlinks settings search index (4 not 3); removed experimental flag gate on remove-symlinks cleanup; switched pre-existing target check to lstat; hid WorktreeSymlinksSection for SSH repos.
Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Neil <neil@orca.local>
- Scope pruneLastVisitedTimestamps per-repo so not-yet-hydrated SSH repos retain persisted Cmd+J recency instead of being wiped at startup.
- Parse lastVisitedAtByWorktreeId leniently: drop only bad entries rather than failing the whole workspace session on one corrupted timestamp.
- Route sidebar agent-tab clicks through activateAndRevealWorktree so cross-repo activation and nav history are not silently skipped.
Co-authored-by: Orca <help@stably.ai>
Adds ssh:needsPassphrasePrompt IPC so tab focus / Cmd+J auto-connect
waits for user-driven connect when a passphrase dialog would otherwise
pop unprompted. No-passphrase targets continue auto-connecting.
Co-authored-by: Orca <help@stably.ai>
* feat(shortcuts): cycle Cmd/Ctrl+Shift+]/[ within current tab type by default
Extract a shared getNextTabWithinActiveType selector into a new
tab-type-cycle.ts helper and route both useTerminalShortcuts.ts and
ipc-tab-switch.ts through it. Cmd/Ctrl+Shift+]/[ now cycles within the
active tab's type (terminal cycles only terminals, editor cycles only
editor tabs, browser cycles only browser tabs). Ctrl+PageDown/Up
remains the dedicated terminal-only chord (PR #1094) regardless of
focus.
useTerminalShortcuts gains optional activeBrowserTabId and
onActivateBrowserTab parameters so browser tabs can dispatch through
the same path. The default behavior matches VS Code's
TerminalContextKeys.focus-gated cycling and Superset's per-scope
chord registry referenced in the issue.
Adds tab-type-cycle.test.ts covering single-type, mixed-type, and
single-tab no-op cases. ipc-tab-switch.test.ts updated to assert
same-type cycling under the new selector.
Closes#1100
* fix(shortcuts): use direction-aware fallback when active tab is missing
When the active tab id is not present in the same-type subset (e.g.
during hydration when an editor is active and Cmd+Shift+]/[ targets
terminals), findIndex returns -1 and the previous modulo math made
backward navigation land on the second-to-last tab instead of the
last. Branch on the -1 case to return the last tab for direction=-1
and the first tab for direction=+1, so both directions are
predictable. Adds a regression test covering 3 terminal tabs with no
active match.
* feat(shortcuts): add Cmd/Ctrl+Alt+[/] to cycle across all tab types
Adds an "all types" variant of the tab cycle chord so users can page
through every tab in the active group regardless of type (terminal /
editor / browser), complementing the type-scoped Cmd/Ctrl+Shift+[/].
- Wires the chord through the main-window keydown handler, the browser
guest shortcut forwarder, and a new ui:switchTabAcrossAllTypes IPC
channel
- Factors getNextTabAcrossAllTypes alongside getNextTabWithinActiveType
and shares the ipc-tab-switch dispatch logic via resolveCycleContext
and applyNextTab helpers to avoid drift between the two chords
- Narrows useTerminalShortcuts activeTabType to 'terminal' | 'editor'
(the hook's unifiedTabs never contains browsers) and drops the
unsound TypeCyclableTab cast
- Updates ShortcutsPane to list the new chord and expands tests
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Typing a path like `Documents/orca-internal` in the browse filter now resolves the path on the remote instead of producing "No matches". Supports `~`, absolute, and relative paths with live preview and Enter to navigate.
Co-authored-by: Orca <help@stably.ai>
* fix(cmd-j): rank empty-query worktrees by focus recency
Persist a per-worktree focus-recency timestamp and use it as the primary
ordering signal for Cmd+J's empty-query Worktrees section, so SSH and
other quiet worktrees surface based on user focus rather than background
activity. See docs/cmd-j-empty-query-ordering.md.
Co-authored-by: Orca <help@stably.ai>
* fix(tests): guard lastVisitedAtByWorktreeId and mock markWorktreeVisited
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Deleting an unselected, scrolled-past worktree snapped the sidebar back
toward the selected item. The virtualizer's viewport key includes row
count so it can remount cleanly when rows change (avoiding stale height
measurements that caused card overlap) — but the fresh mount started at
scrollTop 0.
Persist the scroll offset in a parent-owned ref, seed the new
virtualizer via initialOffset, and re-apply scrollTop in a
useLayoutEffect with a retry loop that tolerates the transient window
where the browser clamps scrollTop while the virtualizer is still
measuring rows.
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): kill all PTYs for a worktree on removal (design §4.3)
Worktree deletion only shut down renderer-tracked terminals, so PTYs owned
by background tabs, split panes, or pre-reload sessions survived the
removal and kept leaking memory. Introduce killAllProcessesForWorktree
with three sweeps (runtime leaves, provider-prefix scan of daemon session
ids, pty-registry by worktreeId) and wire it into both teardown paths:
the CLI-initiated removeManagedWorktree and the renderer-initiated
worktrees:remove IPC handler. OrcaRuntimeService gets a lazy
getLocalProvider thunk so construction order stays robust.
Co-authored-by: Orca <help@stably.ai>
* fix(renderer): purge worktree-scoped state on removal + hydration (design §4.4)
When a worktree is deleted, ~25 worktree-scoped maps (tabsByWorktree,
git caches, browser state, split-tab models, per-file editor drafts,
etc.) kept references to the gone worktree, so SessionsStatusSegment
kept mis-classifying orphaned PTYs as bound and dropdowns rendered stale
ids. Add purgeWorktreeTerminalState as a single atomic action that
wipes every scoped map plus cascades top-level actives. Fire it from
the worktrees:changed listener on the set-diff of removed ids, and once
more at hydration via fetchAllWorktrees to clean up persisted entries
from pre-fix sessions. The hydration-time purge is gated behind a
per-repo success check: a single transient IPC error or an all-empty
fetch defers the purge so a degraded launch cannot wipe legitimate
persisted state.
Co-authored-by: Orca <help@stably.ai>
* test(zombie-worktree): regression coverage for design §4.5
Adds tests for every layer of the zombie-worktree fix:
- worktree-teardown: unit coverage of the three-sweep helper including
best-effort error swallowing across provider/registry.
- orca-runtime: RPC-initiated removeManagedWorktree kills PTYs before
any git mutation + verifies the lazy getLocalProvider thunk resolves
on each call.
- worktrees IPC: renderer-initiated remove kills PTYs before git and
skips the kill helper for SSH-backed repos.
- renderer slice: fetchAllWorktrees defers the purge when any sibling
repo fetch fails or every repo returns empty (F1 regression);
happy-path fires the purge once and does not re-run on subsequent
calls. Direct purgeWorktreeTerminalState coverage pins the cascade
across worktree-keyed, tab-id-keyed, and file-id-keyed maps.
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): log worktree-teardown kill counts (design §4.4 observability)
Breadcrumb lets ops distinguish a renderer-state-induced leak (diff-path
purge non-empty) from a backend-induced one (nothing to kill but memory
still pinned). Emit only when the sweep actually shut anything down so
steady-state logs stay quiet. Added at both call sites —
removeManagedWorktree (CLI path) and the worktrees:remove IPC handler.
Co-authored-by: Orca <help@stably.ai>
* test(zombie-worktree): fix ptyIdsByTabId seed shape to match production type
The purge unit test seeded ptyIdsByTabId as Record<string, string> when
the runtime type is Record<string, string[]>. The unit tests passed
because they never hit the UI renderer, but live e2e surfaced a
TypeError: (ptyIdsByTabId[tabId] ?? []).some is not a function.
Corrected to arrays; 21/21 tests still pass.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Partially reverts #1359: drop the hideIdentityIcon flag on the inline
agent list so the Claude/Gemini/… glyph renders again. The expand
chevron stays hidden — clicking the row still jumps straight to the
agent, and the chevron was the redundant part. Identity is useful when
a worktree has more than one agent of different types.
Co-authored-by: Orca <help@stably.ai>