Repository navigation
feat(accounts): offer to restart running Claude sessions on the new account after a switch - #29898
Conversation
The queued account-switch restart only knew how to relaunch Codex. The queue now records which agent each pane restarts as, and the resume startup builder takes that agent, so the same mounted and detached restart paths can move Claude sessions too. Codex behaviour is unchanged.
…t switch Switching the selected Claude account only reached Claudes started later, so users had to /exit and resume every running session by hand. After a switch from the status bar or Settings, Orca now finds local terminal panes running a resumable Claude session in the switched lane and shows one toast offering to move them. Confirming restarts idle sessions with claude --resume <id> in place; sessions mid-reply move when their turn ends, and are dropped if the pane closes or the user switches again.
…de args A Claude session at a permission prompt is mid-turn, so a move now waits for it like a session that is replying. Resuming a Claude pane with no recorded launch config no longer adds the user's default agent arguments or environment, so a hand-started claude is not resumed with a permission bypass. Codex keeps its existing fallback.
Orca had no record of the account a running Claude used, so an account switch could only assume every session was on the old one. A renderer tracker now attributes sessions that start after launch to their lane's selected account and records the account Orca restarts a session on. Sessions it cannot attribute stay unknown and keep today's behaviour.
…a switch Replaces the move toast. After a Claude account switch, a dialog names the new account and how many sessions are still on the old one, with Keep as is and Restart, plus Remember my choice. Closing or Escape keeps sessions. The remembered choice lives in Settings > Accounts as When you switch accounts while Claude is running: Ask, Always restart, Never restart.
…ts and use one restart queue Which account a running Claude uses now comes from the transcript path its hooks already report (<CLAUDE_CONFIG_DIR>/projects/...), so the renderer tracker and its guesses are gone and unknown sessions are never counted. Restarts queue straight into the store's pending map through one queuePaneAccountRestarts action; the pane effect and the detached scheduler hold a Claude restart while its turn runs and skip it when the session can no longer be resumed. Codex behaviour is unchanged.
…ialog Co-Authored-By: Claude <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 Walkthrough
Priority: ⬇️ Low Merge Risk: 🔵 Low · up to
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments: 1
src/renderer/src/store/terminals/terminal-restart-state.ts: 1
40-59: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueKeep the no-op Claude queue call silent.
When
handleRunningClaudeSessionsAfterSwitchstarts a user-driven account switch, it callsqueueClaudeRestarts([]). If no Claude restart is queued, the Claude branch creates a newpendingPaneAccountRestartsmap with the same entries. Mounted terminal-pane chat hooks then re-evaluate their map selectors, and the scheduler subscription checks the update, although it does not schedule a sweep.The repository treats no-op actions as silent. Keep this as a narrow optional optimization because account switches are user-driven and the empty call does not cause scheduler work.
Suggested fix
set((s) => { // Why replace: a new Claude switch answers afresh, so unrun restarts from the last one drop. - const kept = options?.replaceQueued + const hasQueuedClaude = + options?.replaceQueued && + Object.values(s.pendingPaneAccountRestarts).includes(agent) + if (!hasQueuedClaude && ptyIds.length === 0) { + return s + } + const kept = hasQueuedClaude ? Object.fromEntries( Object.entries(s.pendingPaneAccountRestarts).filter( ([, queued]) => queued !== agent
🔇 Additional comments: 37
src/renderer/src/store/terminals/terminal-contracts.ts: 1
14-16: LGTM!src/renderer/src/store/terminals/terminal-state.ts: 1
14-18: LGTM!Also applies to: 34-35
src/renderer/src/store/terminals/terminal-actions.ts: 1
26-26: LGTM!Also applies to: 167-173
src/renderer/src/store/slices/terminals.ts: 1
34-34: LGTM!src/renderer/src/store/terminals/terminal-pty-bindings.ts: 1
129-129: LGTM!Also applies to: 137-139, 148-149, 184-184
src/renderer/src/store/terminals/terminal-pty-release.ts: 1
69-69: LGTM!Also applies to: 72-72, 76-76, 101-101
src/renderer/src/store/terminals/terminal-shutdown-state.ts: 1
68-68: LGTM!Also applies to: 85-85, 125-125
src/renderer/src/store/slices/ssh-target-cleanup.ts: 1
134-134: LGTM!Also applies to: 141-142, 167-167, 177-177
src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts: 1
180-180: LGTM!src/renderer/src/components/CodexRestartChip.tsx: 1
37-37: LGTM!src/renderer/src/components/status-bar/StatusBarAccountControls.tsx: 1
11-11: LGTM!Also applies to: 54-54
src/renderer/src/components/codex-restart-chip.test.tsx: 1
107-107: LGTM!Also applies to: 173-174, 231-231, 282-282
src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts: 1
21-21: LGTM!Also applies to: 87-87, 92-92, 96-96, 133-133, 144-144
src/renderer/src/store/slices/bulk-worktree-purge-terminal-maps-leak.test.ts: 1
17-17: LGTM!Also applies to: 127-131, 158-160, 172-172
src/renderer/src/store/slices/codex-restart-notice-lifecycle.test.ts: 1
78-78: LGTM!Also applies to: 87-87, 94-94, 207-207, 217-217
src/renderer/src/store/slices/ssh-target-cleanup.test.ts: 1
71-71: LGTM!Also applies to: 82-82
src/renderer/src/store/slices/ssh.test.ts: 1
48-50: LGTM!Also applies to: 140-140, 150-150
src/renderer/src/store/slices/store-session-cascades.test.ts: 1
53-53: LGTM!Also applies to: 83-84, 120-120, 123-123
src/renderer/src/store/slices/store-sleep-exact-runtime-stop.test.ts: 1
442-442: LGTM!Also applies to: 464-464
src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts: 1
41-41: LGTM!Also applies to: 47-47
src/renderer/src/lib/codex-session-restart-account-recheck.test.ts: 1
36-36: LGTM!Also applies to: 69-69, 81-81
src/renderer/src/lib/codex-session-restart-shell-flap.test.ts: 1
50-50: LGTM!src/renderer/src/lib/codex-session-restart.test.ts: 1
87-87: LGTM!Also applies to: 332-332, 340-340, 353-353, 366-366, 483-483, 870-870
src/renderer/src/lib/codex-stale-pane-account-identity.test.ts: 1
46-46: LGTM!src/renderer/src/lib/codex-stale-pane-sweep.test.ts: 1
45-45: LGTM!src/renderer/src/store/terminals/terminal-shutdown-map-identity.test.ts: 1
16-16: LGTM!Also applies to: 25-25, 32-32
src/renderer/src/components/terminal-pane/claude-detached-pane-restart.test.ts: 1
1-135: LGTM!src/renderer/src/components/terminal-pane/codex-detached-pane-restart-scheduler.ts: 1
2-2: LGTM!Also applies to: 15-16, 31-36
src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts: 1
75-75: LGTM!Also applies to: 147-147, 207-207
src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts: 1
2-20: LGTM!Also applies to: 31-31, 40-40, 67-67, 79-93, 140-150, 167-175, 195-195
src/renderer/src/components/terminal-pane/detached-pane-restart-spawn-context.ts: 1
1-37: LGTM!src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts: 1
34-48: LGTM!Also applies to: 262-264
src/renderer/src/components/terminal-pane/use-terminal-pane-process-exit-actions.ts: 1
3-4: LGTM!Also applies to: 13-13, 22-22, 41-42, 59-78, 130-132, 188-197, 242-267
src/renderer/src/components/terminal-pane/use-terminal-pane-store-actions.ts: 1
20-20: LGTM!Also applies to: 51-51
src/renderer/src/lib/agent-account-restart-startup.test.ts: 1
5-12: LGTM!Also applies to: 48-60, 175-263
src/renderer/src/lib/agent-account-restart-startup.ts: 1
22-136: LGTM!src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx: 1
23-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.Use a record in the mock for
pendingPaneAccountRestarts.
pendingPaneAccountRestartsis aRecord<string, AccountRestartAgent>. The mock sets it to an array ([]).Object.values([])works by chance, so the test passes. The mock still does not match the store shape. Use{}.- pendingPaneAccountRestarts: [], + pendingPaneAccountRestarts: {},
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
4a1eca50-85ae-4747-b359-0601e1ad0d70
📒 Files selected for processing: 57
src/renderer/src/App.tsxsrc/renderer/src/components/ClaudeAccountSwitchDialog.test.tsxsrc/renderer/src/components/ClaudeAccountSwitchDialog.tsxsrc/renderer/src/components/CodexRestartChip.tsxsrc/renderer/src/components/codex-restart-chip.test.tsxsrc/renderer/src/components/settings/AccountsPane.tsxsrc/renderer/src/components/settings/accounts-claude-search.tssrc/renderer/src/components/settings/accounts-pane-account-actions.tssrc/renderer/src/components/settings/accounts-pane-claude-section.tsxsrc/renderer/src/components/settings/accounts-search.tssrc/renderer/src/components/settings/claude-running-session-switch-setting.tsxsrc/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsxsrc/renderer/src/components/status-bar/StatusBarAccountControls.tsxsrc/renderer/src/components/terminal-pane/claude-detached-pane-restart.test.tssrc/renderer/src/components/terminal-pane/codex-detached-pane-restart-scheduler.tssrc/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.tssrc/renderer/src/components/terminal-pane/codex-detached-pane-restart.tssrc/renderer/src/components/terminal-pane/detached-pane-restart-spawn-context.tssrc/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsxsrc/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.tssrc/renderer/src/components/terminal-pane/use-terminal-pane-process-exit-actions.tssrc/renderer/src/components/terminal-pane/use-terminal-pane-store-actions.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/lib/agent-account-restart-startup.test.tssrc/renderer/src/lib/agent-account-restart-startup.tssrc/renderer/src/lib/claude-account-switch-sessions.test.tssrc/renderer/src/lib/claude-account-switch-sessions.tssrc/renderer/src/lib/claude-session-account-move.test.tssrc/renderer/src/lib/claude-session-account-move.tssrc/renderer/src/lib/codex-session-restart-account-recheck.test.tssrc/renderer/src/lib/codex-session-restart-shell-flap.test.tssrc/renderer/src/lib/codex-session-restart.test.tssrc/renderer/src/lib/codex-stale-pane-account-identity.test.tssrc/renderer/src/lib/codex-stale-pane-sweep.test.tssrc/renderer/src/store/slices/bulk-worktree-purge-terminal-maps-leak.test.tssrc/renderer/src/store/slices/codex-restart-notice-lifecycle.test.tssrc/renderer/src/store/slices/ssh-target-cleanup.test.tssrc/renderer/src/store/slices/ssh-target-cleanup.tssrc/renderer/src/store/slices/ssh.test.tssrc/renderer/src/store/slices/store-session-cascades.test.tssrc/renderer/src/store/slices/store-sleep-exact-runtime-stop.test.tssrc/renderer/src/store/slices/terminal-pty-identity-replacement.test.tssrc/renderer/src/store/slices/terminals.tssrc/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.tssrc/renderer/src/store/terminals/terminal-actions.tssrc/renderer/src/store/terminals/terminal-contracts.tssrc/renderer/src/store/terminals/terminal-no-op-subscriber.test.tssrc/renderer/src/store/terminals/terminal-pty-bindings.tssrc/renderer/src/store/terminals/terminal-pty-release.tssrc/renderer/src/store/terminals/terminal-restart-state.tssrc/renderer/src/store/terminals/terminal-shutdown-map-identity.test.tssrc/renderer/src/store/terminals/terminal-shutdown-state.tssrc/renderer/src/store/terminals/terminal-state.tssrc/shared/claude-account-switch-settings.tssrc/shared/global-settings-base.tssrc/shared/global-settings-types.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.
…itching note The deferred restart waits on any working or blocked turn, not only a reply. The switch dialog now covers running sessions, so the menu footer note is redundant.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Filter Claude sessions by their recorded launch lane. · claude-session-account-move.ts:149
src/renderer/src/lib/claude-session-account-move.ts:149
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winFilter Claude sessions by their recorded launch lane.
findMovableClaudeSessionsderives the lane from current runtime and WSL settings. If those settings changed after a host pane launched, the pane can be excluded from the account-switch candidates. The account-switch handler then shows no restart prompt and does not queue the session, so it remains on the outgoing account. The opposite drift can queue a WSL session that the host account switch did not affect.Read
listRecordedPaneLanesbefore this filter and passrecordedLaneKeytoresolveCodexPaneSelectionLane. Keep derivation as the fallback when no record exists.Suggested fix
const state = useAppStore.getState() + const localPtyIds = Object.values(state.ptyIdsByTabId) + .flat() + .filter((ptyId) => !isForeignMachineCodexPtyId(ptyId)) + const listRecordedPaneLanes = window.api.codexAccounts.listRecordedPaneLanes + const recordedLanes = + typeof listRecordedPaneLanes === 'function' + ? await listRecordedPaneLanes({ ptyIds: localPtyIds }).catch(() => ({})) + : {} const isLaneInScope = getCodexAccountSwitchLaneMatcher({ owner: args.owner, target: args.target @@ - if (!isLaneInScope(resolveCodexPaneSelectionLane({ state, tab, ptyId }).laneKey)) { + if ( + !isLaneInScope( + resolveCodexPaneSelectionLane({ + state, + tab, + ptyId, + recordedLaneKey: recordedLanes[ptyId] + }).laneKey + ) + ) {
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
2ff140ef-7e61-4a62-a0e9-8f25409bb8ac
📒 Files selected for processing: 4
src/renderer/src/components/ClaudeAccountSwitchDialog.test.tsxsrc/renderer/src/components/ClaudeAccountSwitchDialog.tsxsrc/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsxsrc/renderer/src/i18n/locales/en.json
💤 Files with no reviewable changes: 1
- src/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsx
🚧 Files skipped from review as they are similar to previous changes: 2
- src/renderer/src/components/ClaudeAccountSwitchDialog.tsx
- src/renderer/src/i18n/locales/en.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.
…s are cleared Codex notice cleanup shares the pending-restart map with Claude switch restarts; it now drops only Codex entries.
…cross switches - A Claude permission prompt or question reports 'waiting'; a queued restart now waits for 'done' instead of only 'working'/'blocked', so it no longer drops them. - A later switch keeps accepted restarts still waiting on a turn (they relaunch on whatever is selected) and drops only those whose session is already on the new account, scoped to the switch's lane. The blanket replaceQueued option is gone. - A server account switch returns before touching the local dialog or queue. - Pane restart state reads through one selector to stay inside the subscription budget, and the focus-read baseline follows the restart-startup rename.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
1b6f7903-6657-4171-b0a7-2fe85cca9504
📒 Files selected for processing: 12
config/focus-setting-read-baseline.txtsrc/renderer/src/components/ClaudeAccountSwitchDialog.test.tsxsrc/renderer/src/components/settings/accounts-pane-account-actions.tssrc/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsxsrc/renderer/src/components/terminal-pane/claude-detached-pane-restart.test.tssrc/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.tssrc/renderer/src/lib/claude-account-switch-sessions.test.tssrc/renderer/src/lib/claude-account-switch-sessions.tssrc/renderer/src/lib/claude-session-account-move.test.tssrc/renderer/src/lib/claude-session-account-move.tssrc/renderer/src/store/terminals/terminal-actions.tssrc/renderer/src/store/terminals/terminal-restart-state.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.
…e chat's resume prompt "Don't ask again (restart automatically)" turns on automatic restarts however the dialog is left, including Escape; Settings → AI Provider Accounts turns it off.
… switch With more than one session, the switch dialog lists them by workspace using the native-chat resume tree rows, all ticked; Restart covers only the ticked ones.
…wn's restart queue
…swered A reattach can rebind a pane while the dialog is open and a closed pane has no PTY, so the ids it listed may name nothing.
… System default A saved account with no login of its own runs on System default, so its sessions' transcripts read as System default. The switch now compares where launches run (runsOnSystemDefault on the account summary) and asks nothing when that is unchanged.
…ey the whole prompt The post-switch coordinator now takes the before/after account snapshots and derives launch folders and labels itself, so the status bar and Settings stop assembling the same request. The status bar's settings projection reuses the host's row, which keeps runsOnSystemDefault: a covered account switched from the status bar now finds its System default sessions. Never restart returns before any session scan. The outcome collapses to show-confirmation | handled. The dialog keys one component that owns the whole Dialog with a plain boolean.
…nt restarts Replaces claudeTurnStatus, the whole agent-status map passed through the controller as a re-run signal, with a shallow selector of the PTY ids whose queued restart can run now. Other tabs' status updates no longer re-render or re-run every mounted pane once a Claude restart is queued. The effect re-checks readiness against the current store before consuming. The detached Claude test now runs through the scheduler's real subscription instead of manual sweeps.
…ueued restart The mounted pane now reads its ready restarts from the store through useShallow.
…ch as after one by hand The automatic switch runs in main, so the running-session question from stablyai#29898 never came up for it. Main now tells the renderer which accounts it switched between, and the renderer hands that to the same step a switch from the menu runs: ask, always restart, or never restart.
ELI5
When you switch Claude accounts in Orca (usually because one hit its usage limit), Claude sessions that are already running stay on the old account. Today you have to quit and resume Claude in every terminal by hand. Now Orca asks once, right after the switch, whether to restart those sessions on the new account. One click moves them all, and each conversation continues where it left off.
What Changed
Before: switching accounts only affected Claudes started afterwards. Every running session kept using the old account (often the one that just hit its limit) until you ran
/exitandclaude --resumein each terminal yourself.After:
claude --resume <same session>in the same pane, so the conversation continues on the new account. Idle sessions restart right away; a session that is mid-reply or waiting on a permission prompt restarts when its turn ends, so no in-progress work is lost.Mechanism:
Why
Per-account folders (#24434, re-landed in #26801) fixed the sign-out loop by giving each account its own Claude folder, but a running Claude is tied to the folder it started in. Before that change, Orca swapped the shared login and running Claudes picked it up on their own within about 30 seconds (verified), which is the behaviour users miss. Swapping logins in place is what caused the sign-outs, so restart-and-resume is the safe way to get it back.
Alternatives considered: a toast, a card on every terminal (Codex's pattern; one click per terminal), split buttons, per-terminal account labels, and moving everything automatically (breaks people who run two accounts in parallel). One dialog at switch time, with a remembered choice, was the clearest for users.
Users whose shell sets
CLAUDE_CONFIG_DIRalso need #29890, otherwise the restarted Claude stays on the old folder.Linked Issue
No issue; reported by users on Discord and X after the per-account folder change.
Visual Proof
Real app (dev build, isolated profile), right after switching from a saved account to System default with two Claude sessions running on the saved account (email replaced):
Before this PR there is no prompt: the switch happens and both sessions silently stay on the old account.
Testing
macOS, dev build with an isolated Orca profile and two real Claude logins (account A as System default, account B as a saved account):
claude … --resume <same session id>and the new processes run with account A's folder (checked theirCLAUDE_CONFIG_DIR), same conversations.Also verified earlier in the same setup: a Claude session that hit its weekly limit on one account continued the same conversation after resuming on the other account.
workingandblocked, hidden-tab restarts, skipping unresolvable resumes, existing Codex restart tests unchanged.pnpm tc, 22 changed test files (241 tests), oxlint and the changed-code quality gate pass.Review
Agent skill upstream boundary
Notes
codex-detached-pane-restart*,resolveCodexPaneSelectionLane, …) in a separate rename-only PR.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)Co-Authored-By: Claude noreply@anthropic.com