From 11239140db802ccb165df40a53d18d4927482841 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sun, 5 Apr 2026 10:30:13 -0700 Subject: [PATCH] fix(tui): break infinite re-render loop for images in cmux --- .../interactive/components/tool-execution.ts | 24 +++++++++++++++---- packages/pi-tui/src/components/image.ts | 5 ++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts index 10bd5f02c..e07fcb7c7 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts @@ -3,6 +3,7 @@ import { Container, getCapabilities, Image, + type ImageDimensions, imageFallback, Spacer, Text, @@ -88,6 +89,9 @@ export class ToolExecutionComponent extends Container { 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 = new Map(); + // Cached resolved image dimensions to avoid re-triggering async parsing + // when updateDisplay() recreates Image components (#3455). + private resolvedImageDimensions: Map = new Map(); // Incremental syntax highlighting cache for write tool call args private writeHighlightCache?: WriteHighlightCache; // When true, this component intentionally renders no lines @@ -472,16 +476,28 @@ export class ToolExecutionComponent extends Container { const spacer = new Spacer(1); this.addChild(spacer); this.imageSpacers.push(spacer); + // Pass cached dimensions to avoid re-triggering async parsing + // when updateDisplay() recreates Image components (#3455). + const cachedDims = this.resolvedImageDimensions.get(i); const imageComponent = new Image( imageData, imageMimeType, { fallbackColor: (s: string) => theme.fg("toolOutput", s) }, { maxWidthCells: 60 }, + cachedDims, ); - imageComponent.setOnDimensionsResolved(() => { - this.updateDisplay(); - this.ui.requestRender(); - }); + if (!cachedDims) { + const imgIdx = i; + imageComponent.setOnDimensionsResolved(() => { + // Cache resolved dimensions so future updateDisplay() calls + // don't re-trigger async parsing → infinite loop (#3455). + const dims = imageComponent.getDimensions?.(); + if (dims) this.resolvedImageDimensions.set(imgIdx, dims); + // Just re-render — don't call updateDisplay() which would + // destroy and recreate all Image components. + this.ui.requestRender(); + }); + } this.imageComponents.push(imageComponent); this.addChild(imageComponent); } diff --git a/packages/pi-tui/src/components/image.ts b/packages/pi-tui/src/components/image.ts index c789a0a5b..814167605 100644 --- a/packages/pi-tui/src/components/image.ts +++ b/packages/pi-tui/src/components/image.ts @@ -72,6 +72,11 @@ export class Image implements Component { return this.imageId; } + /** Get the resolved image dimensions (for caching across recreations). */ + getDimensions(): ImageDimensions | undefined { + return this.dimensionsResolved ? this.dimensions : undefined; + } + invalidate(): void { this.cachedLines = undefined; this.cachedWidth = undefined;