diff --git a/src/resources/extensions/gsd/auto-dashboard.ts b/src/resources/extensions/gsd/auto-dashboard.ts index 5f44e763e..f2c77d168 100644 --- a/src/resources/extensions/gsd/auto-dashboard.ts +++ b/src/resources/extensions/gsd/auto-dashboard.ts @@ -390,6 +390,8 @@ export interface WidgetStateAccessors { getCmdCtx(): ExtensionCommandContext | null; getBasePath(): string; isVerbose(): boolean; + /** True while newSession() is in-flight — render must not access session state. */ + isSessionSwitching(): boolean; } export function updateProgressWidget( @@ -460,6 +462,14 @@ export function updateProgressWidget( render(width: number): string[] { if (cachedLines && cachedWidth === width) return cachedLines; + // While newSession() is in-flight, session state is mid-mutation. + // Accessing cmdCtx.sessionManager or cmdCtx.getContextUsage() can + // block the render loop and freeze the TUI. Return the last cached + // frame (or an empty frame on first render) until the switch settles. + if (accessors.isSessionSwitching()) { + return cachedLines ?? []; + } + const ui = makeUI(theme, width); const lines: string[] = []; const pad = INDENT.base; diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 89e227449..495dca4b1 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -195,7 +195,7 @@ import { postUnitPostVerification, } from "./auto-post-unit.js"; import { bootstrapAutoSession, type BootstrapDeps } from "./auto-start.js"; -import { autoLoop, resolveAgentEnd, type LoopDeps } from "./auto-loop.js"; +import { autoLoop, resolveAgentEnd, isSessionSwitchInFlight, type LoopDeps } from "./auto-loop.js"; import { WorktreeResolver, type WorktreeResolverDeps, @@ -1129,6 +1129,7 @@ const widgetStateAccessors: WidgetStateAccessors = { getCmdCtx: () => s.cmdCtx, getBasePath: () => s.basePath, isVerbose: () => s.verbose, + isSessionSwitching: isSessionSwitchInFlight, }; // ─── Preconditions ────────────────────────────────────────────────────────────