From fbd6b7f9ba85afbe8e43731ff8b4c34e16f922b2 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 22 Jan 2026 04:52:55 +0100 Subject: [PATCH] fix(tui): prevent image ID collisions between modules - allocateImageId() now returns random IDs instead of sequential - Static images no longer auto-allocate IDs (transient display) - Only explicit imageId usage (like DOSBox) gets tracked IDs - Suppress emulators exit logging in DOSBox dispose Fixes image replacement bug when extension and main app both allocated sequential IDs starting at 1. --- .../pi-dosbox/src/dosbox-component.ts | 17 ++++++++++++++++- packages/tui/src/components/image.ts | 12 ------------ packages/tui/src/terminal-image.ts | 19 ++++++++++--------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-component.ts b/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-component.ts index 37b91cbb..327036e1 100644 --- a/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-component.ts +++ b/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-component.ts @@ -206,8 +206,23 @@ export class DosboxComponent implements Component { this.kittyPushed = false; } if (this.ci) { - void this.ci.exit().catch(() => undefined); + // Suppress emulators exit logging + const origLog = console.log; + const origError = console.error; + console.log = () => {}; + console.error = () => {}; + const ci = this.ci; this.ci = null; + void ci + .exit() + .catch(() => undefined) + .finally(() => { + // Restore after a delay to catch async logging + setTimeout(() => { + console.log = origLog; + console.error = origError; + }, 100); + }); } } } diff --git a/packages/tui/src/components/image.ts b/packages/tui/src/components/image.ts index f7753020..ca76cddd 100644 --- a/packages/tui/src/components/image.ts +++ b/packages/tui/src/components/image.ts @@ -1,5 +1,4 @@ import { - deleteKittyImage, getCapabilities, getImageDimensions, type ImageDimensions, @@ -102,15 +101,4 @@ export class Image implements Component { return lines; } - - /** - * Delete the terminal image. Call this when done with the image - * to free terminal resources. - */ - dispose(): void { - if (this.imageId !== undefined) { - process.stdout.write(deleteKittyImage(this.imageId)); - this.imageId = undefined; - } - } } diff --git a/packages/tui/src/terminal-image.ts b/packages/tui/src/terminal-image.ts index 7c0af786..ef48e21b 100644 --- a/packages/tui/src/terminal-image.ts +++ b/packages/tui/src/terminal-image.ts @@ -81,13 +81,14 @@ export function resetCapabilitiesCache(): void { cachedCapabilities = null; } -// Counter for generating unique image IDs -let nextImageId = 1; - +/** + * Generate a random image ID for Kitty graphics protocol. + * Uses random IDs to avoid collisions between different module instances + * (e.g., main app vs extensions). + */ export function allocateImageId(): number { - const id = nextImageId; - nextImageId = (nextImageId % 0xffffffff) + 1; // Wrap around at max uint32 - return id; + // Use random ID in range [1, 0xffffffff] to avoid collisions + return Math.floor(Math.random() * 0xfffffffe) + 1; } export function encodeKitty( @@ -342,9 +343,9 @@ export function renderImage( const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions()); if (caps.images === "kitty") { - const imageId = options.imageId ?? allocateImageId(); - const sequence = encodeKitty(base64Data, { columns: maxWidth, rows, imageId }); - return { sequence, rows, imageId }; + // Only use imageId if explicitly provided - static images don't need IDs + const sequence = encodeKitty(base64Data, { columns: maxWidth, rows, imageId: options.imageId }); + return { sequence, rows, imageId: options.imageId }; } if (caps.images === "iterm2") {