Repository navigation
Reopen closed tabs with Cmd+Shift+T, including terminals - #7445
Conversation
Cmd/Ctrl+Shift+T now reopens the most recently closed tab of any kind (terminal, browser, or editor) in true recency order, walking back through close history on repeated presses — Chrome/Ghostty semantics. Terminal tabs previously had no reopen at all, and the shortcut picked browser-vs-editor by focused surface instead of recency. Terminal closes capture a snapshot (startupCwd, shellOverride, customTitle, color) on explicit user closes only; PTY exits, launch cleanup, onboarding, and floating-panel closes are excluded. Reopen spawns a fresh shell through the same createTab path as the "+" button. A per-worktree cross-type kind list (new recently-closed-tabs slice) orders the three existing per-type stacks; both new maps are purged on worktree removal and re-keyed on rename with startupCwd prefix remap. createNewTerminalTab moved to terminal-tab-create.ts to keep terminal-tab-actions.ts under the max-lines budget. Requested via Discord feedback; STA-1287. Co-authored-by: Orca <help@stably.ai>
📝 WalkthroughWalkthroughThis PR adds a new per-worktree recently-closed-tabs store slice for terminal, browser, and editor tabs, with terminal-specific and cross-type reopen actions. Terminal close paths now accept Related issues: None referenced. Related PRs: None referenced. Suggested labels: review_needed_junior_swe, review_depth_standard Suggested reviewers: None specified. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/renderer/src/store/slices/worktrees.ts (1)
1734-1756: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider
pathutilities instead of manual separator handling for thestartupCwdremap.The prefix match/rewrite manually checks for both
/and\and concatenates strings by hand. It's correct today, but path-prefix logic like this is easy to get subtly wrong on edge cases (trailing separators, mixed separators, drive-letter casing on Windows) thatpath.relative/path.isAbsolutewould handle more robustly.♻️ Possible refactor using `path.relative`
+import path from 'node:path' ... renameKey('recentlyClosedTerminalTabsByWorktree', (snapshots: ClosedTerminalTabSnapshot[]) => oldWorktreePath && newWorktreePath ? snapshots.map((snapshot) => { const cwd = snapshot.startupCwd if (!cwd) { return snapshot } - if (cwd === oldWorktreePath) { - return { ...snapshot, startupCwd: newWorktreePath } - } - if (cwd.startsWith(`${oldWorktreePath}/`) || cwd.startsWith(`${oldWorktreePath}\\`)) { - return { ...snapshot, startupCwd: newWorktreePath + cwd.slice(oldWorktreePath.length) } - } - return snapshot + const rel = path.relative(oldWorktreePath, cwd) + if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) { + return { ...snapshot, startupCwd: path.join(newWorktreePath, rel) } + } + return snapshot }) : snapshots )As per coding guidelines,
**/*.{ts,tsx,js,jsx}: "Usepath.joinor Electron/Node path utilities for file paths — never assume/or\for cross-platform compatibility."Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b43ae03a-f756-43d5-8ec0-ba42cfb76d48
📒 Files selected for processing (23)
src/renderer/src/components/Terminal.tsxsrc/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsxsrc/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.tsxsrc/renderer/src/components/right-sidebar/FileExplorer.tsxsrc/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsxsrc/renderer/src/components/terminal/terminal-tab-actions.test.tssrc/renderer/src/components/terminal/terminal-tab-actions.tssrc/renderer/src/components/terminal/terminal-tab-create.tssrc/renderer/src/lib/launch-agent-background-session.tssrc/renderer/src/lib/launch-agent-in-new-tab.tssrc/renderer/src/lib/launch-worktree-background-terminals.tssrc/renderer/src/store/index.tssrc/renderer/src/store/slices/browser.tssrc/renderer/src/store/slices/diffComments.test.tssrc/renderer/src/store/slices/editor.tssrc/renderer/src/store/slices/recently-closed-tabs.test.tssrc/renderer/src/store/slices/recently-closed-tabs.tssrc/renderer/src/store/slices/store-test-helpers.tssrc/renderer/src/store/slices/terminals.tssrc/renderer/src/store/slices/worktree-removal-maps-leak.test.tssrc/renderer/src/store/slices/worktrees.test.tssrc/renderer/src/store/slices/worktrees.tssrc/renderer/src/store/types.ts
Floating-panel, launch-failure, and stale-agent-tab closes now thread
{ captureRecentlyClosed: false } so system-initiated closes don't pollute
the reopen stack. Update the affected test expectations to match the new
call shape.
Co-authored-by: Orca <help@stably.ai>
…losed-tab # Conflicts: # src/renderer/src/store/index.ts # src/renderer/src/store/slices/diffComments.test.ts # src/renderer/src/store/slices/store-test-helpers.ts # src/renderer/src/store/slices/terminals.ts # src/renderer/src/store/types.ts
reopenClosedTerminalTab recreated terminals via a raw local createTab without the isWebRuntimeSessionActive guard every sibling terminal-create path uses, so Cmd+Shift+T on a remote-runtime-owned worktree (paired-web or desktop-attached headless-serve) spawned an unbacked local phantom tab not tied to the host session. Guard at reopen time via the cycle-free getRuntimeEnvironmentIdForWorktree — importing web-runtime-session into a store slice creates a store-index init cycle. Remote terminal reopen stays deferred; the cross-type dispatcher falls through to browser/editor. Adds a regression test and corrects an inaccurate removal-path comment.
The prior test set settings.activeRuntimeEnvironmentId by spreading the store's settings, which widened workspaceDir to string|undefined and broke `pnpm typecheck` (GlobalSettings requires string). Seed the worktree with a `runtime:` hostId instead — getRuntimeEnvironmentIdForWorktree reads it directly, so no settings spread is needed.
Resolve conflicts from main's terminal-close refactors, preserving the Cmd+Shift+T reopen capture-suppression intent: - terminals.ts closeTab reducer: keep both main's closingWorktreeId and the reopen-capture closedTab/closedWorktreeId in one merged loop. - Terminal.tsx / TerminalPaneOverlayLayer.tsx pty-exit: run main's new parked-multi-leaf defer guard first, then the capture-suppressed close. - launch-agent-background-session.ts: main's best-effort failure cleanup now closes the tab with captureRecentlyClosed: false (system close). - removeStaleLocalAgentTabsForWebHostLaunch moved to launch-agent-web-host-tab.ts on main; carry captureRecentlyClosed: false there. - Update the two close-shape test assertions accordingly.
Resolve terminal retirement and close-reason conflicts while preserving user-only reopen history. Bound bulk history work, harden runtime ownership and cross-platform cwd remapping, and suppress autonomous parked exits without changing pinned-close reliability.
* Reopen closed tabs with Cmd+Shift+T, including terminals
Cmd/Ctrl+Shift+T now reopens the most recently closed tab of any kind
(terminal, browser, or editor) in true recency order, walking back
through close history on repeated presses — Chrome/Ghostty semantics.
Terminal tabs previously had no reopen at all, and the shortcut picked
browser-vs-editor by focused surface instead of recency.
Terminal closes capture a snapshot (startupCwd, shellOverride,
customTitle, color) on explicit user closes only; PTY exits, launch
cleanup, onboarding, and floating-panel closes are excluded. Reopen
spawns a fresh shell through the same createTab path as the "+" button.
A per-worktree cross-type kind list (new recently-closed-tabs slice)
orders the three existing per-type stacks; both new maps are purged on
worktree removal and re-keyed on rename with startupCwd prefix remap.
createNewTerminalTab moved to terminal-tab-create.ts to keep
terminal-tab-actions.ts under the max-lines budget.
Requested via Discord feedback; STA-1287.
Co-authored-by: Orca <help@stably.ai>
* Update close-tab assertions for captureRecentlyClosed flag
Floating-panel, launch-failure, and stale-agent-tab closes now thread
{ captureRecentlyClosed: false } so system-initiated closes don't pollute
the reopen stack. Update the affected test expectations to match the new
call shape.
Co-authored-by: Orca <help@stably.ai>
* fix(reopen): skip local terminal reopen on remote-runtime worktrees
reopenClosedTerminalTab recreated terminals via a raw local createTab
without the isWebRuntimeSessionActive guard every sibling terminal-create
path uses, so Cmd+Shift+T on a remote-runtime-owned worktree (paired-web
or desktop-attached headless-serve) spawned an unbacked local phantom tab
not tied to the host session. Guard at reopen time via the cycle-free
getRuntimeEnvironmentIdForWorktree — importing web-runtime-session into a
store slice creates a store-index init cycle. Remote terminal reopen stays
deferred; the cross-type dispatcher falls through to browser/editor.
Adds a regression test and corrects an inaccurate removal-path comment.
* fix(test): seed runtime host id for remote-reopen guard test
The prior test set settings.activeRuntimeEnvironmentId by spreading the
store's settings, which widened workspaceDir to string|undefined and broke
`pnpm typecheck` (GlobalSettings requires string). Seed the worktree with a
`runtime:` hostId instead — getRuntimeEnvironmentIdForWorktree reads it
directly, so no settings spread is needed.
* Remove unrelated formatter changes
---------
Co-authored-by: Orca <help@stably.ai>
Cmd+Shift+T (Ctrl+Shift+T on Windows/Linux) now reopens the most recently closed tab of any kind — terminal, browser, or editor — and repeated presses walk back through the close history, matching Chrome and Ghostty. Previously the shortcut only worked for browser/editor tabs (picked by which surface was focused, not by recency), and a closed terminal tab was gone for good.
Requested by a user on Discord (thread
1522485945746718750): "yes, be it browser or a terminal. just like it works in chrome or in ghostty". Linear: STA-1287.What changes
Cmd+W, middle-click, context menu, close-others/close-to-right) now captures a snapshot — startup cwd, shell override, custom title, color — into a per-worktree stack (cap 10, matching the existing editor/browser stacks).Cmd+Shift+Tpops the truly most-recent closed tab instead of guessing by active surface.What does not change (deliberate, Ghostty-consistent)
claude --continuein the reopened shell. Decided (Brennan, 2026-07-05): do not relaunch/resume the agent on reopen — plain shell is the intended behavior.exitor an agent process dying auto-closes the tab without joining the reopen stack — undo covers close commands, not process death (Ghostty semantics). Same for onboarding terminals, launch-failure cleanup, and floating-panel closes.startupCwd, not the live cwd at close time (no synchronous live-cwd source exists in the renderer; acceptable for this pass).Design / process
Brennan's PR process ran end to end: design doc → design review (2 rounds; caught PTY-exit stack pollution, all three worktree-id lifecycle paths, rename cwd remapping) → implementation → read-only completeness verification (complete, no blockers; independent close-caller audit found zero missed sites) → perf audit (clean: all new work is O(cap) inside existing user-action reducers, reference-equality no-op paths preserved, both maps bounded and purged on worktree removal/rename) → two-reviewer code-review loop (2 rounds, closed clean: one real P2 found and fixed — the legacy no-layout terminal surface routed PTY exits through the capturing close path; now split like the overlay surface, with regression tests — and one P2 rejected with code evidence as pre-existing browser capture policy) → live Electron validation.
Implementation notes:
recently-closed-tabs.ts(kind list, terminal snapshots, unifiedreopenClosedTabdispatcher) following thepinned-tab-close-confirmsmall-slice precedent; capture rides the existingcloseTab/closeBrowserTab/closeFilereducers in the same atomicset().set()(mirrorsreopenClosedBrowserTab's TOCTOU guard) so rapid double-presses can't restore the same entry twice.startupCwdpaths from the old worktree folder to the new one (cross-platform separators).Validation
recently-closed-tabs.test.ts(8 tests): capture on user close, no capture on system close, cap-at-10 eviction, snapshot-faithful reopen (fresh PTY), TOCTOU double-press, cross-type MRU order (editor→terminal→browser closed, reopened in reverse), drained-kind skip, empty no-op.worktree-removal-maps-leak.test.ts(both removal paths × both new maps) and themigrateWorktreeIdentitytest (re-key + cwd remap + outside-path preservation).terminal-tab-actions.test.ts: PTY-exit closes threadcaptureRecentlyClosed: falseto the store; plain user closes keep the original call shape.pnpm run tcandpnpm run lintclean; 406 tests green across the six affected suites.Cmd+Shift+Trestores a fresh shell at the same cwd (proved fresh via a scrollback marker that did not survive); close browser then terminal → two presses restore terminal first, then the browser tab with its URL live; typingexitauto-closes the tab and is not resurrected by the shortcut; editor reopen still works as the third kind; shortcut fires both with terminal focus (orca-first capture toast shown) and tab-bar focus. Zero console errors. Full-window screenshots captured for every step (kept out of the repo per evidence policy; available in the session report for attaching to this PR).Known boundaries
Cmd+Shift+Trestores the tabs. Chrome would reopen into a window here; Orca has no workspace-level equivalent.Ctrl+Shift+Twere not exercised in the manual pass (single shared code path; the reopen spawn goes through the samecreateTabchokepoint as the "+" button, which already handles SSH/WSL, and the shortcut layer is untouched).Fixes STA-1287.
Made with Orca 🐋
Reliability contract