mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 18:05:11 +00:00
Fix non-PNG images not displaying in Kitty protocol terminals
JPEG/GIF/WebP images were not rendering in terminals using the Kitty graphics protocol (Kitty, Ghostty, WezTerm) because it requires PNG format (f=100). Non-PNG images are now converted to PNG using sharp before being sent to the terminal.
This commit is contained in:
parent
9b0ec02405
commit
79c56475e0
3 changed files with 80 additions and 3 deletions
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- JPEG/GIF/WebP images not displaying in terminals using Kitty graphics protocol (Kitty, Ghostty, WezTerm). The protocol requires PNG format, so non-PNG images are now converted before display.
|
||||||
|
|
||||||
## [0.32.2] - 2026-01-03
|
## [0.32.2] - 2026-01-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import stripAnsi from "strip-ansi";
|
||||||
import type { CustomTool } from "../../../core/custom-tools/types.js";
|
import type { CustomTool } from "../../../core/custom-tools/types.js";
|
||||||
import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff.js";
|
import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff.js";
|
||||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
|
||||||
|
import { convertToPng } from "../../../utils/image-convert.js";
|
||||||
import { sanitizeBinaryOutput } from "../../../utils/shell.js";
|
import { sanitizeBinaryOutput } from "../../../utils/shell.js";
|
||||||
import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
|
import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
|
||||||
import { renderDiff } from "./diff.js";
|
import { renderDiff } from "./diff.js";
|
||||||
|
|
@ -68,6 +69,8 @@ export class ToolExecutionComponent extends Container {
|
||||||
// Cached edit diff preview (computed when args arrive, before tool executes)
|
// Cached edit diff preview (computed when args arrive, before tool executes)
|
||||||
private editDiffPreview?: EditDiffResult | EditDiffError;
|
private editDiffPreview?: EditDiffResult | EditDiffError;
|
||||||
private editDiffArgsKey?: string; // Track which args the preview is for
|
private editDiffArgsKey?: string; // Track which args the preview is for
|
||||||
|
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
||||||
|
private convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
toolName: string,
|
toolName: string,
|
||||||
|
|
@ -157,6 +160,39 @@ export class ToolExecutionComponent extends Container {
|
||||||
this.result = result;
|
this.result = result;
|
||||||
this.isPartial = isPartial;
|
this.isPartial = isPartial;
|
||||||
this.updateDisplay();
|
this.updateDisplay();
|
||||||
|
// Convert non-PNG images to PNG for Kitty protocol (async)
|
||||||
|
this.maybeConvertImagesForKitty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert non-PNG images to PNG for Kitty graphics protocol.
|
||||||
|
* Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
|
||||||
|
*/
|
||||||
|
private maybeConvertImagesForKitty(): void {
|
||||||
|
const caps = getCapabilities();
|
||||||
|
// Only needed for Kitty protocol
|
||||||
|
if (caps.images !== "kitty") return;
|
||||||
|
if (!this.result) return;
|
||||||
|
|
||||||
|
const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
|
||||||
|
|
||||||
|
for (let i = 0; i < imageBlocks.length; i++) {
|
||||||
|
const img = imageBlocks[i];
|
||||||
|
if (!img.data || !img.mimeType) continue;
|
||||||
|
// Skip if already PNG or already converted
|
||||||
|
if (img.mimeType === "image/png") continue;
|
||||||
|
if (this.convertedImages.has(i)) continue;
|
||||||
|
|
||||||
|
// Convert async
|
||||||
|
const index = i;
|
||||||
|
convertToPng(img.data, img.mimeType).then((converted) => {
|
||||||
|
if (converted) {
|
||||||
|
this.convertedImages.set(index, converted);
|
||||||
|
this.updateDisplay();
|
||||||
|
this.ui.requestRender();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setExpanded(expanded: boolean): void {
|
setExpanded(expanded: boolean): void {
|
||||||
|
|
@ -249,14 +285,25 @@ export class ToolExecutionComponent extends Container {
|
||||||
const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
|
const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
|
||||||
const caps = getCapabilities();
|
const caps = getCapabilities();
|
||||||
|
|
||||||
for (const img of imageBlocks) {
|
for (let i = 0; i < imageBlocks.length; i++) {
|
||||||
|
const img = imageBlocks[i];
|
||||||
if (caps.images && this.showImages && img.data && img.mimeType) {
|
if (caps.images && this.showImages && img.data && img.mimeType) {
|
||||||
|
// Use converted PNG for Kitty protocol if available
|
||||||
|
const converted = this.convertedImages.get(i);
|
||||||
|
const imageData = converted?.data ?? img.data;
|
||||||
|
const imageMimeType = converted?.mimeType ?? img.mimeType;
|
||||||
|
|
||||||
|
// For Kitty, skip non-PNG images that haven't been converted yet
|
||||||
|
if (caps.images === "kitty" && imageMimeType !== "image/png") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const spacer = new Spacer(1);
|
const spacer = new Spacer(1);
|
||||||
this.addChild(spacer);
|
this.addChild(spacer);
|
||||||
this.imageSpacers.push(spacer);
|
this.imageSpacers.push(spacer);
|
||||||
const imageComponent = new Image(
|
const imageComponent = new Image(
|
||||||
img.data,
|
imageData,
|
||||||
img.mimeType,
|
imageMimeType,
|
||||||
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
||||||
{ maxWidthCells: 60 },
|
{ maxWidthCells: 60 },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
26
packages/coding-agent/src/utils/image-convert.ts
Normal file
26
packages/coding-agent/src/utils/image-convert.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Convert image to PNG format for terminal display.
|
||||||
|
* Kitty graphics protocol requires PNG format (f=100).
|
||||||
|
*/
|
||||||
|
export async function convertToPng(
|
||||||
|
base64Data: string,
|
||||||
|
mimeType: string,
|
||||||
|
): Promise<{ data: string; mimeType: string } | null> {
|
||||||
|
// Already PNG, no conversion needed
|
||||||
|
if (mimeType === "image/png") {
|
||||||
|
return { data: base64Data, mimeType };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sharp = (await import("sharp")).default;
|
||||||
|
const buffer = Buffer.from(base64Data, "base64");
|
||||||
|
const pngBuffer = await sharp(buffer).png().toBuffer();
|
||||||
|
return {
|
||||||
|
data: pngBuffer.toString("base64"),
|
||||||
|
mimeType: "image/png",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Sharp not available or conversion failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue