* fix(tab-bar): open absolute local paths from worktree tab create entry
Local worktrees can paste absolute file paths into tab create; remote and
SSH workspaces stay blocked. Harden classifier ordering, ownership gating,
and render-time fail-closed behavior from review.
* fix(tab-bar): restore typecheck after worktree path refactor
Add the missing editor file-operation import, finish the global file
drop relative-path helper migration, and align the runtime env test mock.
* fix(tab-bar): fail closed for absolute local paths
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix: route folder workspaces in worktree operations (#10251)
Folder workspaces are not Git worktrees and never appear in the repo/worktree
catalogs, so they were falling through to unresolved cross-host routing and
failing closed on all owner-dependent operations. Extract folder workspace
ownership logic to a dedicated module and add dedicated routing for folder
workspace identifiers before checking Git worktree catalogs.
* persist folder workspace metadata on the FolderWorkspace record
Folder workspaces lack worktreeMeta rows; metadata updates (activity bumps, unread status, terminal focus) must call updateFolderWorkspace. Fixes routing so local folder workspaces resolve to 'local' even when unrelated runtimes exist (#10251).
* Fix folder workspace mutations routing through owners
Folder workspace updates and deletions were routing through the currently
focused runtime instead of the owning runtime. Add coordinators for
concurrent-update race prevention and activity-persistence coalescing.
Handle runtime-owned folders in editor file operations and terminal activity
tracking.
* fix(worktree): don't path-sweep sibling sessions when deleting a folder workspace (#10252)
Deleting one folder-workspace instance could kill terminal/agent sessions in
OTHER workspaces sharing the same checkout path — sibling instances, and even
worktrees of a different repo rooted under that directory. Both pi and Claude
Code agent sessions died at once with no recovery.
The `cwdOwned` path fallback in killAllProcessesForWorktree() derives its match
path via splitWorktreeIdForFilesystem(), which strips the `::workspace:<uuid>`
suffix and collapses a folder instance's path to the shared checkout dir. Every
untagged session under that dir then path-matched and got swept (worst case: a
home directory registered as a folder repo).
Disable the path fallback for folder-workspace instances — their filesystem
path can't identify a single instance. The exact `${worktreeId}@@` prefix and
authoritative `session.worktreeId` matches (both carrying the instance uuid)
still tear down the deleted instance's own sessions; normal git worktrees
(unique paths) keep the fallback. The runtime and registry sweeps already
matched by exact worktreeId.
Adds isFolderWorkspaceInstanceId() and regression tests. See
docs/delete-workspace-cwd-owned-sibling-kill.md.
* rm design doc
* fix(terminal): stop the reveal fit from reflow-garbling inline TUIs
grok (and other inline-viewport TUIs like Codex) render garbled after the
floating terminal is minimized and brought back up. On reveal the resume path
fit xterm synchronously right after re-attaching WebGL, whose cell metrics
differ from the DOM renderer's — so it could propose a one-column-off grid,
reflow xterm, then snap back a frame later (a net-zero resize "wiggle"). xterm's
main-buffer wrap→unwrap is not a perfect inverse, and an inline TUI that
diff-paints its pinned region redraws over the corrupted buffer.
Replace the unconditional synchronous reveal fit (fitAllPanes) with a gated fit
(PaneManager.fitAllPanesStable → fitRevealedPane):
- fit synchronously only when the fit element's pixels actually changed while
hidden (a real resize the app must reflow for anyway, kept ahead of the async
PTY size reassert so it can't forward a stale grid);
- if the pixels are unchanged but the grid diverged while hidden (snapshot /
SSH-reattach direct terminal.resize, or an appearance/DPI change), repair it on
a steady grid (requestStablePaneFit) so a sustained mismatch refits while a
transient cell-metric wobble does not reflow;
- otherwise leave the pane alone.
The common minimize→restore is now a hard no-op with zero reflow. Also applied to
the window-wake reveal path.
* refactor(terminal): tighten reveal-fit comments + rename to fitAllRevealedPanes
Quality pass: make fitRevealedPane the single canonical explanation of the
reveal wobble and reduce the duplicated comments at the call sites to short
pointers; rename PaneManager.fitAllPanesStable -> fitAllRevealedPanes ("stable"
only described one of its three branches); symmetric early-returns in
fitRevealedPane. No behavior change.
* fix(workspace-board): sync Linear on context-menu Move to Status
The board's right-click "Move to Status" only wrote the local
workspaceStatus and silently dropped the Linear sync that drag-and-drop
performs. Thread an onAssignWorkspaceStatus callback from the drawer
through the kanban card chain into WorktreeContextMenu so the menu
funnels through the same local-first + Linear-sync path
(moveWorktreesToStatus) as drag-and-drop. Outside the board (sidebar
list) the menu keeps its local-only behavior.
* test(workspace-board): guard context-menu Move to Status routing
Extract the context-menu status-assign routing into a pure
planWorkspaceStatusAssignment helper (behavior-preserving) and unit-test
it, so the board Linear-sync vs sidebar local-only branch — the exact
path #10175 regressed on — cannot silently flip back unnoticed. Covers
board-sync-forwards-all-ids, local-only-writes-only-changed, and the
same-status no-op case.
Addresses code-review finding: the added drawer tests exercised the
sync wiring via a mocked LaneGrid but never the menu's routing branch.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: ElNelyo <ElNelyo@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(worktree): match created worktrees through symlink roots
On immutable Linux, /home is often a symlink to /var/home. git worktree
list reports the realpath while Orca still holds the /home request path,
so creation failed with "Worktree created but not found in listing".
After local worktree add, fall back to realpath when string comparison
misses. Keep WSL listings on string comparison only (host realpath is not
authoritative there).
Closes#10170
* test(worktree): harden symlink reconciliation authority
* fix(worktree): reconcile creation by Git branch identity
* test(worktree): reproduce symlink-root listing with real Git
* test(worktree): cover cross-platform reconciliation
* fix(worktree): keep reconciliation main-only
---------
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(terminal): re-verify cached macOS login-preflight rejections
A conclusive PAM rejection was cached for the process lifetime, so one false
verdict (the probe runs over pipes, not a PTY) disabled the login(1) TCC
attribution wrapper for a daemon that survives app quits and updates for
weeks — reintroducing the every-invocation AppData prompts #7003 fixed.
Rejections now re-verify after 30 minutes; accepted verdicts still cache
for the process lifetime.
Refs #9756
* fix(daemon): replace hosts with stale login preflight cache
Protocol 26 shipped the process-lifetime PAM rejection cache. Preserve its live sessions as a legacy generation, but route fresh terminals through protocol 27 so updating actually loads the expiring-cache fix.
Refs #9756
* fix(terminal): validate rejected login probes under a PTY
Preserve OMP session identity and exact resume paths across cold restoration, AI Vault, mobile, WSL/SSH, and host-authority routes. Add mixed-version fallback and completed-session recovery coverage.
* fix(terminal): hide SSH error toast under the reconnect banner
The z-50 TerminalErrorToast was stacking over the non-blocking SSH reconnect
banner with the raw ssh:connect failure. Suppress the toast while the banner
owns recovery and clear matching toast text so it cannot flash after reconnect.
* fix(terminal): strip only SSH-owned lines from aggregated terminal error
onPtyError newline-joins multiple PTY errors into one string, so the prior
startsWith() classification misfired on aggregated errors: an unrelated error
before the SSH failure left the stale ssh:connect text to flash after reconnect,
and an SSH-first error discarded any unrelated error. Classify per line and drop
only reconnect-owned lines, keeping the rest. Closes CodeRabbit's aggregation edge.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
An SSH connect whose relay deploy fails permanently (e.g. a dev build
missing the platform relay package) drove an unbounded reconnect loop.
doConnect creates the relay session ('idle'), then connectionManager.
connect() drives the raw transport to 'connected' before the relay
establishes. That premature 'connected' was forwarded to the renderer,
which treats it as "host fully up": applySshConnectionStateChange bumps
tab.generation for stranded remote panes -> remounts TerminalPane -> the
pane connect gate re-invokes window.api.ssh.connect -> restarts the whole
cycle forever (and spams connected-gated reads that fail with "Remote
connection dropped").
Hold the premature 'connected' at 'deploying-relay' in onStateChange until
the relay session is 'ready'. doConnect still broadcasts the authoritative
'connected' directly after establish() succeeds. Gate on connectInFlight so
the hold is scoped to a live connect and never wedges a stray transport-blip
'connected' on a session left 'idle' by a relay version mismatch.
* fix(main): log unhandled rejections durably and stop them killing the app (#9441)
Main had no unhandledRejection handler — Node's default killed the process
with a clean exit code and no macOS crash report, which is exactly the
silent startup-restore exit in #9441. Rejections now record a durable crash
breadcrumb (flushed to the trace sink) and the app stays alive; uncaught
exceptions still terminate but record the same breadcrumb first so field
exits stop being undiagnosable.
* fix(main): harden process error guard diagnostics
* fix(main): cap fatal-error records to stop a rejection storm stalling main
One broken resource can reject hundreds of concurrent restore chains, and
each durable record does a synchronous trace flush. Cap records at 20 per
60s window and carry the suppressed count into the next recorded event.
* fix(main): never rate-limit the fatal uncaught-exception breadcrumb
A rejection storm that exhausted the 60s record window would suppress the
one main_uncaught_exception record taken right before the re-throw kills
the process, recreating the undiagnosable death from issue #9441. Scope
the fixed window to main_unhandled_rejection only; the uncaught path
records at most once per install (handler removes itself, EPIPE/EIO never
record), so it cannot storm.
* fix(main): reset fatal-error window on backward clock jumps
The show/restore repaint jiggle mutated the window frame synchronously
inside AppKit's window-state dispatch. On macOS 26 (Tahoe), windows are
scene-backed and a frame update sent re-entrantly from scene-update
handling can self-deadlock the main thread in FrontBoardServices
(kernel-confirmed spindump: performAsyncAndWait on the callout queue the
main thread was draining).
- Defer the setSize nudge to a fresh event-loop turn so it never runs
inside the show/restore dispatch.
- On Darwin 25+ (macOS 26) skip the nudge entirely and rely on
webContents.invalidate(); the nudge works around a pre-Tahoe
black-surface compositor bug.
* fix(worktree): stop terminal removal fence error flashing on delete
Deleting a worktree kills its PTYs for the filesystem teardown, then runs
git worktree remove (~1s). During that window a doomed pane races a fresh
respawn that main correctly fences with TerminalRemovalInProgressError, but
the renderer surfaced that internal fence verbatim as a pane error banner
until the worktree unmounted.
- startFreshSpawn: skip the respawn when the pane's own worktree is being
deleted (isDeleting) — no shell should spawn into a directory being removed
and the pane is about to unmount.
- reportError: swallow the removal fence at the single pane-error sink so it
never reaches the banner, covering the parent-removal-fences-child case the
own-worktree skip cannot see.
- Share the fence messages between main (thrown) and renderer (recognized) via
worktree-removal-fence-error.ts so the thrown text and predicate can't drift.
* test(worktree): assert onError callback captured before invoking
Optional invocation let the fence-suppression test false-pass if the
transport onError wiring broke; require the callback so a broken wire
fails loudly. Addresses CodeRabbit review on #10240.
* fix(app): bound the wake/quit paths implicated in the phone-session-ended freeze (#9447)
- relay-transport: waitForClose now times out (5s) so a half-open post-sleep
socket can't wedge runtimeRpc.stop()
- will-quit: race teardown against a 20s deadline so app.quit() always runs
(Force Quit was the only escape when any teardown member never settled)
- terminal-fit-restore: local restoreTerminalFit invoke gets the same 15s
bound as the remote path so the held-fit modal buttons can't pin disabled
* fix(app): close wake recovery timeout gaps
* fix(relay): drop late frames after forced teardown
* fix(relay): fence detached socket callbacks
* fix(app): close timeout resource gaps
* fix(relay): detach retired mobile transports
* fix(types): exclude absent stat overloads
* fix(runtime): expire wedged terminal restore dedupe
* fix(runtime): keep restore retries on one reclaim
* chore(skills): refresh bundled skill manifests
* fix(window): fence quit acknowledgements by request
* fix(relay): bound revoked device socket cleanup
* fix(claude-accounts): quote resolved claude path for Windows shell spawn
runClaudeCommand spawns the resolved claude command with shell:true on
Windows, but spawn concatenates the command into the cmd.exe line without
quoting. When the CLI resolves to a path containing spaces (e.g.
C:\Users\First Last\AppData\Roaming\npm\claude.cmd), cmd.exe splits at the
first space and account add fails with:
'C:\Users\First' is not recognized as an internal or external command
Quote the command the same way claude-pty.ts and quoteWindowsCmdArg
already do for other Windows spawns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(claude-accounts): own Windows cmd invocation
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Portal the non-blocking reconnect banner into each pane container so WebGL
compositing cannot paint terminal text through it, raise z-index above
pane-local chrome, and use a fully opaque card background.
* feat(agent-rows): opt-in conversation-name labels for agent rows
Sidebar worktree-card and dashboard agent rows always show the last
message sent to the agent, so rows relabel on every turn and a 'continue'
prompt becomes the row's name. Add an opt-in Agents setting that labels
rows with the conversation name instead, resolved with the tab bar's
precedence: manual rename, quick-command label, OpenCode session title,
generated title (behind its existing setting), then the agent-set live
title. Live titles count only when they carry a real name - status
decoration is stripped, and pure status, identity-echo, spinner+cwd, and
placeholder titles fall back to the last-message label. Subagent child
rows keep their own descriptions.
Locale note: sync:localization-catalog also restored parity for keys
already missing on main (add-host, sleep-worktree copy).
* fix(agent-rows): read the live tab so late renames and titles surface
Row data patches live entries in place and keeps the tab snapshot from
row creation, so a rename or agent-set title landing after the row was
built never reached the conversation-name resolver. Select the current
tab from the store in the hook; retained rows without a live tab keep
the snapshot fallback.
* fix(agent-rows): reject status labels and bound lookup work
Reject native provider/status titles and Windows/UNC cwd frames so conversation-name mode falls back to the user's last message instead of relabeling rows with identity or path text. Keep default-off and subagent rows off the tab map, and share a WeakMap-backed tab index across mounted rows to avoid repeated linear scans on store writes.
* feat(agent-rows): make conversation names the default
Always prefer a usable conversation name for sidebar and dashboard agent rows, falling back to the last message when the resolver rejects a title. Remove the preference, settings UI, search copy, and catalog entries. Keep subagent labels unchanged and reject generic Terminal N placeholders so partial tab snapshots cannot hide meaningful prompts.
* chore(i18n): sync catalog after main merge
* fix(agent-rows): preserve same-tab child labels
* fix(agent-status): resume finished agents after macOS logout instead of a bare shell (#9454)
After the daemon-retirement fix (#9826) recovers terminals across a macOS
logout, a running Claude Code (or other resumable TUI agent) that had
finished its turn came back as a BARE SHELL — the user had to open a new
tab and `claude --resume` in every worktree to get logged back in.
Root cause: setAgentStatus writes a persisted 'live recovery' sleeping
record (the cold-restore --resume anchor) for a live resumable agent, but
drops it once the turn is 'done' — except for Pi, which was special-cased
(retainsPiRecoveryIdentity). A finished Claude/Codex turn leaves the TUI
alive and resumable at its prompt just like Pi, so an idle agent lost its
anchor; a cold restore after an abrupt app death (logout) then found
nothing to resume and dropped to a bare shell.
Fix: generalize the Pi-only recovery-identity retention to every resumable
TUI agent (isResumableTuiAgent), gated on a valid resume argv, so a finished
agent keeps its persisted providerSession anchor and the already-shipped
cold-restore --resume path brings it back logged in. Extends an existing
pattern; no new subsystem.
Reproduced + validated live in Electron via the login-session-death seam:
idle Claude -> daemon retire -> app relaunch previously bare-shelled; now
spawns 'claude --resume <session>' and restores the conversation logged in.
* test(agent-status): update sleep-hygiene assertions for retained live recovery anchor (#9454)