fix(tui,coding-agent): clear fullscreen selection on transcript rebuild (#10567)

Selection coordinates survived session switches and other transcript rebuilds, highlighting unrelated text in the new transcript. Reset selection and multi-click state before re-rendering session entries.

Based on #9310.

fixes #9311

Co-authored-by: xdagiz <xdagiz@protonmail.com>
This commit is contained in:
Christian Klotz 2026-10-06 16:35:24 +02:00 • committed by GitHub
parent 8b5708dbb1
commit 83c9e26453
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 48 additions and 0 deletions

View file

@ -4054,6 +4054,8 @@ export class InteractiveMode {
entries: SessionEntry[],
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
): void {
// Selection coordinates point into the transcript being replaced (#9311).
if (this.renderer instanceof TuiAltScreen) this.renderer.resetTextSelection();
const items = entries.flatMap((entry): RenderSessionItem[] => {
if (entry.type === "custom" || (entry.type === "usage" && entry.kind === "cache_warm")) {
return [entry];

View file

@ -363,6 +363,46 @@ describe("InteractiveMode copy confirmation", () => {
});
});
type RenderSessionEntriesContext = {
renderer: ReturnType<typeof createInteractiveTui>;
renderSessionItems: (items: unknown[]) => void;
};
describe("InteractiveMode transcript rebuild", () => {
it("drops the fullscreen selection when session entries are re-rendered", async () => {
// Regression test for #9311: selection coordinates survived session switches.
const terminal = new RecordingTerminal(40, 4);
const ui = createInteractiveTui({
tuiMode: "fullscreen",
showHardwareCursor: false,
logDirectory: "/tmp",
terminal,
fullscreenCopyOnSelect: false,
});
ui.addChild(new Text("alpha\nbeta\ngamma\ndelta", 0, 0));
const context: RenderSessionEntriesContext = { renderer: ui, renderSessionItems: vi.fn() };
const { renderSessionEntries } = InteractiveMode.prototype as unknown as {
renderSessionEntries(this: RenderSessionEntriesContext, entries: unknown[]): void;
};
ui.start();
try {
await terminal.waitForRender();
terminal.sendInput("\x1b[<0;1;1M");
terminal.sendInput("\x1b[<32;4;2M");
terminal.sendInput("\x1b[<0;4;2m");
await terminal.waitForRender();
expect(ui.hasActiveSelection()).toBe(true);
renderSessionEntries.call(context, []);
expect(ui.hasActiveSelection()).toBe(false);
} finally {
ui.stop();
}
});
});
type StatusEditor = {
embedWorkingStatus: boolean;
setWorkingStatusIndicator: (indicator: StatusIndicator | undefined) => void;

View file

@ -313,6 +313,12 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
return this.copyTextToClipboard(text);
}
/** Drop the text selection and multi-click history, e.g. before the host replaces the transcript. */
resetTextSelection(): void {
this.clearTextSelection();
this.lastClick = undefined;
}
/** The lines of the last rendered frame, one per terminal row, as written to the terminal. */
getScreenLines(): string[] {
return [...this.previousScreen];