import assert from "node:assert/strict"; import test from "node:test"; import { createBashToolDefinition, createCodemodeExtension, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createPowerShellToolDefinition, createReadToolDefinition, createWriteToolDefinition, type CodemodeToolDetails, type ToolDefinition, type ToolRendererResolver, type ToolRenderers, } from "@selesai/code"; import { createToolHtmlRenderer } from "../../../../dist/core/export-html/tool-renderer.js"; import { registerToolDisplayOverrides } from "../src/tool-overrides.ts"; import { DEFAULT_TOOL_DISPLAY_CONFIG, type ToolDisplayConfig } from "../src/types.ts"; import { resolveToolRenderers } from "./tool-renderer-harness.ts"; const THEME = { fg: (_color: string, value: string): string => value, bold: (value: string): string => value, } as never; function createCodemodeHostRenderers(): ToolRenderers { let renderers: ToolRenderers | undefined; const factory = createCodemodeExtension({ models: false }); factory({ registerTool(tool: ToolRenderers): void { renderers = tool; }, getSettings: () => ({}), getAllTools: () => [], appendEntry(): void {}, } as never); if (!renderers) throw new Error("codemode extension did not register its definition"); return renderers; } function createHostDefinitions(cwd: string): Map { const definitions = [ createReadToolDefinition(cwd), createGrepToolDefinition(cwd), createFindToolDefinition(cwd), createLsToolDefinition(cwd), createBashToolDefinition(cwd), createPowerShellToolDefinition(cwd), createEditToolDefinition(cwd), createWriteToolDefinition(cwd), ]; const byName = new Map(definitions.map((definition) => [definition.name, definition])); byName.set("codemode", createCodemodeHostRenderers() as never); return byName; } function createExportRenderer(cwd: string, config: ToolDisplayConfig = DEFAULT_TOOL_DISPLAY_CONFIG) { const registered: ToolRendererResolver[] = []; const api = { registerTool(): void { // Built-ins are never registered. }, registerToolRenderer(resolver: ToolRendererResolver): void { registered.push(resolver); }, on(): void {}, getAllTools(): unknown[] { return []; }, getCommands(): unknown[] { return []; }, } as never; registerToolDisplayOverrides(api, () => config); const definitions = createHostDefinitions(cwd); return createToolHtmlRenderer({ getToolRenderers: (name: string) => resolveToolRenderers(registered, name, () => definitions.get(name) as never), theme: THEME, cwd, width: 80, }); } test("HTML export renders boxed built-in calls and results through the resolver", () => { const renderer = createExportRenderer(process.cwd(), { ...DEFAULT_TOOL_DISPLAY_CONFIG, readOutputMode: "summary", }); const callHtml = renderer.renderCall("call-1", "read", { path: "src/app.ts" }); assert.ok(callHtml, "read call renders to HTML"); assert.match(callHtml, /read/); assert.match(callHtml, /src\/app\.ts/); const result = renderer.renderResult( "call-1", "read", [{ type: "text", text: "one\ntwo\n" }], { truncation: { truncated: false, totalLines: 2 } }, false, ); assert.ok(result, "read result renders to HTML"); assert.match(result.collapsed ?? "", /loaded 2 lines/); assert.match(result.expanded ?? "", /one/); assert.match(result.expanded ?? "", /two/); const editCall = renderer.renderCall("call-edit", "edit", { path: "demo.txt", oldText: "old line", newText: "new line", }); assert.ok(editCall, "edit call renders to HTML"); assert.match(editCall, /demo\.txt/); const editResult = renderer.renderResult( "call-edit", "edit", [{ type: "text", text: "Edited demo.txt" }], { diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1 +1 @@\n-old line\n+new line\n", patch: "", }, false, ); assert.ok(editResult, "edit result renders to HTML"); assert.match(editResult.expanded ?? "", />oldnew { const renderer = createExportRenderer(process.cwd()); const first = renderer.renderCall("bash-1", "bash", { command: "echo hi" }); assert.ok(first, "bash call renders to HTML"); assert.match(first, /echo hi/); // Spinner frames render only when the export context reports a partial call. assert.match(first, /⠋/); const second = renderer.renderCall("bash-1", "bash", { command: "echo hi" }); assert.equal(second, first, "reusing the last component keeps the call stable"); }); test("HTML export spinner timer is unref'd so the process can exit", () => { const originalSetInterval = globalThis.setInterval; const originalClearInterval = globalThis.clearInterval; const intervals: ReturnType[] = []; globalThis.setInterval = ((fn: (...args: unknown[]) => unknown, ms?: number) => { const id = originalSetInterval(fn as never, ms); intervals.push(id); return id; }) as typeof globalThis.setInterval; try { const renderer = createExportRenderer(process.cwd()); const html = renderer.renderCall("bash-unref", "bash", { command: "sleep 1" }); assert.ok(html); assert.ok(intervals.length > 0, "spinner interval created for the partial call"); for (const interval of intervals) { assert.equal( (interval as unknown as { hasRef(): boolean }).hasRef(), false, "spinner interval must not keep the process alive", ); } } finally { for (const interval of intervals) { originalClearInterval(interval); } globalThis.setInterval = originalSetInterval; globalThis.clearInterval = originalClearInterval; } }); test("HTML export renders codemode collapsed then expanded without throwing and is idempotent", () => { const renderer = createExportRenderer(process.cwd(), { ...DEFAULT_TOOL_DISPLAY_CONFIG, codemodeDisplay: true, }); const callHtml = renderer.renderCall("codemode-1", "codemode", { code: "const answer = 42;" }); assert.ok(callHtml, "codemode call renders to HTML (renderer did not throw)"); assert.match(callHtml, /codemode/); assert.match(callHtml, /const answer = 42/); const calls = Array.from({ length: 12 }, (_, index) => ({ id: `codemode-1/${index}`, name: "read", args: JSON.stringify({ path: `src/file-${index}.ts` }), status: "ok", durationMs: 5 + index, result: { text: `content ${index}`, textLines: 1, textBytes: 9, truncated: false, images: 0, isError: false, }, })); const content = [ { type: "text", text: "Script completed\nWall time 0.1 seconds\nOutput:\n" }, { type: "text", text: "script output" }, ]; const details = { calls } as CodemodeToolDetails; const first = renderer.renderResult("codemode-1", "codemode", content, details, false); assert.ok(first, "codemode result renders to HTML (renderer did not throw)"); for (let index = 0; index < 12; index++) { assert.match(first.collapsed ?? first.expanded ?? "", new RegExp(`file-${index}\\.ts`)); assert.match(first.expanded ?? "", new RegExp(`"path": "src/file-${index}\\.ts"`)); } assert.match(first.expanded ?? "", /script output/); const second = renderer.renderResult("codemode-1", "codemode", content, details, false); assert.ok(second, "second render did not throw"); assert.equal(second.expanded, first.expanded, "expanded lastComponent reuse is idempotent"); assert.equal(second.collapsed ?? second.expanded, first.collapsed ?? first.expanded); });