import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { registerAgents } from "../src/agent-types.js"; import subagentsExtension from "../src/index.js"; import type { AgentConfig, AgentRecord } from "../src/types.js"; import { type AgentActivity, AgentWidget } from "../src/ui/agent-widget.js"; import { ConversationViewer } from "../src/ui/conversation-viewer.js"; import { FleetList, type FleetUICtx } from "../src/ui/fleet-list.js"; const TYPE = "colored-reviewer"; const DISPLAY_NAME = "Code Reviewer"; const PURPLE_BACKGROUND = "\u001b[48;2;130;125;189m"; const config: AgentConfig = { name: TYPE, displayName: DISPLAY_NAME, color: "purple", description: "Reviews code", extensions: false, skills: false, systemPrompt: "Review code.", promptMode: "replace", }; const theme = { fg: (color: string, text: string) => `<${color}>${text}`, bold: (text: string) => `*${text}*`, getBgAnsi: (color: string) => `<${color}>`, getColorMode: () => "truecolor" as const, }; type RenderedComponent = { render(width?: number): string[] }; type WidgetFactory = ( tui: { terminal: { columns: number; rows?: number }; requestRender: ReturnType }, activeTheme: typeof theme, ) => RenderedComponent; type SessionHandler = (...args: unknown[]) => unknown; interface RegisteredTool { name: string; renderCall( args: Record, activeTheme: typeof theme, context: { isPartial: boolean; isError: boolean }, ): RenderedComponent; } function registerColoredReviewer(color = "purple"): void { registerAgents(new Map([[TYPE, { ...config, color }]])); } function makeRecord(): AgentRecord { return { id: "review-1", type: TYPE, description: "Review this change", status: "running", toolUses: 0, startedAt: Date.now(), session: { messages: [], subscribe: vi.fn(() => vi.fn()), } as unknown as AgentRecord["session"], lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 }, compactionCount: 0, }; } function makeActivity(): AgentActivity { return { activeTools: new Map(), toolUses: 0, responseText: "", turnCount: 1, lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 }, }; } function makePi() { const tools = new Map(); const handlers = new Map(); const pi = { registerMessageRenderer: vi.fn(), registerTool: vi.fn((tool: unknown) => { const registered = tool as RegisteredTool; tools.set(registered.name, registered); }), registerCommand: vi.fn(), on: vi.fn((event: string, handler: unknown) => handlers.set(event, handler as SessionHandler)), events: { emit: vi.fn(), on: vi.fn(() => vi.fn()) }, appendEntry: vi.fn(), sendMessage: vi.fn(), } as unknown as Parameters[0]; return { pi, tools, handlers }; } beforeEach(() => { registerColoredReviewer(); }); afterEach(() => { registerAgents(new Map()); }); describe("custom agent color runtime surfaces", () => { it("renders the registered Agent tool call header with the display name and color", async () => { const { pi, tools, handlers } = makePi(); subagentsExtension(pi); registerColoredReviewer(); try { const tool = tools.get("Agent"); if (!tool) throw new Error("Agent tool was not registered"); const render = (context: { isPartial: boolean; isError: boolean }) => tool.renderCall( { subagent_type: TYPE, description: "Review this change" }, theme, context, ).render(120).join("\n"); const output = render({ isPartial: false, isError: false }); expect(output).toContain(DISPLAY_NAME); expect(output).toContain(PURPLE_BACKGROUND); // The row tint is opened by the line itself and restored after the badge, so the // line reads the same whether or not the caller paints one (HTML export does not). expect(output.indexOf("")).toBeLessThan(output.indexOf(PURPLE_BACKGROUND)); expect(output.split("")).toHaveLength(3); // opened once, restored after the badge // Left open on purpose: Box pads to width and then wraps, so a reset here would // leave the padding untinted. Nothing after the badge may close the background. expect(output).not.toContain("\u001b[49m"); expect(render({ isPartial: true, isError: false })).toContain(""); expect(render({ isPartial: false, isError: true })).toContain(""); const missingType = tool.renderCall( { description: "Review this change" }, theme, { isPartial: false, isError: false }, ).render(120).join("\n"); expect(missingType).toContain("*Agent*"); expect(missingType).not.toContain(PURPLE_BACKGROUND); // An agent without a color must render the pre-badge line byte for byte: // no badge, and no row background of our own for HTML export to pick up. registerAgents(new Map([[TYPE, { ...config, color: undefined }]])); const uncolored = render({ isPartial: false, isError: false }); expect(uncolored.trimEnd()).toBe(`▸ *${DISPLAY_NAME}* Review this change`); } finally { await handlers.get("session_shutdown")?.({}, { hasUI: false, ui: {} }); } }); it("renders the above-editor Agent widget with the display name and color", () => { const record = makeRecord(); const widget = new AgentWidget( { listAgents: () => [record] } as unknown as ConstructorParameters[0], new Map([[record.id, makeActivity()]]), () => "all", ); let factory: WidgetFactory | undefined; let placement: string | undefined; widget.setUICtx({ setStatus: vi.fn(), setWidget: (_key, content, options) => { if (typeof content === "function") factory = content as WidgetFactory; placement = options?.placement; }, }); try { widget.update(); const output = factory?.( { terminal: { columns: 120 }, requestRender: vi.fn() }, theme, ).render().join("\n"); expect(placement).toBe("aboveEditor"); expect(output).toContain(DISPLAY_NAME); expect(output).toContain(PURPLE_BACKGROUND); } finally { widget.dispose(); } }); it("renders the FleetView row with the display name and color", () => { const record = makeRecord(); const manager = { listAgents: () => [record], abort: vi.fn(() => true), steer: vi.fn(() => true), } as unknown as ConstructorParameters[0]; const fleet = new FleetList(manager, new Map()); let factory: WidgetFactory | undefined; fleet.setUICtx({ setWidget: (_key, content) => { if (typeof content === "function") factory = content as WidgetFactory; }, onTerminalInput: vi.fn(() => vi.fn()), getEditorText: vi.fn(() => ""), notify: vi.fn(), custom: (() => new Promise(() => {})) as FleetUICtx["custom"], }); try { fleet.update(); const output = factory?.( { requestRender: vi.fn(), terminal: { columns: 120, rows: 40 } }, theme, ).render(120).join("\n"); expect(output).toContain(DISPLAY_NAME); expect(output).toContain(PURPLE_BACKGROUND); registerColoredReviewer("invalid"); const fallback = factory?.( { requestRender: vi.fn(), terminal: { columns: 120, rows: 40 } }, theme, ).render(120).join("\n"); expect(fallback).toContain(`${DISPLAY_NAME}`); expect(fallback).not.toContain(PURPLE_BACKGROUND); } finally { fleet.dispose(); } }); it("renders the conversation viewer header with the display name and color", () => { const record = makeRecord(); const viewer = new ConversationViewer( { terminal: { rows: 30, columns: 120 }, requestRender: vi.fn() } as unknown as ConstructorParameters[0], record.session!, record, undefined, theme, vi.fn(), ); try { const output = viewer.render(120).join("\n"); expect(output).toContain(DISPLAY_NAME); expect(output).toContain(PURPLE_BACKGROUND); } finally { viewer.dispose(); } }); });