diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index a7884f6a..4cb6b8e5 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -18,6 +18,10 @@ export interface SkillsSettings { enabled?: boolean; // default: true } +export interface TerminalSettings { + showImages?: boolean; // default: true (only relevant if terminal supports images) +} + export interface Settings { lastChangelogVersion?: string; defaultProvider?: string; @@ -33,6 +37,7 @@ export interface Settings { hooks?: string[]; // Array of hook file paths hookTimeout?: number; // Timeout for hook execution in ms (default: 30000) skills?: SkillsSettings; + terminal?: TerminalSettings; } export class SettingsManager { @@ -237,4 +242,16 @@ export class SettingsManager { this.settings.skills.enabled = enabled; this.save(); } + + getShowImages(): boolean { + return this.settings.terminal?.showImages ?? true; + } + + setShowImages(show: boolean): void { + if (!this.settings.terminal) { + this.settings.terminal = {}; + } + this.settings.terminal.showImages = show; + this.save(); + } } diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index 529eb558..b5b3849f 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -30,6 +30,10 @@ function replaceTabs(text: string): string { return text.replace(/\t/g, " "); } +export interface ToolExecutionOptions { + showImages?: boolean; // default: true (only used if terminal supports images) +} + /** * Component that renders a tool call with its result (updateable) */ @@ -39,16 +43,18 @@ export class ToolExecutionComponent extends Container { private toolName: string; private args: any; private expanded = false; + private showImages: boolean; private result?: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError: boolean; details?: any; }; - constructor(toolName: string, args: any) { + constructor(toolName: string, args: any, options: ToolExecutionOptions = {}) { super(); this.toolName = toolName; this.args = args; + this.showImages = options.showImages ?? true; this.addChild(new Spacer(1)); this.contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text)); this.addChild(this.contentText); @@ -94,7 +100,8 @@ export class ToolExecutionComponent extends Container { const caps = getCapabilities(); for (const img of imageBlocks) { - if (caps.images && img.data && img.mimeType) { + // Show inline image only if terminal supports it AND user setting allows it + if (caps.images && this.showImages && img.data && img.mimeType) { const imageComponent = new Image( img.data, img.mimeType, @@ -124,7 +131,8 @@ export class ToolExecutionComponent extends Container { .join("\n"); const caps = getCapabilities(); - if (imageBlocks.length > 0 && !caps.images) { + // Show text fallback if terminal doesn't support images OR if user disabled inline images + if (imageBlocks.length > 0 && (!caps.images || !this.showImages)) { const imageIndicators = imageBlocks .map((img: any) => { const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 3f73bee4..13918eac 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -12,10 +12,12 @@ import { CombinedAutocompleteProvider, type Component, Container, + getCapabilities, Input, Loader, Markdown, ProcessTerminal, + SelectList, Spacer, Text, TruncatedText, @@ -52,7 +54,7 @@ import { ThinkingSelectorComponent } from "./components/thinking-selector.js"; import { ToolExecutionComponent } from "./components/tool-execution.js"; import { UserMessageComponent } from "./components/user-message.js"; import { UserMessageSelectorComponent } from "./components/user-message-selector.js"; -import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "./theme/theme.js"; +import { getEditorTheme, getMarkdownTheme, getSelectListTheme, onThemeChange, setTheme, theme } from "./theme/theme.js"; export class InteractiveMode { private session: AgentSession; @@ -160,6 +162,11 @@ export class InteractiveMode { { name: "resume", description: "Resume a different session" }, ]; + // Add image toggle command only if terminal supports images + if (getCapabilities().images) { + slashCommands.push({ name: "show-images", description: "Toggle inline image display" }); + } + // Load hide thinking block setting this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); @@ -585,6 +592,11 @@ export class InteractiveMode { this.editor.setText(""); return; } + if (text === "/show-images") { + this.handleShowImagesCommand(); + this.editor.setText(""); + return; + } if (text === "/debug") { this.handleDebugCommand(); this.editor.setText(""); @@ -691,7 +703,9 @@ export class InteractiveMode { if (content.type === "toolCall") { if (!this.pendingTools.has(content.id)) { this.chatContainer.addChild(new Text("", 0, 0)); - const component = new ToolExecutionComponent(content.name, content.arguments); + const component = new ToolExecutionComponent(content.name, content.arguments, { + showImages: this.settingsManager.getShowImages(), + }); this.chatContainer.addChild(component); this.pendingTools.set(content.id, component); } else { @@ -731,7 +745,9 @@ export class InteractiveMode { case "tool_execution_start": { if (!this.pendingTools.has(event.toolCallId)) { - const component = new ToolExecutionComponent(event.toolName, event.args); + const component = new ToolExecutionComponent(event.toolName, event.args, { + showImages: this.settingsManager.getShowImages(), + }); this.chatContainer.addChild(component); this.pendingTools.set(event.toolCallId, component); this.ui.requestRender(); @@ -958,7 +974,9 @@ export class InteractiveMode { for (const content of assistantMsg.content) { if (content.type === "toolCall") { - const component = new ToolExecutionComponent(content.name, content.arguments); + const component = new ToolExecutionComponent(content.name, content.arguments, { + showImages: this.settingsManager.getShowImages(), + }); this.chatContainer.addChild(component); if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") { @@ -1634,6 +1652,43 @@ export class InteractiveMode { this.showStatus(`Auto-compaction: ${newState ? "on" : "off"}`); } + private handleShowImagesCommand(): void { + // Only available if terminal supports images + const caps = getCapabilities(); + if (!caps.images) { + this.showWarning("Your terminal does not support inline images"); + return; + } + + const currentValue = this.settingsManager.getShowImages(); + const items = [ + { value: "yes", label: "Yes", description: "Show images inline in terminal" }, + { value: "no", label: "No", description: "Show text placeholder instead" }, + ]; + + const selector = new SelectList(items, 5, getSelectListTheme()); + selector.setSelectedIndex(currentValue ? 0 : 1); + + selector.onSelect = (item) => { + const newValue = item.value === "yes"; + this.settingsManager.setShowImages(newValue); + this.showStatus(`Inline images: ${newValue ? "on" : "off"}`); + this.chatContainer.removeChild(selector); + this.ui.setFocus(this.editor); + this.ui.requestRender(); + }; + + selector.onCancel = () => { + this.chatContainer.removeChild(selector); + this.ui.setFocus(this.editor); + this.ui.requestRender(); + }; + + this.chatContainer.addChild(selector); + this.ui.setFocus(selector); + this.ui.requestRender(); + } + private async executeCompaction(customInstructions?: string, isAuto = false): Promise { // Stop loading animation if (this.loadingAnimation) {