Expose theme-aware status widget integration

This commit is contained in:
jb
2026-08-30 13:00:14 -04:00
parent 9a8169523b
commit bc3e3d6049
4 changed files with 871 additions and 146 deletions
+390 -26
View File
@@ -141,6 +141,37 @@ async function emitMockEvent(
}
}
type SharedRuntimeTestState = {
runningSubagents: Map<string, unknown>;
latestCtx: unknown;
latestPi: unknown;
widgetInterval: ReturnType<typeof setInterval> | null;
statusInterval: ReturnType<typeof setInterval> | null;
standaloneWidgetSyncScheduled: boolean;
pendingDeliveries: { length: number };
deliveryFlushScheduled: boolean;
activeRuntimeToken: symbol | null;
registeredRuntimeToken: symbol | null;
statusWidgetClaim: unknown;
statusWidget?: unknown;
};
function resetSharedRuntimeForTest(shared: SharedRuntimeTestState): void {
if (shared.widgetInterval) clearInterval(shared.widgetInterval);
if (shared.statusInterval) clearInterval(shared.statusInterval);
shared.widgetInterval = null;
shared.statusInterval = null;
shared.standaloneWidgetSyncScheduled = false;
shared.runningSubagents.clear();
shared.latestCtx = null;
shared.latestPi = null;
shared.statusWidgetClaim = null;
shared.pendingDeliveries.length = 0;
shared.deliveryFlushScheduled = false;
shared.activeRuntimeToken = null;
shared.registeredRuntimeToken = null;
}
function restoreEnvVar(name: string, value: string | undefined) {
if (value === undefined) {
delete process.env[name];
@@ -1860,33 +1891,11 @@ describe("commands", () => {
describe("reload lifecycle", () => {
const testApi = (subagentsModule as any).__test__;
const shared = testApi.sharedRuntime as {
runningSubagents: Map<string, any>;
latestCtx: any;
latestPi: any;
widgetInterval: ReturnType<typeof setInterval> | null;
statusInterval: ReturnType<typeof setInterval> | null;
pendingDeliveries: Array<(pi: any) => void>;
deliveryFlushScheduled: boolean;
activeRuntimeToken: symbol | null;
};
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
const ctx = { hasUI: false, ui: { setWidget() {} } };
function resetSharedRuntime() {
if (shared.widgetInterval) clearInterval(shared.widgetInterval);
if (shared.statusInterval) clearInterval(shared.statusInterval);
shared.widgetInterval = null;
shared.statusInterval = null;
shared.runningSubagents.clear();
shared.latestCtx = null;
shared.latestPi = null;
shared.pendingDeliveries.length = 0;
shared.deliveryFlushScheduled = false;
shared.activeRuntimeToken = null;
}
beforeEach(resetSharedRuntime);
after(resetSharedRuntime);
beforeEach(() => resetSharedRuntimeForTest(shared));
after(() => resetSharedRuntimeForTest(shared));
it("preserves a live subagent and delivers its queued result through the fresh runtime", async () => {
const oldRuntime = createMockExtensionApi();
@@ -2623,6 +2632,71 @@ describe("subagent startup delay", () => {
});
});
describe("subagents widget rendering", () => {
function createThemeSpy() {
const calls: Array<{ color: string; text: string }> = [];
return {
calls,
theme: {
fg(color: string, text: string) {
calls.push({ color, text });
return `\x1b[38;5;${calls.length}m${text}\x1b[0m`;
},
},
};
}
function createWidgetAgent(
id: string,
kind: "active" | "running" | "stalled" | "starting" | "waiting",
) {
const startTime = kind === "starting" ? 1_061_000 - 5_000 : 1_000_000 - 5_000;
let statusState = createStatusState({
source: kind === "running" ? "claude" : "pi",
startTimeMs: startTime,
});
if (kind === "active" || kind === "waiting") {
statusState = observeStatus(
statusState,
{
snapshot: "present",
updatedAt: startTime + 1_000,
sequence: 1,
phase: kind,
...(kind === "active"
? {
active: true,
activeScope: "tool",
activeSince: startTime + 1_000,
activityLabel: "bash",
}
: { waitingSince: startTime + 1_000 }),
},
startTime + 1_000,
);
} else if (kind === "stalled") {
statusState = observeStatus(
statusState,
{ snapshot: "missing" },
startTime + 1_000,
);
}
return {
id,
name: id,
agent: "worker",
task: "",
surface: "surface",
startTime,
sessionFile: `${id}.jsonl`,
statusState,
};
}
function createActiveWidgetAgent(id: string) {
return createWidgetAgent(id, "active");
}
it("keeps every rendered line within a very narrow width", () => {
const testApi = (subagentsModule as any).__test__;
assert.ok(testApi, "expected subagents test helpers to be exported");
@@ -2663,13 +2737,33 @@ describe("subagents widget rendering", () => {
assert.deepEqual(
lines.map((line: string) => visibleWidth(line)),
[16, 16, 16, 16, 16],
[16, 16, 16, 16],
);
assert.ok(lines.every((line: string) => !/[╭╮╰╯│]/.test(line)));
} finally {
Date.now = originalNow;
}
});
it("sanitizes whitespace in agent names before rendering rows", () => {
const testApi = (subagentsModule as any).__test__;
const startTime = Date.now() - 5_000;
const lines = testApi.renderSubagentWidgetLines([
{
id: "unsafe",
name: "worker\nwith\ttabs",
task: "",
surface: "s1",
startTime,
sessionFile: "sess1",
statusState: createStatusState({ source: "pi", startTimeMs: startTime }),
},
], 80);
assert.ok(lines.every((line: string) => !/[\r\n]/.test(line)));
assert.match(lines[1] ?? "", /worker with tabs/);
});
it("truncates the right-hand status instead of overflowing when it alone is too wide", () => {
const testApi = (subagentsModule as any).__test__;
assert.ok(testApi, "expected subagents test helpers to be exported");
@@ -2707,6 +2801,276 @@ describe("subagents widget rendering", () => {
}
}
});
it("uses the standalone widget factory theme for semantic status colors", () => {
const testApi = (subagentsModule as any).__test__;
const widgetCalls: any[] = [];
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
shared.latestCtx = {
hasUI: true,
ui: { setWidget: (...args: any[]) => widgetCalls.push(args) },
};
shared.runningSubagents.set("active", createActiveWidgetAgent("active"));
testApi.updateWidget();
const { theme, calls } = createThemeSpy();
const component = widgetCalls[0][1](undefined, theme);
const lines = withMockedNow(1_000_000, () => component.render(80));
assert.equal(lines.length, 2, "header plus one body row");
assert.equal(calls.find((call) => call.text === "Subagents")?.color, "accent");
assert.equal(calls.find((call) => call.text === "1 running")?.color, "muted");
assert.ok(calls.some((call) => call.color === "warning" && call.text.includes("active")));
assert.equal(calls.find((call) => call.text === " (worker)")?.color, "dim");
assert.ok(calls.some((call) => call.color === "dim" && call.text === "00:05"));
assert.ok(lines.every((line: string) => !/[╭╮╰╯│]/.test(line)));
});
it("uses the optional theme passed to hosted rendering", () => {
const testApi = (subagentsModule as any).__test__;
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
shared.runningSubagents.set("active", createActiveWidgetAgent("active"));
const { theme, calls } = createThemeSpy();
const lines = withMockedNow(1_000_000, () => testApi.statusWidget.render(80, undefined, theme));
assert.equal(lines.length, 2, "header plus one body row");
assert.ok(calls.some((call) => call.color === "accent" && call.text === "Subagents"));
assert.ok(calls.some((call) => call.color === "muted" && call.text === "1 running"));
assert.ok(calls.some((call) => call.color === "warning" && call.text.includes("active")));
assert.ok(calls.some((call) => call.color === "dim" && call.text === "00:05"));
assert.ok(lines.every((line: string) => !/[╭╮╰╯│]/.test(line)));
});
it("maps every status kind to a semantic row color", () => {
const testApi = (subagentsModule as any).__test__;
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
for (const [id, kind] of [
["active", "active"],
["running", "running"],
["stalled", "stalled"],
["starting", "starting"],
["waiting", "waiting"],
] as const) {
shared.runningSubagents.set(id, createWidgetAgent(id, kind));
}
const { theme, calls } = createThemeSpy();
withMockedNow(1_061_000, () => testApi.statusWidget.render(120, undefined, theme));
const expected = {
active: "warning",
running: "warning",
stalled: "error",
starting: "dim",
waiting: "accent",
} as const;
for (const [id, kind] of [
["active", "active"],
["running", "running"],
["stalled", "stalled"],
["starting", "starting"],
["waiting", "waiting"],
] as const) {
assert.ok(
calls.some((call) => call.text === id && call.color === expected[kind]),
`${id} should use ${expected[kind]}`,
);
}
});
});
describe("status widget host integration", () => {
const testApi = (subagentsModule as any).__test__;
const statusWidget = (globalThis as any).__pi_interactive_subagents.statusWidget;
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
function addRunning(id: string) {
const startTime = Date.now();
shared.runningSubagents.set(id, {
id,
name: id,
task: "",
surface: "surface",
startTime,
sessionFile: `${id}.jsonl`,
statusState: createStatusState({ source: "pi", startTimeMs: startTime }),
});
}
beforeEach(() => resetSharedRuntimeForTest(shared));
after(() => resetSharedRuntimeForTest(shared));
it("falls back to the standalone widget when unclaimed", () => {
const widgetCalls: any[] = [];
shared.latestCtx = {
hasUI: true,
ui: { setWidget: (...args: any[]) => widgetCalls.push(args) },
};
addRunning("worker");
testApi.updateWidget();
assert.equal(widgetCalls.length, 1);
assert.equal(widgetCalls[0][0], "subagent-status");
assert.equal(typeof widgetCalls[0][1], "function");
});
it("suppresses, notifies, and restores the standalone widget around a claim", async () => {
const widgetCalls: any[] = [];
shared.latestCtx = {
hasUI: true,
ui: { setWidget: (...args: any[]) => widgetCalls.push(args) },
};
addRunning("worker");
testApi.updateWidget();
let changes = 0;
const onChange = () => changes++;
const release = statusWidget.claim(onChange);
assert.equal(statusWidget.isClaimed(onChange), true);
assert.equal(statusWidget.isClaimed(() => {}), false);
assert.equal(typeof widgetCalls.at(-1)[1], "function", "claim sync is deferred");
await Promise.resolve();
assert.equal(widgetCalls.at(-1)[1], undefined);
testApi.updateWidget();
assert.equal(changes, 1);
release();
assert.equal(statusWidget.isClaimed(onChange), false);
assert.equal(widgetCalls.at(-1)[1], undefined, "release sync is deferred");
await Promise.resolve();
assert.equal(typeof widgetCalls.at(-1)[1], "function");
release();
assert.equal(widgetCalls.length, 4, "release is idempotent");
});
it("does not restore standalone UI between an immediate release and re-claim", async () => {
const widgetCalls: any[] = [];
shared.latestCtx = {
hasUI: true,
ui: { setWidget: (...args: any[]) => widgetCalls.push(args) },
};
addRunning("worker");
testApi.updateWidget();
const firstChange = () => {};
const releaseFirst = statusWidget.claim(firstChange);
releaseFirst();
const secondChange = () => {};
const releaseSecond = statusWidget.claim(secondChange);
await Promise.resolve();
assert.equal(statusWidget.isClaimed(firstChange), false);
assert.equal(statusWidget.isClaimed(secondChange), true);
assert.equal(widgetCalls.at(-1)[1], undefined);
releaseSecond();
await Promise.resolve();
assert.equal(typeof widgetCalls.at(-1)[1], "function");
});
it("caps body rows with an honest overflow row", () => {
addRunning("worker");
addRunning("scout");
addRunning("reviewer");
const lines = statusWidget.render(40, 2);
assert.equal(lines.length, 3, "header + one agent + overflow");
assert.ok(lines.every((line: string) => !/[╭╮╰╯│]/.test(line)));
assert.match(lines[2], /\+2 more running\./);
assert.equal(statusWidget.getBodyRowCount(), 3);
assert.equal(statusWidget.hasContent(), true);
});
it("keeps capped output within narrow widths", () => {
addRunning("worker");
addRunning("scout");
for (const width of [0, 1, 2, 4, 8]) {
for (const line of statusWidget.render(width, 1)) {
assert.ok(
visibleWidth(line) <= width,
`expected line width <= ${width}, got ${visibleWidth(line)}`,
);
}
}
});
it("drops old claims during reload and allows a fresh runtime to claim", async () => {
const oldRuntime = createMockExtensionApi();
(subagentsModule as any).default(oldRuntime.api);
const oldCtx = {
hasUI: true,
ui: { setWidget() {} },
};
await emitMockEvent(oldRuntime, "session_start", { reason: "startup" }, oldCtx);
addRunning("worker");
let oldChanges = 0;
const oldOnChange = () => oldChanges++;
const oldRelease = statusWidget.claim(oldOnChange);
// Bind the replacement before the old runtime emits shutdown. The old
// shutdown must not undo the fresh binding or retain its old callback.
const freshRuntime = createMockExtensionApi();
(subagentsModule as any).default(freshRuntime.api);
await emitMockEvent(freshRuntime, "session_start", { reason: "reload" }, oldCtx);
// A delayed old start must also be ignored once the fresh runtime is bound.
await emitMockEvent(oldRuntime, "session_start", { reason: "reload" }, oldCtx);
assert.equal(shared.latestPi, freshRuntime.api);
assert.equal(statusWidget.isClaimed(oldOnChange), false);
await emitMockEvent(oldRuntime, "session_shutdown", { reason: "reload" }, oldCtx);
testApi.updateWidget();
assert.equal(oldChanges, 0);
assert.equal(shared.statusWidgetClaim, null);
assert.equal(shared.runningSubagents.size, 1);
let freshChanges = 0;
const freshRelease = statusWidget.claim(() => freshChanges++);
oldRelease();
testApi.updateWidget();
assert.equal(freshChanges, 1, "a stale release cannot affect the fresh claim");
freshRelease();
});
it("replaces malformed and old widget contracts on a duplicate load", async () => {
const globalApi = (globalThis as any).__pi_interactive_subagents;
(globalThis as any).__pi_interactive_subagents = "old-api";
shared.statusWidgetClaim = { id: 1, onChange: () => {} };
await import(`../pi-extension/subagents/index.ts?malformed-global=${Date.now()}`);
const repairedApi = (globalThis as any).__pi_interactive_subagents;
assert.equal(typeof repairedApi.registerToolExtension, "function");
assert.equal(repairedApi.statusWidget, globalApi.statusWidget);
shared.statusWidgetClaim = { id: 1, onChange: () => {} };
await import(`../pi-extension/subagents/index.ts?old-widget-claim=${Date.now()}`);
assert.equal(shared.statusWidgetClaim, null);
const oldContract = {
hasContent: () => true,
getBodyRowCount: () => 1,
render: () => [],
claim: () => () => {},
};
shared.statusWidget = oldContract;
(globalThis as any).__pi_interactive_subagents.statusWidget = oldContract;
await import(`../pi-extension/subagents/index.ts?old-widget-contract=${Date.now()}`);
const currentApi = (globalThis as any).__pi_interactive_subagents;
const current = currentApi.statusWidget;
assert.equal(currentApi, repairedApi);
assert.notEqual(current, oldContract);
assert.equal(typeof current.hasContent, "function");
assert.equal(typeof current.getBodyRowCount, "function");
assert.equal(typeof current.render, "function");
assert.equal(typeof current.claim, "function");
assert.equal(typeof current.isClaimed, "function");
assert.equal(shared.statusWidget, current);
});
});
describe("subagent display helpers", () => {