fix(terminal): stop worktree park title feedback loop

This commit is contained in:
Neil
2026-08-09 19:10:51 -07:00
parent a3ca887a3c
commit 56df57e39e
3 changed files with 266 additions and 1 deletions
@@ -0,0 +1,231 @@
/** @vitest-environment happy-dom */
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Tab, TabGroup, TerminalTab } from '../../../../shared/types'
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
const harness = vi.hoisted(() => ({
worktreeId: 'repo::/cold-park-pre-gate',
coverageCalls: 0,
syncCalls: 0,
slotRenders: 0,
renderedParkedSets: [] as string[][]
}))
vi.mock('../../store', async () => {
const { create } = await import('zustand')
const useAppStore = create(() => ({
activeGroupIdByWorktree: {} as Record<string, string | undefined>,
groupsByWorktree: {} as Record<string, TabGroup[]>,
pendingStartupByTabId: {} as Record<string, unknown>,
runtimeStatusByEnvironmentId: new Map<string, unknown>(),
settings: {} as Record<string, unknown>,
sleepingAgentSessionsByPaneKey: {} as Record<string, unknown>,
tabsByWorktree: {} as Record<string, TerminalTab[]>,
terminalLayoutsByTabId: {} as Record<string, unknown>,
unifiedTabsByWorktree: {} as Record<string, Tab[]>,
consumeSuppressedPtyExit: () => false,
focusGroup: () => {},
reconcileWorktreeTabModel: () => ({ renderableTabCount: 2 }),
setActiveWorktree: () => {}
}))
return { useAppStore }
})
vi.mock('../native-chat/use-native-chat-toggle-shortcut', () => ({
useNativeChatToggleShortcut: () => {}
}))
vi.mock('./TerminalOverlaySlot', () => ({
TerminalOverlaySlot: () => {
harness.slotRenders += 1
return null
}
}))
vi.mock('./terminal-parked-tab-watchers', async () => {
const { useAppStore } = await import('../../store')
return {
canWatcherCoverParkedTerminalTab: () => {
harness.coverageCalls += 1
return harness.coverageCalls % 2 === 1
},
disposeParkedTerminalWatchersForWorktree: () => {},
syncParkedTerminalTabWatchers: (args: {
worktreeId: string
parkedTabIds: ReadonlySet<string>
}) => {
harness.syncCalls += 1
harness.renderedParkedSets.push([...args.parkedTabIds].sort())
if (args.parkedTabIds.size === 0) {
return
}
;(
useAppStore as unknown as {
setState: (update: (state: ParkingStoreState) => Partial<ParkingStoreState>) => void
}
).setState((state) => ({
tabsByWorktree: {
...state.tabsByWorktree,
[args.worktreeId]: state.tabsByWorktree[args.worktreeId].map((tab) => ({
...tab,
title: `${tab.id}-${harness.syncCalls}`
}))
}
}))
}
}
})
vi.mock('./terminal-parking-e2e-overrides', () => ({
getTerminalParkingPolicyOverrides: () => ({ coldParkDelayMs: 0, hotRetainMs: 0 })
}))
vi.mock('@/lib/crash-breadcrumb-recorder', () => ({
recordRendererCrashBreadcrumb: vi.fn()
}))
import { useAppStore } from '../../store'
import TerminalPaneOverlayLayer from './TerminalPaneOverlayLayer'
const TAB_IDS = ['tab-a', 'tab-b'] as const
const GROUP_ID = 'group-a'
type ParkingStoreState = {
tabsByWorktree: Record<string, TerminalTab[]>
}
const parkingStore = useAppStore as unknown as {
getState: () => ParkingStoreState
setState: (partial: Record<string, unknown>) => void
}
function terminalTab(id: string): TerminalTab {
return {
id,
worktreeId: harness.worktreeId,
ptyId: `${harness.worktreeId}@@session-${id}`,
title: id,
generation: 0
} as TerminalTab
}
function unifiedTerminalTab(id: string): Tab {
return {
id: `unified-${id}`,
entityId: id,
worktreeId: harness.worktreeId,
groupId: GROUP_ID,
contentType: 'terminal',
label: id,
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}
}
function renderProductionLayer(root: Root, coldParkTerminalPanes = true): unknown {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
let thrown: unknown = null
try {
act(() => {
root.render(
<TerminalPaneOverlayLayer
worktreeId={harness.worktreeId}
worktreePath="cold-park-pre-gate"
isWorktreeActive={false}
coldParkTerminalPanes={coldParkTerminalPanes}
/>
)
})
} catch (error) {
thrown = error
}
consoleError.mockRestore()
return thrown
}
describe('TerminalPaneOverlayLayer cold-park pre-gate loop', () => {
let container: HTMLDivElement
let root: Root | undefined
beforeEach(() => {
harness.coverageCalls = 0
harness.syncCalls = 0
harness.slotRenders = 0
harness.renderedParkedSets.length = 0
const tabs = TAB_IDS.map(terminalTab)
const unifiedTabs = TAB_IDS.map(unifiedTerminalTab)
parkingStore.setState({
activeGroupIdByWorktree: { [harness.worktreeId]: GROUP_ID },
groupsByWorktree: {
[harness.worktreeId]: [
{
id: GROUP_ID,
worktreeId: harness.worktreeId,
activeTabId: unifiedTabs[0].id,
tabOrder: unifiedTabs.map((tab) => tab.id),
recentTabIds: [unifiedTabs[0].id]
}
]
},
tabsByWorktree: { [harness.worktreeId]: tabs },
unifiedTabsByWorktree: { [harness.worktreeId]: unifiedTabs }
})
container = document.createElement('div')
document.body.appendChild(container)
})
afterEach(() => {
try {
act(() => root?.unmount())
} catch {
// A failed initial commit leaves no mounted tree to clean up.
}
root = undefined
container.remove()
})
it('settles title publications without suppressing PTY rechecks', () => {
root = createRoot(container)
const thrown = renderProductionLayer(root)
expect(thrown).toBeNull()
expect(harness.coverageCalls).toBe(1)
expect(harness.syncCalls).toBeLessThan(10)
expect(harness.renderedParkedSets).not.toHaveLength(0)
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
new Set([TAB_IDS.join(',')])
)
expect(harness.slotRenders).toBe(0)
const settledSyncCalls = harness.syncCalls
const state = parkingStore.getState()
act(() => {
parkingStore.setState({
tabsByWorktree: {
...state.tabsByWorktree,
[harness.worktreeId]: state.tabsByWorktree[harness.worktreeId].map((tab) =>
tab.id === TAB_IDS[1] ? { ...tab, ptyId: `${tab.ptyId}-replacement` } : tab
)
}
})
})
expect(harness.coverageCalls).toBe(2)
expect(harness.syncCalls).toBeGreaterThan(settledSyncCalls)
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
new Set([TAB_IDS.join(',')])
)
expect(harness.slotRenders).toBe(0)
expect(renderProductionLayer(root, false)).toBeNull()
expect(harness.slotRenders).toBeGreaterThan(0)
expect(renderProductionLayer(root)).toBeNull()
expect(harness.renderedParkedSets.at(-1)).toEqual([...TAB_IDS])
})
})
@@ -36,6 +36,7 @@ import {
disposeParkedTerminalWatchersForWorktree,
syncParkedTerminalTabWatchers
} from './terminal-parked-tab-watchers'
import { useWorktreeParkTerminalTabs } from './use-worktree-park-terminal-tabs'
type TerminalOverlayTabAssignment = {
groupId: string
@@ -77,7 +78,7 @@ export function useTerminalTabColdParking(args: {
}): ReadonlySet<string> {
const {
worktreeId,
terminalTabs,
terminalTabs: publishedTerminalTabs,
assignments,
isWorktreeActive,
coldParkTerminalPanes,
@@ -86,6 +87,7 @@ export function useTerminalTabColdParking(args: {
activityTerminalPortals,
activationDeferredMountTabIds
} = args
const terminalTabs = useWorktreeParkTerminalTabs(publishedTerminalTabs, coldParkTerminalPanes)
const pendingStartupByTabId = useAppStore((state) => state.pendingStartupByTabId)
const terminalParkingEnabled = useAppStore(
(state) => state.settings?.terminalHiddenViewParking !== false
@@ -0,0 +1,32 @@
import { useRef } from 'react'
import type { TerminalTab } from '../../../../shared/types'
function haveSameTerminalParkingInputs(
left: readonly TerminalTab[],
right: readonly TerminalTab[]
): boolean {
return (
left.length === right.length &&
left.every(
(tab, index) =>
tab.id === right[index].id &&
tab.ptyId === right[index].ptyId &&
tab.pendingActivationSpawn === right[index].pendingActivationSpawn
)
)
}
export function useWorktreeParkTerminalTabs(
terminalTabs: readonly TerminalTab[],
coldParkTerminalPanes: boolean
): readonly TerminalTab[] {
const stableTabsRef = useRef(terminalTabs)
// Why: a dominant worktree park hides pre-gate churn from final-verdict damping.
if (
!coldParkTerminalPanes ||
!haveSameTerminalParkingInputs(stableTabsRef.current, terminalTabs)
) {
stableTabsRef.current = terminalTabs
}
return stableTabsRef.current
}