From 45b023cd831688bc18761fccadac8abfe509e47e Mon Sep 17 00:00:00 2001 From: Ant39140 Date: Thu, 13 Aug 2026 22:13:37 +0800 Subject: [PATCH] fix(tui): keep btw panels out of scrollback --- packages/coding-agent/CHANGELOG.md | 4 ++ .../src/modes/interactive-mode.ts | 12 +++-- packages/tui/test/component-render.test.ts | 45 +++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 14334fddd..ed6d72538 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed repeated `/btw` panels committing transient frames to native scrollback and replaying conversation history after dismissal. + ## [17.3.0] - 2026-08-13 ### Breaking Changes diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index acf8f49d4..226af077b 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -376,16 +376,20 @@ export interface InteractiveModeOptions { /** * Anchored live-region container for the HUD/status rows between the transcript * and the editor (working loader, todo + subagent HUDs, transient notification - * panels). While it has content every row is live: it reports a seam at 0 so the - * engine never commits these anchored, rebuilt-in-place rows to native - * scrollback — otherwise stale duplicates pile up above the live copy on short - * terminals once the loader sits below a tall HUD. The transcript's own seam, + * panels). While it has content every row is live: it reports a seam at 0 and + * pins that live region so the engine never commits these anchored, + * rebuilt-in-place rows to native scrollback — otherwise stale duplicates pile + * up above the live copy on short terminals once the loader sits below a tall HUD. The transcript's own seam, * when present, sits higher and wins (topmost-seam merge in TUI.render). */ class AnchoredLiveContainer extends Container implements NativeScrollbackLiveRegion { getNativeScrollbackLiveRegionStart(): number | undefined { return this.children.length > 0 ? 0 : undefined; } + + isNativeScrollbackLiveRegionPinned(): boolean { + return true; + } } /** diff --git a/packages/tui/test/component-render.test.ts b/packages/tui/test/component-render.test.ts index 0d6fd6950..15ad5cfc3 100644 --- a/packages/tui/test/component-render.test.ts +++ b/packages/tui/test/component-render.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { type Component, Container, + CURSOR_MARKER, Editor, type Focusable, type NativeScrollbackCommittedRows, @@ -71,6 +72,10 @@ class AnchoredStatusContainer extends Container implements NativeScrollbackLiveR const hasAnchoredRows = this.children.length > 0; return hasAnchoredRows ? 0 : undefined; } + + isNativeScrollbackLiveRegionPinned(): boolean { + return true; + } } function strip(rows: string[]): string[] { @@ -351,6 +356,46 @@ describe("TUI.requestComponentRender", () => { await term.flush(); } }); + it("keeps removed pinned panels and repeated transcript copies out of scrollback", async () => { + const term = new VirtualTerminal(40, 8, 1_000); + const scheduler = new StressRenderScheduler(); + const tui = new TUI(term, undefined, { renderScheduler: scheduler }); + const markers = Array.from({ length: 5 }, (_unused, index) => `HIST-${index}`); + const transcript = new CountingLines(markers); + const status = new AnchoredStatusContainer(); + const editor = new CountingLines([`editor${CURSOR_MARKER}`]); + tui.addChild(transcript); + tui.addChild(status); + tui.addChild(editor); + + try { + tui.start(); + await scheduler.drain(term); + for (let cycle = 1; cycle <= 3; cycle++) { + const panel = new CountingLines([`panel-${cycle}-0`]); + status.addChild(panel); + tui.requestRender(); + await scheduler.drain(term); + for (let tick = 1; tick <= 8; tick++) { + panel.set(Array.from({ length: tick + 1 }, (_row, index) => `panel-${cycle}-${tick}-${index}`)); + tui.requestComponentRender(panel); + await scheduler.drain(term); + } + status.clear(); + tui.requestRender(); + await scheduler.drain(term); + } + + const buffer = strip(term.getScrollBuffer()).join("\n"); + for (const marker of markers) { + expect(buffer.split(marker)).toHaveLength(2); + } + expect(buffer).not.toContain("panel-"); + } finally { + tui.stop(); + await term.flush(); + } + }); }); describe("TUI keystroke-scoped render", () => {