Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/tui/src/app-keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
14 changes: 14 additions & 0 deletions packages/tui/src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
20 changes: 20 additions & 0 deletions packages/tui/test/image-budget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down