import { describe, expect, it } from "bun:test"; import type { AgentTool } from "@oh-my-pi/pi-agent-core"; import { editToolRenderer } from "@oh-my-pi/pi-coding-agent/edit/renderer"; import { HL_EDIT_SEP } from "@oh-my-pi/pi-coding-agent/hashline/hash"; import { ToolExecutionComponent } from "@oh-my-pi/pi-coding-agent/modes/components/tool-execution"; import * as themeModule from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import type { TUI } from "@oh-my-pi/pi-tui"; async function getUiTheme() { await themeModule.initTheme(false, undefined, undefined, "dark", "light"); const theme = await themeModule.getThemeByName("dark"); expect(theme).toBeDefined(); return theme!; } describe("editToolRenderer", () => { it("shows the target path from partial JSON while edit args stream", async () => { const uiTheme = await getUiTheme(); const component = editToolRenderer.renderCall( { edits: [{}], __partialJson: '{"edits":[{"path":"packages/coding-agent/src/edit/renderer.ts","old_text":"before', }, { expanded: false, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "replace" } }, uiTheme, ); const rendered = Bun.stripANSI(component.render(160).join("\n")); expect(rendered).toContain("packages/coding-agent/src/edit/renderer.ts"); }); it("uses hashline input headers for streaming call path without apply_patch errors", async () => { const uiTheme = await getUiTheme(); const component = editToolRenderer.renderCall( { input: "@packages/coding-agent/src/edit/renderer.ts\n+ EOF\n|// preview", }, { expanded: false, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "hashline" } }, uiTheme, ); const rendered = Bun.stripANSI(component.render(160).join("\n")); expect(rendered).toContain("packages/coding-agent/src/edit/renderer.ts"); expect(rendered).not.toContain("The first line of the patch must be"); }); it("shows hashline envelope input while preview diff is not computable yet", async () => { await getUiTheme(); const uiStub = { requestRender() {} } as unknown as TUI; const hashlineTool = { name: "edit", label: "Edit", mode: "hashline" } as unknown as AgentTool; const component = new ToolExecutionComponent( "edit", { input: [ "*** Begin Patch", "@crates/pi-natives/src/shell.rs", "+ EOF", `${HL_EDIT_SEP}pub fn streaming_preview() {`, ].join("\n"), }, {}, hashlineTool, uiStub, ); const rendered = Bun.stripANSI(component.render(160).join("\n")); expect(rendered).toContain("crates/pi-natives/src/shell.rs"); expect(rendered).toContain("+ EOF"); expect(rendered).toContain(`${HL_EDIT_SEP}pub fn streaming_preview() {`); expect(rendered).not.toContain("*** Begin Patch"); }); it("recognizes compact and quoted hashline input headers", async () => { const uiTheme = await getUiTheme(); const compactComponent = editToolRenderer.renderCall( { input: "@foo bar.ts\n+ BOF\n|// preview", }, { expanded: true, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "hashline" } }, uiTheme, ); const quotedComponent = editToolRenderer.renderCall( { input: "@'baz qux.ts'\n+ BOF\n|// preview", }, { expanded: false, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "hashline" } }, uiTheme, ); const compactRendered = Bun.stripANSI(compactComponent.render(160).join("\n")); const quotedRendered = Bun.stripANSI(quotedComponent.render(160).join("\n")); expect(compactRendered).toContain("foo bar.ts"); expect(quotedRendered).toContain("baz qux.ts"); }); it("uses hashline input headers for completed single-file result path", async () => { const uiTheme = await getUiTheme(); const component = editToolRenderer.renderResult( { content: [{ type: "text", text: "Updated packages/coding-agent/src/edit/renderer.ts" }], details: { diff: "+1|// preview", op: "update", }, }, { expanded: false, isPartial: false, renderContext: { editMode: "hashline" } }, uiTheme, { input: "@packages/coding-agent/src/edit/renderer.ts\n+ EOF\n|// preview", }, ); const rendered = Bun.stripANSI(component.render(160).join("\n")); expect(rendered).toContain("packages/coding-agent/src/edit/renderer.ts"); expect(rendered).not.toContain(" …"); }); });