|
|
|
@@ -212,6 +212,11 @@ export interface OverlayFocusOwner {
|
|
|
|
|
* dropped row or an audit re-anchor spray. Provisional live blocks (collapsing
|
|
|
|
|
* tool/edit previews whose head is a throwaway tail window) omit it. Defaults to
|
|
|
|
|
* `commitSafeEnd ?? liveRegionStart` when absent.
|
|
|
|
|
* `getNativeScrollbackOfferSafeEnd` optionally reports the deepest prefix row
|
|
|
|
|
* that may physically enter native scrollback while still remaining audited.
|
|
|
|
|
* This is for finalized lower siblings under a live block: the rows may scroll
|
|
|
|
|
* off, but a later live-block insertion above them must trigger repair instead
|
|
|
|
|
* of becoming durable audit-exempt history.
|
|
|
|
|
*
|
|
|
|
|
* When several root children report a seam in the same frame, the topmost
|
|
|
|
|
* one (and its commit-safe / snapshot-safe extension) defines the boundary:
|
|
|
|
@@ -222,6 +227,7 @@ export interface NativeScrollbackLiveRegion {
|
|
|
|
|
getNativeScrollbackLiveRegionStart(): number | undefined;
|
|
|
|
|
getNativeScrollbackCommitSafeEnd?(): number | undefined;
|
|
|
|
|
getNativeScrollbackSnapshotSafeEnd?(): number | undefined;
|
|
|
|
|
getNativeScrollbackOfferSafeEnd?(): number | undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface NativeScrollbackCommittedRows {
|
|
|
|
@@ -251,6 +257,10 @@ function getNativeScrollbackSnapshotSafeEnd(component: Component): number | unde
|
|
|
|
|
return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackSnapshotSafeEnd?.();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getNativeScrollbackOfferSafeEnd(component: Component): number | undefined {
|
|
|
|
|
return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackOfferSafeEnd?.();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Opt-in stability report for components that mutate their returned render
|
|
|
|
|
* array in place across frames (instead of returning a fresh array per
|
|
|
|
@@ -628,6 +638,7 @@ interface FrameSegment {
|
|
|
|
|
liveLocalStart?: number;
|
|
|
|
|
commitLocalEnd?: number;
|
|
|
|
|
snapshotLocalEnd?: number;
|
|
|
|
|
offerLocalEnd?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Depth-first identity search through `Container`-shaped children. */
|
|
|
|
@@ -1009,23 +1020,27 @@ export class TUI extends Container {
|
|
|
|
|
// #auditCommittedPrefix). Holds references to component-cached strings, so
|
|
|
|
|
// the audit is a pointer walk in the common case.
|
|
|
|
|
#committedPrefix: string[] = [];
|
|
|
|
|
// The committed prefix [0, committedRows) splits into three audit zones by
|
|
|
|
|
// two monotone marks auditRows ≤ durableRows ≤ committedRows:
|
|
|
|
|
// The committed prefix [0, committedRows) splits into four zones by three
|
|
|
|
|
// monotone marks auditRows ≤ durableRows ≤ offerRows ≤ committedRows:
|
|
|
|
|
// [0, auditRows) BYTE-STABLE — audited (re-anchor on any shift).
|
|
|
|
|
// [auditRows, durableRows) DURABLE snapshot — exempt: rows may drift in
|
|
|
|
|
// place (a streaming table widening) without re-anchoring, so their
|
|
|
|
|
// expected drift never sprays duplicate snapshots.
|
|
|
|
|
// [durableRows, committedRows) FORCED-overflow — audited: rows committed
|
|
|
|
|
// [durableRows, offerRows) OFFERED — audited: rows were allowed into
|
|
|
|
|
// native scrollback while a live block above could still shift them.
|
|
|
|
|
// A mismatch here requires a destructive replay, not a duplicate tail.
|
|
|
|
|
// [offerRows, committedRows) FORCED-overflow — audited: rows committed
|
|
|
|
|
// only because they scrolled above the window under a commit-unstable
|
|
|
|
|
// barrier; auditing them re-anchors (duplication, never loss) when the
|
|
|
|
|
// barrier later shifts/finalizes/removes, instead of stranding a stale
|
|
|
|
|
// prefix that silently drops the rows beneath it.
|
|
|
|
|
// Both marks re-base on a wholesale re-slice (full paint / shrink / geometry)
|
|
|
|
|
// and otherwise advance per the persistence rules in #updateCommittedAuditRows.
|
|
|
|
|
// Marks re-base on a wholesale re-slice (full paint / shrink / geometry) and
|
|
|
|
|
// otherwise advance per the persistence rules in #updateCommittedAuditRows.
|
|
|
|
|
// #auditCommittedPrefix audits [0, committedRows) skipping the exempt window
|
|
|
|
|
// [auditRows, durableRows).
|
|
|
|
|
#committedPrefixAuditRows = 0;
|
|
|
|
|
#committedPrefixDurableRows = 0;
|
|
|
|
|
#committedPrefixOfferRows = 0;
|
|
|
|
|
// Frame row currently mapped to screen row 0. Monotonic between full
|
|
|
|
|
// paints: a shrink never re-exposes scrolled-off rows (they cannot be
|
|
|
|
|
// un-scrolled without rewriting history); live rows repaint at fixed
|
|
|
|
@@ -1036,6 +1051,7 @@ export class TUI extends Container {
|
|
|
|
|
#nativeScrollbackLiveRegionStart: number | undefined;
|
|
|
|
|
#nativeScrollbackCommitSafeEnd: number | undefined;
|
|
|
|
|
#nativeScrollbackSnapshotSafeEnd: number | undefined;
|
|
|
|
|
#nativeScrollbackOfferSafeEnd: number | undefined;
|
|
|
|
|
#fullRedrawCount = 0;
|
|
|
|
|
// Caps how many inline images render as live graphics; older ones fall back
|
|
|
|
|
// to text via a purge + full redraw. Cap is configured by the host app.
|
|
|
|
@@ -1155,6 +1171,7 @@ export class TUI extends Container {
|
|
|
|
|
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
|
|
|
this.#nativeScrollbackCommitSafeEnd = undefined;
|
|
|
|
|
this.#nativeScrollbackSnapshotSafeEnd = undefined;
|
|
|
|
|
this.#nativeScrollbackOfferSafeEnd = undefined;
|
|
|
|
|
const children = this.children;
|
|
|
|
|
const previousSegments = this.#frameSegments;
|
|
|
|
|
const segments: FrameSegment[] = new Array(children.length);
|
|
|
|
@@ -1177,12 +1194,14 @@ export class TUI extends Container {
|
|
|
|
|
let liveLocalStart: number | undefined;
|
|
|
|
|
let commitLocalEnd: number | undefined;
|
|
|
|
|
let snapshotLocalEnd: number | undefined;
|
|
|
|
|
let offerLocalEnd: number | undefined;
|
|
|
|
|
let reported: number | undefined;
|
|
|
|
|
if (reuse) {
|
|
|
|
|
childLines = previous.lines;
|
|
|
|
|
liveLocalStart = previous.liveLocalStart;
|
|
|
|
|
commitLocalEnd = previous.commitLocalEnd;
|
|
|
|
|
snapshotLocalEnd = previous.snapshotLocalEnd;
|
|
|
|
|
offerLocalEnd = previous.offerLocalEnd;
|
|
|
|
|
} else {
|
|
|
|
|
// Feed the engine's committed-row claim (from the previous frame's
|
|
|
|
|
// emit) before rendering so the child can skip re-deriving blocks
|
|
|
|
@@ -1211,6 +1230,13 @@ export class TUI extends Container {
|
|
|
|
|
? Math.max(snapshotFloor, Math.min(childLines.length, Math.trunc(snapshotSafeEnd)))
|
|
|
|
|
: childLines.length;
|
|
|
|
|
}
|
|
|
|
|
const offerSafeEnd = getNativeScrollbackOfferSafeEnd(child);
|
|
|
|
|
if (offerSafeEnd !== undefined) {
|
|
|
|
|
const offerFloor = snapshotLocalEnd ?? commitLocalEnd ?? liveLocalStart;
|
|
|
|
|
offerLocalEnd = Number.isFinite(offerSafeEnd)
|
|
|
|
|
? Math.max(offerFloor, Math.min(childLines.length, Math.trunc(offerSafeEnd)))
|
|
|
|
|
: childLines.length;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Consume the stability report unconditionally for implementers:
|
|
|
|
|
// reading re-bases the component's baseline to the state this
|
|
|
|
@@ -1234,6 +1260,9 @@ export class TUI extends Container {
|
|
|
|
|
if (snapshotLocalEnd !== undefined) {
|
|
|
|
|
this.#nativeScrollbackSnapshotSafeEnd = offset + snapshotLocalEnd;
|
|
|
|
|
}
|
|
|
|
|
if (offerLocalEnd !== undefined) {
|
|
|
|
|
this.#nativeScrollbackOfferSafeEnd = offset + offerLocalEnd;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (chainStable) {
|
|
|
|
|
if (previous !== undefined && previous.component === child && previous.start === offset) {
|
|
|
|
@@ -1264,6 +1293,7 @@ export class TUI extends Container {
|
|
|
|
|
liveLocalStart,
|
|
|
|
|
commitLocalEnd,
|
|
|
|
|
snapshotLocalEnd,
|
|
|
|
|
offerLocalEnd,
|
|
|
|
|
};
|
|
|
|
|
offset += childLines.length;
|
|
|
|
|
}
|
|
|
|
@@ -2664,6 +2694,7 @@ export class TUI extends Container {
|
|
|
|
|
const liveRegionStart = this.#nativeScrollbackLiveRegionStart;
|
|
|
|
|
const commitSafeEnd = this.#nativeScrollbackCommitSafeEnd;
|
|
|
|
|
const snapshotSafeEnd = this.#nativeScrollbackSnapshotSafeEnd;
|
|
|
|
|
const offerSafeEnd = this.#nativeScrollbackOfferSafeEnd;
|
|
|
|
|
|
|
|
|
|
// Commit boundaries (also used by the window/commit math in section 3),
|
|
|
|
|
// hoisted above the audit gate because the resync needs byteStableBoundary
|
|
|
|
@@ -2671,22 +2702,27 @@ export class TUI extends Container {
|
|
|
|
|
// The commit floor is windowTop in every non-frozen path (see chunkTo), so
|
|
|
|
|
// whatever scrolls above the window is committed — never committed nowhere
|
|
|
|
|
// AND painted nowhere (the loss bug). The boundaries no longer gate the
|
|
|
|
|
// commit; they define the audit-exempt span. byteStableBoundary: rows below
|
|
|
|
|
// commit; they define audit and repair spans. byteStableBoundary: rows below
|
|
|
|
|
// it are byte-stable (never re-layout), audited. durableBoundary: rows in
|
|
|
|
|
// [byteStableBoundary, durableBoundary) are durable — permanent on scroll-off
|
|
|
|
|
// but may drift in place (a streaming table re-aligning), committed
|
|
|
|
|
// audit-EXEMPT. Rows at/beyond durableBoundary committed only because they
|
|
|
|
|
// scrolled above the window (a commit-unstable barrier over a long tail) are
|
|
|
|
|
// forced-overflow rows: audited, so a later shift/finalize/removal re-anchors
|
|
|
|
|
// (duplication, never loss) instead of stranding a stale prefix. Built on the
|
|
|
|
|
// finalized prefix (live-region start); the whole frame when the root reports
|
|
|
|
|
// no seam (shell semantics: whatever scrolls is final).
|
|
|
|
|
// audit-EXEMPT. offerBoundary: rows in [durableBoundary, offerBoundary)
|
|
|
|
|
// were explicitly allowed to enter native scrollback while remaining
|
|
|
|
|
// audited; if they later shift, the stale physical history is repaired by a
|
|
|
|
|
// destructive replay instead of duplicate recommit. Rows at/beyond
|
|
|
|
|
// offerBoundary committed only because they scrolled above the window (a
|
|
|
|
|
// commit-unstable barrier over a long tail) are forced-overflow rows:
|
|
|
|
|
// audited, so a later shift/finalize/removal re-anchors (duplication, never
|
|
|
|
|
// loss) instead of stranding a stale prefix. Built on the finalized prefix
|
|
|
|
|
// (live-region start); the whole frame when the root reports no seam (shell
|
|
|
|
|
// semantics: whatever scrolls is final).
|
|
|
|
|
const frameLength = rawFrame.length;
|
|
|
|
|
const byteStableBoundary = Math.max(0, Math.min(frameLength, commitSafeEnd ?? liveRegionStart ?? frameLength));
|
|
|
|
|
const durableBoundary = Math.max(
|
|
|
|
|
byteStableBoundary,
|
|
|
|
|
Math.min(frameLength, snapshotSafeEnd ?? byteStableBoundary),
|
|
|
|
|
);
|
|
|
|
|
const offerBoundary = Math.max(durableBoundary, Math.min(frameLength, offerSafeEnd ?? durableBoundary));
|
|
|
|
|
|
|
|
|
|
// 2. Transition state captured before any emitter runs.
|
|
|
|
|
const prevWindowTop = this.#windowTopRow;
|
|
|
|
@@ -2731,6 +2767,7 @@ export class TUI extends Container {
|
|
|
|
|
this.#committedPrefixDurableRows < this.#committedRows ? this.#committedRows : this.#committedPrefixAuditRows;
|
|
|
|
|
const hardAuditEnd = Math.min(this.#committedRows, durableBoundary);
|
|
|
|
|
const needHardAudit = this.#committedPrefixDurableRows < hardAuditEnd;
|
|
|
|
|
let repairOfferedScrollback = false;
|
|
|
|
|
const auditRan =
|
|
|
|
|
this.#hasEverRendered &&
|
|
|
|
|
!geometryChanged &&
|
|
|
|
@@ -2738,14 +2775,21 @@ export class TUI extends Container {
|
|
|
|
|
(this.#renderStablePrefixRows < auditUpper || needHardAudit);
|
|
|
|
|
if (auditRan) {
|
|
|
|
|
const committedRowsBeforeAudit = this.#committedRows;
|
|
|
|
|
const offeredRowsBeforeAudit = this.#committedPrefixOfferRows;
|
|
|
|
|
const durableRowsBeforeAudit = this.#committedPrefixDurableRows;
|
|
|
|
|
this.#auditCommittedPrefix(rawFrame, durableBoundary);
|
|
|
|
|
committedRowsResynced = this.#committedRows !== committedRowsBeforeAudit;
|
|
|
|
|
repairOfferedScrollback =
|
|
|
|
|
offeredRowsBeforeAudit > durableRowsBeforeAudit &&
|
|
|
|
|
committedRowsResynced &&
|
|
|
|
|
this.#committedRows < offeredRowsBeforeAudit;
|
|
|
|
|
}
|
|
|
|
|
// Committed-prefix state this frame's commit math extends from (post-audit).
|
|
|
|
|
// Drives the audit-rows / durable-rows caps recomputed after the emit.
|
|
|
|
|
const preCommitRows = this.#committedRows;
|
|
|
|
|
const preCommitAuditRows = this.#committedPrefixAuditRows;
|
|
|
|
|
const preCommitDurableRows = this.#committedPrefixDurableRows;
|
|
|
|
|
const preCommitOfferRows = this.#committedPrefixOfferRows;
|
|
|
|
|
|
|
|
|
|
// 3. Window and commit math (lengths only; content prepared below).
|
|
|
|
|
let hasVisibleOverlay = false;
|
|
|
|
@@ -2763,7 +2807,7 @@ export class TUI extends Container {
|
|
|
|
|
// place, because an ED3 rewrap is unsafe (pane scrollback / alt-screen
|
|
|
|
|
// feedback loop), so committed history keeps its old wrap.
|
|
|
|
|
const firstPaint = !this.#hasEverRendered;
|
|
|
|
|
const replaceRequested = this.#clearScrollbackOnNextRender;
|
|
|
|
|
const replaceRequested = this.#clearScrollbackOnNextRender || repairOfferedScrollback;
|
|
|
|
|
const geometryRebuild = geometryChanged && !resizeRepaintsInPlace();
|
|
|
|
|
const fullPaint = firstPaint || replaceRequested || geometryRebuild;
|
|
|
|
|
let windowTop: number;
|
|
|
|
@@ -2875,8 +2919,10 @@ export class TUI extends Container {
|
|
|
|
|
preCommitRows,
|
|
|
|
|
preCommitAuditRows,
|
|
|
|
|
preCommitDurableRows,
|
|
|
|
|
preCommitOfferRows,
|
|
|
|
|
byteStableBoundary,
|
|
|
|
|
durableBoundary,
|
|
|
|
|
offerBoundary,
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
this.#clearScrollbackOnNextRender = false;
|
|
|
|
@@ -2904,8 +2950,10 @@ export class TUI extends Container {
|
|
|
|
|
preCommitRows,
|
|
|
|
|
preCommitAuditRows,
|
|
|
|
|
preCommitDurableRows,
|
|
|
|
|
preCommitOfferRows,
|
|
|
|
|
byteStableBoundary,
|
|
|
|
|
durableBoundary,
|
|
|
|
|
offerBoundary,
|
|
|
|
|
auditRan,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
@@ -2932,6 +2980,7 @@ export class TUI extends Container {
|
|
|
|
|
this.#committedRows = resyncTo;
|
|
|
|
|
this.#committedPrefixAuditRows = Math.min(this.#committedPrefixAuditRows, resyncTo);
|
|
|
|
|
this.#committedPrefixDurableRows = Math.min(this.#committedPrefixDurableRows, resyncTo);
|
|
|
|
|
this.#committedPrefixOfferRows = Math.min(this.#committedPrefixOfferRows, resyncTo);
|
|
|
|
|
prefix.length = resyncTo;
|
|
|
|
|
if ($flag("PI_DEBUG_REDRAW")) {
|
|
|
|
|
const msg = `[${new Date().toISOString()}] commit resync: committed prefix diverged at row ${resyncTo}; recommitting\n`;
|
|
|
|
@@ -2944,21 +2993,24 @@ export class TUI extends Container {
|
|
|
|
|
* #committedPrefixAuditRows field doc for the three audit zones).
|
|
|
|
|
*
|
|
|
|
|
* auditRows tracks the byte-stable boundary; durableRows the durable snapshot
|
|
|
|
|
* boundary. A wholesale re-slice (full paint / shrink / geometry) re-bases
|
|
|
|
|
* each mark from the current frame (min(committed, boundary)). An incremental
|
|
|
|
|
* extend keeps a mark once a row past it has committed (mark < committed): a
|
|
|
|
|
* later RISE in a boundary (a table finalizing) must neither pull
|
|
|
|
|
* already-committed stale snapshots back under the byte-stable cap nor
|
|
|
|
|
* retroactively exempt forced-overflow rows already audited. durableRows is
|
|
|
|
|
* floored at auditRows so the exempt window can never invert.
|
|
|
|
|
* boundary; offerRows the deepest explicit audited-offer boundary. A wholesale
|
|
|
|
|
* re-slice (full paint / shrink / geometry) re-bases each mark from the
|
|
|
|
|
* current frame. An incremental extend keeps a mark once a row past it has
|
|
|
|
|
* committed (mark < committed): a later RISE in a boundary (a table finalizing)
|
|
|
|
|
* must neither pull already-committed stale snapshots back under the
|
|
|
|
|
* byte-stable cap nor retroactively exempt forced-overflow rows already
|
|
|
|
|
* audited. durableRows is floored at auditRows; offerRows is floored at
|
|
|
|
|
* durableRows.
|
|
|
|
|
*/
|
|
|
|
|
#updateCommittedAuditRows(
|
|
|
|
|
resliced: boolean,
|
|
|
|
|
preCommittedRows: number,
|
|
|
|
|
preAuditRows: number,
|
|
|
|
|
preDurableRows: number,
|
|
|
|
|
preOfferRows: number,
|
|
|
|
|
byteStableBoundary: number,
|
|
|
|
|
durableBoundary: number,
|
|
|
|
|
offerBoundary: number,
|
|
|
|
|
hardAudited: boolean,
|
|
|
|
|
): void {
|
|
|
|
|
const committed = this.#committedRows;
|
|
|
|
@@ -2975,8 +3027,13 @@ export class TUI extends Container {
|
|
|
|
|
resliced || preDurableRows >= preCommittedRows || hardAudited
|
|
|
|
|
? Math.min(committed, durableBoundary)
|
|
|
|
|
: Math.min(preDurableRows, committed);
|
|
|
|
|
const offerRows =
|
|
|
|
|
resliced || preOfferRows >= preCommittedRows
|
|
|
|
|
? Math.min(committed, offerBoundary)
|
|
|
|
|
: Math.min(preOfferRows, committed);
|
|
|
|
|
this.#committedPrefixAuditRows = auditRows;
|
|
|
|
|
this.#committedPrefixDurableRows = Math.max(auditRows, durableRows);
|
|
|
|
|
this.#committedPrefixOfferRows = Math.max(this.#committedPrefixDurableRows, offerRows);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|