fix(coding-agent): configure inline tool image width

closes #3508
This commit is contained in:
Mario Zechner
2026-04-22 00:56:01 +02:00
parent 780d536727
commit 97a38bf652
6 changed files with 57 additions and 3 deletions

View File

@@ -4,6 +4,7 @@
### Fixed ### Fixed
- Added configurable interactive inline image width via `terminal.imageWidthCells` and `/settings`, so tool-output images are no longer hard-capped to 60 terminal cells ([#3508](https://github.com/badlogic/pi-mono/issues/3508))
- Fixed `sessionDir` in `settings.json` to expand `~`, so portable session-directory settings no longer require a shell wrapper ([#3514](https://github.com/badlogic/pi-mono/issues/3514)) - Fixed `sessionDir` in `settings.json` to expand `~`, so portable session-directory settings no longer require a shell wrapper ([#3514](https://github.com/badlogic/pi-mono/issues/3514))
- Fixed parallel tool-call rows to leave the pending state as soon as each tool is finalized, while still appending persisted tool results in assistant source order ([#3503](https://github.com/badlogic/pi-mono/issues/3503)) - Fixed parallel tool-call rows to leave the pending state as soon as each tool is finalized, while still appending persisted tool results in assistant source order ([#3503](https://github.com/badlogic/pi-mono/issues/3503))
- Fixed exported session markdown to render Markdown while showing HTML-like message content such as `<file name="...">...</file>` verbatim, so shared sessions match the TUI instead of letting the browser interpret message text ([#3484](https://github.com/badlogic/pi-mono/issues/3484)) - Fixed exported session markdown to render Markdown while showing HTML-like message content such as `<file name="...">...</file>` verbatim, so shared sessions match the TUI instead of letting the browser interpret message text ([#3484](https://github.com/badlogic/pi-mono/issues/3484))

View File

@@ -108,6 +108,7 @@ When a provider requests a retry delay longer than `maxDelayMs` (e.g., Google's
| Setting | Type | Default | Description | | Setting | Type | Default | Description |
|---------|------|---------|-------------| |---------|------|---------|-------------|
| `terminal.showImages` | boolean | `true` | Show images in terminal (if supported) | | `terminal.showImages` | boolean | `true` | Show images in terminal (if supported) |
| `terminal.imageWidthCells` | number | `60` | Preferred inline image width in terminal cells |
| `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when content shrinks (can cause flicker) | | `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when content shrinks (can cause flicker) |
| `images.autoResize` | boolean | `true` | Resize images to 2000x2000 max | | `images.autoResize` | boolean | `true` | Resize images to 2000x2000 max |
| `images.blockImages` | boolean | `false` | Block all images from being sent to LLM | | `images.blockImages` | boolean | `false` | Block all images from being sent to LLM |

View File

@@ -25,6 +25,7 @@ export interface RetrySettings {
export interface TerminalSettings { export interface TerminalSettings {
showImages?: boolean; // default: true (only relevant if terminal supports images) showImages?: boolean; // default: true (only relevant if terminal supports images)
imageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)
clearOnShrink?: boolean; // default: false (clear empty rows when content shrinks) clearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)
} }
@@ -870,6 +871,23 @@ export class SettingsManager {
this.save(); this.save();
} }
getImageWidthCells(): number {
const width = this.settings.terminal?.imageWidthCells;
if (typeof width !== "number" || !Number.isFinite(width)) {
return 60;
}
return Math.max(1, Math.floor(width));
}
setImageWidthCells(width: number): void {
if (!this.globalSettings.terminal) {
this.globalSettings.terminal = {};
}
this.globalSettings.terminal.imageWidthCells = Math.max(1, Math.floor(width));
this.markModified("terminal", "imageWidthCells");
this.save();
}
getClearOnShrink(): boolean { getClearOnShrink(): boolean {
// Settings takes precedence, then env var, then default false // Settings takes precedence, then env var, then default false
if (this.settings.terminal?.clearOnShrink !== undefined) { if (this.settings.terminal?.clearOnShrink !== undefined) {

View File

@@ -31,6 +31,7 @@ const THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {
export interface SettingsConfig { export interface SettingsConfig {
autoCompact: boolean; autoCompact: boolean;
showImages: boolean; showImages: boolean;
imageWidthCells: number;
autoResizeImages: boolean; autoResizeImages: boolean;
blockImages: boolean; blockImages: boolean;
enableSkillCommands: boolean; enableSkillCommands: boolean;
@@ -56,6 +57,7 @@ export interface SettingsConfig {
export interface SettingsCallbacks { export interface SettingsCallbacks {
onAutoCompactChange: (enabled: boolean) => void; onAutoCompactChange: (enabled: boolean) => void;
onShowImagesChange: (enabled: boolean) => void; onShowImagesChange: (enabled: boolean) => void;
onImageWidthCellsChange: (width: number) => void;
onAutoResizeImagesChange: (enabled: boolean) => void; onAutoResizeImagesChange: (enabled: boolean) => void;
onBlockImagesChange: (blocked: boolean) => void; onBlockImagesChange: (blocked: boolean) => void;
onEnableSkillCommandsChange: (enabled: boolean) => void; onEnableSkillCommandsChange: (enabled: boolean) => void;
@@ -292,10 +294,17 @@ export class SettingsSelectorComponent extends Container {
currentValue: config.showImages ? "true" : "false", currentValue: config.showImages ? "true" : "false",
values: ["true", "false"], values: ["true", "false"],
}); });
items.splice(2, 0, {
id: "image-width-cells",
label: "Image width",
description: "Preferred inline image width in terminal cells",
currentValue: String(config.imageWidthCells),
values: ["60", "80", "120"],
});
} }
// Image auto-resize toggle (always available, affects both attached and read images) // Image auto-resize toggle (always available, affects both attached and read images)
items.splice(supportsImages ? 2 : 1, 0, { items.splice(supportsImages ? 3 : 1, 0, {
id: "auto-resize-images", id: "auto-resize-images",
label: "Auto-resize images", label: "Auto-resize images",
description: "Resize large images to 2000x2000 max for better model compatibility", description: "Resize large images to 2000x2000 max for better model compatibility",
@@ -378,6 +387,9 @@ export class SettingsSelectorComponent extends Container {
case "show-images": case "show-images":
callbacks.onShowImagesChange(newValue === "true"); callbacks.onShowImagesChange(newValue === "true");
break; break;
case "image-width-cells":
callbacks.onImageWidthCellsChange(parseInt(newValue, 10));
break;
case "auto-resize-images": case "auto-resize-images":
callbacks.onAutoResizeImagesChange(newValue === "true"); callbacks.onAutoResizeImagesChange(newValue === "true");
break; break;

View File

@@ -7,6 +7,7 @@ import { theme } from "../theme/theme.js";
export interface ToolExecutionOptions { export interface ToolExecutionOptions {
showImages?: boolean; showImages?: boolean;
imageWidthCells?: number;
} }
export class ToolExecutionComponent extends Container { export class ToolExecutionComponent extends Container {
@@ -23,6 +24,7 @@ export class ToolExecutionComponent extends Container {
private args: any; private args: any;
private expanded = false; private expanded = false;
private showImages: boolean; private showImages: boolean;
private imageWidthCells: number;
private isPartial = true; private isPartial = true;
private toolDefinition?: ToolDefinition<any, any>; private toolDefinition?: ToolDefinition<any, any>;
private builtInToolDefinition?: ToolDefinition<any, any>; private builtInToolDefinition?: ToolDefinition<any, any>;
@@ -54,6 +56,7 @@ export class ToolExecutionComponent extends Container {
this.toolDefinition = toolDefinition; this.toolDefinition = toolDefinition;
this.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName]; this.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];
this.showImages = options.showImages ?? true; this.showImages = options.showImages ?? true;
this.imageWidthCells = options.imageWidthCells ?? 60;
this.ui = ui; this.ui = ui;
this.cwd = cwd; this.cwd = cwd;
@@ -205,6 +208,11 @@ export class ToolExecutionComponent extends Container {
this.updateDisplay(); this.updateDisplay();
} }
setImageWidthCells(width: number): void {
this.imageWidthCells = Math.max(1, Math.floor(width));
this.updateDisplay();
}
override invalidate(): void { override invalidate(): void {
super.invalidate(); super.invalidate();
this.updateDisplay(); this.updateDisplay();
@@ -312,7 +320,7 @@ export class ToolExecutionComponent extends Container {
imageData, imageData,
imageMimeType, imageMimeType,
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) }, { fallbackColor: (s: string) => theme.fg("toolOutput", s) },
{ maxWidthCells: 60 }, { maxWidthCells: this.imageWidthCells },
); );
this.imageComponents.push(imageComponent); this.imageComponents.push(imageComponent);
this.addChild(imageComponent); this.addChild(imageComponent);

View File

@@ -2633,6 +2633,7 @@ export class InteractiveMode {
content.arguments, content.arguments,
{ {
showImages: this.settingsManager.getShowImages(), showImages: this.settingsManager.getShowImages(),
imageWidthCells: this.settingsManager.getImageWidthCells(),
}, },
this.getRegisteredToolDefinition(content.name), this.getRegisteredToolDefinition(content.name),
this.ui, this.ui,
@@ -2701,6 +2702,7 @@ export class InteractiveMode {
event.args, event.args,
{ {
showImages: this.settingsManager.getShowImages(), showImages: this.settingsManager.getShowImages(),
imageWidthCells: this.settingsManager.getImageWidthCells(),
}, },
this.getRegisteredToolDefinition(event.toolName), this.getRegisteredToolDefinition(event.toolName),
this.ui, this.ui,
@@ -3031,7 +3033,10 @@ export class InteractiveMode {
content.name, content.name,
content.id, content.id,
content.arguments, content.arguments,
{ showImages: this.settingsManager.getShowImages() }, {
showImages: this.settingsManager.getShowImages(),
imageWidthCells: this.settingsManager.getImageWidthCells(),
},
this.getRegisteredToolDefinition(content.name), this.getRegisteredToolDefinition(content.name),
this.ui, this.ui,
this.sessionManager.getCwd(), this.sessionManager.getCwd(),
@@ -3650,6 +3655,7 @@ export class InteractiveMode {
{ {
autoCompact: this.session.autoCompactionEnabled, autoCompact: this.session.autoCompactionEnabled,
showImages: this.settingsManager.getShowImages(), showImages: this.settingsManager.getShowImages(),
imageWidthCells: this.settingsManager.getImageWidthCells(),
autoResizeImages: this.settingsManager.getImageAutoResize(), autoResizeImages: this.settingsManager.getImageAutoResize(),
blockImages: this.settingsManager.getBlockImages(), blockImages: this.settingsManager.getBlockImages(),
enableSkillCommands: this.settingsManager.getEnableSkillCommands(), enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
@@ -3684,6 +3690,14 @@ export class InteractiveMode {
} }
} }
}, },
onImageWidthCellsChange: (width) => {
this.settingsManager.setImageWidthCells(width);
for (const child of this.chatContainer.children) {
if (child instanceof ToolExecutionComponent) {
child.setImageWidthCells(width);
}
}
},
onAutoResizeImagesChange: (enabled) => { onAutoResizeImagesChange: (enabled) => {
this.settingsManager.setImageAutoResize(enabled); this.settingsManager.setImageAutoResize(enabled);
}, },