Commit Graph
2455 Commits
Author SHA1 Message Date
Brennan Benson d56e2fbbe4 feat(agent-dashboard): choose in-window board or pop-out window (#10243)
* feat(agent-dashboard): choose in-window screen popover or pop-out window

The experimental Agent Dashboard opened only as a separate pop-out
window. Add an "Open as" mode under the experimental toggle so it can
open as an in-window screen popover (new default) or a pop-out window
(prior behavior). The mode row appears only when the feature is on.

- New setting `experimentalAgentDashboardMode: 'in-window' | 'popout'`
  (default in-window); sidebar entry branches on it.
- In-window: AgentDashboardOverlay renders the shared AgentKanbanBoard
  in a near-fullscreen dialog, snapshot built locally via
  useLiveDashboardSnapshot (the pop-out relays over IPC; in-window has
  no relay). Ack/reveal act on the local store — the pop-out IPC
  handlers are gated to the pop-out renderer.
- AgentKanbanBoard gains containerClassName/onAckAgent/onRevealAgent/
  onClose props; defaults preserve the pop-out behavior.
- Extracted AgentDashboardExperimentalSetting to keep ExperimentalPane
  under the max-lines cap.

* fix(agent-dashboard): admit main renderer to terminal-preview IPC for in-window dialog

The terminalPreview:* handlers gated every channel to the pop-out
renderer, so the in-window overlay's terminal dialog (running in the
main renderer) got { snapshot: null } from connect and falsely showed
"No live terminal — this agent's pane has closed." for live agents.
Accept the trusted UI renderer too — it already has full PTY access
through the regular terminal channels, so this adds no reach.

* fix(agent-dashboard): sync locale catalogs for new mode/close keys

verify:localization-catalog (part of lint CI) fails when en.json keys are
missing from the other locale catalogs; run sync:localization-catalog so
the six new agent-dashboard keys exist everywhere (English fallback text;
translated copy remains the documented follow-up).

* feat(agent-dashboard): present in-window mode as a companion board sheet

The in-window dashboard now uses the same non-modal left sheet as the
workspace kanban board — anchored to the sidebar edge, chrome/status-bar
bounds, sidebar stays interactive — instead of a near-fullscreen modal
dialog. Both companion boards are mutually exclusive; the sidebar entry
toggles the drawer. Removes the modal focus-restore timing coupling on
reveal.

* fix(agent-dashboard): ignore Radix dismiss requests like the workspace board

Non-modal Radix layers also request dismissal for interactions the drawer's
outside guards cannot classify — focus moving outside carries no pointer
coordinates, and clicks in the status bar / top chrome fall outside the
right-side dismiss band. Forward only open requests from the Sheet, matching
WorkspaceKanbanDrawer, so only the drawer's own escape/outside/close paths
close it.

* fix(agent-dashboard): guard reveal relay and refresh stale mode copy

CodeRabbit review: revealAgent lacked the ?. HMR-skew guard its sibling
ackAgent has (both channels shipped together, so a stale dev preload
lacks both). The es/ja/ko/zh catalogs also still described the dashboard
as pop-out-only in stale English, contradicting the new in-window
default; refreshed to the current English source.

* feat(agent-dashboard): add board settings menu to the in-window header

Mirrors the workspace board's settings gear: an Open as segmented control
in the board header so the mode is changeable without opening Settings.
Switching to pop-out hands the surface over (closes the drawer, opens the
window) instead of leaving a board the setting says should be a window.
In-window only via an optional headerActions slot - the pop-out renderer
has no store to drive it.

* fix(agent-dashboard): reset the settings-menu flag when the drawer closes

The pop-out hand-off closes the sheet while the menu is still open, so the
menu unmounts without Radix reporting onOpenChange(false). The stale
menuOpen=true then blocked outside-dismiss permanently on the next
in-window open. Mirror closeWorkspaceBoard by resetting the flag in close.

* fix(agent-dashboard): reset the menu flag on store-driven drawer closes

Cmd+B sidebar collapse and the workspace-board exclusivity effect close
the drawer via setAgentDashboardDrawerOpen directly, bypassing close();
a settings menu open at that moment unmounted without Radix reporting
onOpenChange(false), leaving menuOpen stuck true and outside-dismiss
disabled on the next open. Sync the flag to the open state so every
close path resets it.
2026-07-24 12:48:18 -07:00
YoelandCursor d1ccfcff40 fix(settings): normalize and validate branch prefixes (#7772)
* Normalize branch prefixes and flag invalid ones in settings

A custom branch prefix ending in a slash (e.g. "team/") produced a
double-slashed branch name like "team//feature" that git rejects, and
the raw check-ref-format error gave no hint that the prefix caused it.

- Normalize the configured prefix (trim whitespace, strip leading/
  trailing and duplicate slashes) in the shared branch-name builder so
  the common trailing-slash case just works, for local and SSH worktrees.
- Validate the prefix on the worktree-create path (computeValidatedBranchName)
  so a genuinely invalid prefix fails fast with a clear
  "update it in Settings -> Git" message instead of an opaque git error.
- Add a live BranchPrefixFeedback under the Branch Prefix setting: previews
  the resulting branch name, warns on invalid characters, and notes when a
  prefix collapses to none.
- Keep the background first-work rename on the non-throwing builder since the
  prefix is already validated at create time.

* Keep caret in place when editing the branch prefix

The custom branch prefix input was directly controlled by settings, but
updateSettings persists through an async IPC round-trip, so the value
updated a tick late and React re-assigned it, snapping the caret to the
end on mid-string edits. Drive the input from a local draft and only
adopt genuine external settings changes so the caret stays put (and fast
typing survives slow SSH round-trips).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Return ReactNode from BranchPrefixFeedback

JSX.Element needlessly excludes null/string/number returns; ReactNode
keeps the component's return type from over-constraining future changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 00:39:32 -07:00
Brennan Benson 1b5db4bc2a fix(window): reflow on macOS occlusion-reveal so the bottom bar is not clipped (#10056)
* fix(window): reflow on macOS occlusion-reveal so the bottom bar is not clipped

Fixes STA-2383.

On macOS the window is background-throttled while hidden; on occlusion-uncover only 'focus' fires and its handler runs webContents.invalidate() (the setSize jiggle is skipped to avoid SIGWINCH-ing terminals). invalidate() repaints but does not reflow, so the app-shell h-dvh root keeps a stale dynamic-viewport height and the StatusBar is clipped below the viewport ('no bottom bar'); a manual resize restores it.

Fix: renderer relays a genuine hidden->visible reveal (visibilitychange) to main via new ui.notifyWindowRevealed IPC; main runs the same proven forceRepaint. Occlusion-gated (no per-Cmd+Tab SIGWINCH regression), darwin-scoped, sender-guarded, cleaned up on close.

Test plan: vitest createMainWindow + web-preload-api green. Recommend on-device macOS occlusion QA before merge.

* fix(window): preserve user resizes during reveal repaint
2026-07-24 00:36:18 -07:00
Wooseong Kim 559f04d297 fix(daemon): gate agent startup cwd after safe default fallback (#10035)
createPtySubprocess asserted assertSafeAgentStartupCwd on the raw
opts.cwd before applying opts.cwd || getDefaultCwd(). An omitted cwd
was treated as root-like and threw even though the post-fallback home
default is safe — while LocalPtyProvider already gates the effective
cwd after fallback.

Compute requestedCwd first, then gate. Update the daemon unit test to
assert omitted-cwd agent launch uses the safe default (#9578).
2026-07-24 00:29:23 -07:00
Kaynan Sampaio de Camargoandkaynan 69d05b6e24 fix(mobile): resolve permission args when a new session launches an agent (#8469)
The New Workspace flow built a bare launch command client-side and sent it as
`startupCommand`, so the host ran it verbatim and never applied the default
launch args. The first Claude session therefore started in manual mode, while
opening another Claude via the "+" tab (which sends the agent id and lets the
host resolve args) started with `--dangerously-skip-permissions`.

Send `startupAgent` from every client-built create path (blank, reuse-branch,
and new-branch) so the host resolves the launch command, args, env, and
host-shell quoting through the same path the "+" new-tab and CLI use. The
work-item path already delegated via `startupDraft`. Custom `agentDefaultArgs`
are now honored on all paths.

Adds a shared `agentLaunchCreateFields` helper and removes the now-unused
client-side command map, which had also drifted from the canonical launch
commands for continue, hermes, command-code, kiro, and mistral-vibe.

Claude-Session: https://claude.ai/code/session_014iufZnQwPD2obYuvdahjaE

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
2026-07-24 00:29:19 -07:00
Wooseong Kim dc18ba9cda fix(win): taskkill agent PTY descendant trees on stop (#10100)
* fix(win): taskkill agent PTY descendant trees on stop

Windows agent teardown previously degraded to shell-only kill because
descendant snapshot is POSIX-only. Orphaned claude/codex/MCP children
kept worktree cwd handles open and blocked worktree remove.

Use taskkill /T /F on the PTY root in killWithDescendantSweep (daemon +
local agent shutdown) so the ConPTY tree is cleared before teardown.

Fixes #10004

* fix(win): bound taskkill with timeout and windowsHide

Prevent a wedged taskkill from delaying killRoot, and hide the console
flash during agent PTY tree teardown (CodeRabbit on #10100).
2026-07-24 00:27:40 -07:00
Taras Kornichuk e3cc08f185 fix(worktree): don't flag a disabled sparse checkout as sparse (#9922)
`git sparse-checkout disable` restores the full working tree and sets
core.sparseCheckout=false, but deliberately leaves <gitdir>/info/sparse-checkout
in place so the checkout can be re-enabled with the same patterns.
detectSparseCheckout treated the mere presence of that pattern file as "sparse",
so a fully-populated worktree kept showing the sparse badge and the misleading
"Partial checkout. Files outside these paths are not on disk." tooltip.

Gate the fast-path fs.stat behind a config read that confirms core.sparseCheckout
is actually enabled (shared repo config or per-worktree config.worktree, honoring
git's precedence). The config read runs only when a non-empty pattern file
exists, so it does not reintroduce the per-poll subprocess fan-out PR #1290
removed, and it reads git's config files directly (no subprocess).

Adds a real-git regression test (enable -> disable leaves file -> not sparse) and
unit tests for the git-config boolean parser.
2026-07-24 00:26:34 -07:00
Tingde Liu 1ace87c155 fix(wsl): route global CLI fallbacks to user-pinned terminalWindowsWslDistro (#9734) 2026-07-24 00:26:25 -07:00
Gideon cb19e79504 fix(ai-vault): resume Claude sessions from their start directory (#9499)
The AI Vault session scanner recorded a session's cwd by overwriting it with
every transcript record that carried one, so a session that changed working
directory mid-run ended up with its last-seen cwd. That value drives the resume
command (cd <cwd> && claude --resume <id>) and the history panel's grouping and
filtering.

For Claude this breaks resume: transcripts live under
~/.claude/projects/<slug-of-start-dir>/, and claude --resume <id> only searches
the project dir derived from the current cwd, so resuming from the drifted cwd
fails with "No conversation found". Capture the first-seen (start) cwd instead,
which transcripts write up front; this fixes resume, grouping, and filtering at
once. updateLatestLocation is only used by the Claude parser, so no other agent
is affected.

Fixes #9361
2026-07-24 00:26:11 -07:00
Avery Bloom cd28da13f4 fix(orchestration): treat ask as a long-poll so it survives the 30s socket idle wall (#9351)
orchestration.ask blocks server-side until a reply lands or its timeout
(default 600s) elapses, holding the RPC open — but isLongPollRequest never
classified it as a long-poll. So the keepalive that resets the 30s
RUNTIME_RPC_SOCKET_IDLE_TIMEOUT_MS was never armed, and any ask left
unanswered for 30s died with a misleading runtime_unavailable ("The Orca
runtime closed the connection"), regardless of --timeout-ms. The same
omission left the handler's abort signal unwired (it is only passed for
long-polls), so the client-disconnect release path guarded by
signal?.aborted was dead code.

Add orchestration.ask to isLongPollRequest so it gets the keepalive, the
abort signal, and long-poll admission. The client already extends its
per-call socket timeout for ask (handlers/orchestration.ts passes
timeoutMs + 5s at the call site), so only the server-side classification
was missing.
2026-07-24 00:25:53 -07:00
c2371c0cd8 fix: don't hydrate a headless mobile session for a repo that no longer exists (#9343)
`hydrateHeadlessMobileSessionTabsFromWorkspaceSession` iterates
`workspaceSession.tabsByWorktree` with no repo gate. Those keys are
`${repoId}::${path}` and are not pruned when a repo disappears from this
client's view, so a stale key re-materializes a phantom "unknown"/duplicate
workspace with no live repo behind it, on boot, with no `session-created` event.

Skip entries whose parsed repoId is not in the live `repos`; leave unparseable
keys alone (conservative). Defensive complement to the delete-path (#9025) and
load-time (#9200) fixes — this closes the mobile/headless hydration path.

Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 00:25:44 -07:00
d8499fae16 fix: don't mint worktreeMeta from a sidebar sort-order snapshot (#9342)
`worktrees:persistSortOrder` (ipc/worktrees.ts) and `persistManagedWorktreeSortOrder`
(orca-runtime.ts) call `store.setWorktreeMeta(id, { sortOrder })` for every id the
renderer sends. `setWorktreeMeta` has no repo-existence check, so a stale id the
client still lists — e.g. a removed repo's `${repoId}::${path}` that lingers in the
renderer's order — gets a brand-new `worktreeMeta` entry minted on every sidebar
snapshot, resurrecting an orphan/duplicate workspace on the next launch.

A sort-order snapshot must only reorder worktrees that already exist, never create
one. Guard both call sites with `getWorktreeMeta` so absent ids are skipped.

Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 00:25:39 -07:00
Mark Xian dd642cb3e3 fix(rate-limits): refetch claude usage when the last live Claude PTY exits (#9325)
The managed token refresh is deferred while a live claude session owns
the credentials, but nothing reacted to the last live PTY exiting: the
deferred result is a terminal-classified error whose retry lane backs
off to the 15-minute cadence and is window-focus-gated, so the usage
panel stayed on "Waiting for Claude session" long after the blocker was
gone.

Notify on the live-PTY 1->0 transition (covering both markClaudePtyExited
and confirmSeededClaudeLivePtys releasing the last dead seeded id) and
force a claude-only refetch iff the current state is deferred.

Closes #9324
2026-07-24 00:25:35 -07:00
253ccd29f5 fix: prune workspace session state when a project is removed (#9024) (#9025)
* fix: prune workspace session state when a project is removed

removeProject → pruneWorktreeStateForRepo cleared worktreeMeta, lineage,
and workspace lineage, but left the removed repo's worktrees behind in
workspaceSession (lastVisitedAtByWorktreeId, sleepingAgentSessionsByPaneKey,
tab state, …) and in every workspaceSessionsByHostId partition.

Those dangling references caused the runtime to treat the deleted repo's
worktrees as recently-active on the next launch and re-materialize their
worktreeMeta. With no owning project left, the UI rendered them as an
orphaned "unknown" workspace that returned after every restart.

Reuse the existing removeWorkspaceSessionOwner cleanup (already used by
removeFolderWorkspace and deleteProjectGroup) against the legacy session
blob and each per-host partition, collecting owner keys before the
worktreeMeta delete loop so host classification still works.

Fixes #9024

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: scope session cleanup to the removed host in removeProjectForHost

The workspace-session cleanup applied every collected owner key to the legacy
(local) session blob AND every workspaceSessionsByHostId partition. Owner keys
are `${repoId}::${path}` and carry no host, so a host-scoped removal
(removeProjectForHost) of one host wrongly wiped a surviving host's session for
a shared repo id/path — discarding its lastVisitedAt, tabs, sleeping-agent
state, and active-worktree pointer.

Scope the session prune to the removed host: prune the legacy blob only when the
target host is local (or on a full removeProject where hostId is null), and
prune only the matching workspaceSessionsByHostId partition for a non-local
host; a full removal still clears every partition. Collect session owner keys by
`${repoId}::` prefix (not belongsToHost, which is worktreeMeta-host-classified)
so a partition-only key is still prunable.

Adds regression tests for a shared repo id + path across local and an SSH host,
covering removeProjectForHost on the SSH host (local session survives) and on
the local host (SSH session survives).

Addresses review feedback on #9024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: cover third surviving host in removeProjectForHost session prune

Add a regression case where a shared repo id + path exists on local and two
SSH hosts. Removing one non-local host must prune only that host's session
partition, leaving both the legacy/local session and the other surviving SSH
host's partition intact. Locks in that the host-scoped cleanup never touches a
non-targeted third partition (per review feedback on #9024).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf: prune workspace session in a single clone+scan per partition

Project removal collected every owner key to prune, then called
removeWorkspaceSessionOwner once per owner AND per host partition. Each
call structuredClones and scans the entire session, so removal was
O(worktrees x hosts x session size) in full-session clones on the main
thread.

Add removeWorkspaceSessionOwners(session, ownerKeys) which clones once
and scans each collection once for the whole set, and route
pruneWorktreeStateForRepo through it. The per-owner O(1) field deletes
and the single-scan pane-key passes are factored into shared functions
so removeWorkspaceSessionOwner keeps identical behavior for its other
callers. No behavior change.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: PannenetsF <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-24 00:25:26 -07:00
Yu Sun 2cbcf03b0f fix: qualify ssh fallback worktree paths (#7764) 2026-07-24 00:24:58 -07:00
6d39e49480 fix(ssh): accept GitHub restricted-shell SSH probes (#6988) (#7659)
* fix(ssh): accept GitHub restricted-shell SSH probes (#6988)

* fix: match first stderr line for GitHub restricted-shell probe (bug-bash takeover)

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-24 00:24:53 -07:00
Matteo Musacchio c1d2c4be08 Fix PowerShell worktree terminal cwd (#7435) 2026-07-24 00:24:48 -07:00
Fazal KadivarandCursor 34caad787c fix(browser): unstick Ctrl+Tab switcher when opened from a focused browser guest (#9966)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 23:48:07 -07:00
Rod Boev 817197fc31 fix(terminal): close tabless PTYs through the live pane path (#9288) 2026-07-23 23:33:08 -07:00
c03e8f6f64 fix: enumerate IPv6 addresses for mobile pairing on IPv6-only hosts (#9130) (#9131)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: PannenetsF <fanyunqian.1@bytedance.com>
2026-07-23 23:31:10 -07:00
moseoh 832aa69ce8 fix(tasks): resolve PR work items upstream-first under 'auto' like issues (#8727) 2026-07-23 23:20:52 -07:00
BingZ 4c7bbed2fb fix(windows): detect Cursor Agent Node wrapper (#8266) 2026-07-23 23:18:50 -07:00
Antonio LourencoandOrcaWin fde063618b fix(remote): create paired agent sessions without host focus (#10193)
* fix(remote): create paired agent sessions without host focus

* test(remote): assert structured resume request

* test(remote): preserve provider-separated resume coverage

* test(remote): assert paired agent focus authority

* fix(remote): separate agent host creation from viewer focus

* test(remote): harden agent-session authority validation

* test(remote): validate retired pane identity

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-23 20:20:00 -07:00
Jinjing 381e81e7bd fix(worktree): don't path-sweep sibling sessions when deleting a folder workspace (#10252) (#10268)
* 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
2026-07-23 19:43:34 -07:00
1b01385872 fix(worktree): reconcile symlinked creates by Git identity (#10266)
* 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>
2026-07-23 19:12:54 -07:00
Brennan Benson e4202093c0 fix(terminal): re-verify cached macOS login-preflight rejections (#9973)
* 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
2026-07-23 19:12:13 -07:00
Mark Xian e3adb20917 fix(agents): include OMP terminals in cold session restoration (#8991)
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.
2026-07-23 19:05:22 -07:00
Neil 943f9a077b perf(daemon): cache droppable stream membership (#10121) 2026-07-23 19:00:43 -07:00
Neil c8a251279a perf(pty): make delivery pressure tracking constant time (#10113) 2026-07-23 19:00:25 -07:00
Neil d95eae5ead perf(workspace): cancel local worktree scans (#10107) 2026-07-23 19:00:00 -07:00
Neil 645ed9a3be fix(ssh): hold premature transport 'connected' until the relay is ready (#10157)
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.
2026-07-23 18:59:03 -07:00
Brennan Benson 4e27e8e339 test(main): deflake worktree-watch non-ENOENT readdir test with atomic EACCES injection (#10261) 2026-07-23 18:58:26 -07:00
Jinwoo HongandOrcaWin fd72a89260 fix: bound Relay desktop setup handshakes (#10260)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-23 18:46:38 -07:00
NeilandOrca aab112933e Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai>
2026-07-23 18:35:31 -07:00
Brennan Benson 6eb70d8370 fix(main): stop unhandled rejections from silently killing the app during startup restore (#9970)
* 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
2026-07-23 18:31:02 -07:00
Brennan Benson 4569a4fe71 fix(window): defer the repaint size nudge and skip it on macOS 26 (#10253)
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.
2026-07-23 18:28:52 -07:00
Brennan Benson 00cda62ca9 fix(worktree): stop terminal removal fence error flashing on delete (#10240)
* 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.
2026-07-23 18:04:15 -07:00
Brennan Benson bded1fb2c0 fix(app): bound the wake/quit paths implicated in the phone-session-ended freeze (#9447) (#9853)
* 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
2026-07-23 18:03:43 -07:00
87c59dd27d fix(claude-accounts): quote resolved claude path for Windows shell spawn (#10237)
* 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>
2026-07-23 17:44:43 -07:00
Brennan Benson fbd5c61f4d perf(terminal): bound and de-synchronize cold-restore replay on the main process (#9990)
* fix(terminal): bound cold restore replay

* fix(terminal): bound cold restore replay slices

* perf(terminal): avoid rescanning cold restore payloads
2026-07-23 16:56:01 -07:00
Brennan Benson ef985ed800 fix(codex): re-land TUI settings promotion with anchored baseline (#10213)
* Reapply "Preserve Codex [tui] settings across managed CODEX_HOME remirrors (#9475)" (#10085)

This reverts commit 09756dfaff.

* fix(codex): anchor settings baseline migration
2026-07-23 16:08:46 -07:00
gatsby74andBrennan Benson d4387b17d9 fix(status-bar): Resource Manager closed badge terminal count + RAM seed (#9387)
* fix(status-bar): seed Resource Manager closed badge from daemon inventory

The closed chip counted tab/layout PTY wake hints (inflating terminal
count) and never fetched memory until the popover opened (showing "—").
Cache listSessions for the badge, seed memory on session ready, and drop
the wake-hint closed selector.

* fix(status-bar): update session inventory ref in an effect

CodeRabbit/React Doctor flagged mutating sessionInventoryRef during
render; keep the write in useEffect after commit.

* fix(status-bar): follow daemon session lifecycle events

* perf(status-bar): skip inventory scans for known PTYs

* perf(status-bar): bound resource inventory refreshes

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-23 16:07:59 -07:00
kazu-42 4a09ede8b1 fix(codex): classify rate-limit windows by duration (#10136)
Co-authored-by: kazu-42 <91968271+kazu-42@users.noreply.github.com>
2026-07-23 15:58:01 -07:00
Brennan Benson babf1ff9eb fix(codex-accounts): stop account mutations blocking on the quota refresh (#10225)
Switching, adding, re-authing, or removing a Codex account awaited
refreshForCodexAccountChange before resolving the IPC call. Since the
per-account CODEX_HOME rollout (#9501) that probe runs against a cold
home (10s RPC + 15s PTY fallback, 25s WSL) and can queue behind an
in-flight global usage fetch, so the switcher sat unresponsive for tens
of seconds and a fresh login looked stuck on the loading screen.

Worse, in addAccount the awaited refresh sat inside the login cleanup
try/catch: a refresh rejection after the account was durably committed
deleted the just-created managed home, leaving a registered account
with no home ("account never connects").

Run the refresh as best-effort background work instead. Its synchronous
prefix still flips usage to "fetching" before the first await, so the
switcher updates instantly and usage fills in via the normal
rate-limit state pushes; a probe failure is logged and can never
trigger managed-home cleanup.

Fixes #10141
2026-07-23 15:54:02 -07:00
OrcaWin 0da5380a49 fix: recover Relay broker startup after transient failures (#10147) 2026-07-23 14:31:15 -07:00
Brennan Benson eb2508e7f3 feat(diagnostics): name what grew in renderer OOM crash reports (#9984)
* feat(diagnostics): name what grew in renderer OOM crash reports

Renderer OOMs are the dominant crash class (heap pinned at the ~3.5GB V8
ceiling in crash-channel reports) but renderer_memory breadcrumbs carry only
heap totals, so reports say "it grew" without saying what.

Add a one-shot renderer_memory_highwater breadcrumb at 60%/80% of the heap
limit carrying leak-diagnosis counts: DOM node census, terminal element
count, and per-subsystem counts from a new contributor registry. The store
registers the first contributor, reporting its 20 largest top-level
collections. Counts only, capped per contributor; zero work on the 60s
sample path while below threshold.

* fix(diagnostics): retain renderer heap profiles

* test(agent-status): remove subagent row order race

* fix(diagnostics): bound aggregate heap profile work

* fix(diagnostics): bound heap profile contributor calls

* fix(types): avoid overloaded stat return inference

* fix(diagnostics): fully bound heap profile registry

* chore(skills): refresh release snapshot manifest

* fix(diagnostics): retain browser counts in heap profiles
2026-07-23 12:22:33 -07:00
Neil 8f40ddf328 fix(memory): bound OOM-prone accumulators (#10179) 2026-07-23 06:22:56 -07:00
Mark XianandOrcaWin 7ab601487c fix(remote): don't classify a stale/gone remote handle as agent completion (#9263)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-23 01:45:24 -07:00
e8d5d50c35 fix(quick-open): guide rg install after fallback budget errors (#9627)
* fix(quick-open): guide rg install after fallback budget errors

* fix(quick-open): show local host wording for local install guidance

The install-rg guidance component hardcoded 'on the remote', so the new
local fallback path told local users to install ripgrep 'on the remote'
— wrong for the exact case #9627 targets. Parse the location out of the
message and render the matching wording; add the local locale string and
a render test that guards against the 'on the remote' regression. Also
harden the reason capture against a stray ')' in the error text.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-23 00:27:06 -07:00
Jinjing 746a8f3e48 fix: retry crash reports on primary endpoint instead of fallback (#10023)
Replace the api.onorca.dev fallback pattern with same-host retries on
www.onorca.dev, the endpoint that owns crash delivery. This ensures
consistent behavior and clearer error reporting when failures occur.
2026-07-23 00:13:43 -07:00