Merge PR #8444: fix(tui): keep btw panels out of scrollback (@Ant39140)

This commit is contained in:
can1357
2026-08-16 02:13:38 +02:00
3 changed files with 56 additions and 4 deletions
+3
View File
@@ -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
@@ -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;
}
}
/**
@@ -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", () => {