fix(terminal): defer startup mounts until hydration

This commit is contained in:
OrcaWin
2026-07-29 18:48:39 -07:00
parent db1e41ed98
commit 35f924cf68
4 changed files with 49 additions and 2 deletions
+1
View File
@@ -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"
]
+11 -2
View File
@@ -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 =
@@ -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()
@@ -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 {