import { describe, expect, it } from "bun:test"; import { TranscriptContainer } from "@oh-my-pi/pi-coding-agent/modes/components/transcript-container"; import { type Component, TERMINAL } from "@oh-my-pi/pi-tui"; type MutableTerminalInfo = { eagerEraseScrollbackRisk: boolean }; const mutableTerminalInfo = TERMINAL as unknown as MutableTerminalInfo; async function withTerminalRisk(risk: boolean, run: () => T | Promise): Promise { const saved = TERMINAL.eagerEraseScrollbackRisk; mutableTerminalInfo.eagerEraseScrollbackRisk = risk; try { return await run(); } finally { mutableTerminalInfo.eagerEraseScrollbackRisk = saved; } } class MutableLiveBlock implements Component { #lines: string[]; constructor(lines: string[]) { this.#lines = [...lines]; } render(width: number): string[] { return this.#lines.map(line => line.slice(0, width)); } setLines(lines: string[]): void { this.#lines = [...lines]; } isTranscriptBlockFinalized(): boolean { return false; } } describe("transcript streaming commit (assistant text)", () => { it("treats in-place growth of the trailing line as append-only", async () => { await withTerminalRisk(true, () => { const chat = new TranscriptContainer(); // Models a streaming assistant reply: stable head rows plus a current // line that grows token-by-token without adding a new row. const block = new MutableLiveBlock(["para one", "para two", "the quick brown"]); chat.addChild(block); chat.render(80); block.setLines(["para one", "para two", "the quick brown fox"]); chat.render(80); // The head rows never changed; only the trailing line grew. Its scrolled- // off head must be committable to native scrollback (tmux pane history). expect(chat.getNativeScrollbackCommitSafeEnd()).toBe(3); }); }); });