feat(tui): added viewport-only transcript tail rendering and deferred resize repaint
- Added tail-only transcript rendering via `renderViewportTail`, stopping at `maxRows` and returning `EMPTY_TAIL`. - Added `ViewportTailProvider`/`asViewportTailProvider` API and resize getters for viewport state. - Implemented viewport-only painting during non-mux resize with deferred full repaint after settle. - Added settle-resize helpers and coverage for deferred repaint timing and cancel paths.
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
type NativeScrollbackCommittedRows,
|
||||
type NativeScrollbackLiveRegion,
|
||||
type RenderStablePrefix,
|
||||
type ViewportTailProvider,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
|
||||
const kSnapshot = Symbol("transcript.liveDiffSnapshot");
|
||||
@@ -139,6 +140,8 @@ interface BlockSegment {
|
||||
}
|
||||
|
||||
const EMPTY_SEGMENTS: BlockSegment[] = [];
|
||||
/** Shared empty result for an empty viewport-tail render (no allocation). */
|
||||
const EMPTY_TAIL: readonly string[] = [];
|
||||
|
||||
interface LiveCommitState {
|
||||
appendOnly: boolean;
|
||||
@@ -415,7 +418,7 @@ function deriveLiveCommitState(
|
||||
*/
|
||||
export class TranscriptContainer
|
||||
extends Container
|
||||
implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix
|
||||
implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider
|
||||
{
|
||||
// Bumped to retire every block's diff snapshot at once (theme change /
|
||||
// clear); a snapshot is only honored when its stored generation matches.
|
||||
@@ -520,6 +523,49 @@ export class TranscriptContainer
|
||||
return index === children.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render only the bottom `maxRows` rows of the transcript at `width`, walking
|
||||
* blocks from the last toward the first and stopping the instant enough rows
|
||||
* are collected — blocks above the fold are never rendered. The engine's
|
||||
* resize viewport fast path uses this so a drag (a SIGWINCH burst, each event
|
||||
* a fresh width that misses every per-width cache) re-lays-out only the
|
||||
* handful of visible blocks instead of the whole history every event.
|
||||
*
|
||||
* State-isolated by contract: touches none of the persistent full-compose
|
||||
* fields (#lines, #segments, the per-block diff snapshots, the commit/stable
|
||||
* bookkeeping), so the authoritative full render on settle reconciles exactly
|
||||
* as if this never ran. Calling each block's render() still warms its own
|
||||
* per-width cache, which that settle render then reuses for free.
|
||||
*
|
||||
* Consecutive visible blocks are joined by exactly one blank separator, the
|
||||
* same rule render() applies, so the result equals the bottom of a full
|
||||
* render except for an at-most-one-row separator on the topmost included
|
||||
* block — a transient discrepancy the settle paint overwrites.
|
||||
*/
|
||||
renderViewportTail(width: number, maxRows: number): readonly string[] {
|
||||
width = Math.max(1, width);
|
||||
if (maxRows <= 0) return EMPTY_TAIL;
|
||||
const collected: (readonly string[])[] = [];
|
||||
let total = 0;
|
||||
for (let i = this.children.length - 1; i >= 0 && total < maxRows; i--) {
|
||||
const contribution = stripPlainBlankEdges(this.children[i]!.render(width));
|
||||
if (contribution.length === 0) continue;
|
||||
// One blank separator sits between this block and the (already
|
||||
// collected) visible block below it.
|
||||
if (collected.length > 0) total += 1;
|
||||
collected.push(contribution);
|
||||
total += contribution.length;
|
||||
}
|
||||
if (collected.length === 0) return EMPTY_TAIL;
|
||||
const rows: string[] = [];
|
||||
for (let k = collected.length - 1; k >= 0; k--) {
|
||||
if (rows.length > 0) rows.push("");
|
||||
const body = collected[k]!;
|
||||
for (let j = 0; j < body.length; j++) rows.push(body[j]!);
|
||||
}
|
||||
return rows.length > maxRows ? rows.slice(rows.length - maxRows) : rows;
|
||||
}
|
||||
|
||||
override render(width: number): readonly string[] {
|
||||
width = Math.max(1, width);
|
||||
this.#nativeScrollbackLiveRegionStart = undefined;
|
||||
|
||||
Reference in New Issue
Block a user