@oh-my-pi/pi-tui 16.1.9 → 16.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/types/tui.d.ts +32 -13
- package/package.json +3 -3
- package/src/tui.ts +191 -79
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.10] - 2026-06-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed streaming output being lost from native scrollback below a commit-unstable "barrier" block (a provisional/collapsed tool preview, a displaceable `job` poll, or a reflowing-markdown reply) once the content under it overflowed the viewport. The engine committed native scrollback only up to the barrier's seam, so rows that scrolled above the window under the barrier were committed nowhere and repainted nowhere — they vanished, and a later shift/finalize/removal of the barrier silently dropped the rows beneath it. The append-only commit floor is now `windowTop` in every non-frozen paint path (ordinary update, shrink re-slice, full paint), so whatever scrolls above the window always reaches history; the seam boundaries now only classify which committed rows stay byte-stable-audited vs. durable-exempt. The committed-prefix audit is range-aware: it audits the forced-overflow suffix in full (re-anchoring — duplication, never loss — when a barrier finalizes), exempts the durable middle (a streaming table re-aligning its columns) from re-anchor spray, and runs a full hard scan of rows a frame newly marks permanent so a single-row finalize edit far above the commit boundary still re-anchors instead of being dropped.
|
|
10
|
+
|
|
5
11
|
## [16.1.8] - 2026-06-20
|
|
6
12
|
|
|
7
13
|
### Added
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -294,21 +294,40 @@ export declare function coalesceAdjacentSgr(line: string): string;
|
|
|
294
294
|
* re-anchor the commit index when it does not. Returns the resync row index,
|
|
295
295
|
* or -1 when no resync is needed.
|
|
296
296
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
297
|
+
* Audits the committed prefix [0, auditTo) EXCEPT the exempt window
|
|
298
|
+
* [exemptFrom, exemptTo): rows in the window are durable snapshots (a streaming
|
|
299
|
+
* table re-aligning its columns) that may drift legitimately, so their drift
|
|
300
|
+
* never triggers a re-anchor. Rows below the window — including forced-overflow
|
|
301
|
+
* rows committed only because they scrolled above the viewport under a
|
|
302
|
+
* commit-unstable barrier — ARE audited.
|
|
303
|
+
*
|
|
304
|
+
* Two detectors run over the audited rows:
|
|
305
|
+
*
|
|
306
|
+
* 1. Hard scan of the now-permanent forced suffix [exemptTo, permanentEnd):
|
|
307
|
+
* forced-overflow rows that THIS frame asserts are durable/permanent (index <
|
|
308
|
+
* permanentEnd — the barrier above them finalized or cleared, so durableBoundary
|
|
309
|
+
* rose past them). A content change there is real finalized content, so ANY
|
|
310
|
+
* mismatch re-anchors. Scanned in FULL, not sampled, so a single edit far above
|
|
311
|
+
* the commit boundary with an unchanged tail still re-anchors (duplication,
|
|
312
|
+
* never loss) instead of being committed nowhere and painted nowhere.
|
|
313
|
+
* 2. Tail sample (only when the hard scan is clean): exploits the asymmetry
|
|
314
|
+
* between the two mutation classes — an in-place edit/restyle of a committed
|
|
315
|
+
* row disturbs only the touched rows (alignment below intact; the stale copy
|
|
316
|
+
* in history is the long-accepted artifact), while an insertion/deletion
|
|
317
|
+
* shifts EVERY row below it. So up to 8 non-blank rows within the last 24
|
|
318
|
+
* audited rows are compared SGR-stripped (theme changes stay quiet),
|
|
319
|
+
* tolerating a SINGLE non-hard mismatch (a legitimate one-row edit): aligned ⇒
|
|
320
|
+
* no resync; misaligned ⇒ resync at the first non-equivalent audited row. The
|
|
321
|
+
* tolerance keeps both an offscreen still-live barrier (a ticking spinner) and
|
|
322
|
+
* a no-seam in-place row edit from spraying duplicate snapshots every frame;
|
|
323
|
+
* the hard scan above is what forbids it from swallowing a finalized row.
|
|
324
|
+
*
|
|
325
|
+
* Highly repetitive tails (identical filler rows) can mask a shift in the tail
|
|
326
|
+
* sample, in which case the skipped rows are content-identical to the committed
|
|
327
|
+
* ones — observationally harmless. Exported for the render-stress harness, whose
|
|
309
328
|
* shadow commit ledger must mirror the engine's law exactly.
|
|
310
329
|
*/
|
|
311
|
-
export declare function findCommittedPrefixResync(frame: readonly string[], prefix: readonly string[],
|
|
330
|
+
export declare function findCommittedPrefixResync(frame: readonly string[], prefix: readonly string[], auditTo?: number, exemptFrom?: number, exemptTo?: number, permanentEnd?: number): number;
|
|
312
331
|
/**
|
|
313
332
|
* TUI - Main class for managing terminal UI with differential rendering
|
|
314
333
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.11",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.1.11",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.1.11",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.5"
|
|
44
44
|
},
|
package/src/tui.ts
CHANGED
|
@@ -812,52 +812,94 @@ const RESYNC_TAIL_SAMPLES = 8;
|
|
|
812
812
|
* re-anchor the commit index when it does not. Returns the resync row index,
|
|
813
813
|
* or -1 when no resync is needed.
|
|
814
814
|
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
815
|
+
* Audits the committed prefix [0, auditTo) EXCEPT the exempt window
|
|
816
|
+
* [exemptFrom, exemptTo): rows in the window are durable snapshots (a streaming
|
|
817
|
+
* table re-aligning its columns) that may drift legitimately, so their drift
|
|
818
|
+
* never triggers a re-anchor. Rows below the window — including forced-overflow
|
|
819
|
+
* rows committed only because they scrolled above the viewport under a
|
|
820
|
+
* commit-unstable barrier — ARE audited.
|
|
821
|
+
*
|
|
822
|
+
* Two detectors run over the audited rows:
|
|
823
|
+
*
|
|
824
|
+
* 1. Hard scan of the now-permanent forced suffix [exemptTo, permanentEnd):
|
|
825
|
+
* forced-overflow rows that THIS frame asserts are durable/permanent (index <
|
|
826
|
+
* permanentEnd — the barrier above them finalized or cleared, so durableBoundary
|
|
827
|
+
* rose past them). A content change there is real finalized content, so ANY
|
|
828
|
+
* mismatch re-anchors. Scanned in FULL, not sampled, so a single edit far above
|
|
829
|
+
* the commit boundary with an unchanged tail still re-anchors (duplication,
|
|
830
|
+
* never loss) instead of being committed nowhere and painted nowhere.
|
|
831
|
+
* 2. Tail sample (only when the hard scan is clean): exploits the asymmetry
|
|
832
|
+
* between the two mutation classes — an in-place edit/restyle of a committed
|
|
833
|
+
* row disturbs only the touched rows (alignment below intact; the stale copy
|
|
834
|
+
* in history is the long-accepted artifact), while an insertion/deletion
|
|
835
|
+
* shifts EVERY row below it. So up to 8 non-blank rows within the last 24
|
|
836
|
+
* audited rows are compared SGR-stripped (theme changes stay quiet),
|
|
837
|
+
* tolerating a SINGLE non-hard mismatch (a legitimate one-row edit): aligned ⇒
|
|
838
|
+
* no resync; misaligned ⇒ resync at the first non-equivalent audited row. The
|
|
839
|
+
* tolerance keeps both an offscreen still-live barrier (a ticking spinner) and
|
|
840
|
+
* a no-seam in-place row edit from spraying duplicate snapshots every frame;
|
|
841
|
+
* the hard scan above is what forbids it from swallowing a finalized row.
|
|
842
|
+
*
|
|
843
|
+
* Highly repetitive tails (identical filler rows) can mask a shift in the tail
|
|
844
|
+
* sample, in which case the skipped rows are content-identical to the committed
|
|
845
|
+
* ones — observationally harmless. Exported for the render-stress harness, whose
|
|
827
846
|
* shadow commit ledger must mirror the engine's law exactly.
|
|
828
847
|
*/
|
|
829
848
|
export function findCommittedPrefixResync(
|
|
830
849
|
frame: readonly string[],
|
|
831
850
|
prefix: readonly string[],
|
|
832
|
-
|
|
851
|
+
auditTo: number = prefix.length,
|
|
852
|
+
exemptFrom: number = auditTo,
|
|
853
|
+
exemptTo: number = exemptFrom,
|
|
854
|
+
permanentEnd = 0,
|
|
833
855
|
): number {
|
|
834
|
-
|
|
835
|
-
// under a durable snapshot end (beyond auditLimit) may drift legitimately and
|
|
836
|
-
// are exempt, so their drift never triggers a re-anchor.
|
|
837
|
-
const committed = Math.min(prefix.length, Math.max(0, Math.trunc(auditLimit)));
|
|
856
|
+
const committed = Math.min(prefix.length, Math.max(0, Math.trunc(auditTo)));
|
|
838
857
|
if (committed === 0) return -1;
|
|
858
|
+
// Exempt window [exFrom, exTo) clamped into the committed prefix. Rows there
|
|
859
|
+
// are durable-snapshot drift and skipped by both detectors and the scan.
|
|
860
|
+
const exFrom = Math.max(0, Math.min(committed, Math.trunc(exemptFrom)));
|
|
861
|
+
const exTo = Math.max(exFrom, Math.min(committed, Math.trunc(exemptTo)));
|
|
862
|
+
const audited = (i: number): boolean => i < exFrom || i >= exTo;
|
|
839
863
|
if (frame.length >= committed) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
864
|
+
// 1. Hard scan: forced-overflow rows now asserted permanent. Full scan, no
|
|
865
|
+
// tolerance — a finalized row that changed must re-anchor.
|
|
866
|
+
const hardEnd = Math.min(committed, Math.max(0, Math.trunc(permanentEnd)));
|
|
867
|
+
let hardMismatch = false;
|
|
868
|
+
for (let i = exTo; i < hardEnd; i++) {
|
|
869
|
+
if (!rowsEquivalent(frame[i]!, prefix[i]!)) {
|
|
870
|
+
hardMismatch = true;
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (!hardMismatch) {
|
|
875
|
+
// 2. Tail sample. Walk up from the commit boundary, skipping exempt
|
|
876
|
+
// rows, until LOOKBACK audited rows or SAMPLES non-blank comparisons.
|
|
877
|
+
let samples = 0;
|
|
878
|
+
let mismatches = 0;
|
|
879
|
+
let scanned = 0;
|
|
880
|
+
for (let j = 1; j <= committed && scanned < RESYNC_TAIL_LOOKBACK && samples < RESYNC_TAIL_SAMPLES; j++) {
|
|
881
|
+
const idx = committed - j;
|
|
882
|
+
if (!audited(idx)) continue;
|
|
883
|
+
scanned++;
|
|
884
|
+
const row = frame[idx]!;
|
|
885
|
+
const old = prefix[idx]!;
|
|
886
|
+
if (row === old) {
|
|
887
|
+
if (!isBlankRow(row)) samples++;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (isBlankRow(row) && isBlankRow(old)) continue;
|
|
891
|
+
samples++;
|
|
892
|
+
if (!rowsEquivalent(row, old)) mismatches++;
|
|
849
893
|
}
|
|
850
|
-
|
|
851
|
-
samples
|
|
852
|
-
if (!rowsEquivalent(row, old)) mismatches++;
|
|
894
|
+
// No signal (all-blank/all-exempt tail) or at most one edited row: aligned.
|
|
895
|
+
if (samples === 0 || mismatches <= 1) return -1;
|
|
853
896
|
}
|
|
854
|
-
// No signal (all-blank tail) or at most one edited row: aligned.
|
|
855
|
-
if (samples === 0 || mismatches <= 1) return -1;
|
|
856
897
|
}
|
|
857
|
-
// Misaligned (or the frame no longer covers
|
|
858
|
-
// first row whose content
|
|
898
|
+
// Misaligned (hard mismatch, tail-sample shift, or the frame no longer covers
|
|
899
|
+
// the prefix): re-anchor at the first audited row whose content changed.
|
|
859
900
|
const limit = Math.min(committed, frame.length);
|
|
860
901
|
for (let i = 0; i < limit; i++) {
|
|
902
|
+
if (!audited(i)) continue;
|
|
861
903
|
if (!rowsEquivalent(frame[i]!, prefix[i]!)) return i;
|
|
862
904
|
}
|
|
863
905
|
return limit < committed ? limit : -1;
|
|
@@ -957,15 +999,23 @@ export class TUI extends Container {
|
|
|
957
999
|
// #auditCommittedPrefix). Holds references to component-cached strings, so
|
|
958
1000
|
// the audit is a pointer walk in the common case.
|
|
959
1001
|
#committedPrefix: string[] = [];
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
//
|
|
1002
|
+
// The committed prefix [0, committedRows) splits into three audit zones by
|
|
1003
|
+
// two monotone marks auditRows ≤ durableRows ≤ committedRows:
|
|
1004
|
+
// [0, auditRows) BYTE-STABLE — audited (re-anchor on any shift).
|
|
1005
|
+
// [auditRows, durableRows) DURABLE snapshot — exempt: rows may drift in
|
|
1006
|
+
// place (a streaming table widening) without re-anchoring, so their
|
|
1007
|
+
// expected drift never sprays duplicate snapshots.
|
|
1008
|
+
// [durableRows, committedRows) FORCED-overflow — audited: rows committed
|
|
1009
|
+
// only because they scrolled above the window under a commit-unstable
|
|
1010
|
+
// barrier; auditing them re-anchors (duplication, never loss) when the
|
|
1011
|
+
// barrier later shifts/finalizes/removes, instead of stranding a stale
|
|
1012
|
+
// prefix that silently drops the rows beneath it.
|
|
1013
|
+
// Both marks re-base on a wholesale re-slice (full paint / shrink / geometry)
|
|
1014
|
+
// and otherwise advance per the persistence rules in #updateCommittedAuditRows.
|
|
1015
|
+
// #auditCommittedPrefix audits [0, committedRows) skipping the exempt window
|
|
1016
|
+
// [auditRows, durableRows).
|
|
968
1017
|
#committedPrefixAuditRows = 0;
|
|
1018
|
+
#committedPrefixDurableRows = 0;
|
|
969
1019
|
// Frame row currently mapped to screen row 0. Monotonic between full
|
|
970
1020
|
// paints: a shrink never re-exposes scrolled-off rows (they cannot be
|
|
971
1021
|
// un-scrolled without rewriting history); live rows repaint at fixed
|
|
@@ -2530,6 +2580,29 @@ export class TUI extends Container {
|
|
|
2530
2580
|
const commitSafeEnd = this.#nativeScrollbackCommitSafeEnd;
|
|
2531
2581
|
const snapshotSafeEnd = this.#nativeScrollbackSnapshotSafeEnd;
|
|
2532
2582
|
|
|
2583
|
+
// Commit boundaries (also used by the window/commit math in section 3),
|
|
2584
|
+
// hoisted above the audit gate because the resync needs byteStableBoundary
|
|
2585
|
+
// to tell a now-permanent forced row (must re-anchor) from a still-live one.
|
|
2586
|
+
// The commit floor is windowTop in every non-frozen path (see chunkTo), so
|
|
2587
|
+
// whatever scrolls above the window is committed — never committed nowhere
|
|
2588
|
+
// AND painted nowhere (the loss bug). The boundaries no longer gate the
|
|
2589
|
+
// commit; they define the audit-exempt span. byteStableBoundary: rows below
|
|
2590
|
+
// it are byte-stable (never re-layout), audited. durableBoundary: rows in
|
|
2591
|
+
// [byteStableBoundary, durableBoundary) are durable — permanent on scroll-off
|
|
2592
|
+
// but may drift in place (a streaming table re-aligning), committed
|
|
2593
|
+
// audit-EXEMPT. Rows at/beyond durableBoundary committed only because they
|
|
2594
|
+
// scrolled above the window (a commit-unstable barrier over a long tail) are
|
|
2595
|
+
// forced-overflow rows: audited, so a later shift/finalize/removal re-anchors
|
|
2596
|
+
// (duplication, never loss) instead of stranding a stale prefix. Built on the
|
|
2597
|
+
// finalized prefix (live-region start); the whole frame when the root reports
|
|
2598
|
+
// no seam (shell semantics: whatever scrolls is final).
|
|
2599
|
+
const frameLength = rawFrame.length;
|
|
2600
|
+
const byteStableBoundary = Math.max(0, Math.min(frameLength, commitSafeEnd ?? liveRegionStart ?? frameLength));
|
|
2601
|
+
const durableBoundary = Math.max(
|
|
2602
|
+
byteStableBoundary,
|
|
2603
|
+
Math.min(frameLength, snapshotSafeEnd ?? byteStableBoundary),
|
|
2604
|
+
);
|
|
2605
|
+
|
|
2533
2606
|
// 2. Transition state captured before any emitter runs.
|
|
2534
2607
|
const prevWindowTop = this.#windowTopRow;
|
|
2535
2608
|
const prevHardwareCursorRow = this.#hardwareCursorRow;
|
|
@@ -2559,23 +2632,37 @@ export class TUI extends Container {
|
|
|
2559
2632
|
// that provably did not change since the last (aligned) frame cannot
|
|
2560
2633
|
// have diverged.
|
|
2561
2634
|
let committedRowsResynced = false;
|
|
2562
|
-
|
|
2635
|
+
// Audit covers [0, auditRows) and the forced suffix [durableRows,
|
|
2636
|
+
// committedRows); the durable middle [auditRows, durableRows) is exempt
|
|
2637
|
+
// (in-place drift). Two reasons to run the audit this frame:
|
|
2638
|
+
// - the stable prefix does not cover every audited row (auditUpper); or
|
|
2639
|
+
// - a forced-overflow row this frame became durable/permanent
|
|
2640
|
+
// (committedPrefixDurableRows < hardAuditEnd): the barrier above it
|
|
2641
|
+
// finalized, so its committed bytes must be re-checked even though the
|
|
2642
|
+
// stable prefix says nothing moved — a stale committed copy there would
|
|
2643
|
+
// silently drop the row. The hard scan in findCommittedPrefixResync
|
|
2644
|
+
// covers [durableRows, hardAuditEnd) in full (no tail-sample miss).
|
|
2645
|
+
const auditUpper =
|
|
2646
|
+
this.#committedPrefixDurableRows < this.#committedRows ? this.#committedRows : this.#committedPrefixAuditRows;
|
|
2647
|
+
const hardAuditEnd = Math.min(this.#committedRows, durableBoundary);
|
|
2648
|
+
const needHardAudit = this.#committedPrefixDurableRows < hardAuditEnd;
|
|
2649
|
+
const auditRan =
|
|
2563
2650
|
this.#hasEverRendered &&
|
|
2564
2651
|
!geometryChanged &&
|
|
2565
2652
|
!this.#clearScrollbackOnNextRender &&
|
|
2566
|
-
this.#renderStablePrefixRows <
|
|
2567
|
-
) {
|
|
2653
|
+
(this.#renderStablePrefixRows < auditUpper || needHardAudit);
|
|
2654
|
+
if (auditRan) {
|
|
2568
2655
|
const committedRowsBeforeAudit = this.#committedRows;
|
|
2569
|
-
this.#auditCommittedPrefix(rawFrame);
|
|
2656
|
+
this.#auditCommittedPrefix(rawFrame, durableBoundary);
|
|
2570
2657
|
committedRowsResynced = this.#committedRows !== committedRowsBeforeAudit;
|
|
2571
2658
|
}
|
|
2572
2659
|
// Committed-prefix state this frame's commit math extends from (post-audit).
|
|
2573
|
-
// Drives the
|
|
2660
|
+
// Drives the audit-rows / durable-rows caps recomputed after the emit.
|
|
2574
2661
|
const preCommitRows = this.#committedRows;
|
|
2575
2662
|
const preCommitAuditRows = this.#committedPrefixAuditRows;
|
|
2663
|
+
const preCommitDurableRows = this.#committedPrefixDurableRows;
|
|
2576
2664
|
|
|
2577
2665
|
// 3. Window and commit math (lengths only; content prepared below).
|
|
2578
|
-
const frameLength = rawFrame.length;
|
|
2579
2666
|
let hasVisibleOverlay = false;
|
|
2580
2667
|
for (const entry of this.overlayStack) {
|
|
2581
2668
|
if (this.#isOverlayVisible(entry)) {
|
|
@@ -2583,18 +2670,6 @@ export class TUI extends Container {
|
|
|
2583
2670
|
break;
|
|
2584
2671
|
}
|
|
2585
2672
|
}
|
|
2586
|
-
// Two commit boundaries. byteStableBoundary: rows below it are byte-stable
|
|
2587
|
-
// (asserted never to re-layout) and stay under the committed-prefix audit.
|
|
2588
|
-
// durableBoundary: rows below it are durable — their scroll-off snapshot is
|
|
2589
|
-
// permanent (dropping them is forbidden) but may still drift afterward, so
|
|
2590
|
-
// they commit audit-EXEMPT. Both build on the finalized prefix (live-region
|
|
2591
|
-
// start); the whole frame when the root reports no seam (shell semantics:
|
|
2592
|
-
// whatever scrolls is final).
|
|
2593
|
-
const byteStableBoundary = Math.max(0, Math.min(frameLength, commitSafeEnd ?? liveRegionStart ?? frameLength));
|
|
2594
|
-
const durableBoundary = Math.max(
|
|
2595
|
-
byteStableBoundary,
|
|
2596
|
-
Math.min(frameLength, snapshotSafeEnd ?? byteStableBoundary),
|
|
2597
|
-
);
|
|
2598
2673
|
|
|
2599
2674
|
// 4. Classify. A resize is an explicit user gesture: normally the engine
|
|
2600
2675
|
// erases and replays so history rewraps at the new geometry (the reader
|
|
@@ -2612,7 +2687,7 @@ export class TUI extends Container {
|
|
|
2612
2687
|
if (fullPaint) {
|
|
2613
2688
|
committedPrefixResliced = true;
|
|
2614
2689
|
windowTop = Math.max(0, frameLength - height);
|
|
2615
|
-
chunkTo =
|
|
2690
|
+
chunkTo = windowTop;
|
|
2616
2691
|
} else if (
|
|
2617
2692
|
frameLength <= this.#committedRows ||
|
|
2618
2693
|
(committedRowsResynced &&
|
|
@@ -2630,7 +2705,7 @@ export class TUI extends Container {
|
|
|
2630
2705
|
// is preferable to a live editor gap and matches the existing
|
|
2631
2706
|
// "duplication, never loss" resync contract.
|
|
2632
2707
|
windowTop = Math.max(0, frameLength - height);
|
|
2633
|
-
chunkTo =
|
|
2708
|
+
chunkTo = windowTop;
|
|
2634
2709
|
committedPrefixResliced = true;
|
|
2635
2710
|
this.#committedRows = chunkTo;
|
|
2636
2711
|
this.#committedPrefix = rawFrame.slice(0, chunkTo);
|
|
@@ -2648,10 +2723,7 @@ export class TUI extends Container {
|
|
|
2648
2723
|
// pane keeps its own (old-wrap) history — and re-bases the audit
|
|
2649
2724
|
// prefix at the new width so the accepted wrap drift does not read
|
|
2650
2725
|
// as a violation on the next ordinary frame.
|
|
2651
|
-
chunkTo =
|
|
2652
|
-
hasVisibleOverlay || geometryChanged
|
|
2653
|
-
? this.#committedRows
|
|
2654
|
-
: Math.max(this.#committedRows, Math.min(durableBoundary, windowTop));
|
|
2726
|
+
chunkTo = hasVisibleOverlay || geometryChanged ? this.#committedRows : windowTop;
|
|
2655
2727
|
if (geometryChanged) {
|
|
2656
2728
|
committedPrefixResliced = true;
|
|
2657
2729
|
this.#committedPrefix = rawFrame.slice(0, this.#committedRows);
|
|
@@ -2711,7 +2783,15 @@ export class TUI extends Container {
|
|
|
2711
2783
|
windowTop,
|
|
2712
2784
|
});
|
|
2713
2785
|
this.#committedPrefix = rawFrame.slice(0, chunkTo);
|
|
2714
|
-
this.#updateCommittedAuditRows(
|
|
2786
|
+
this.#updateCommittedAuditRows(
|
|
2787
|
+
true,
|
|
2788
|
+
preCommitRows,
|
|
2789
|
+
preCommitAuditRows,
|
|
2790
|
+
preCommitDurableRows,
|
|
2791
|
+
byteStableBoundary,
|
|
2792
|
+
durableBoundary,
|
|
2793
|
+
false,
|
|
2794
|
+
);
|
|
2715
2795
|
this.#clearScrollbackOnNextRender = false;
|
|
2716
2796
|
this.#hasEverRendered = true;
|
|
2717
2797
|
if (!firstPaint && frameLength > height) this.#armPostFullPaintSettle();
|
|
@@ -2730,7 +2810,15 @@ export class TUI extends Container {
|
|
|
2730
2810
|
for (let i = this.#committedPrefix.length; i < chunkTo; i++) {
|
|
2731
2811
|
this.#committedPrefix.push(rawFrame[i] ?? "");
|
|
2732
2812
|
}
|
|
2733
|
-
this.#updateCommittedAuditRows(
|
|
2813
|
+
this.#updateCommittedAuditRows(
|
|
2814
|
+
committedPrefixResliced,
|
|
2815
|
+
preCommitRows,
|
|
2816
|
+
preCommitAuditRows,
|
|
2817
|
+
preCommitDurableRows,
|
|
2818
|
+
byteStableBoundary,
|
|
2819
|
+
durableBoundary,
|
|
2820
|
+
auditRan,
|
|
2821
|
+
);
|
|
2734
2822
|
}
|
|
2735
2823
|
|
|
2736
2824
|
/**
|
|
@@ -2740,13 +2828,21 @@ export class TUI extends Container {
|
|
|
2740
2828
|
* restyles keep their alignment and are left alone (stale styling in
|
|
2741
2829
|
* history was always the accepted artifact).
|
|
2742
2830
|
*/
|
|
2743
|
-
#auditCommittedPrefix(rawFrame: readonly string[]): void {
|
|
2831
|
+
#auditCommittedPrefix(rawFrame: readonly string[], permanentEnd: number): void {
|
|
2744
2832
|
const prefix = this.#committedPrefix;
|
|
2745
2833
|
if (prefix.length === 0) return;
|
|
2746
|
-
const resyncTo = findCommittedPrefixResync(
|
|
2834
|
+
const resyncTo = findCommittedPrefixResync(
|
|
2835
|
+
rawFrame,
|
|
2836
|
+
prefix,
|
|
2837
|
+
prefix.length,
|
|
2838
|
+
this.#committedPrefixAuditRows,
|
|
2839
|
+
this.#committedPrefixDurableRows,
|
|
2840
|
+
permanentEnd,
|
|
2841
|
+
);
|
|
2747
2842
|
if (resyncTo < 0) return;
|
|
2748
2843
|
this.#committedRows = resyncTo;
|
|
2749
2844
|
this.#committedPrefixAuditRows = Math.min(this.#committedPrefixAuditRows, resyncTo);
|
|
2845
|
+
this.#committedPrefixDurableRows = Math.min(this.#committedPrefixDurableRows, resyncTo);
|
|
2750
2846
|
prefix.length = resyncTo;
|
|
2751
2847
|
if ($flag("PI_DEBUG_REDRAW")) {
|
|
2752
2848
|
const msg = `[${new Date().toISOString()}] commit resync: committed prefix diverged at row ${resyncTo}; recommitting\n`;
|
|
@@ -2755,27 +2851,43 @@ export class TUI extends Container {
|
|
|
2755
2851
|
}
|
|
2756
2852
|
|
|
2757
2853
|
/**
|
|
2758
|
-
* Recompute the
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
2761
|
-
*
|
|
2762
|
-
* wholesale re-slice (full paint / shrink / geometry) re-bases
|
|
2763
|
-
*
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
2854
|
+
* Recompute the audit-rows / durable-rows marks after a commit (see the
|
|
2855
|
+
* #committedPrefixAuditRows field doc for the three audit zones).
|
|
2856
|
+
*
|
|
2857
|
+
* auditRows tracks the byte-stable boundary; durableRows the durable snapshot
|
|
2858
|
+
* boundary. A wholesale re-slice (full paint / shrink / geometry) re-bases
|
|
2859
|
+
* each mark from the current frame (min(committed, boundary)). An incremental
|
|
2860
|
+
* extend keeps a mark once a row past it has committed (mark < committed): a
|
|
2861
|
+
* later RISE in a boundary (a table finalizing) must neither pull
|
|
2862
|
+
* already-committed stale snapshots back under the byte-stable cap nor
|
|
2863
|
+
* retroactively exempt forced-overflow rows already audited. durableRows is
|
|
2864
|
+
* floored at auditRows so the exempt window can never invert.
|
|
2767
2865
|
*/
|
|
2768
2866
|
#updateCommittedAuditRows(
|
|
2769
2867
|
resliced: boolean,
|
|
2770
2868
|
preCommittedRows: number,
|
|
2771
2869
|
preAuditRows: number,
|
|
2870
|
+
preDurableRows: number,
|
|
2772
2871
|
byteStableBoundary: number,
|
|
2872
|
+
durableBoundary: number,
|
|
2873
|
+
hardAudited: boolean,
|
|
2773
2874
|
): void {
|
|
2774
2875
|
const committed = this.#committedRows;
|
|
2775
|
-
|
|
2876
|
+
const auditRows =
|
|
2776
2877
|
resliced || preAuditRows >= preCommittedRows
|
|
2777
2878
|
? Math.min(committed, byteStableBoundary)
|
|
2778
2879
|
: Math.min(preAuditRows, committed);
|
|
2880
|
+
// durableRows also advances when a hard audit ran this frame: the resync's
|
|
2881
|
+
// full hard scan verified the forced suffix [durableRows, min(committed,
|
|
2882
|
+
// durableBoundary)) (re-anchoring on any divergence), so those rows are now
|
|
2883
|
+
// proven durable and may leave the audited set — otherwise the durable-rise
|
|
2884
|
+
// gate would re-fire the full scan every frame (and spray on later drift).
|
|
2885
|
+
const durableRows =
|
|
2886
|
+
resliced || preDurableRows >= preCommittedRows || hardAudited
|
|
2887
|
+
? Math.min(committed, durableBoundary)
|
|
2888
|
+
: Math.min(preDurableRows, committed);
|
|
2889
|
+
this.#committedPrefixAuditRows = auditRows;
|
|
2890
|
+
this.#committedPrefixDurableRows = Math.max(auditRows, durableRows);
|
|
2779
2891
|
}
|
|
2780
2892
|
|
|
2781
2893
|
/**
|