From 21c68b929824b7f902a3bc402223d959cfaeb239 Mon Sep 17 00:00:00 2001 From: F0Rextasy Date: Mon, 21 Sep 2026 02:09:31 +0300 Subject: [PATCH] fix(tui): retransmit dropped Kitty payloads on demand Fixes #12595 --- docs/keybindings.md | 1 + .../src/modes/controllers/input-controller.ts | 5 +++++ packages/tui/src/app-keybindings.ts | 1 + packages/tui/src/tui.ts | 14 +++++++++++++ packages/tui/test/image-budget.test.ts | 20 +++++++++++++++++++ 5 files changed, 41 insertions(+) diff --git a/docs/keybindings.md b/docs/keybindings.md index d80f3df4491..d45adf477e1 100644 --- a/docs/keybindings.md +++ b/docs/keybindings.md @@ -48,6 +48,7 @@ app.history.search: [] | `app.stt.toggle` | Unbound (hold `Space`) | Toggle speech-to-text. By default there is no key chord — hold the space bar to record (push-to-talk) and release to transcribe; bind a chord here for a press-to-toggle alternative | | `app.live.toggle` | `Ctrl+L` | Start or stop live voice mode (same as `/live`) | | `app.agents.hub` | `Alt+A` | [Open the Agent Hub](./agent-hub.md) | +| `app.images.retransmit` | Unbound | Re-send image data the terminal may have dropped (e.g. Kitty payloads emitted while a tmux window was hidden); repairs blank placeholders on repaint | ## Recover a cleared prompt diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index 596fe64c2c2..cf4972b8238 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -390,6 +390,11 @@ export class InputController { this.toggleToolActivityVisibility(); return { consume: true }; } + if (this.ctx.keybindings.matches(data, "app.images.retransmit")) { + if (this.ctx.ui.hasOverlay()) return undefined; + this.ctx.ui.retransmitInlineImages(); + return { consume: true }; + } return undefined; }); } diff --git a/packages/tui/src/app-keybindings.ts b/packages/tui/src/app-keybindings.ts index 258939da8af..b70ee31ec6a 100644 --- a/packages/tui/src/app-keybindings.ts +++ b/packages/tui/src/app-keybindings.ts @@ -62,6 +62,7 @@ interface AppKeybindings { "app.history.search": true; "app.stt.toggle": true; "app.live.toggle": true; + "app.images.retransmit": true; } /** Application action identifier registered alongside the base TUI keybindings. */ diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 5b54b1d1ee5..7a8e47eca16 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -992,6 +992,20 @@ export class TUI extends Container { } } + /** + * Retransmit image data the terminal may have dropped — e.g. Kitty payloads + * emitted while a tmux window was hidden, where tmux replays the + * placeholders on selection without the image data (issue #12595). Drops + * transmit tracking so the next render re-sends resident payloads with + * their placements, then forces a repaint. + */ + retransmitInlineImages(): void { + if (this.#stopped) return; + if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return; + this.#imageBudget.forgetTransmitted(); + this.requestRender(); + } + getShowHardwareCursor(): boolean { return this.#showHardwareCursor; } diff --git a/packages/tui/test/image-budget.test.ts b/packages/tui/test/image-budget.test.ts index e3bb868eec0..bd268f16a1b 100644 --- a/packages/tui/test/image-budget.test.ts +++ b/packages/tui/test/image-budget.test.ts @@ -296,6 +296,26 @@ describe("ImageBudget", () => { expect(after.purge).toEqual([]); }); }); +describe("ImageBudget hidden-tmux-window recovery (issue #12595)", () => { + it("retransmits after forgetTransmitted so a dropped Kitty payload repairs on repaint", () => { + const budget = new ImageBudget(3, () => {}); + budget.beginPass(); + budget.enqueueTransmit(7, "TX7"); + expect(budget.shouldTransmit(7)).toBe(false); + expect(budget.takeTransmits()).toEqual(["TX7"]); + + // The hidden tmux window dropped the transmit: placeholders replay + // without image data, and the budget still believes it sent. + expect(budget.shouldTransmit(7)).toBe(false); + + // Window visible again: forget + repaint re-queues the payload. + budget.forgetTransmitted(); + expect(budget.shouldTransmit(7)).toBe(true); + budget.beginPass(); + budget.enqueueTransmit(7, "TX7"); + expect(budget.takeTransmits()).toEqual(["TX7"]); + }); +}); describe("encodeKittyDeleteImage", () => { it("emits an APC delete-by-id that frees the image and suppresses the reply", () => {