Merge pull request #4150 from jeremymcs/claude/debug-tui-auto-mode-vCnxA
Split Container.clear() into clear() and detachChildren()
This commit is contained in:
commit
fc7d195e09
5 changed files with 63 additions and 3 deletions
|
|
@ -1402,7 +1402,9 @@ export class InteractiveMode {
|
|||
|
||||
// widgetContainerAbove: spacer collapses when pinned content is visible
|
||||
// so there's no extra blank line between pinned output and the editor border.
|
||||
this.widgetContainerAbove.clear();
|
||||
// Use detachChildren() (not clear()) — the extensionWidgetsAbove map owns
|
||||
// disposal; clear() would dispose every mounted widget on every re-render.
|
||||
this.widgetContainerAbove.detachChildren();
|
||||
const pinned = this.pinnedMessageContainer;
|
||||
this.widgetContainerAbove.addChild({
|
||||
render: () => pinned.children.length > 0 ? [] : [""],
|
||||
|
|
@ -1422,7 +1424,9 @@ export class InteractiveMode {
|
|||
spacerWhenEmpty: boolean,
|
||||
leadingSpacer: boolean,
|
||||
): void {
|
||||
container.clear();
|
||||
// Detach without disposing — the widgets map owns lifecycle; disposing
|
||||
// here would kill refresh timers and subscriptions on every re-render.
|
||||
container.detachChildren();
|
||||
|
||||
if (widgets.size === 0) {
|
||||
if (spacerWhenEmpty) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { TUI } from "../tui.js";
|
||||
import { Container, TUI } from "../tui.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import type { Terminal } from "../terminal.js";
|
||||
|
||||
function makeTerminal(): Terminal {
|
||||
|
|
@ -48,3 +49,39 @@ describe("TUI", () => {
|
|||
assert.equal(anyTui.inputBuffer, "");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Container", () => {
|
||||
function makeDisposableChild(counter: { disposed: number }): Component & { dispose(): void } {
|
||||
return {
|
||||
render: () => [],
|
||||
invalidate() {},
|
||||
dispose() {
|
||||
counter.disposed++;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("detachChildren() removes children without disposing them", () => {
|
||||
const c = new Container();
|
||||
const counter = { disposed: 0 };
|
||||
c.addChild(makeDisposableChild(counter));
|
||||
c.addChild(makeDisposableChild(counter));
|
||||
|
||||
c.detachChildren();
|
||||
|
||||
assert.equal(c.children.length, 0);
|
||||
assert.equal(counter.disposed, 0);
|
||||
});
|
||||
|
||||
it("clear() still disposes children (regression guard for detach/dispose split)", () => {
|
||||
const c = new Container();
|
||||
const counter = { disposed: 0 };
|
||||
c.addChild(makeDisposableChild(counter));
|
||||
c.addChild(makeDisposableChild(counter));
|
||||
|
||||
c.clear();
|
||||
|
||||
assert.equal(c.children.length, 0);
|
||||
assert.equal(counter.disposed, 2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -197,6 +197,17 @@ export class Container implements Component {
|
|||
this._prevRender = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all children without calling dispose on them.
|
||||
* Use when child lifecycle is owned elsewhere and the container is only a
|
||||
* render mount (e.g. extension widget containers in InteractiveMode, where
|
||||
* the extensionWidgets* maps own disposal).
|
||||
*/
|
||||
detachChildren(): void {
|
||||
this.children = [];
|
||||
this._prevRender = null;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
for (const child of this.children) {
|
||||
child.invalidate?.();
|
||||
|
|
|
|||
|
|
@ -806,6 +806,9 @@ export async function bootstrapAutoSession(
|
|||
|
||||
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
||||
ctx.ui.setFooter(hideFooter);
|
||||
// Hide gsd-health during AUTO — gsd-progress is the single source of truth
|
||||
// for last-commit / cost / health signal while auto is running.
|
||||
ctx.ui.setWidget("gsd-health", undefined);
|
||||
const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
|
||||
const pendingCount = (state.registry ?? []).filter(
|
||||
(m) => m.status !== "complete" && m.status !== "parked",
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ import {
|
|||
postUnitPostVerification,
|
||||
} from "./auto-post-unit.js";
|
||||
import { bootstrapAutoSession, openProjectDbIfPresent, type BootstrapDeps } from "./auto-start.js";
|
||||
import { initHealthWidget } from "./health-widget.js";
|
||||
import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight, type LoopDeps, type ErrorContext } from "./auto-loop.js";
|
||||
// Slice-level parallelism (#2340)
|
||||
import { getEligibleSlices } from "./slice-parallel-eligibility.js";
|
||||
|
|
@ -650,6 +651,7 @@ function handleLostSessionLock(
|
|||
ctx?.ui.setStatus("gsd-auto", undefined);
|
||||
ctx?.ui.setWidget("gsd-progress", undefined);
|
||||
ctx?.ui.setFooter(undefined);
|
||||
if (ctx) initHealthWidget(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -684,6 +686,7 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
|
|||
ctx.ui.setStatus("gsd-auto", undefined);
|
||||
ctx.ui.setWidget("gsd-progress", undefined);
|
||||
ctx.ui.setFooter(undefined);
|
||||
initHealthWidget(ctx);
|
||||
}
|
||||
|
||||
// Restore CWD out of worktree back to original project root
|
||||
|
|
@ -943,6 +946,7 @@ export async function stopAuto(
|
|||
ctx?.ui.setStatus("gsd-auto", undefined);
|
||||
ctx?.ui.setWidget("gsd-progress", undefined);
|
||||
ctx?.ui.setFooter(undefined);
|
||||
if (ctx) initHealthWidget(ctx);
|
||||
restoreProjectRootEnv();
|
||||
restoreMilestoneLockEnv();
|
||||
|
||||
|
|
@ -1044,6 +1048,7 @@ export async function pauseAuto(
|
|||
ctx?.ui.setStatus("gsd-auto", "paused");
|
||||
ctx?.ui.setWidget("gsd-progress", undefined);
|
||||
ctx?.ui.setFooter(undefined);
|
||||
if (ctx) initHealthWidget(ctx);
|
||||
const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
|
||||
ctx?.ui.notify(
|
||||
`${s.stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue