fix(tui): render xdev tool images inline

This commit is contained in:
Vu Anh Nguyen
2026-08-18 12:09:39 +07:00
parent 644ad30d6e
commit cea6ecd3d7
3 changed files with 63 additions and 8 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased] ## [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 ## [17.3.7] - 2026-08-17
### Changed ### Changed
@@ -14,7 +14,7 @@ import {
Text, Text,
type TUI, type TUI,
} from "@oh-my-pi/pi-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 { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
import type { Theme } from "../../modes/theme/theme"; import type { Theme } from "../../modes/theme/theme";
import { getThemeEpoch, 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( function displaceableToolName(
toolName: string, toolName: string,
result: { details?: unknown; isError?: boolean }, 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. * Get all image blocks from result content and details.
* Some tools (like generate_image) store images in details to avoid bloating model context. * 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 []; if (!this.#result) return [];
const contentImages = this.#result.content?.filter((c: any) => c.type === "image") || []; const contentImages = this.#result.content.filter(block => block.type === "image");
const detailImages = this.#result.details?.images || []; const details = this.#result.details;
return [...contentImages, ...detailImages]; const detailImages = imageBlocksFromDetails(details);
const xdevImages = isRecord(details) && isRecord(details.xdev) ? imageBlocksFromDetails(details.xdev.inner) : [];
return [...contentImages, ...detailImages, ...xdevImages];
} }
/** /**
@@ -12,7 +12,7 @@ const IMAGE: ImageContent = {
}; };
const originalProtocol = TERMINAL.imageProtocol; const originalProtocol = TERMINAL.imageProtocol;
describe("Kitty non-PNG conversion failures", () => { describe("Tool image rendering", () => {
beforeAll(async () => { beforeAll(async () => {
await initTheme(); await initTheme();
}); });
@@ -48,4 +48,36 @@ describe("Kitty non-PNG conversion failures", () => {
component.updateResult({ content: [IMAGE] }, false); component.updateResult({ content: [IMAGE] }, false);
expect(requestRender).not.toHaveBeenCalled(); 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");
});
}); });