9d94a222d3
- Added an anchored Subagents HUD renderer that formats active subagent sessions as `Id: description` rows. - Integrated a new interactive-mode container and observer-driven render path so the HUD updates with session events and clears when no subagents are active. - Exported `formatTaskId` for shared HUD formatting and added tests covering active-only filtering, fallback task/progress text, and line truncation.
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
/**
|
|
* Contract: the anchored subagent HUD (rendered above the editor, next to the
|
|
* Todos block) lists exactly the running subagents as `Id: description` rows
|
|
* and yields no output once nothing is running, so the block self-clears.
|
|
*/
|
|
import { beforeAll, describe, expect, it } from "bun:test";
|
|
import { renderSubagentHudLines } from "@oh-my-pi/pi-coding-agent/modes/interactive-mode";
|
|
import type { ObservableSession } from "@oh-my-pi/pi-coding-agent/modes/session-observer-registry";
|
|
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
|
import type { AgentProgress } from "@oh-my-pi/pi-coding-agent/task";
|
|
|
|
function makeSession(overrides: Partial<ObservableSession> & { id: string }): ObservableSession {
|
|
return {
|
|
kind: "subagent",
|
|
label: overrides.id,
|
|
status: "active",
|
|
lastUpdate: Date.now(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeProgress(overrides: Partial<AgentProgress> & { id: string }): AgentProgress {
|
|
return {
|
|
index: 0,
|
|
agent: "task",
|
|
agentSource: "bundled",
|
|
status: "running",
|
|
task: "",
|
|
recentTools: [],
|
|
recentOutput: [],
|
|
toolCount: 0,
|
|
requests: 0,
|
|
tokens: 0,
|
|
cost: 0,
|
|
durationMs: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function render(sessions: ObservableSession[], columns = 120): string {
|
|
return Bun.stripANSI(renderSubagentHudLines(sessions, columns).join("\n"));
|
|
}
|
|
|
|
describe("subagent HUD lines", () => {
|
|
beforeAll(async () => {
|
|
await initTheme();
|
|
});
|
|
|
|
it("renders running subagents as Id: description under a Subagents header", () => {
|
|
const out = render([
|
|
makeSession({ id: "AuthLoader", description: "Refactoring the auth flow" }),
|
|
makeSession({ id: "SchemaMigrator", description: "Migrating the users table" }),
|
|
]);
|
|
expect(out).toContain("Subagents");
|
|
expect(out).toContain("AuthLoader: Refactoring the auth flow");
|
|
expect(out).toContain("SchemaMigrator: Migrating the users table");
|
|
});
|
|
|
|
it("only shows active subagents and clears once everything finished", () => {
|
|
const finishedStates = ["completed", "failed", "aborted"] as const;
|
|
const sessions: ObservableSession[] = [
|
|
{ id: "main", kind: "main", label: "Main Session", status: "active", lastUpdate: Date.now() },
|
|
...finishedStates.map(status => makeSession({ id: `Done-${status}`, status, description: "old work" })),
|
|
];
|
|
expect(renderSubagentHudLines(sessions, 120)).toEqual([]);
|
|
|
|
const out = render([...sessions, makeSession({ id: "StillRunning", description: "live work" })]);
|
|
expect(out).toContain("StillRunning: live work");
|
|
expect(out).not.toContain("Done-");
|
|
expect(out).not.toContain("Main Session");
|
|
});
|
|
|
|
it("falls back to the description and task carried by progress snapshots", () => {
|
|
const fromProgressDesc = render([
|
|
makeSession({ id: "Worker", progress: makeProgress({ id: "Worker", description: "From progress" }) }),
|
|
]);
|
|
expect(fromProgressDesc).toContain("Worker: From progress");
|
|
|
|
const fromTask = render([
|
|
makeSession({ id: "Worker", progress: makeProgress({ id: "Worker", task: "Investigate flaky CI on macOS" }) }),
|
|
]);
|
|
expect(fromTask).toContain("Worker Investigate flaky CI on macOS");
|
|
});
|
|
|
|
it("renders nested ids as a breadcrumb and truncates long descriptions to the viewport", () => {
|
|
const out = render([makeSession({ id: "Anna.Bob", description: `start ${"x".repeat(300)} end` })], 60);
|
|
expect(out).toContain("Anna>Bob:");
|
|
expect(out).not.toContain("end");
|
|
for (const line of out.split("\n")) {
|
|
expect(Bun.stringWidth(line)).toBeLessThanOrEqual(60);
|
|
}
|
|
});
|
|
});
|