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.
This commit is contained in:
Mario Zechner 2026-01-22 04:52:55 +01:00
parent df1d5c40ea
commit fbd6b7f9ba
3 changed files with 26 additions and 22 deletions

View file

@ -206,8 +206,23 @@ export class DosboxComponent implements Component {
this.kittyPushed = false; this.kittyPushed = false;
} }
if (this.ci) { 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; 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);
});
} }
} }
} }

View file

@ -1,5 +1,4 @@
import { import {
deleteKittyImage,
getCapabilities, getCapabilities,
getImageDimensions, getImageDimensions,
type ImageDimensions, type ImageDimensions,
@ -102,15 +101,4 @@ export class Image implements Component {
return lines; 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;
}
}
} }

View file

@ -81,13 +81,14 @@ export function resetCapabilitiesCache(): void {
cachedCapabilities = null; 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 { export function allocateImageId(): number {
const id = nextImageId; // Use random ID in range [1, 0xffffffff] to avoid collisions
nextImageId = (nextImageId % 0xffffffff) + 1; // Wrap around at max uint32 return Math.floor(Math.random() * 0xfffffffe) + 1;
return id;
} }
export function encodeKitty( export function encodeKitty(
@ -342,9 +343,9 @@ export function renderImage(
const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions()); const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
if (caps.images === "kitty") { if (caps.images === "kitty") {
const imageId = options.imageId ?? allocateImageId(); // Only use imageId if explicitly provided - static images don't need IDs
const sequence = encodeKitty(base64Data, { columns: maxWidth, rows, imageId }); const sequence = encodeKitty(base64Data, { columns: maxWidth, rows, imageId: options.imageId });
return { sequence, rows, imageId }; return { sequence, rows, imageId: options.imageId };
} }
if (caps.images === "iterm2") { if (caps.images === "iterm2") {