diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8230b30c6..63d22490f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -31,6 +31,9 @@ - Reduced peak memory for persisted subagent revival probes by streaming large file-backed session journals instead of loading the complete journal ([#8117](https://github.com/can1357/oh-my-pi/issues/8117)). - Kept streaming edit previews responsive for large diffs by selecting and highlighting only the visible tail instead of scanning the discarded prefix on every update. +### Fixed + +- Fixed repeated `/btw` panels committing transient frames to native scrollback and replaying conversation history after dismissal. ## [17.3.4] - 2026-08-14 ### Changed diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 4baf76c67..95087cfac 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -390,16 +390,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", () => {