diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2f7b500ff..2a1c954d4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed images returned by tools mounted under `xd://` rendering only as file links instead of inline terminal graphics. + ## [17.3.7] - 2026-08-17 ### Changed diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index 37ccd198f..b1f15d507 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -14,7 +14,7 @@ import { Text, type TUI, } from "@oh-my-pi/pi-tui"; -import { getProjectDir, logger, sanitizeText } from "@oh-my-pi/pi-utils"; +import { getProjectDir, isRecord, logger, sanitizeText } from "@oh-my-pi/pi-utils"; import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit"; import type { Theme } from "../../modes/theme/theme"; import { getThemeEpoch, theme } from "../../modes/theme/theme"; @@ -73,6 +73,21 @@ function isTodoToolDetails(details: unknown): details is TodoToolDetails { ); } +interface ToolImageBlock { + data?: string; + mimeType?: string; +} + +function imageBlocksFromDetails(details: unknown): ToolImageBlock[] { + if (!isRecord(details) || !Array.isArray(details.images)) return []; + return details.images.filter( + (image): image is ToolImageBlock => + isRecord(image) && + (image.data === undefined || typeof image.data === "string") && + (image.mimeType === undefined || typeof image.mimeType === "string"), + ); +} + function displaceableToolName( toolName: string, result: { details?: unknown; isError?: boolean }, @@ -617,14 +632,18 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac } /** - * Get all image blocks from result content and details.images. - * Some tools (like generate_image) store images in details to avoid bloating model context. + * Get all image blocks from result content and details. + * Some tools (like generate_image) store images in details to avoid bloating + * model context. Xdev-dispatched tools preserve those details under + * details.xdev.inner. */ - #getAllImageBlocks(): Array<{ data?: string; mimeType?: string }> { + #getAllImageBlocks(): ToolImageBlock[] { if (!this.#result) return []; - const contentImages = this.#result.content?.filter((c: any) => c.type === "image") || []; - const detailImages = this.#result.details?.images || []; - return [...contentImages, ...detailImages]; + const contentImages = this.#result.content.filter(block => block.type === "image"); + const details = this.#result.details; + const detailImages = imageBlocksFromDetails(details); + const xdevImages = isRecord(details) && isRecord(details.xdev) ? imageBlocksFromDetails(details.xdev.inner) : []; + return [...contentImages, ...detailImages, ...xdevImages]; } /** diff --git a/packages/coding-agent/test/kitty-image-conversion.test.ts b/packages/coding-agent/test/kitty-image-conversion.test.ts index 5aaa8aa41..6965023a5 100644 --- a/packages/coding-agent/test/kitty-image-conversion.test.ts +++ b/packages/coding-agent/test/kitty-image-conversion.test.ts @@ -12,7 +12,7 @@ const IMAGE: ImageContent = { }; const originalProtocol = TERMINAL.imageProtocol; -describe("Kitty non-PNG conversion failures", () => { +describe("Tool image rendering", () => { beforeAll(async () => { await initTheme(); }); @@ -48,4 +48,36 @@ describe("Kitty non-PNG conversion failures", () => { component.updateResult({ content: [IMAGE] }, false); expect(requestRender).not.toHaveBeenCalled(); }); + + it("surfaces images returned through xdev write results", () => { + const component = new ToolExecutionComponent( + "write", + { path: "xd://generate_image" }, + { showImages: true }, + undefined, + { + requestRender: vi.fn(), + requestComponentRender: vi.fn(), + resetDisplay: vi.fn(), + }, + ); + + component.updateResult( + { + content: [{ type: "text", text: "Generated 1 image" }], + details: { + xdev: { + tool: "generate_image", + mode: "execute", + inner: { + images: [{ data: IMAGE.data, mimeType: "image/png" }], + }, + }, + }, + }, + false, + ); + + expect(component.render(80).join("\n")).toContain("\x1b_G"); + }); });