import { afterEach, describe, expect, test } from "bun:test" import { createSignal, For, Show } from "solid-js" import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core" import { testRender, type JSX } from "@opentui/solid" import { formatCompletedSubagentDetail, formatSubagentRetry, formatSubagentTitle, formatSubagentToolcalls, InlineToolRow, parseApplyPatchFiles, parseDiagnostics, parseQuestionAnswers, parseQuestions, parseTodos, alwaysSeparate, toolDisplay, } from "../../../src/routes/session" let testSetup: Awaited> | undefined afterEach(() => { testSetup?.renderer.destroy() testSetup = undefined }) type ToolFixture = { icon: string; label: string; error?: string } const tools: readonly ToolFixture[] = [ { icon: "✱", label: 'Grep "KIRINCODE.*DB|database|sqlite|drizzle|dev.*db|data.*dir|xdg|APPDATA" in packages/kirincode/src (151 matches)', }, { icon: "✱", label: 'Glob "**/*db*" in packages/opencode (6 matches)', }, { icon: "→", label: "Read packages/kirincode/src/storage/db.ts [offset=1, limit=130]", }, { icon: "→", label: "Read packages/kirincode/src/index.ts [offset=1, limit=100]", error: "No LSP server available for this file type.", }, { icon: "✱", label: 'Grep "export const KIRINCODE_DB|KIRINCODE_DB|KIRINCODE_DEV|Global\\.Path\\.data|data =" in packages/kirincode/src (115 matches)', }, ] as const function ShellOutput() { return ( alwaysSeparate.add(el)} marginTop={1} paddingTop={1} paddingBottom={1} paddingLeft={2} gap={1} > $ ls file.ts ) } function UserMessage() { return ( alwaysSeparate.add(el)}> Check whether the next tool remains separated. ) } function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" }) { return ( {props.before === "shell" && } {props.before === "user" && } {(item) => ( {item.label} )} ) } function TaskRowsFixture() { return ( Grep "Task" (2 matches) Explore Task — Inspect active task spacing {"General Task — Confirm completed task spacing\n↳ 1 toolcall · 501ms"} Read src/cli/cmd/tui/routes/session/index.tsx ) } function LoadedReadBeforeTaskFixture() { return ( Read src/cli/cmd/tui/routes/session/index.tsx ↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx {"Explore Task — Inspect active task spacing\n↳ 1 toolcall · 501ms"} ) } function AssistantSummaryBeforeInlineFixture() { return ( alwaysSeparate.add(el)} paddingLeft={3}> ▣ Build · Little Frank · 53.1s {"Build Task — Review changes\n↳ 48 toolcalls · 1m 40s"} ) } function AssistantErrorBeforeInlineFixture() { return ( alwaysSeparate.add(el)} border={["left"]} paddingTop={1} paddingBottom={1} paddingLeft={2} > Managed inference requires an active Member plan {"Build Task — Review changes\n↳ 48 toolcalls · 1m 40s"} ) } function StickyScrollFixture(props: { separated: boolean; scroll: (scroll: ScrollBoxRenderable) => void }) { return ( First row Second row alwaysSeparate.add(el)}> Assistant text Read src/cli/cmd/tui/routes/session/index.tsx ) } function FailedPendingToolFixture() { return ( Patch ) } function FailedCompleteToolFixture() { return ( Read src/index.ts ) } async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) { testSetup = await testRender(component, options) await testSetup.renderOnce() await testSetup.renderOnce() return testSetup .captureCharFrame() .split("\n") .map((line) => line.trimEnd()) .join("\n") .trimEnd() } describe("TUI inline tool wrapping", () => { test("falls back for unknown tool names", () => { expect(toolDisplay("bash")).toBe("bash") expect(toolDisplay("plugin_tool")).toBe("generic") }) test("replaces pending copy when a tool fails before completion", async () => { const frame = await renderFrame(() => , { width: 72, height: 3 }) expect(frame).toContain("Patch failed") expect(frame).not.toContain("Preparing patch") }) test("preserves useful completed copy when a tool fails", async () => { const frame = await renderFrame(() => , { width: 72, height: 3 }) expect(frame).toContain("Read src/index.ts") expect(frame).not.toContain("Read failed") }) test("filters malformed nested tool wire data", () => { expect( parseApplyPatchFiles([ null, { type: "add" }, { type: "add", relativePath: "a.ts", filePath: "a.ts", patch: "diff", deletions: 0 }, ]), ).toEqual([ { type: "add", relativePath: "a.ts", filePath: "a.ts", patch: "diff", deletions: 0, movePath: undefined }, ]) expect(parseTodos([null, { status: "pending" }, { status: "pending", content: "Safe" }])).toEqual([ { status: "pending", content: "Safe" }, ]) expect(parseQuestions([{}, { question: 1 }, { question: "Continue?" }])).toEqual([{ question: "Continue?" }]) expect(parseQuestionAnswers([null, ["yes", 1], "no"])).toEqual([[], ["yes"], []]) expect(parseQuestionAnswers({})).toBeUndefined() }) test("ignores diagnostics with malformed nested ranges", () => { expect( parseDiagnostics( { "a.ts": [ { severity: 1, message: "missing range" }, { severity: 1, message: "bad line", range: { start: { line: "0", character: 1 } } }, { severity: 1, message: "valid", range: { start: { line: 2, character: 3 } } }, ], }, "a.ts", ), ).toEqual([{ message: "valid", range: { start: { line: 2, character: 3 } } }]) }) test("formats completed subagent toolcall details", () => { expect(formatCompletedSubagentDetail(0, "501ms")).toBe("501ms") expect(formatCompletedSubagentDetail(1, "501ms")).toBe("1 toolcall · 501ms") expect(formatCompletedSubagentDetail(2, "501ms")).toBe("2 toolcalls · 501ms") expect(formatSubagentToolcalls(0)).toBe("0 toolcalls") }) test("keeps background state attached to the subagent identity", () => { expect(formatSubagentTitle("Explore", "Inspect renderer", false)).toBe("Explore Task — Inspect renderer") expect(formatSubagentTitle("Explore", "Inspect renderer", true)).toBe( "Explore Task (background) — Inspect renderer", ) }) test("keeps retry status ahead of wrapping messages", () => { expect(formatSubagentRetry(2, "Rate limited by provider")).toBe("Retrying (attempt 2) · Rate limited by provider") }) test("snapshots consecutive grep, glob, and read rows at a narrow width", async () => { expect(await renderFrame(() => , { width: 72, height: 12 })).toMatchSnapshot() }) test("snapshots expanded tool errors under the tool text", async () => { expect(await renderFrame(() => , { width: 72, height: 12 })).toMatchSnapshot() }) test("keeps separation after a shell output block", async () => { expect(await renderFrame(() => , { width: 72, height: 16 })).toMatchSnapshot() }) test("keeps separation after a padded user message", async () => { expect(await renderFrame(() => , { width: 72, height: 14 })).toMatchSnapshot() }) test("separates after a multi-line task row", async () => { expect(await renderFrame(() => , { width: 72, height: 10 })).toMatchSnapshot() }) test("separates a task row from a preceding inline detail", async () => { expect(await renderFrame(() => , { width: 72, height: 8 })).toMatchSnapshot() }) test("separates an inline row from the previous assistant summary", async () => { expect(await renderFrame(() => , { width: 72, height: 5 })).toMatchSnapshot() }) test("separates an inline row from the previous assistant error", async () => { expect(await renderFrame(() => , { width: 72, height: 7 })).toMatchSnapshot() }) test("updates sticky-bottom geometry when a text separator mounts and unmounts", async () => { const [separated, setSeparated] = createSignal(false) let scroll: ScrollBoxRenderable | undefined testSetup = await testRender( () => (scroll = value)} />, { width: 72, height: 3, }, ) await testSetup.renderOnce() expect(scroll?.scrollHeight).toBe(3) expect(scroll?.scrollTop).toBe(Math.max(0, scroll!.scrollHeight - scroll!.viewport.height)) setSeparated(true) await testSetup.renderOnce() expect(scroll?.scrollHeight).toBe(5) expect(scroll?.scrollTop).toBe(Math.max(0, scroll!.scrollHeight - scroll!.viewport.height)) setSeparated(false) await testSetup.renderOnce() expect(scroll?.scrollHeight).toBe(3) expect(scroll?.scrollTop).toBe(Math.max(0, scroll!.scrollHeight - scroll!.viewport.height)) }) })