Skip to content

Reopen closed tabs with Cmd+Shift+T, including terminals - #7445

Merged
brennanb2025 merged 9 commits into
mainfrom
brennanb2025/reopen-closed-tab
Jul 15, 2026
Merged

brennanb2025 merged 9 commits into
mainfrom
brennanb2025/reopen-closed-tab

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Closing a terminal tab (close button, 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).
  • A new per-worktree cross-type MRU list records close order across all three stacks, so Cmd+Shift+T pops the truly most-recent closed tab instead of guessing by active surface.
  • Reopening a terminal creates a fresh shell through the exact same path as the "+" button (so SSH/WSL worktrees behave identically), restores the custom title/color, and lands at the end of the tab strip.

What does not change (deliberate, Ghostty-consistent)

  • Process state is not restored. Reopen recreates the surface: a fresh shell in the same startup directory. No scrollback, no running process.
  • Agent tabs reopen as plain shells. Auto-relaunching e.g. a Claude tab would start a brand-new session (burning tokens without restoring the conversation); the user can run claude --continue in the reopened shell. Decided (Brennan, 2026-07-05): do not relaunch/resume the agent on reopen — plain shell is the intended behavior.
  • Process exits are not captured. Typing exit or 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.
  • Reopened terminals use the tab's original startupCwd, not the live cwd at close time (no synchronous live-cwd source exists in the renderer; acceptable for this pass).
  • The stacks are session-scoped (transient renderer state), matching the existing editor/browser reopen behavior.

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:

  • New focused slice recently-closed-tabs.ts (kind list, terminal snapshots, unified reopenClosedTab dispatcher) following the pinned-tab-close-confirm small-slice precedent; capture rides the existing closeTab/closeBrowserTab/closeFile reducers in the same atomic set().
  • Pop is atomic inside set() (mirrors reopenClosedBrowserTab's TOCTOU guard) so rapid double-presses can't restore the same entry twice.
  • Worktree lifecycle: both new maps are purged on bulk purge and single removal, and re-keyed on folder-rename id migration — including prefix-remapping snapshot startupCwd paths from the old worktree folder to the new one (cross-platform separators).
  • Kind entries can outlive their per-type snapshot (tighter per-type caps); the dispatcher skips drained kinds instead of dead-ending.

Validation

  • New 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.
  • Extended worktree-removal-maps-leak.test.ts (both removal paths × both new maps) and the migrateWorktreeIdentity test (re-key + cwd remap + outside-path preservation).
  • New regression tests in terminal-tab-actions.test.ts: PTY-exit closes thread captureRecentlyClosed: false to the store; plain user closes keep the original call shape.
  • Full pnpm run tc and pnpm run lint clean; 406 tests green across the six affected suites.
  • Manual Electron validation (dev build from this branch, macOS, verdict land): close terminal → Cmd+Shift+T restores 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; typing exit auto-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

  • Paired web/mobile clients: host-owned terminal tabs close via the host republish path, so capture there is best-effort; the desktop app (local + SSH worktrees) is fully covered.
  • Floating terminal panel keeps its existing behavior (no reopen affordance there today, unchanged). Browser closes remain captured unconditionally (pre-existing browser stack policy; floating-panel browser kind entries land under the floating pseudo-worktree key, which the main-view shortcut never reads).
  • Closing the last tab of a workspace deactivates the workspace (pre-existing model), and the shortcut has no handler on the home screen — the close history survives, so re-activating the worktree and pressing Cmd+Shift+T restores the tabs. Chrome would reopen into a window here; Orca has no workspace-level equivalent.
  • Live SSH/remote reopen and Windows/Linux Ctrl+Shift+T were not exercised in the manual pass (single shared code path; the reopen spawn goes through the same createTab chokepoint as the "+" button, which already handles SSH/WSL, and the shortcut layer is untouched).

Fixes STA-1287.

Made with Orca 🐋

Reliability contract

  • Invariant (terminal-session.explicit-close-retirement): an explicit terminal close retires the old provider-owned session once, while Cmd/Ctrl+Shift+T creates a fresh renderer-owned terminal for local and SSH workspaces; natural PTY exits and cleanup closes never enter user reopen history.
  • Failure source: STA-1287 / Discord request, plus reconciliation with main's newer terminal retirement and cold-parking lifecycle.
  • Oracle: 589 affected tests passed after the final main merge; the Node 24 terminal-session.explicit-close-retirement gate passed 439 tests. Tests prove fresh PTY identity, cross-type MRU ordering, cleanup/PTY-exit suppression, parked-exit pinned confirmation, local-versus-runtime ownership, SSH cwd preservation, worktree rename/removal cleanup, and bounded history.
  • Gate: config/reliability-gates.jsonc entry terminal-session.explicit-close-retirement. The deterministic gate was rerun; the live parked-close E2E behavior is unchanged and retains the evidence already recorded by that gate on main.
  • Provider/platform coverage: local and SSH renderer-owned reopen paths are covered; Windows case-insensitive path remapping and POSIX separator behavior are covered deterministically. WSL shares the renderer-owned creation path but was not exercised live. Paired-runtime terminal reopen remains an accepted gap and safely declines local phantom creation.
  • Performance budget: terminal snapshots are capped at 10 and cross-type kinds at 30; bulk close caps allocation before constructing the kind list, with a Number.MAX_SAFE_INTEGER regression test. No polling, provider inventory scan, subprocess, timer, listener, renderer wake loop, or unbounded cache was added.
  • Diagnostics: existing terminal-retirement warnings report unroutable handles and provider teardown failures with tab identity and failure counts; deterministic gate/test failures identify the violated close/reopen stage without retaining terminal output.
  • Residual gaps: live Windows/Linux/WSL/SSH reopen was not rerun in this review; paired-runtime terminal reopen is still deferred as documented above.

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>
@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This 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 captureRecentlyClosed, and PTY-exit and other programmatic closes pass false so they are excluded from reopen tracking. Worktree rename, purge, and removal logic now remaps or evicts the new state. createNewTerminalTab was moved into a new module, and tests were updated for the new slice and close behavior.

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)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: Cmd/Ctrl+Shift+T now reopens closed tabs, including terminals.
Description check ✅ Passed The description is detailed and covers summary, validation, notes, and review findings, but it lacks the template's explicit Screenshots and Security Audit sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/renderer/src/store/slices/worktrees.ts (1)

1734-1756: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider path utilities instead of manual separator handling for the startupCwd remap.

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) that path.relative/path.isAbsolute would 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}: "Use path.join or 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

📥 Commits

Reviewing files that changed from the base of the PR and between e5008b1 and 5ccbf2d.

📒 Files selected for processing (23)
  • src/renderer/src/components/Terminal.tsx
  • src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
  • src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.tsx
  • src/renderer/src/components/right-sidebar/FileExplorer.tsx
  • src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx
  • src/renderer/src/components/terminal/terminal-tab-actions.test.ts
  • src/renderer/src/components/terminal/terminal-tab-actions.ts
  • src/renderer/src/components/terminal/terminal-tab-create.ts
  • src/renderer/src/lib/launch-agent-background-session.ts
  • src/renderer/src/lib/launch-agent-in-new-tab.ts
  • src/renderer/src/lib/launch-worktree-background-terminals.ts
  • src/renderer/src/store/index.ts
  • src/renderer/src/store/slices/browser.ts
  • src/renderer/src/store/slices/diffComments.test.ts
  • src/renderer/src/store/slices/editor.ts
  • src/renderer/src/store/slices/recently-closed-tabs.test.ts
  • src/renderer/src/store/slices/recently-closed-tabs.ts
  • src/renderer/src/store/slices/store-test-helpers.ts
  • src/renderer/src/store/slices/terminals.ts
  • src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts
  • src/renderer/src/store/slices/worktrees.test.ts
  • src/renderer/src/store/slices/worktrees.ts
  • src/renderer/src/store/types.ts

brennanb2025 and others added 8 commits July 5, 2026 12:09
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.
@brennanb2025
brennanb2025 merged commit 5739e18 into main Jul 15, 2026
1 check passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant