refactor(modes): removed scrollback compaction from TranscriptContainer
- Removed `#compactedChildStart` state, `compactable` segment field, and `#compactCommittedPrefix()` method, stopping the local-frame pruning of committed finalized rows. - Post-finalize mutations now surface on every render via version tracking, instead of requiring a destructive replay to rehydrate the compacted block. - Changed `print-mode.ts` to use `session.getLastAssistantMessage()` instead of array tail read, fixing access to terminal assistant messages after compaction removal.
This commit is contained in:
@@ -3,7 +3,6 @@ import {
|
||||
Container,
|
||||
type NativeScrollbackCommittedRows,
|
||||
type NativeScrollbackLiveRegion,
|
||||
type NativeScrollbackReplay,
|
||||
type RenderStablePrefix,
|
||||
type ViewportTailProvider,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
@@ -19,17 +18,11 @@ interface FinalizableBlock {
|
||||
/**
|
||||
* Monotonic content version for blocks that can still mutate *after*
|
||||
* reporting finalized (e.g. `AssistantMessageComponent`: the inline error
|
||||
* restored at the next turn's `agent_start`, late tool-result images). While
|
||||
* the block's rows are still on screen (not yet fully committed), the
|
||||
* committed-scrollback render bypass replays its previous rows only when the
|
||||
* version is unchanged; a bump forces a real render so the TUI's
|
||||
* committed-prefix audit can observe and re-anchor the change. Once the rows
|
||||
* fully commit to native scrollback they are dropped from the local frame
|
||||
* (compacted) — a later mutation no longer recommits on an ordinary frame
|
||||
* (immutable history the terminal owns; recommitting would duplicate it) and
|
||||
* instead rehydrates on the next destructive full replay
|
||||
* ({@link "@oh-my-pi/pi-tui".NativeScrollbackReplay}). Blocks that never
|
||||
* mutate post-finalize simply omit the method.
|
||||
* restored at the next turn's `agent_start`, late tool-result images). The
|
||||
* committed-scrollback render bypass only replays a block's previous rows
|
||||
* when the version is unchanged; without this signal a post-finalize
|
||||
* mutation would stay invisible until a global invalidation. Blocks that
|
||||
* never mutate post-finalize simply omit the method.
|
||||
*/
|
||||
getTranscriptBlockVersion?(): number;
|
||||
/**
|
||||
@@ -130,8 +123,6 @@ interface BlockSegment {
|
||||
sep: number;
|
||||
/** Whether the block reported finalized when this segment was rendered. */
|
||||
finalized: boolean;
|
||||
/** Safe to drop from the local frame once its rows fully commit to native scrollback: produced while finalized. */
|
||||
compactable: boolean;
|
||||
/** Block version observed when this segment was rendered (see {@link FinalizableBlock}). */
|
||||
version: number | undefined;
|
||||
}
|
||||
@@ -167,12 +158,7 @@ const EMPTY_TAIL: readonly string[] = [];
|
||||
*/
|
||||
export class TranscriptContainer
|
||||
extends Container
|
||||
implements
|
||||
NativeScrollbackLiveRegion,
|
||||
NativeScrollbackCommittedRows,
|
||||
NativeScrollbackReplay,
|
||||
RenderStablePrefix,
|
||||
ViewportTailProvider
|
||||
implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider
|
||||
{
|
||||
// Bumped to retire every block segment at once (theme change / clear); a
|
||||
// segment is only reused when its stored generation matches.
|
||||
@@ -190,14 +176,6 @@ export class TranscriptContainer
|
||||
// Finalized blocks wholly before this boundary are immutable on-screen history;
|
||||
// their previous contribution can be replayed without calling render().
|
||||
#committedRows = 0;
|
||||
// Leading children whose rows were handed to native scrollback and dropped
|
||||
// from the local frame. Children remain owned by the session and can be
|
||||
// re-rendered when the TUI prepares a destructive full replay.
|
||||
#compactedChildStart = 0;
|
||||
// Suppresses re-compaction for the rehydrating render. The TUI feeds its old
|
||||
// committed-row count immediately before render, so resetting that count
|
||||
// alone cannot distinguish a replay from an ordinary update.
|
||||
#replayPending = false;
|
||||
// Stable-prefix floor accumulated across renders since the last
|
||||
// getRenderStablePrefixRows() read (see RenderStablePrefix: reading
|
||||
// consumes the report and re-bases the baseline). Out-of-band renders
|
||||
@@ -213,14 +191,12 @@ export class TranscriptContainer
|
||||
override clear(): void {
|
||||
this.#generation++;
|
||||
super.clear();
|
||||
this.#compactedChildStart = 0;
|
||||
this.#committedRows = 0;
|
||||
this.#replayPending = false;
|
||||
}
|
||||
|
||||
override setNativeScrollbackCommittedRows(rows: number): void {
|
||||
this.#committedRows = Number.isFinite(rows) ? Math.max(0, Math.trunc(rows)) : 0;
|
||||
for (let i = this.#compactedChildStart; i < this.children.length; i++) {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
const child = this.children[i]!;
|
||||
const segment = this.#segments[i];
|
||||
if (segment === undefined || segment.component !== child) continue;
|
||||
@@ -243,18 +219,6 @@ export class TranscriptContainer
|
||||
}
|
||||
}
|
||||
|
||||
override prepareNativeScrollbackReplay(): void {
|
||||
// Replay retires the old terminal tape, so descendants may discard layout
|
||||
// locks whose only purpose was keeping that immutable history byte-stable.
|
||||
super.prepareNativeScrollbackReplay();
|
||||
if (this.#compactedChildStart === 0) return;
|
||||
this.#compactedChildStart = 0;
|
||||
this.#replayPending = true;
|
||||
this.#generation++;
|
||||
this.#lines.length = 0;
|
||||
this.#stableRowsFloor = 0;
|
||||
}
|
||||
|
||||
getRenderStablePrefixRows(): number {
|
||||
const value = Math.min(this.#stableRowsFloor, this.#lines.length);
|
||||
this.#stableRowsFloor = this.#lines.length;
|
||||
@@ -275,14 +239,8 @@ export class TranscriptContainer
|
||||
* committed rows and is safely removable.
|
||||
*/
|
||||
isBlockUncommitted(component: Component): boolean {
|
||||
const index = this.children.indexOf(component);
|
||||
// Compacted prefix is already committed native history and must not be
|
||||
// retracted. Compacted slots may be sparse holes after a later re-render
|
||||
// (render only fills from #compactedChildStart), so the loop below must
|
||||
// skip undefined entries.
|
||||
if (index >= 0 && index < this.#compactedChildStart) return false;
|
||||
for (const segment of this.#segments) {
|
||||
if (segment === undefined || segment.component !== component) continue;
|
||||
if (segment.component !== component) continue;
|
||||
return segment.rowCount === 0 || segment.startRow >= this.#committedRows;
|
||||
}
|
||||
return true;
|
||||
@@ -336,7 +294,7 @@ export class TranscriptContainer
|
||||
if (maxRows <= 0) return EMPTY_TAIL;
|
||||
const collected: (readonly string[])[] = [];
|
||||
let total = 0;
|
||||
for (let i = this.children.length - 1; i >= this.#compactedChildStart && total < maxRows; i--) {
|
||||
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
|
||||
@@ -360,7 +318,6 @@ export class TranscriptContainer
|
||||
this.#nativeScrollbackLiveRegionStart = undefined;
|
||||
|
||||
const count = this.children.length;
|
||||
if (this.#compactedChildStart > count) this.#compactedChildStart = count;
|
||||
|
||||
// Seal displaceable snapshots whose rows are already on the tape (per the
|
||||
// previous frame's segments — the geometry the committed count was
|
||||
@@ -370,7 +327,7 @@ export class TranscriptContainer
|
||||
// frame, and every frame so a block that BECAME displaceable after its
|
||||
// pending-preview rows committed (late result on a scrolled-off call) is
|
||||
// caught too.
|
||||
for (let i = this.#compactedChildStart; i < count && i < this.#segments.length; i++) {
|
||||
for (let i = 0; i < count && i < this.#segments.length; i++) {
|
||||
const previous = this.#segments[i];
|
||||
if (previous === undefined) continue;
|
||||
if (previous.startRow >= this.#committedRows) break;
|
||||
@@ -386,7 +343,7 @@ export class TranscriptContainer
|
||||
// reaches.
|
||||
let liveStartIndex = -1;
|
||||
let hasLiveBlock = false;
|
||||
for (let i = this.#compactedChildStart; i < count; i++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (!isBlockFinalized(this.children[i]!)) {
|
||||
liveStartIndex = i;
|
||||
hasLiveBlock = true;
|
||||
@@ -418,7 +375,7 @@ export class TranscriptContainer
|
||||
// Frame row cursor: rows emitted (reused or pushed) so far.
|
||||
let row = 0;
|
||||
let stableRows = 0;
|
||||
for (let i = this.#compactedChildStart; i < count; i++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const child = this.children[i]!;
|
||||
|
||||
// This child's contribution: its current render with plain-blank
|
||||
@@ -459,7 +416,6 @@ export class TranscriptContainer
|
||||
previous.width === width &&
|
||||
previous.generation === this.#generation);
|
||||
const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
|
||||
const compactable = finalized && previous?.finalized !== false;
|
||||
|
||||
// Empty (or stripped-to-nothing) children contribute nothing and never
|
||||
// affect spacing. An empty still-live child still gates the commit
|
||||
@@ -484,7 +440,6 @@ export class TranscriptContainer
|
||||
rowCount: 0,
|
||||
sep: 0,
|
||||
finalized,
|
||||
compactable,
|
||||
version,
|
||||
};
|
||||
continue;
|
||||
@@ -536,7 +491,6 @@ export class TranscriptContainer
|
||||
rowCount,
|
||||
sep,
|
||||
finalized,
|
||||
compactable,
|
||||
version,
|
||||
};
|
||||
row += rowCount;
|
||||
@@ -546,72 +500,8 @@ export class TranscriptContainer
|
||||
if (lines.length !== row) lines.length = row;
|
||||
this.#segments = segments;
|
||||
this.#stableRowsFloor = Math.min(stableFloorBefore, stableRows, row);
|
||||
if (this.#replayPending) {
|
||||
this.#replayPending = false;
|
||||
} else {
|
||||
this.#compactCommittedPrefix();
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
#compactCommittedPrefix(): void {
|
||||
if (this.#committedRows <= 0 || this.#compactedChildStart >= this.children.length) return;
|
||||
const lines = this.#lines;
|
||||
const segments = this.#segments;
|
||||
let dropRows = 0;
|
||||
let dropUntil = this.#compactedChildStart;
|
||||
for (let i = this.#compactedChildStart; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
if (segment === undefined || !segment.compactable) break;
|
||||
const segmentEnd = segment.startRow + segment.rowCount;
|
||||
if (segmentEnd > this.#committedRows) break;
|
||||
dropRows = segmentEnd;
|
||||
dropUntil = i + 1;
|
||||
}
|
||||
const retained = segments[dropUntil];
|
||||
if (retained !== undefined && retained.sep > 0) {
|
||||
const committedSeparatorRows = this.#committedRows - retained.startRow;
|
||||
if (committedSeparatorRows <= 0) {
|
||||
dropRows = 0;
|
||||
dropUntil = this.#compactedChildStart;
|
||||
} else {
|
||||
const trim = Math.min(retained.sep, committedSeparatorRows);
|
||||
dropRows += trim;
|
||||
retained.sep -= trim;
|
||||
retained.rowCount -= trim;
|
||||
}
|
||||
}
|
||||
if (dropRows === 0) return;
|
||||
|
||||
lines.splice(0, dropRows);
|
||||
for (let i = this.#compactedChildStart; i < dropUntil; i++) {
|
||||
const segment = segments[i];
|
||||
if (segment === undefined) continue;
|
||||
segments[i] = {
|
||||
component: segment.component,
|
||||
rawRef: EMPTY_TAIL,
|
||||
contribution: EMPTY_TAIL,
|
||||
width: segment.width,
|
||||
generation: segment.generation,
|
||||
startRow: 0,
|
||||
rowCount: 0,
|
||||
sep: 0,
|
||||
finalized: true,
|
||||
compactable: false,
|
||||
version: segment.version,
|
||||
};
|
||||
}
|
||||
for (let i = dropUntil; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
if (segment !== undefined) segment.startRow -= dropRows;
|
||||
}
|
||||
this.#compactedChildStart = dropUntil;
|
||||
this.#committedRows = Math.max(0, this.#committedRows - dropRows);
|
||||
this.#stableRowsFloor = 0;
|
||||
if (this.#nativeScrollbackLiveRegionStart !== undefined) {
|
||||
this.#nativeScrollbackLiveRegionStart = Math.max(0, this.#nativeScrollbackLiveRegionStart - dropRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user