174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test"
|
|
import { For } from "solid-js"
|
|
import { testRender, type JSX } from "@opentui/solid"
|
|
import {
|
|
formatCompletedSubagentDetail,
|
|
formatSubagentToolcalls,
|
|
InlineToolRow,
|
|
} from "../../../src/cli/cmd/tui/routes/session/index"
|
|
|
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined
|
|
|
|
afterEach(() => {
|
|
testSetup?.renderer.destroy()
|
|
testSetup = undefined
|
|
})
|
|
|
|
type ToolFixture = { icon: string; label: string; error?: string }
|
|
|
|
const tools: readonly ToolFixture[] = [
|
|
{
|
|
icon: "✱",
|
|
label:
|
|
'Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.*dir|xdg|APPDATA" in packages/opencode/src (151 matches)',
|
|
},
|
|
{
|
|
icon: "✱",
|
|
label: 'Glob "**/*db*" in packages/opencode (6 matches)',
|
|
},
|
|
{
|
|
icon: "→",
|
|
label: "Read packages/opencode/src/storage/db.ts [offset=1, limit=130]",
|
|
},
|
|
{
|
|
icon: "→",
|
|
label: "Read packages/opencode/src/index.ts [offset=1, limit=100]",
|
|
error: "No LSP server available for this file type.",
|
|
},
|
|
{
|
|
icon: "✱",
|
|
label:
|
|
'Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.Path\\.data|data =" in packages/opencode/src (115 matches)',
|
|
},
|
|
] as const
|
|
|
|
function ShellOutput() {
|
|
return (
|
|
<box id="tool-block-shell" marginTop={1} paddingTop={1} paddingBottom={1} paddingLeft={2} gap={1}>
|
|
<text paddingLeft={3}># List files</text>
|
|
<box gap={1}>
|
|
<text>$ ls</text>
|
|
<text>file.ts</text>
|
|
</box>
|
|
</box>
|
|
)
|
|
}
|
|
|
|
function UserMessage() {
|
|
return (
|
|
<box id="message-user">
|
|
<box paddingTop={1} paddingBottom={1} paddingLeft={2}>
|
|
<text>Check whether the next tool remains separated.</text>
|
|
</box>
|
|
</box>
|
|
)
|
|
}
|
|
|
|
function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" }) {
|
|
return (
|
|
<box flexDirection="column" width={72}>
|
|
<box flexDirection="column">
|
|
{props.before === "shell" && <ShellOutput />}
|
|
{props.before === "user" && <UserMessage />}
|
|
<For each={tools}>
|
|
{(item) => (
|
|
<InlineToolRow
|
|
icon={item.icon}
|
|
complete={true}
|
|
pending=""
|
|
failed={Boolean(item.error)}
|
|
error={item.error}
|
|
errorExpanded={props.errorExpanded}
|
|
separateAfter={(id) => id === "message-user"}
|
|
>
|
|
{item.label}
|
|
</InlineToolRow>
|
|
)}
|
|
</For>
|
|
</box>
|
|
</box>
|
|
)
|
|
}
|
|
|
|
function SubagentGroupFixture() {
|
|
return (
|
|
<box flexDirection="column" width={72}>
|
|
<InlineToolRow id="tool-inline-before" icon="✱" complete={true} pending="">
|
|
Grep "Task" (2 matches)
|
|
</InlineToolRow>
|
|
<InlineToolRow id="tool-inline-subagent-one" icon="⠙" complete={true} pending="" subagent={true}>
|
|
Explore Task — Inspect active task spacing
|
|
</InlineToolRow>
|
|
<InlineToolRow id="tool-inline-subagent-two" icon="✓" complete={true} pending="" subagent={true}>
|
|
{"General Task — Confirm completed task spacing\n↳ 1 toolcall · 501ms"}
|
|
</InlineToolRow>
|
|
<InlineToolRow id="tool-inline-after" icon="→" complete={true} pending="">
|
|
Read src/cli/cmd/tui/routes/session/index.tsx
|
|
</InlineToolRow>
|
|
</box>
|
|
)
|
|
}
|
|
|
|
function LoadedReadBeforeSubagentFixture() {
|
|
return (
|
|
<box flexDirection="column" width={72}>
|
|
<InlineToolRow id="tool-inline-read" icon="→" complete={true} pending="">
|
|
Read src/cli/cmd/tui/routes/session/index.tsx
|
|
</InlineToolRow>
|
|
<box id="tool-inline-loaded-read-child" paddingLeft={3}>
|
|
<text paddingLeft={3}>↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx</text>
|
|
</box>
|
|
<InlineToolRow id="tool-inline-subagent-after-read" icon="✓" complete={true} pending="" subagent={true}>
|
|
{"Explore Task — Inspect active task spacing\n↳ 1 toolcall · 501ms"}
|
|
</InlineToolRow>
|
|
</box>
|
|
)
|
|
}
|
|
|
|
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
|
|
testSetup = await testRender(component, options)
|
|
await testSetup.renderOnce()
|
|
await Bun.sleep(25)
|
|
await testSetup.renderOnce()
|
|
|
|
return testSetup
|
|
.captureCharFrame()
|
|
.split("\n")
|
|
.map((line) => line.trimEnd())
|
|
.join("\n")
|
|
.trimEnd()
|
|
}
|
|
|
|
describe("TUI inline tool wrapping", () => {
|
|
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("snapshots consecutive grep, glob, and read rows at a narrow width", async () => {
|
|
expect(await renderFrame(() => <Fixture />, { width: 72, height: 12 })).toMatchSnapshot()
|
|
})
|
|
|
|
test("snapshots expanded tool errors under the tool text", async () => {
|
|
expect(await renderFrame(() => <Fixture errorExpanded />, { width: 72, height: 12 })).toMatchSnapshot()
|
|
})
|
|
|
|
test("keeps separation after a shell output block", async () => {
|
|
expect(await renderFrame(() => <Fixture before="shell" />, { width: 72, height: 16 })).toMatchSnapshot()
|
|
})
|
|
|
|
test("keeps separation after a padded user message", async () => {
|
|
expect(await renderFrame(() => <Fixture before="user" />, { width: 72, height: 14 })).toMatchSnapshot()
|
|
})
|
|
|
|
test("separates a contiguous subagent group from inline tools", async () => {
|
|
expect(await renderFrame(() => <SubagentGroupFixture />, { width: 72, height: 10 })).toMatchSnapshot()
|
|
})
|
|
|
|
test("separates a subagent group after an expanded read", async () => {
|
|
expect(await renderFrame(() => <LoadedReadBeforeSubagentFixture />, { width: 72, height: 8 })).toMatchSnapshot()
|
|
})
|
|
})
|