fix(tabs): restore Cmd/Ctrl+Shift+T tab positions (#12236)

This commit is contained in:
Neil 2026-08-03 00:08:09 -07:00 committed by GitHub
parent d48cac7d0f
commit 18dbcf001f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 422 additions and 43 deletions

View File

@ -24,7 +24,12 @@ import {
} from '../../../../shared/workspace-session-browser-history'
import { pickNeighbor } from './tab-group-state'
import { destroyWorkspaceWebviews } from './browser-webview-cleanup'
import { pushRecentlyClosedTabKind } from './recently-closed-tabs'
import {
getRecentlyClosedTabPosition,
restoreRecentlyClosedTabPosition,
pushRecentlyClosedTabKind
} from './recently-closed-tabs'
import type { RecentlyClosedTabPosition } from './recently-closed-tabs'
import { callRuntimeRpc, type RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
import type {
@ -84,6 +89,7 @@ type BrowserTabPageState = {
type ClosedBrowserWorkspaceSnapshot = {
workspace: BrowserWorkspace
pages: BrowserPage[]
position?: RecentlyClosedTabPosition
}
function sanitizeBrowserPageAnnotation(annotation: BrowserPageAnnotation): BrowserPageAnnotation {
@ -771,8 +777,13 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
const nextRecentlyClosedBrowserTabsByWorktree = { ...s.recentlyClosedBrowserTabsByWorktree }
const existingSnapshots = nextRecentlyClosedBrowserTabsByWorktree[owningWorktreeId] ?? []
const position = getRecentlyClosedTabPosition(s, owningWorktreeId, tabId)
nextRecentlyClosedBrowserTabsByWorktree[owningWorktreeId] = [
{ workspace: closedWorkspace, pages: closedPages },
{
workspace: closedWorkspace,
pages: closedPages,
...(position ? { position } : {})
},
...existingSnapshots.filter((entry) => entry.workspace.id !== closedWorkspace.id)
].slice(0, 10)
const nextRecentlyClosedTabKindsByWorktree = pushRecentlyClosedTabKind(
@ -890,8 +901,10 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
title: snap.title,
activate: true,
sessionProfileId,
sessionPartition
sessionPartition,
targetGroupId: entryToRestore.position?.groupId
})
restoreRecentlyClosedTabPosition(get, worktreeId, restored.id, entryToRestore.position)
return get().browserTabsByWorktree[worktreeId]?.find((tab) => tab.id === restored.id) ?? null
}
@ -902,6 +915,7 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
activate: true,
sessionProfileId,
sessionPartition,
targetGroupId: entryToRestore.position?.groupId,
browserRuntimeEnvironmentId: firstPage.browserRuntimeEnvironmentId
})
@ -924,6 +938,8 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
}
}
restoreRecentlyClosedTabPosition(get, worktreeId, restored.id, entryToRestore.position)
return get().browserTabsByWorktree[worktreeId]?.find((tab) => tab.id === restored.id) ?? null
},

View File

@ -66,6 +66,13 @@ function createEditorTabsStore(): StoreApi<AppState> {
browserTabsByWorktree: {},
activeBrowserTabId: null,
activeBrowserTabIdByWorktree: {},
activeTabId: null,
activeTabIdByWorktree: {},
tabBarOrderByWorktree: {},
setTabBarOrder: (worktreeId: string, order: string[]) =>
args[0]((state: AppState) => ({
tabBarOrderByWorktree: { ...state.tabBarOrderByWorktree, [worktreeId]: order }
})),
repos: [{ id: 'repo-1', path: '/repo' }],
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1', path: '/repo' }] },
folderWorkspaces: [],
@ -663,7 +670,7 @@ describe('createEditorSlice openDiff', () => {
it('bumps fileContentReloadNonce when re-opening an existing clean file with reload requested', () => {
const store = createEditorStore()
const openFileWithReloadRequest = (): void =>
const openFileWithReloadRequest = (): void => {
store.getState().openFile(
{
filePath: '/repo/file.ts',
@ -674,6 +681,7 @@ describe('createEditorSlice openDiff', () => {
},
{ forceContentReload: true }
)
}
openFileWithReloadRequest()
expect(store.getState().openFiles[0]?.fileContentReloadNonce).toBeUndefined()
@ -869,7 +877,7 @@ describe('createEditorSlice openDiff', () => {
it('keeps an existing preview replaceable when it is opened as preview again', () => {
const store = createEditorTabsStore()
const openPreviewFile = (): void =>
const openPreviewFile = (): void => {
store.getState().openFile(
{
filePath: '/repo/a.ts',
@ -880,6 +888,7 @@ describe('createEditorSlice openDiff', () => {
},
{ preview: true }
)
}
openPreviewFile()
openPreviewFile()
@ -1624,6 +1633,85 @@ describe('createEditorSlice recently closed editor tabs', () => {
expect(store.getState().openFiles.at(-1)).toMatchObject({ filePath: '/repo/notes.md' })
expect(store.getState().openFiles.at(-1)).not.toHaveProperty('mirroredFromRuntimeSession')
})
it('restores the exact same-path owner instead of moving the local editor', () => {
const store = createEditorTabsStore()
const localId = store.getState().openFile({
filePath: '/repo/notes.md',
relativePath: 'notes.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'edit'
})
const remoteId = store.getState().openFile({
filePath: '/repo/notes.md',
relativePath: 'notes.md',
worktreeId: 'wt-1',
language: 'markdown',
runtimeEnvironmentId: 'env-1',
mode: 'edit'
})
store.getState().setTabBarOrder('wt-1', [localId, remoteId])
store.getState().closeFile(remoteId)
expect(store.getState().reopenClosedEditorTab('wt-1')).toBe(true)
const openIds = store.getState().openFiles.map((file) => file.id)
expect(openIds).toEqual([localId, remoteId])
expect(store.getState().tabBarOrderByWorktree['wt-1']).toEqual([localId, remoteId])
})
it('keeps same-path edit and diff tabs as separate entities on reopen', () => {
const store = createEditorTabsStore()
const editId = store.getState().openFile({
filePath: '/repo/notes.md',
relativePath: 'notes.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'edit'
})
store.getState().openDiff('wt-1', '/repo/notes.md', 'notes.md', 'markdown', false)
const diffId = 'wt-1::diff::unstaged::notes.md'
store.getState().setTabBarOrder('wt-1', [editId, diffId])
store.getState().closeFile(diffId)
expect(store.getState().reopenClosedEditorTab('wt-1')).toBe(true)
expect(store.getState().openFiles.map((file) => file.id)).toEqual([editId, diffId])
expect(store.getState().tabBarOrderByWorktree['wt-1']).toEqual([editId, diffId])
})
it('keeps close-all editor snapshots positioned for reopen', () => {
const store = createEditorTabsStore()
const firstId = store.getState().openFile({
filePath: '/repo/first.md',
relativePath: 'first.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'edit'
})
const middleId = store.getState().openFile({
filePath: '/repo/middle.md',
relativePath: 'middle.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'edit'
})
const lastId = store.getState().openFile({
filePath: '/repo/last.md',
relativePath: 'last.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'edit'
})
store.getState().setTabBarOrder('wt-1', [firstId, middleId, lastId])
store.getState().closeAllFiles()
expect(store.getState().reopenClosedEditorTab('wt-1')).toBe(true)
expect(store.getState().openFiles[0]?.id).toBe(firstId)
expect(store.getState().tabBarOrderByWorktree['wt-1']).toEqual([firstId])
})
})
describe('createEditorSlice markdown view state', () => {
@ -4941,7 +5029,7 @@ describe('closeFile host mirroring', () => {
describe('read-only editor tabs (AI Vault View Log)', () => {
const LOG_PATH = '/home/user/.claude/sessions/log.jsonl'
const openReadOnlyLog = (store: StoreApi<AppState>): void =>
const openReadOnlyLog = (store: StoreApi<AppState>): void => {
store.getState().openFile(
{
filePath: LOG_PATH,
@ -4955,6 +5043,7 @@ describe('read-only editor tabs (AI Vault View Log)', () => {
},
{ preview: false, forceContentReload: true, suppressActiveRuntimeFallback: true }
)
}
it('creates a permanent read-only edit tab', () => {
const store = createEditorStore()

View File

@ -1,7 +1,12 @@
/* eslint-disable max-lines */
import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import { pushRecentlyClosedTabKind } from './recently-closed-tabs'
import {
getRecentlyClosedTabPosition,
restoreRecentlyClosedTabPosition,
pushRecentlyClosedTabKind
} from './recently-closed-tabs'
import type { RecentlyClosedTabPosition } from './recently-closed-tabs'
import { joinPath } from '@/lib/path'
import { toast } from 'sonner'
import { isPathInsideOrEqual } from '../../../../shared/cross-platform-path'
@ -276,12 +281,15 @@ export type MarkdownViewMode = 'source' | 'rich' | 'preview'
// Why: orthogonal to MarkdownViewMode; 'changes' renders diff-vs-HEAD in place of the editor without a separate tab. See reviews/changes-view-mode-plan.md.
export type EditorViewMode = 'edit' | 'changes'
/** Enough state to restore a tab via `openFile` after `closeFile` (id is always filePath). */
/** Enough state to restore a tab via `openFile` after `closeFile`. */
// Why: omit mirroredFromRuntimeSession so a user-reopened tab isn't treated as host-owned and culled by the next web session sync.
export type ClosedEditorTabSnapshot = Omit<
OpenFile,
'id' | 'isDirty' | 'mirroredFromRuntimeSession'
>
> & {
reopenId?: string
position?: RecentlyClosedTabPosition
}
const MAX_RECENT_CLOSED_EDITOR_TABS = 10
@ -464,8 +472,9 @@ export type EditorSlice = {
suppressActiveRuntimeFallback?: boolean
forceContentReload?: boolean
focusEditor?: boolean
reopenId?: string
}
) => void
) => string
openNewMarkdownInActiveWorkspace: (groupId: string) => Promise<void>
// Why: sequences openFile/setMarkdownViewMode/reveal around an async Monaco remount. See docs/markdown-internal-link-opening-design.md.
activateMarkdownLink: (
@ -1668,13 +1677,16 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
matchesEditorMode(f, reusableOpenFileModes) &&
isSameEditorOwner(f, worktreeId, runtimeEnvironmentId)
)
const id = resolveEditorFileIdForOwner(
s,
file.filePath,
worktreeId,
runtimeEnvironmentId,
reusableOpenFileModes
)
const id =
options?.reopenId && !s.openFiles.some((candidate) => candidate.id === options.reopenId)
? options.reopenId
: resolveEditorFileIdForOwner(
s,
file.filePath,
worktreeId,
runtimeEnvironmentId,
reusableOpenFileModes
)
editorItemFileId = id
const isPreview = options?.preview ?? false
const recordReplacedPreview = options?.recordReplacedPreview ?? false
@ -1799,12 +1811,17 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
...snap
} = replacedPreview
const stack = s.recentlyClosedEditorTabsByWorktree[worktreeId] ?? []
const position = getRecentlyClosedTabPosition(s, worktreeId, replacedPreview.id)
nextRecentlyClosed = {
...s.recentlyClosedEditorTabsByWorktree,
[worktreeId]: [snap as ClosedEditorTabSnapshot, ...stack].slice(
0,
MAX_RECENT_CLOSED_EDITOR_TABS
)
[worktreeId]: [
{
...(snap as ClosedEditorTabSnapshot),
reopenId: replacedPreview.id,
...(position ? { position } : {})
},
...stack
].slice(0, MAX_RECENT_CLOSED_EDITOR_TABS)
}
nextRecentlyClosedKinds = pushRecentlyClosedTabKind(
s.recentlyClosedTabKindsByWorktree,
@ -1886,6 +1903,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
}
})
}
return editorItemFileId
},
openNewMarkdownInActiveWorkspace: async (groupId) => {
@ -2188,12 +2206,17 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
...snap
} = closedFile
const stack = s.recentlyClosedEditorTabsByWorktree[wtRecent] ?? []
const position = getRecentlyClosedTabPosition(s, wtRecent, fileId)
nextRecentlyClosed = {
...s.recentlyClosedEditorTabsByWorktree,
[wtRecent]: [snap as ClosedEditorTabSnapshot, ...stack].slice(
0,
MAX_RECENT_CLOSED_EDITOR_TABS
)
[wtRecent]: [
{
...(snap as ClosedEditorTabSnapshot),
reopenId: fileId,
...(position ? { position } : {})
},
...stack
].slice(0, MAX_RECENT_CLOSED_EDITOR_TABS)
}
nextRecentlyClosedKinds = pushRecentlyClosedTabKind(
s.recentlyClosedTabKindsByWorktree,
@ -2264,7 +2287,12 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
[worktreeId]: (s.recentlyClosedEditorTabsByWorktree[worktreeId] ?? []).slice(1)
}
}))
get().openFile(next)
const { position, reopenId, ...file } = next
const restoredFileId = get().openFile(file, {
targetGroupId: position?.groupId,
reopenId
})
restoreRecentlyClosedTabPosition(get, worktreeId, restoredFileId, position)
return true
},
@ -2375,10 +2403,15 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
continue
}
const { id: _id, isDirty: _dirty, mirroredFromRuntimeSession: _mirrored, ...snap } = f
nextRecentClosed = [snap as ClosedEditorTabSnapshot, ...nextRecentClosed].slice(
0,
MAX_RECENT_CLOSED_EDITOR_TABS
)
const position = getRecentlyClosedTabPosition(s, activeWorktreeId, f.id)
nextRecentClosed = [
{
...(snap as ClosedEditorTabSnapshot),
reopenId: f.id,
...(position ? { position } : {})
},
...nextRecentClosed
].slice(0, MAX_RECENT_CLOSED_EDITOR_TABS)
capturedCloseCount += 1
}

View File

@ -34,10 +34,18 @@ const mockApi = {
// @ts-expect-error -- minimal window.api stub for the store under test
globalThis.window = { api: mockApi }
import { createTestStore, seedStore, makeWorktree, makeOpenFile } from './store-test-helpers'
import {
createTestStore,
seedStore,
makeWorktree,
makeOpenFile,
makeTabGroup,
makeUnifiedTab
} from './store-test-helpers'
import {
pushRecentlyClosedTabKind,
remapClosedTerminalTabSnapshotCwds
remapClosedTerminalTabSnapshotCwds,
restoreRecentlyClosedTabPosition
} from './recently-closed-tabs'
const WT = 'repo1::/path/wt1'
@ -69,7 +77,12 @@ describe('terminal recently-closed capture', () => {
store.getState().closeTab(tab.id)
expect(store.getState().recentlyClosedTerminalTabsByWorktree[WT]).toEqual([
{ startupCwd: '/path/wt1/packages/app', customTitle: 'build shell', color: '#ff0000' }
expect.objectContaining({
startupCwd: '/path/wt1/packages/app',
customTitle: 'build shell',
color: '#ff0000',
position: expect.objectContaining({ groupIndex: 0 })
})
])
expect(store.getState().recentlyClosedTabKindsByWorktree[WT]).toEqual(['terminal'])
})
@ -140,6 +153,30 @@ describe('terminal snapshot cwd remapping', () => {
})
describe('reopenClosedTerminalTab', () => {
it('restores the tab bar and group position of a closed middle tab', () => {
const store = makeSeededStore()
const first = store.getState().createTab(WT)
const middle = store.getState().createTab(WT)
const last = store.getState().createTab(WT)
store.getState().setTabBarOrder(WT, [first.id, middle.id, last.id])
store.getState().closeTab(middle.id)
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(true)
const restored = store
.getState()
.tabsByWorktree[WT]?.find((tab) => tab.id !== first.id && tab.id !== last.id)
expect(restored).toBeDefined()
expect(store.getState().tabBarOrderByWorktree[WT]).toEqual([first.id, restored!.id, last.id])
const group = store.getState().groupsByWorktree[WT]?.[0]
expect(
group?.tabOrder.map(
(id) => store.getState().unifiedTabsByWorktree[WT]?.find((tab) => tab.id === id)?.entityId
)
).toEqual([first.id, restored!.id, last.id])
})
it('recreates a fresh terminal with the snapshot cwd, shell, title, and color', () => {
const store = makeSeededStore()
const tab = store
@ -232,6 +269,98 @@ describe('reopenClosedTerminalTab', () => {
})
})
describe('restoreRecentlyClosedTabPosition', () => {
it('selects the unified tab in the captured group when an entity is shared', () => {
const reordered = vi.fn()
const otherTab = makeUnifiedTab({
id: 'other-tab',
entityId: 'shared-file',
groupId: 'other-group',
worktreeId: WT,
contentType: 'editor'
})
const capturedTab = makeUnifiedTab({
id: 'captured-tab',
entityId: 'shared-file',
groupId: 'captured-group',
worktreeId: WT,
contentType: 'editor'
})
const capturedSibling = makeUnifiedTab({
id: 'captured-sibling',
entityId: 'sibling-file',
groupId: 'captured-group',
worktreeId: WT,
contentType: 'editor'
})
const state = {
tabBarOrderByWorktree: { [WT]: ['shared-file'] },
groupsByWorktree: {
[WT]: [
makeTabGroup({
id: 'other-group',
worktreeId: WT,
activeTabId: 'other-tab',
tabOrder: ['other-tab']
}),
makeTabGroup({
id: 'captured-group',
worktreeId: WT,
activeTabId: 'captured-sibling',
tabOrder: ['captured-sibling', 'captured-tab']
})
]
},
unifiedTabsByWorktree: { [WT]: [otherTab, capturedTab, capturedSibling] }
}
restoreRecentlyClosedTabPosition(
() => ({
...state,
setTabBarOrder: vi.fn(),
reorderUnifiedTabs: reordered
}),
WT,
'shared-file',
{ groupId: 'captured-group', groupIndex: 0 }
)
expect(reordered).toHaveBeenCalledWith('captured-group', ['captured-tab', 'captured-sibling'], {
recordInteraction: false
})
})
it('does not reuse a stale flat index after group drag reordering', () => {
const store = makeSeededStore()
const first = store.getState().createTab(WT)
const middle = store.getState().createTab(WT)
const last = store.getState().createTab(WT)
const groupId = store.getState().groupsByWorktree[WT]?.[0]?.id
if (!groupId) {
throw new Error('Expected a root tab group')
}
store.getState().setTabBarOrder(WT, [first.id, middle.id, last.id])
store.getState().reorderUnifiedTabs(groupId, [first.id, last.id, middle.id])
store.getState().closeTab(middle.id)
expect(store.getState().recentlyClosedTerminalTabsByWorktree[WT]?.[0]?.position).toEqual({
groupId,
groupIndex: 2
})
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(true)
const restored = store
.getState()
.tabsByWorktree[WT]?.find((tab) => tab.id !== first.id && tab.id !== last.id)
expect(store.getState().tabBarOrderByWorktree[WT]).toEqual([first.id, last.id, restored?.id])
expect(store.getState().groupsByWorktree[WT]?.[0]?.tabOrder).toEqual([
first.id,
last.id,
restored?.id
])
})
})
describe('reopenClosedTab cross-type MRU', () => {
it('reopens closed tabs of mixed kinds in most-recent-first order', () => {
const store = makeSeededStore()

View File

@ -6,6 +6,114 @@ import {
relativePathInsideRoot
} from '../../../../shared/cross-platform-path'
export type RecentlyClosedTabPosition = {
tabBarIndex?: number
groupId?: string
groupIndex?: number
}
export function getRecentlyClosedTabPosition(
state: Pick<AppState, 'tabBarOrderByWorktree' | 'groupsByWorktree' | 'unifiedTabsByWorktree'>,
worktreeId: string,
entityId: string
): RecentlyClosedTabPosition | undefined {
const tabBarOrder = state.tabBarOrderByWorktree?.[worktreeId]
const unifiedTabs = state.unifiedTabsByWorktree?.[worktreeId] ?? []
const tabBarIndex = tabBarOrder?.indexOf(entityId) ?? -1
const unifiedTab = unifiedTabs.find((tab) => tab.entityId === entityId)
const group = unifiedTab
? (state.groupsByWorktree?.[worktreeId] ?? []).find(
(candidate) => candidate.id === unifiedTab.groupId
)
: undefined
const groupIndex = group?.tabOrder.indexOf(unifiedTab?.id ?? '') ?? -1
const groupTabEntityIds = group
? group.tabOrder.map((tabId) => unifiedTabs.find((tab) => tab.id === tabId)?.entityId)
: []
const tabBarGroupEntityIds = group
? (tabBarOrder ?? [])
.map((tabId) => unifiedTabs.find((tab) => tab.entityId === tabId))
.filter((tab) => tab?.groupId === group.id)
.map((tab) => tab?.entityId)
: []
const groupOrderMatchesTabBar =
!group ||
(groupTabEntityIds.length === tabBarGroupEntityIds.length &&
groupTabEntityIds.every((entityId, index) => entityId === tabBarGroupEntityIds[index]))
if (tabBarIndex < 0 && (!group || groupIndex < 0)) {
return undefined
}
return {
...(tabBarIndex >= 0 && groupOrderMatchesTabBar ? { tabBarIndex } : {}),
...(group && groupIndex >= 0 ? { groupId: group.id, groupIndex } : {})
}
}
export function insertTabAtRecentlyClosedPosition(
order: readonly string[],
tabId: string,
position?: RecentlyClosedTabPosition
): string[] {
const nextOrder = order.filter((id) => id !== tabId)
const index = position?.tabBarIndex
if (index === undefined) {
return [...nextOrder, tabId]
}
nextOrder.splice(Math.min(Math.max(index, 0), nextOrder.length), 0, tabId)
return nextOrder
}
export function restoreRecentlyClosedTabPosition(
getState: () => Pick<
AppState,
| 'tabBarOrderByWorktree'
| 'groupsByWorktree'
| 'unifiedTabsByWorktree'
| 'setTabBarOrder'
| 'reorderUnifiedTabs'
>,
worktreeId: string,
entityId: string,
position?: RecentlyClosedTabPosition
): void {
if (!position) {
return
}
const state = getState()
const order = state.tabBarOrderByWorktree?.[worktreeId]
if (order && typeof state.setTabBarOrder === 'function') {
state.setTabBarOrder(worktreeId, insertTabAtRecentlyClosedPosition(order, entityId, position))
}
if (position?.groupIndex === undefined) {
return
}
const unifiedTab = (getState().unifiedTabsByWorktree?.[worktreeId] ?? []).find(
(candidate) =>
candidate.entityId === entityId &&
(position.groupId === undefined || candidate.groupId === position.groupId)
)
if (!unifiedTab) {
return
}
const group = (getState().groupsByWorktree?.[worktreeId] ?? []).find(
(candidate) => candidate.id === unifiedTab.groupId
)
if (!group) {
return
}
if (typeof getState().reorderUnifiedTabs === 'function') {
getState().reorderUnifiedTabs(
group.id,
insertTabAtRecentlyClosedPosition(group.tabOrder, unifiedTab.id, {
tabBarIndex: position.groupIndex
}),
{ recordInteraction: false }
)
}
}
/** Snapshot of a terminal tab captured at user-initiated close time. Reopen
* recreates a fresh shell in the same startup directory (Ghostty semantics)
* never the old PTY, scrollback, or a relaunched agent session. */
@ -14,6 +122,7 @@ export type ClosedTerminalTabSnapshot = {
shellOverride?: string
customTitle?: string
color?: string
position?: RecentlyClosedTabPosition
}
export type RecentlyClosedTabKind = 'terminal' | 'browser' | 'editor'
@ -135,7 +244,7 @@ export const createRecentlyClosedTabsSlice: StateCreator<
return false
}
const tab = get().createTab(worktreeId, undefined, snapshot.shellOverride, {
const tab = get().createTab(worktreeId, snapshot.position?.groupId, snapshot.shellOverride, {
...(snapshot.startupCwd ? { startupCwd: snapshot.startupCwd } : {}),
activate: true
})
@ -146,13 +255,7 @@ export const createRecentlyClosedTabsSlice: StateCreator<
get().setTabColor(tab.id, snapshot.color)
}
get().setActiveTabType('terminal')
// Why: with a stored order the strip appends unknown ids last already, but
// an explicit append keeps the reopened tab at the end even after future
// reorders write the stored order back.
const order = get().tabBarOrderByWorktree[worktreeId]
if (order && !order.includes(tab.id)) {
get().setTabBarOrder(worktreeId, [...order, tab.id])
}
restoreRecentlyClosedTabPosition(get, worktreeId, tab.id, snapshot.position)
return true
},

View File

@ -56,7 +56,11 @@ import { forgetAgentHibernationTabOutput } from '@/lib/agent-hibernation-output-
import { forgetForegroundTerminalTabs } from '@/lib/foreground-terminal-tabs'
import { forgetAgentStartupDeliveriesForTabs } from '@/lib/agent-startup-delivery-guards'
import { clearTransientTerminalState, emptyLayoutSnapshot } from './terminal-helpers'
import { pushClosedTerminalTabSnapshot, pushRecentlyClosedTabKind } from './recently-closed-tabs'
import {
getRecentlyClosedTabPosition,
pushClosedTerminalTabSnapshot,
pushRecentlyClosedTabKind
} from './recently-closed-tabs'
import { isClaudeAgent } from '@/lib/agent-status'
import { recordTerminalInputActivity } from '@/lib/terminal-input-activity-coalescing'
import { classifyTitleActivity } from '@/lib/pane-agent-evidence'
@ -1651,6 +1655,10 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
}
}
// Why: only explicit user closes feed the Cmd+Shift+T reopen stack; cleanup/PTY-exit closes must not pollute undo history.
const closedPosition =
closedWorktreeId && closedTab
? getRecentlyClosedTabPosition(s, closedWorktreeId, closedTab.id)
: undefined
const capturedSnapshot =
closeReason === 'user' &&
opts?.captureRecentlyClosed !== false &&
@ -1660,7 +1668,8 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
...(closedTab.startupCwd ? { startupCwd: closedTab.startupCwd } : {}),
...(closedTab.shellOverride ? { shellOverride: closedTab.shellOverride } : {}),
...(closedTab.customTitle ? { customTitle: closedTab.customTitle } : {}),
...(closedTab.color ? { color: closedTab.color } : {})
...(closedTab.color ? { color: closedTab.color } : {}),
...(closedPosition ? { position: closedPosition } : {})
}
: null
const nextExpanded = { ...s.expandedPaneByTabId }