import assert from "node:assert/strict"; import { createRequire } from "node:module"; import { describe, it, type TestContext } from "node:test"; import { fileURLToPath, pathToFileURL } from "node:url"; import { visibleWidth } from "@earendil-works/pi-tui"; import { CustomMessageComponent, initTheme } from "@selesai/code"; import { renderSubagentEntry, renderSubagentMessage } from "../../src/tui/subagent-messages.ts"; // Markdown bodies read Pi's markdown theme, which Pi initializes at startup. initTheme("dark"); const plainTheme = { fg: (_token: string, text: string) => text, bg: (_token: string, text: string) => text, bold: (text: string) => text, }; const tokenTheme = { fg: (token: string, text: string) => `<${token}>${text}`, bg: (token: string, text: string) => `[${token}]${text.trimEnd()}[/${token}]`, bold: (text: string) => `${text}`, }; type TestTheme = typeof plainTheme; function renderMessage(customType: string, details: unknown, options: { content?: string; expanded?: boolean; width?: number; theme?: TestTheme } = {}): string[] { const message = { customType, content: options.content ?? "", details }; // SAFETY: the renderer reads only fg, bg, and bold from Pi's theme. const component = renderSubagentMessage(message, { expanded: options.expanded ?? false }, (options.theme ?? plainTheme) as never); assert.ok(component, `no renderer for ${customType}`); return component.render(options.width ?? 100).map((line) => line.trimEnd()); } function renderEntry(customType: string, data: unknown, options: { expanded?: boolean; width?: number; theme?: TestTheme } = {}): string[] { // SAFETY: the renderer reads only fg, bg, and bold from Pi's theme. const component = renderSubagentEntry({ customType, data }, { expanded: options.expanded ?? false }, (options.theme ?? plainTheme) as never); assert.ok(component, `no renderer for ${customType}`); return component.render(options.width ?? 100).map((line) => line.trimEnd()); } function visibleLines(lines: string[]): string[] { return lines.map((line) => stripAnsi(line).trimEnd()).filter((line) => line.trim().length > 0); } function stripAnsi(text: string): string { return text.replace(/\x1b\[[0-9;]*m/g, ""); } /** The collapsed line's label color and its headline before the hint, with theme tokens marked. */ function tokenLine(line: string | undefined): { label: string; headline: string } | undefined { const match = /^\[customMessageBg\] <(\w+)>\[subagent\]<\/b><\/\1> (.*) \((?:click|ctrl\+o) to expand\)<\/dim>\[\/customMessageBg\]$/.exec(line ?? ""); return match ? { label: match[1]!, headline: match[2]! } : undefined; } function tonedHeadline(customType: string, details: unknown, content = ""): string | undefined { return tokenLine(renderMessage(customType, details, { content, theme: tokenTheme, width: 400 })[1])?.headline; } // keyText resolves Pi's expand key through the pi-tui copy that pi-coding-agent loads. async function bindExpandKey(t: TestContext, keys: string): Promise { const agentEntry = fileURLToPath(import.meta.resolve("@selesai/code")); // SAFETY: this path is pi-tui's package entry, resolved from where pi-coding-agent loads it. const tui = await import(pathToFileURL(createRequire(agentEntry).resolve("@earendil-works/pi-tui")).href) as typeof import("@earendil-works/pi-tui"); const previous = tui.getKeybindings(); tui.setKeybindings(new tui.KeybindingsManager({ ...tui.TUI_KEYBINDINGS, "app.tools.expand": { defaultKeys: keys, description: "Toggle tool output" } })); t.after(() => tui.setKeybindings(previous)); } const request = { id: "request-1", requestId: "request-1", runId: "run-1", agent: "worker", childIndex: 0, expectsReply: true }; const reply = { requestId: "request-1", reason: "need_decision", runId: "run-1", agent: "worker", childIndex: 1, childTarget: "child-worker", message: "Approved; continue.", createdAt: 123 }; const watchdogWarning = { severity: "blocker", importance: "high", category: "other", source: "main", agent: "worker", summary: "Edits a migration without a backfill", evidence: "diff touches db/migrations/0042.sql", recommendedAction: "Add a reversible backfill" }; describe("subagent messages in the main chat", () => { it("shows a supervisor request as one [subagent] line", () => { const headline = (details: unknown) => visibleLines(renderMessage("subagent_supervisor_request", details)); assert.deepEqual(headline({ ...request, reason: "need_decision" }), [" [subagent] worker needs a decision (click to expand)"]); assert.deepEqual(headline({ ...request, reason: "interview_request" }), [" [subagent] worker asks for structured answers (click to expand)"]); assert.deepEqual(headline({ ...request, reason: "progress_update" }), [" [subagent] worker sent a progress update (click to expand)"]); assert.deepEqual(headline(request), [" [subagent] worker needs a decision (click to expand)"]); assert.deepEqual(headline({ ...request, agent: 42 }), [" [subagent] supervisor request (click to expand)"]); assert.deepEqual(headline(undefined), [" [subagent] supervisor request (click to expand)"]); }); it("shows attention and steering notices as one [subagent] line with toned status words", () => { const rows: Array<[string, unknown, string, string]> = [ ["subagent_control_notice", { event: { type: "needs_attention", agent: "worker", runId: "run-1", message: "stuck", ts: 1 } }, "worker needs attention", "worker needs attention"], ["subagent_control_notice", { event: { type: "active_long_running", agent: "worker", runId: "run-1", message: "slow", ts: 1 } }, "worker active long running", "worker active long running"], ["subagent_control_notice", {}, "subagent notice", "subagent notice"], ["subagent_steering_notice", { type: "subagent.steering.notice", ts: 1, runId: "run-1", requestId: "steer-1", state: "failed", message: "child exited" }, "steering for run run-1 failed", "steering for run run-1 failed"], ["subagent_steering_notice", { type: "subagent.steering.notice", ts: 1, runId: "run-1", requestId: "steer-1", state: "partial", message: "one child missed it" }, "steering for run run-1 partial", "steering for run run-1 partial"], ["subagent_steering_notice", { type: "subagent.steering.notice", ts: 1, runId: "run-1", requestId: "steer-1", state: "recovered", message: "delivered on retry" }, "steering for run run-1 recovered", "steering for run run-1 recovered"], ["subagent_steering_notice", { runId: "run-1", state: "lost" }, "steering notice", "steering notice"], ]; for (const [customType, details, text, toned] of rows) { assert.deepEqual(visibleLines(renderMessage(customType, details)), [` [subagent] ${text} (click to expand)`], customType); assert.equal(tonedHeadline(customType, details), toned, customType); } }); it("shows completion notices as one [subagent] line, including notices saved without details", () => { const rows: Array<[unknown, string, string, string]> = [ [{ runs: [{ agent: "scout", status: "completed" }] }, "", "scout completed", "scout completed"], [{ runs: [{ agent: "worker", status: "failed" }] }, "", "worker failed", "worker failed"], [{ runs: [{ agent: "scout", status: "completed" }, { agent: "worker", status: "failed" }, { agent: "reviewer", status: "stopped" }] }, "", "3 background runs finished · 1 failed · 1 stopped", "3 background runs finished · 1 failed · 1 stopped"], [{ runs: [{ agent: "scout", status: "completed" }, { agent: "worker", status: "completed" }] }, "", "2 background runs completed", "2 background runs completed"], [undefined, "Background task paused: **worker** (Migrate the index)\n\nNeeds a decision.", "worker paused", "worker paused"], [undefined, "Detached foreground task completed: **reviewer**\n\nRecovered final review", "reviewer completed", "reviewer completed"], [undefined, "Background tasks completed (2): **scout**, **worker**\n\n1. scout\nDone", "2 background runs finished", "2 background runs finished"], [undefined, "Something else", "background run finished", "background run finished"], ]; for (const [details, content, text, toned] of rows) { assert.deepEqual(visibleLines(renderMessage("subagent-notify", details, { content })), [` [subagent] ${text} (click to expand)`], text); assert.equal(tonedHeadline("subagent-notify", details, content), toned, text); } }); it("shows reminders, bg_wait wakes, and workflow notices as one [subagent] line", () => { const rows: Array<[string, unknown, string, string, string]> = [ ["subagent-supervisor-unanswered", { requestIds: ["request-1"] }, "", "1 supervisor request unanswered", "1 supervisor request unanswered"], ["subagent-supervisor-unanswered", { requestIds: ["request-1", "request-2"] }, "", "2 supervisor requests unanswered", "2 supervisor requests unanswered"], ["subagent-supervisor-unanswered", undefined, "", "supervisor requests unanswered", "supervisor requests unanswered"], ["subagent-supervisor-blocked", { blocked: true, requestIds: ["request-1"] }, "", "1 supervisor request blocked", "1 supervisor request blocked"], ["subagent-completion-unanswered", undefined, "", "completion results unanswered", "completion results unanswered"], ["subagent-completion-unhandled", undefined, "", "completion results unhandled", "completion results unhandled"], ["subagent-wait-subscription", { token: "wait-1", runId: "run-1", outcome: "completed" }, "", "bg_wait: run run-1 completed", "bg_wait: run run-1 completed"], ["subagent-wait-subscription", { token: "wait-1", runId: "run-1", outcome: "timed out" }, "", "bg_wait: run run-1 timed out", "bg_wait: run run-1 timed out"], ["subagent-wait-subscription", { token: "wait-1", runId: "run-1", outcome: "reconciliation failed" }, "", "bg_wait: run run-1 reconciliation failed", "bg_wait: run run-1 reconciliation failed"], ["subagent-wait-subscription", { token: "wait-1", runId: "run-1", outcome: "detached" }, "", "bg_wait: run run-1 detached", "bg_wait: run run-1 detached"], ["subagent-wait-subscription", undefined, "", "bg_wait fired", "bg_wait fired"], ["subagent-incremental-child-notify", { childKey: "review", outcome: "failed" }, "", "workflow child review failed", "workflow child review failed"], ["subagent-incremental-child-notify", undefined, "Workflow child paused (needs attention): **review**\nWorkflow run: wf-1", "workflow child review paused", "workflow child review paused"], ["subagent-incremental-child-notify", undefined, "Something else", "workflow child update", "workflow child update"], ["subagent-workflow-result-write-failed", undefined, "", "workflow result save failed", "workflow result save failed"], ]; for (const [customType, details, content, text, toned] of rows) { assert.deepEqual(visibleLines(renderMessage(customType, details, { content })), [` [subagent] ${text} (click to expand)`], text); assert.equal(tonedHeadline(customType, details, content), toned, text); } }); it("shows watchdog warnings and clarifications as one [subagent] line", () => { const rows: Array<[string, unknown, string, string, string]> = [ ["subagent_watchdog_warning", watchdogWarning, "", "watchdog blocker", "watchdog blocker"], ["subagent_watchdog_warning", { ...watchdogWarning, severity: "concern" }, "", "watchdog concern", "watchdog concern"], ["subagent_watchdog_warning", { severity: "blocker" }, "", "watchdog warning", "watchdog warning"], ["subagent_watchdog_clarification", undefined, "Which migration is current?", "watchdog needs clarification", "watchdog needs clarification"], ]; for (const [customType, details, content, text, toned] of rows) { assert.deepEqual(visibleLines(renderMessage(customType, details, { content })), [` [subagent] ${text} (click to expand)`], text); assert.equal(tonedHeadline(customType, details, content), toned, text); } assert.deepEqual(visibleLines(renderEntry("subagent_watchdog_warning", watchdogWarning)), [" [subagent] watchdog blocker (click to expand)"]); assert.deepEqual(visibleLines(renderEntry("subagent_watchdog_warning", { summary: "No evidence" })), [" [subagent] watchdog warning (click to expand)"]); }); it("keeps a watchdog warning's lifecycle state on the collapsed line", () => { const rows: Array<[unknown, string]> = [ [{ ...watchdogWarning, state: "displayed" }, "watchdog blocker"], [{ ...watchdogWarning, state: "stalemate", stalemateRepeats: 3 }, "watchdog blocker · stalemate"], [{ ...watchdogWarning, severity: "concern", stale: true, state: "stale" }, "watchdog concern · stale"], [{ ...watchdogWarning, state: "failed", error: "review timed out" }, "watchdog blocker · failed review"], ]; for (const [details, text] of rows) { assert.deepEqual(visibleLines(renderMessage("subagent_watchdog_warning", details, { content: "" })), [` [subagent] ${text} (click to expand)`], text); assert.deepEqual(visibleLines(renderEntry("subagent_watchdog_warning", details)), [` [subagent] ${text} (click to expand)`], text); } }); it("expands watchdog warnings to the readable warning instead of the model's XML", () => { const readable = [ " [subagent] watchdog blocker", " Subagent watchdog Blocker: Edits a migration without a backfill", " Evidence: diff touches db/migrations/0042.sql", " Recommended action: Add a reversible backfill", " Importance: High · Category: Other · Source: main · Agent: worker", ]; assert.deepEqual(visibleLines(renderMessage("subagent_watchdog_warning", watchdogWarning, { content: "", expanded: true, width: 200 })), readable); assert.deepEqual(visibleLines(renderEntry("subagent_watchdog_warning", watchdogWarning, { expanded: true, width: 200 })), readable); }); it("shows the supervisor reply entry as one [subagent] line", () => { assert.deepEqual(visibleLines(renderEntry("subagent_supervisor_reply", reply)), [" [subagent] reply sent to worker (click to expand)"]); assert.deepEqual(visibleLines(renderEntry("subagent_supervisor_reply", { ...reply, message: ["bad"] })), [" [subagent] supervisor reply (click to expand)"]); }); it("expands a message to the text the main agent read, as markdown", () => { const content = [ "Subagent needs a supervisor decision.", "Run: run-1", "Agent: worker", "Child index: 0", "", "Should I drop the old `users.email` index first?", "", "Reply with: subagent_supervisor({ action: \"reply\", replyTo: \"request-1\", message: \"...\" })", ].join("\n"); assert.deepEqual(visibleLines(renderMessage("subagent_supervisor_request", { ...request, reason: "need_decision" }, { content, expanded: true, width: 200 })), [ " [subagent] worker needs a decision", " Subagent needs a supervisor decision.", " Run: run-1", " Agent: worker", " Child index: 0", " Should I drop the old users.email index first?", " Reply with: subagent_supervisor({ action: \"reply\", replyTo: \"request-1\", message: \"...\" })", ]); }); it("expands the supervisor reply entry to the reply and the request it answers", () => { assert.deepEqual(visibleLines(renderEntry("subagent_supervisor_reply", reply, { expanded: true })), [ " [subagent] reply sent to worker", " Request: request-1", " Run: run-1", " Child index: 1", " Child target: child-worker", " Approved; continue.", ]); }); it("names Pi's expand key in the hint, or a click when no key is bound", async (t) => { const collapsed = () => visibleLines(renderMessage("subagent_supervisor_request", { ...request, reason: "need_decision" })); assert.deepEqual(collapsed(), [" [subagent] worker needs a decision (click to expand)"]); await bindExpandKey(t, "ctrl+o"); assert.deepEqual(collapsed(), [" [subagent] worker needs a decision (ctrl+o to expand)"]); }); it("colors the [subagent] label by message kind", () => { const label = (lines: string[]) => tokenLine(lines[1])?.label; const messages: Array<[string, unknown, string, string]> = [ ["subagent_supervisor_request", { ...request, reason: "need_decision" }, "", "customMessageLabel"], ["subagent-supervisor-unanswered", { requestIds: ["request-1"] }, "", "customMessageLabel"], ["subagent-supervisor-blocked", { blocked: true, requestIds: ["request-1"] }, "", "customMessageLabel"], ["subagent-notify", { runs: [{ agent: "scout", status: "completed" }] }, "", "mdLink"], ["subagent-completion-unanswered", undefined, "", "mdLink"], ["subagent-completion-unhandled", undefined, "", "mdLink"], ["subagent-incremental-child-notify", { childKey: "review", outcome: "failed" }, "", "mdLink"], ["subagent-wait-subscription", { token: "wait-1", runId: "run-1", outcome: "completed" }, "", "mdLink"], ["subagent-workflow-result-write-failed", undefined, "", "mdLink"], ["subagent_control_notice", { event: { type: "needs_attention", agent: "worker", runId: "run-1", message: "stuck", ts: 1 } }, "", "customMessageLabel"], ["subagent_steering_notice", { runId: "run-1", state: "failed" }, "", "customMessageLabel"], ["subagent_watchdog_warning", watchdogWarning, "", "toolTitle"], ["subagent_watchdog_clarification", undefined, "Which migration is current?", "toolTitle"], ]; for (const [customType, details, content, color] of messages) { assert.equal(label(renderMessage(customType, details, { content, theme: tokenTheme, width: 400 })), color, customType); } assert.equal(label(renderEntry("subagent_supervisor_reply", reply, { theme: tokenTheme, width: 400 })), "customMessageText"); assert.equal(label(renderEntry("subagent_watchdog_warning", watchdogWarning, { theme: tokenTheme, width: 400 })), "toolTitle"); }); it("keeps the collapsed block to one line at narrow widths", () => { const lines = visibleLines(renderMessage("subagent_supervisor_request", { ...request, reason: "interview_request" }, { width: 24 })); assert.deepEqual(lines, [" [subagent] worker ask…"]); assert.ok(visibleWidth(lines[0]!) <= 24); }); it("draws Pi's custom-message block and tones only the status word", () => { const lines = renderMessage("subagent_supervisor_request", { ...request, reason: "need_decision" }, { theme: tokenTheme, width: 300 }); assert.deepEqual(lines, [ "[customMessageBg][/customMessageBg]", "[customMessageBg] [subagent] worker needs a decision (click to expand)[/customMessageBg]", "[customMessageBg][/customMessageBg]", ]); const expanded = renderMessage("subagent_supervisor_request", { ...request, reason: "need_decision" }, { theme: tokenTheme, width: 300, expanded: true, content: "First line\n\n- second line" }); assert.equal(expanded.length, 7); for (const line of expanded) assert.match(line, /^\[customMessageBg\].*\[\/customMessageBg\]$/); }); it("renders escape sequences from message text inert", () => { const details = { ...request, agent: "wor\x1b[31mker", reason: "need_decision" }; const content = "Run \x1b]0;pwned\x07this"; for (const expanded of [false, true]) { const text = renderMessage("subagent_supervisor_request", details, { content, expanded, width: 200 }).join("\n"); assert.match(text, /wor\[U\+001B\]\[31mker/); assert.equal(text.includes("\x1b[31m"), false); assert.equal(text.includes("\x1b]0;"), false); } const expandedText = renderMessage("subagent_supervisor_request", details, { content, expanded: true, width: 200 }).join("\n"); assert.match(expandedText, /Run \[U\+001B\]\]0;pwned\[U\+0007\]this/); }); it("toggles one block on a click, and Pi's expand key discards earlier clicks", () => { const message = { customType: "subagent_supervisor_request", content: "Should I continue?", details: { ...request, reason: "need_decision" } }; const otherMessage = { ...message }; const render = (target: typeof message, expandedByPi: boolean) => { // SAFETY: the renderer reads only fg, bg, and bold from Pi's theme. const component = renderSubagentMessage(target, { expanded: expandedByPi }, plainTheme as never); assert.ok(component); return component; }; const click = (component: ReturnType, button: "left" | "right" = "left") => { const height = component.render(100).length; return component.handleMouse?.({ type: "click", button, x: 2, y: 1, screenX: 2, screenY: 1, width: 100, height, shift: false, alt: false, ctrl: false }); }; const collapsed = [" [subagent] worker needs a decision (click to expand)"]; const expanded = [" [subagent] worker needs a decision", " Should I continue?"]; const block = render(message, false); assert.equal(click(block, "right"), undefined); assert.deepEqual(visibleLines(block.render(100)), collapsed); assert.equal(click(block)?.handled, true); assert.deepEqual(visibleLines(block.render(100)), expanded); assert.deepEqual(visibleLines(render(message, false).render(100)), expanded); assert.deepEqual(visibleLines(render(otherMessage, false).render(100)), collapsed); assert.deepEqual(visibleLines(render(message, true).render(100)), expanded); assert.deepEqual(visibleLines(render(message, false).render(100)), collapsed); }); it("opens and closes inside Pi's own message component, by Pi's expand toggle or a click", () => { const message = { role: "custom" as const, customType: "subagent_supervisor_request", content: "Should I continue?", display: true, details: { ...request, reason: "need_decision" }, timestamp: 1 }; const host = new CustomMessageComponent(message, renderSubagentMessage); const shown = () => visibleLines(host.render(100)); const collapsed = [" [subagent] worker needs a decision (click to expand)"]; const expanded = [" [subagent] worker needs a decision", " Should I continue?"]; assert.deepEqual(shown(), collapsed); host.setExpanded(true); assert.deepEqual(shown(), expanded); host.setExpanded(false); assert.deepEqual(shown(), collapsed); const height = host.render(100).length; assert.equal(host.handleMouse({ type: "click", button: "left", x: 2, y: 2, screenX: 2, screenY: 2, width: 100, height, shift: false, alt: false, ctrl: false })?.handled, true); assert.deepEqual(shown(), expanded); }); });