fix(tui): render xdev tool images inline
This commit is contained in:
@@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user