Wire terminal-initiated worktree navigation to back-and-forth stack (#6243)

* Unify terminal-driven worktree activation and Cmd+J recency marking.
* Record worktree visits in the back/forward history stack unless navigating history.
This commit is contained in:
Jinjing 2026-06-24 13:15:38 -07:00 committed by GitHub
parent a1ec8d25a7
commit 04ec8e6276
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 140 additions and 20 deletions

View File

@ -1286,6 +1286,8 @@ describe('useIpcEvents updater integration', () => {
const createTab = vi.fn(() => ({ id: 'tab-new' }))
const setActiveView = vi.fn()
const setActiveWorktree = vi.fn()
const markWorktreeVisited = vi.fn()
const recordWorktreeVisit = vi.fn()
const setActiveTabType = vi.fn()
const setActiveTab = vi.fn()
const revealWorktreeInSidebar = vi.fn()
@ -1300,13 +1302,17 @@ describe('useIpcEvents updater integration', () => {
const dispatchEvent = vi.fn()
const createFloatingWorkspaceTerminalTab = vi.fn()
const createWebRuntimeSessionTerminal = vi.fn().mockResolvedValue(false)
const focusRuntimeTerminalSurface = vi.fn(() => false)
const focusTerminalTabSurface = vi.fn()
let floatingPanelFocused = false
const storeState = {
setUpdateStatus: vi.fn(),
createTab,
setActiveView,
setActiveWorktree,
markWorktreeVisited: vi.fn(),
markWorktreeVisited,
recordWorktreeVisit,
isNavigatingHistory: false,
setActiveTabType,
setActiveTab,
revealWorktreeInSidebar,
@ -1386,6 +1392,18 @@ describe('useIpcEvents updater integration', () => {
}) => void)
| null
} = { current: null }
const focusTerminalListenerRef: {
current:
| ((data: {
tabId: string
worktreeId: string
leafId?: string | null
ackPaneKeyOnSuccess?: string
flashFocusedPane?: boolean
scrollToBottomIfOutputSinceLastView?: boolean
}) => void)
| null
} = { current: null }
const newTerminalTabListenerRef: { current: (() => void) | null } = { current: null }
vi.resetModules()
@ -1443,7 +1461,13 @@ describe('useIpcEvents updater integration', () => {
isWebRuntimeSessionActive: vi.fn(() => false)
}))
vi.doMock('@/lib/focus-terminal-tab-surface', () => ({
focusTerminalTabSurface: vi.fn()
focusTerminalTabSurface
}))
vi.doMock('@/runtime/sync-runtime-graph', () => ({
focusRuntimeTerminalSurface
}))
vi.doMock('@/lib/activate-tab-and-focus-pane', () => ({
activateTabAndFocusPane: vi.fn()
}))
vi.stubGlobal('window', {
@ -1514,7 +1538,19 @@ describe('useIpcEvents updater integration', () => {
replyTerminalCreate,
onSplitTerminal: () => () => {},
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onFocusTerminal: (
listener: (data: {
tabId: string
worktreeId: string
leafId?: string | null
ackPaneKeyOnSuccess?: string
flashFocusedPane?: boolean
scrollToBottomIfOutputSinceLastView?: boolean
}) => void
) => {
focusTerminalListenerRef.current = listener
return () => {}
},
onFocusEditorTab: () => () => {},
onCloseSessionTab: () => () => {},
onMoveSessionTab: () => () => {},
@ -1632,6 +1668,8 @@ describe('useIpcEvents updater integration', () => {
expect(setActiveView).toHaveBeenCalledWith('terminal')
expect(setActiveWorktree).toHaveBeenCalledWith('wt-2')
expect(markWorktreeVisited).toHaveBeenCalledWith('wt-2')
expect(recordWorktreeVisit).toHaveBeenCalledWith('wt-2')
expect(createTab).toHaveBeenCalledWith('wt-2')
expect(setActiveTabType).toHaveBeenCalledWith('terminal')
expect(setActiveTab).toHaveBeenCalledWith('tab-new')
@ -1648,6 +1686,42 @@ describe('useIpcEvents updater integration', () => {
createTab.mockClear()
setActiveView.mockClear()
setActiveWorktree.mockClear()
markWorktreeVisited.mockClear()
recordWorktreeVisit.mockClear()
setActiveTabType.mockClear()
setActiveTab.mockClear()
revealWorktreeInSidebar.mockClear()
setTabCustomTitle.mockClear()
queueTabStartupCommand.mockClear()
replyTerminalCreate.mockClear()
requestTerminalCreateListenerRef.current({
requestId: 'req-focused',
worktreeId: 'wt-3',
title: 'Shell'
})
expect(setActiveView).toHaveBeenCalledWith('terminal')
expect(setActiveWorktree).toHaveBeenCalledWith('wt-3')
expect(markWorktreeVisited).toHaveBeenCalledWith('wt-3')
expect(recordWorktreeVisit).toHaveBeenCalledWith('wt-3')
expect(createTab).toHaveBeenCalledWith('wt-3', undefined, undefined, undefined)
expect(setActiveTabType).toHaveBeenCalledWith('terminal')
expect(setActiveTab).toHaveBeenCalledWith('tab-new')
expect(revealWorktreeInSidebar).toHaveBeenCalledWith('wt-3')
expect(setTabCustomTitle).toHaveBeenCalledWith('tab-new', 'Shell', {
recordInteraction: false
})
expect(replyTerminalCreate).toHaveBeenCalledWith({
requestId: 'req-focused',
tabId: 'tab-new',
title: 'Shell'
})
createTab.mockClear()
setActiveView.mockClear()
setActiveWorktree.mockClear()
markWorktreeVisited.mockClear()
recordWorktreeVisit.mockClear()
setActiveTabType.mockClear()
setActiveTab.mockClear()
revealWorktreeInSidebar.mockClear()
@ -1673,6 +1747,8 @@ describe('useIpcEvents updater integration', () => {
})
expect(setActiveView).not.toHaveBeenCalled()
expect(setActiveWorktree).not.toHaveBeenCalled()
expect(markWorktreeVisited).not.toHaveBeenCalled()
expect(recordWorktreeVisit).not.toHaveBeenCalled()
expect(setActiveTabType).not.toHaveBeenCalled()
expect(setActiveTab).not.toHaveBeenCalled()
expect(revealWorktreeInSidebar).not.toHaveBeenCalled()
@ -1699,6 +1775,53 @@ describe('useIpcEvents updater integration', () => {
title: 'Codex'
})
if (typeof focusTerminalListenerRef.current !== 'function') {
throw new Error('Expected focus-terminal listener to be registered')
}
setActiveView.mockClear()
setActiveWorktree.mockClear()
markWorktreeVisited.mockClear()
recordWorktreeVisit.mockClear()
setActiveTab.mockClear()
revealWorktreeInSidebar.mockClear()
focusRuntimeTerminalSurface.mockClear()
focusTerminalTabSurface.mockClear()
focusTerminalListenerRef.current({
worktreeId: 'wt-4',
tabId: 'tab-focus',
leafId: 'leaf-focus'
})
expect(setActiveView).toHaveBeenCalledWith('terminal')
expect(setActiveWorktree).toHaveBeenCalledWith('wt-4')
expect(markWorktreeVisited).toHaveBeenCalledWith('wt-4')
expect(recordWorktreeVisit).toHaveBeenCalledWith('wt-4')
expect(setActiveTab).toHaveBeenCalledWith('tab-focus')
expect(revealWorktreeInSidebar).toHaveBeenCalledWith('wt-4')
expect(focusRuntimeTerminalSurface).toHaveBeenCalledWith('tab-focus', 'leaf-focus')
expect(focusTerminalTabSurface).toHaveBeenCalledWith('tab-focus', 'leaf-focus')
storeState.isNavigatingHistory = true
setActiveView.mockClear()
setActiveWorktree.mockClear()
markWorktreeVisited.mockClear()
recordWorktreeVisit.mockClear()
setActiveTab.mockClear()
revealWorktreeInSidebar.mockClear()
focusTerminalListenerRef.current({
worktreeId: 'wt-history',
tabId: 'tab-history'
})
expect(setActiveView).toHaveBeenCalledWith('terminal')
expect(setActiveWorktree).toHaveBeenCalledWith('wt-history')
expect(markWorktreeVisited).toHaveBeenCalledWith('wt-history')
expect(recordWorktreeVisit).not.toHaveBeenCalled()
expect(setActiveTab).toHaveBeenCalledWith('tab-history')
expect(revealWorktreeInSidebar).toHaveBeenCalledWith('wt-history')
storeState.isNavigatingHistory = false
createTab.mockClear()
registerAgentLaunchConfig.mockClear()
createTerminalListenerRef.current({

View File

@ -270,6 +270,17 @@ function resolveActiveBrowserPageId(state: AppState): string | null {
return workspace.activePageId ?? workspace.pageIds?.[0] ?? pages[0]?.id ?? null
}
function activateTerminalInitiatedWorktree(store: AppState, worktreeId: string): void {
store.setActiveView('terminal')
store.setActiveWorktree(worktreeId)
// Why: CLI/runtime terminal focus is user-visible worktree navigation, so it
// must feed both Cmd+J recency and the titlebar back/forward stack.
store.markWorktreeVisited(worktreeId)
if (!store.isNavigatingHistory) {
store.recordWorktreeVisit(worktreeId)
}
}
type TerminalSplitDirection = 'horizontal' | 'vertical'
function insertLeafAfterSource(
@ -1358,13 +1369,7 @@ export function useIpcEvents(): void {
const store = useAppStore.getState()
const shouldActivate = activate !== false
if (shouldActivate) {
store.setActiveView('terminal')
store.setActiveWorktree(worktreeId)
// Why: CLI-driven terminal focus is a user-initiated worktree switch
// and must stamp focus recency for Cmd+J. Doesn't route through
// activateAndRevealWorktree because it has custom terminal-creation
// logic; see docs/cmd-j-empty-query-ordering.md.
store.markWorktreeVisited(worktreeId)
activateTerminalInitiatedWorktree(store, worktreeId)
}
const existingTab = ptyId
? (store.tabsByWorktree[worktreeId] ?? []).find(
@ -1537,11 +1542,7 @@ export function useIpcEvents(): void {
}
const shouldActivate = data.activate !== false
if (shouldActivate) {
store.setActiveView('terminal')
store.setActiveWorktree(worktreeId)
// Why: CLI-driven focused terminal-create requests are user-initiated
// worktree switches; unfocused renderer-backed creates must not reorder Cmd+J.
store.markWorktreeVisited(worktreeId)
activateTerminalInitiatedWorktree(store, worktreeId)
} else {
// Why: renderer-backed Codex startup must mount a TerminalPane so the
// PTY is born in the renderer, but it must not switch the active UI.
@ -1650,11 +1651,7 @@ export function useIpcEvents(): void {
scrollToBottomIfOutputSinceLastView
}) => {
const store = useAppStore.getState()
store.setActiveWorktree(worktreeId)
// Why: CLI-driven focus is a user-initiated switch; stamp focus
// recency for Cmd+J. See docs/cmd-j-empty-query-ordering.md.
store.markWorktreeVisited(worktreeId)
store.setActiveView('terminal')
activateTerminalInitiatedWorktree(store, worktreeId)
store.setActiveTab(tabId)
store.revealWorktreeInSidebar(worktreeId)
if (ackPaneKeyOnSuccess || flashFocusedPane || scrollToBottomIfOutputSinceLastView) {