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]
### 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
@@ -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];
}
/**
@@ -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");
});
});