diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 16706a7f908..890366dcb3e 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -4270,6 +4270,7 @@ { "file": "src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts", "assertions": [ + "startup waits for hydration before mounting terminal panes while degraded mode remains interactive", "only an explicit local execution host can defer cold activation", "SSH, remote-runtime, and unresolved owners remain eager" ] diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index ddbe6249dc1..5f9c333eaa3 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -69,6 +69,7 @@ import { scheduleBackgroundTerminalWorktreeMeasure } from './terminal/background import { applyBackgroundMountTabRestriction, canDeferColdActivationTabsForHost, + canMountTerminalWorkspaceForStartup, planColdActivationTabDeferral, pruneClosedBackgroundMountTabs, revealActivationDeferredTabs, @@ -327,6 +328,7 @@ function Terminal(): React.JSX.Element | null { const expandedPaneByTabId = useAppStore((s) => s.expandedPaneByTabId) const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady) const hydrationSucceeded = useAppStore((s) => s.hydrationSucceeded) + const startupWorktreeRefreshCompleted = useAppStore((s) => s.startupWorktreeRefreshCompleted) const openFiles = useAppStore((s) => s.openFiles) const activeFileId = useAppStore((s) => s.activeFileId) const activeBrowserTabId = useAppStore((s) => s.activeBrowserTabId) @@ -1152,8 +1154,15 @@ function Terminal(): React.JSX.Element | null { terminalSshParkingEnabled, workspaceSurfaces ]) - // Why: gate on workspaceSessionReady so TerminalPane doesn't mount and spawn a duplicate PTY before reconnectPersistedTerminals() finishes. - if (renderedActiveWorktreeId && workspaceSessionReady) { + // Why: a slow post-reconnect step exposes workspaceSessionReady before hydration can populate snapshot capabilities. + if ( + renderedActiveWorktreeId && + canMountTerminalWorkspaceForStartup({ + workspaceSessionReady, + hydrationSucceeded, + startupWorktreeRefreshCompleted + }) + ) { // Why: mounting every saved tab at once (scrollback replay + WebGL + sync-IPC snapshot per pane) freezes the renderer, so hidden tabs defer and mount on first reveal. const worktreeTabs = tabsByWorktree[renderedActiveWorktreeId] ?? [] const coldActivationDeferralEnabled = diff --git a/src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts b/src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts index 85209b8c633..f98e39cc355 100644 --- a/src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts +++ b/src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts @@ -11,6 +11,7 @@ import { addBackgroundMountedTerminalWorktree, applyBackgroundMountTabRestriction, canDeferColdActivationTabsForHost, + canMountTerminalWorkspaceForStartup, collectDeferredMountTabIds, hasRequestedBackgroundTerminalWorktreeMount, planColdActivationTabDeferral, @@ -22,6 +23,32 @@ import { shouldMountBackgroundWorktreeTab } from './background-terminal-worktree-mount' +describe('terminal workspace startup mount gate', () => { + it('waits for hydration unless startup entered degraded mode', () => { + expect( + canMountTerminalWorkspaceForStartup({ + workspaceSessionReady: true, + hydrationSucceeded: false, + startupWorktreeRefreshCompleted: false + }) + ).toBe(false) + expect( + canMountTerminalWorkspaceForStartup({ + workspaceSessionReady: true, + hydrationSucceeded: true, + startupWorktreeRefreshCompleted: false + }) + ).toBe(true) + expect( + canMountTerminalWorkspaceForStartup({ + workspaceSessionReady: true, + hydrationSucceeded: false, + startupWorktreeRefreshCompleted: true + }) + ).toBe(true) + }) +}) + describe('background terminal mount request registry', () => { it('replays a request made before the Terminal listener mounts', () => { takeAllPendingBackgroundTerminalWorktreeMounts() diff --git a/src/renderer/src/components/terminal/background-terminal-worktree-mount.ts b/src/renderer/src/components/terminal/background-terminal-worktree-mount.ts index f0428baa06f..ae8c677c293 100644 --- a/src/renderer/src/components/terminal/background-terminal-worktree-mount.ts +++ b/src/renderer/src/components/terminal/background-terminal-worktree-mount.ts @@ -141,6 +141,16 @@ export function shouldMountBackgroundWorktreeTab( // until first reveal, parked byte watchers own their side effects meanwhile. export const COLD_ACTIVATION_TAB_DEFER_THRESHOLD = 4 +export function canMountTerminalWorkspaceForStartup(args: { + workspaceSessionReady: boolean + hydrationSucceeded: boolean + startupWorktreeRefreshCompleted: boolean +}): boolean { + return ( + args.workspaceSessionReady && (args.hydrationSucceeded || args.startupWorktreeRefreshCompleted) + ) +} + export function canDeferColdActivationTabsForHost(args: { executionHostId: string | null }): boolean {