@oh-my-pi/pi-tui 16.3.5 → 16.3.7
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 +23 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/components/box.d.ts +1 -1
- package/dist/types/components/cancellable-loader.d.ts +1 -1
- package/dist/types/components/editor.d.ts +4 -4
- package/dist/types/components/image.d.ts +2 -2
- package/dist/types/components/input.d.ts +1 -1
- package/dist/types/components/loader.d.ts +2 -2
- package/dist/types/components/markdown.d.ts +12 -2
- package/dist/types/components/scroll-view.d.ts +2 -2
- package/dist/types/components/select-list.d.ts +3 -3
- package/dist/types/components/settings-list.d.ts +2 -2
- package/dist/types/components/spacer.d.ts +1 -1
- package/dist/types/components/tab-bar.d.ts +1 -1
- package/dist/types/components/text.d.ts +1 -1
- package/dist/types/components/truncated-text.d.ts +1 -1
- package/dist/types/desktop-notify.d.ts +1 -1
- package/dist/types/editor-component.d.ts +2 -2
- package/dist/types/index.d.ts +32 -32
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/terminal-capabilities.d.ts +6 -0
- package/dist/types/terminal.d.ts +1 -1
- package/dist/types/tui.d.ts +37 -65
- package/package.json +3 -3
- package/src/autocomplete.ts +57 -45
- package/src/components/image.ts +18 -2
- package/src/components/markdown.ts +58 -5
- package/src/terminal-capabilities.ts +16 -0
- package/src/tui.ts +190 -318
package/src/tui.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Minimal TUI implementation with differential rendering.
|
|
3
3
|
*
|
|
4
4
|
* Append-only render contract: rows committed to native scrollback are
|
|
5
|
-
* immutable
|
|
6
|
-
* history exactly once, in order
|
|
7
|
-
*
|
|
5
|
+
* immutable — the tape is the terminal's visual record. Whatever scrolls
|
|
6
|
+
* above the window enters history exactly once, in order: as exact-final
|
|
7
|
+
* bytes when the component seam (`NativeScrollbackLiveRegion`) declared them
|
|
8
|
+
* final, else as a frozen snapshot of what was on screen. ED3 (`CSI 3 J`) is
|
|
8
9
|
* emitted only for gesture-driven replays (session replace, resize,
|
|
9
10
|
* resetDisplay) where snapping the viewport is acceptable. The engine never
|
|
10
11
|
* probes or guesses the terminal's scroll position, and the hot path clamps
|
|
@@ -183,45 +184,23 @@ export interface OverlayFocusOwner {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
|
-
* Component seam for append-only native-scrollback commits. A component
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
187
|
+
* Component seam for append-only native-scrollback commits. A component whose
|
|
188
|
+
* rendered rows can still change reports, after each render, the local line
|
|
189
|
+
* index where that mutable suffix begins. Rows above the boundary are declared
|
|
190
|
+
* FINAL — byte-stable at the current width for the component's lifetime — and
|
|
191
|
+
* commit to native scrollback as exact, audited content. Rows at/after the
|
|
192
|
+
* boundary repaint in place inside the visible window; when they scroll above
|
|
193
|
+
* the window top they still commit — the tape records what was on screen —
|
|
194
|
+
* but as frozen visual snapshots that are permanently audit-exempt: later
|
|
195
|
+
* re-layout of their source never re-anchors or recommits them. A root that
|
|
196
|
+
* reports no seam commits everything that scrolls as final (shell semantics).
|
|
192
197
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* Rows in `[liveRegionStart, commitSafeEnd)` may commit even though they are
|
|
197
|
-
* technically live, because they will never change. Without it, a single live
|
|
198
|
-
* block that alone overflows the window would hold its scrolled-off head out
|
|
199
|
-
* of history until it finalizes. Volatile live blocks (tool previews that
|
|
200
|
-
* collapse) omit it. Defaults to `liveRegionStart` when absent; a root that
|
|
201
|
-
* reports no seam at all commits everything that scrolls (shell semantics).
|
|
202
|
-
* `getNativeScrollbackSnapshotSafeEnd` optionally reports a still deeper
|
|
203
|
-
* boundary: the line index up to which the live region is *durable* — its rows
|
|
204
|
-
* may still change bytes later (a streaming markdown table re-aligning its
|
|
205
|
-
* columns every row), but their CURRENT snapshot is permanent content, so
|
|
206
|
-
* dropping them when they scroll above the window is forbidden. Unlike
|
|
207
|
-
* `commitSafeEnd` (byte-stable: offered rows are asserted never to re-layout and
|
|
208
|
-
* stay under the committed-prefix audit), rows committed under the snapshot end
|
|
209
|
-
* are audit-EXEMPT once they pass the window top — the engine appends their
|
|
210
|
-
* scroll-off snapshot and never recommits them, so later layout drift becomes a
|
|
211
|
-
* frozen stale row in history (duplication never loss) instead of either a
|
|
212
|
-
* dropped row or an audit re-anchor spray. Provisional live blocks (collapsing
|
|
213
|
-
* tool/edit previews whose head is a throwaway tail window) omit it. Defaults to
|
|
214
|
-
* `commitSafeEnd ?? liveRegionStart` when absent.
|
|
215
|
-
*
|
|
216
|
-
* When several root children report a seam in the same frame, the topmost
|
|
217
|
-
* one (and its commit-safe / snapshot-safe extension) defines the boundary:
|
|
218
|
-
* commits are prefix-only, so everything below the first seam is already
|
|
219
|
-
* excluded.
|
|
198
|
+
* When several root children report a seam in the same frame, the topmost one
|
|
199
|
+
* defines the boundary: exactness is prefix-only, so everything below the
|
|
200
|
+
* first seam is already excluded.
|
|
220
201
|
*/
|
|
221
202
|
export interface NativeScrollbackLiveRegion {
|
|
222
203
|
getNativeScrollbackLiveRegionStart(): number | undefined;
|
|
223
|
-
getNativeScrollbackCommitSafeEnd?(): number | undefined;
|
|
224
|
-
getNativeScrollbackSnapshotSafeEnd?(): number | undefined;
|
|
225
204
|
}
|
|
226
205
|
|
|
227
206
|
export interface NativeScrollbackCommittedRows {
|
|
@@ -243,14 +222,6 @@ function getNativeScrollbackLiveRegionStart(component: Component): number | unde
|
|
|
243
222
|
return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackLiveRegionStart?.();
|
|
244
223
|
}
|
|
245
224
|
|
|
246
|
-
function getNativeScrollbackCommitSafeEnd(component: Component): number | undefined {
|
|
247
|
-
return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackCommitSafeEnd?.();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function getNativeScrollbackSnapshotSafeEnd(component: Component): number | undefined {
|
|
251
|
-
return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackSnapshotSafeEnd?.();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
225
|
/**
|
|
255
226
|
* Opt-in stability report for components that mutate their returned render
|
|
256
227
|
* array in place across frames (instead of returning a fresh array per
|
|
@@ -617,7 +588,7 @@ interface CursorControlResult extends HardwareCursorUpdate {
|
|
|
617
588
|
* One root child's contribution to the composed frame: the array reference its
|
|
618
589
|
* render() returned, the frame row it starts at, the row count recorded at
|
|
619
590
|
* compose time (in-place mutators keep the reference but may change length),
|
|
620
|
-
* and the child-local seam
|
|
591
|
+
* and the child-local seam report captured at render time — replayed verbatim
|
|
621
592
|
* when a component-scoped frame reuses this segment without re-rendering.
|
|
622
593
|
*/
|
|
623
594
|
interface FrameSegment {
|
|
@@ -626,8 +597,6 @@ interface FrameSegment {
|
|
|
626
597
|
start: number;
|
|
627
598
|
rowCount: number;
|
|
628
599
|
liveLocalStart?: number;
|
|
629
|
-
commitLocalEnd?: number;
|
|
630
|
-
snapshotLocalEnd?: number;
|
|
631
600
|
}
|
|
632
601
|
|
|
633
602
|
/** Depth-first identity search through `Container`-shaped children. */
|
|
@@ -803,33 +772,27 @@ const RESYNC_TAIL_SAMPLES = 8;
|
|
|
803
772
|
* re-anchor the commit index when it does not. Returns the resync row index,
|
|
804
773
|
* or -1 when no resync is needed.
|
|
805
774
|
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
775
|
+
* Zones (verifiedTo ≤ finalTo ≤ prefix.length):
|
|
776
|
+
* [0, verifiedTo) VERIFIED exact rows — sampled with tolerance.
|
|
777
|
+
* [verifiedTo, finalTo) NEWLY-FINAL rows — frozen visual snapshots whose
|
|
778
|
+
* source just became declared-final (the block finalized / a barrier
|
|
779
|
+
* cleared). Hard-scanned in FULL with no tolerance: any content change
|
|
780
|
+
* (a pending header settling, a preview replaced by its result, a tail
|
|
781
|
+
* shifting up after a barrier removal) re-anchors so the final content
|
|
782
|
+
* recommits below the frozen snapshot — duplication, never loss —
|
|
783
|
+
* instead of being committed nowhere and painted nowhere.
|
|
784
|
+
* [finalTo, prefix.length) FROZEN visual snapshots of still-live rows —
|
|
785
|
+
* exempt: their drift is expected (a collapsing preview, a ticking
|
|
786
|
+
* progress tree) and must never spray re-anchors mid-run.
|
|
814
787
|
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
* between the two mutation classes — an in-place edit/restyle of a committed
|
|
824
|
-
* row disturbs only the touched rows (alignment below intact; the stale copy
|
|
825
|
-
* in history is the long-accepted artifact), while an insertion/deletion
|
|
826
|
-
* shifts EVERY row below it. So up to 8 non-blank rows within the last 24
|
|
827
|
-
* audited rows are compared SGR-stripped (theme changes stay quiet),
|
|
828
|
-
* tolerating a SINGLE non-hard mismatch (a legitimate one-row edit): aligned ⇒
|
|
829
|
-
* no resync; misaligned ⇒ resync at the first non-equivalent audited row. The
|
|
830
|
-
* tolerance keeps both an offscreen still-live barrier (a ticking spinner) and
|
|
831
|
-
* a no-seam in-place row edit from spraying duplicate snapshots every frame;
|
|
832
|
-
* the hard scan above is what forbids it from swallowing a finalized row.
|
|
788
|
+
* The verified zone's sampled check exploits the asymmetry between the two
|
|
789
|
+
* mutation classes: an in-place edit/restyle disturbs only the touched rows
|
|
790
|
+
* (alignment below stays intact; the stale copy in history is the accepted
|
|
791
|
+
* artifact), while an insertion/deletion shifts EVERY row below it. Up to 8
|
|
792
|
+
* non-blank rows within the last 24 verified rows are compared SGR-stripped
|
|
793
|
+
* (theme changes stay quiet), tolerating a SINGLE mismatch. The tolerance is
|
|
794
|
+
* load-bearing for roots that report NO seam: an animated row already in
|
|
795
|
+
* history would otherwise re-anchor on every glyph tick.
|
|
833
796
|
*
|
|
834
797
|
* Highly repetitive tails (identical filler rows) can mask a shift in the tail
|
|
835
798
|
* sample, in which case the skipped rows are content-identical to the committed
|
|
@@ -839,39 +802,30 @@ const RESYNC_TAIL_SAMPLES = 8;
|
|
|
839
802
|
export function findCommittedPrefixResync(
|
|
840
803
|
frame: readonly string[],
|
|
841
804
|
prefix: readonly string[],
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
exemptTo: number = exemptFrom,
|
|
845
|
-
permanentEnd = 0,
|
|
805
|
+
verifiedTo: number = prefix.length,
|
|
806
|
+
finalTo: number = verifiedTo,
|
|
846
807
|
): number {
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const audited = (i: number): boolean => i < exFrom || i >= exTo;
|
|
854
|
-
if (frame.length >= committed) {
|
|
855
|
-
// 1. Hard scan: forced-overflow rows now asserted permanent. Full scan, no
|
|
856
|
-
// tolerance — a finalized row that changed must re-anchor.
|
|
857
|
-
const hardEnd = Math.min(committed, Math.max(0, Math.trunc(permanentEnd)));
|
|
808
|
+
const verified = Math.min(prefix.length, Math.max(0, Math.trunc(verifiedTo)));
|
|
809
|
+
const hardEnd = Math.min(prefix.length, Math.max(verified, Math.trunc(finalTo)));
|
|
810
|
+
if (hardEnd === 0) return -1;
|
|
811
|
+
if (frame.length >= hardEnd) {
|
|
812
|
+
// 1. Hard scan: frozen snapshots whose source just became final. Full
|
|
813
|
+
// scan, no tolerance — a finalized row that changed must re-anchor.
|
|
858
814
|
let hardMismatch = false;
|
|
859
|
-
for (let i =
|
|
815
|
+
for (let i = verified; i < hardEnd; i++) {
|
|
860
816
|
if (!rowsEquivalent(frame[i]!, prefix[i]!)) {
|
|
861
817
|
hardMismatch = true;
|
|
862
818
|
break;
|
|
863
819
|
}
|
|
864
820
|
}
|
|
865
821
|
if (!hardMismatch) {
|
|
866
|
-
// 2. Tail sample
|
|
867
|
-
//
|
|
822
|
+
// 2. Tail sample over the verified zone (only when the hard scan is
|
|
823
|
+
// clean): walk up from its end until LOOKBACK rows or SAMPLES
|
|
824
|
+
// non-blank comparisons.
|
|
868
825
|
let samples = 0;
|
|
869
826
|
let mismatches = 0;
|
|
870
|
-
let
|
|
871
|
-
|
|
872
|
-
const idx = committed - j;
|
|
873
|
-
if (!audited(idx)) continue;
|
|
874
|
-
scanned++;
|
|
827
|
+
for (let j = 1; j <= verified && j <= RESYNC_TAIL_LOOKBACK && samples < RESYNC_TAIL_SAMPLES; j++) {
|
|
828
|
+
const idx = verified - j;
|
|
875
829
|
const row = frame[idx]!;
|
|
876
830
|
const old = prefix[idx]!;
|
|
877
831
|
if (row === old) {
|
|
@@ -882,18 +836,18 @@ export function findCommittedPrefixResync(
|
|
|
882
836
|
samples++;
|
|
883
837
|
if (!rowsEquivalent(row, old)) mismatches++;
|
|
884
838
|
}
|
|
885
|
-
// No signal (all-blank
|
|
839
|
+
// No signal (all-blank tail) or at most one edited row: aligned.
|
|
886
840
|
if (samples === 0 || mismatches <= 1) return -1;
|
|
887
841
|
}
|
|
888
842
|
}
|
|
889
|
-
// Misaligned (hard mismatch, tail-sample shift, or the frame no longer
|
|
890
|
-
// the
|
|
891
|
-
|
|
843
|
+
// Misaligned (hard mismatch, tail-sample shift, or the frame no longer
|
|
844
|
+
// covers the checked zones): re-anchor at the first row whose content
|
|
845
|
+
// changed.
|
|
846
|
+
const limit = Math.min(hardEnd, frame.length);
|
|
892
847
|
for (let i = 0; i < limit; i++) {
|
|
893
|
-
if (!audited(i)) continue;
|
|
894
848
|
if (!rowsEquivalent(frame[i]!, prefix[i]!)) return i;
|
|
895
849
|
}
|
|
896
|
-
return limit <
|
|
850
|
+
return limit < hardEnd ? limit : -1;
|
|
897
851
|
}
|
|
898
852
|
|
|
899
853
|
/**
|
|
@@ -999,33 +953,31 @@ export class TUI extends Container {
|
|
|
999
953
|
#cursorEndSequence = this.#synchronizedOutputEnabled ? CURSOR_END : CURSOR_END_NO_SYNC;
|
|
1000
954
|
// Rows of the current frame physically committed to the terminal tape
|
|
1001
955
|
// (native scrollback or scrolled past the window top). Immutable by
|
|
1002
|
-
// contract: the engine never rewrites them
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
956
|
+
// contract: the engine never rewrites them. Rows below
|
|
957
|
+
// #committedPrefixAuditRows entered as exact-final bytes (the component
|
|
958
|
+
// seam declared them); rows at/after it are frozen visual snapshots that
|
|
959
|
+
// scrolled off the window top while still live.
|
|
1005
960
|
#committedRows = 0;
|
|
1006
961
|
// Raw rows mirroring [0, #committedRows) — the engine's claim of what it
|
|
1007
|
-
// committed
|
|
1008
|
-
//
|
|
1009
|
-
// #auditCommittedPrefix). Holds
|
|
1010
|
-
// the audit is a pointer walk
|
|
962
|
+
// committed. The audited prefix [0, #committedPrefixAuditRows) is checked
|
|
963
|
+
// each ordinary frame against the current render to detect components
|
|
964
|
+
// re-laying-out declared-final content (see #auditCommittedPrefix). Holds
|
|
965
|
+
// references to component-cached strings, so the audit is a pointer walk
|
|
966
|
+
// in the common case.
|
|
1011
967
|
#committedPrefix: string[] = [];
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
// and otherwise advance per the persistence rules in #updateCommittedAuditRows.
|
|
1025
|
-
// #auditCommittedPrefix audits [0, committedRows) skipping the exempt window
|
|
1026
|
-
// [auditRows, durableRows).
|
|
968
|
+
// Rows of the committed prefix that were HARD-VERIFIED as exact-final
|
|
969
|
+
// bytes (committed below the exactness boundary, or frozen snapshots that
|
|
970
|
+
// passed the one-time strict scan when the boundary rose past them). Rows
|
|
971
|
+
// in [#committedPrefixAuditRows, #committedRows) are frozen visual
|
|
972
|
+
// snapshots of still-live content — the terminal's record of what was on
|
|
973
|
+
// screen when it scrolled off — and are audit-exempt while their source
|
|
974
|
+
// remains live, so a collapsing preview never sprays re-anchors mid-run.
|
|
975
|
+
// When the exactness boundary rises past them (the block finalized), they
|
|
976
|
+
// are strict-scanned exactly once: unchanged rows join the verified zone,
|
|
977
|
+
// a divergence re-anchors so the final content recommits below the frozen
|
|
978
|
+
// snapshot (duplication, never loss). Re-based on full paints / shrinks /
|
|
979
|
+
// geometry frames.
|
|
1027
980
|
#committedPrefixAuditRows = 0;
|
|
1028
|
-
#committedPrefixDurableRows = 0;
|
|
1029
981
|
// Frame row currently mapped to screen row 0. Monotonic between full
|
|
1030
982
|
// paints: a shrink never re-exposes scrolled-off rows (they cannot be
|
|
1031
983
|
// un-scrolled without rewriting history); live rows repaint at fixed
|
|
@@ -1034,8 +986,6 @@ export class TUI extends Container {
|
|
|
1034
986
|
// Exactly what is painted on the screen rows (post-composite, prepared).
|
|
1035
987
|
#previousWindow: string[] = [];
|
|
1036
988
|
#nativeScrollbackLiveRegionStart: number | undefined;
|
|
1037
|
-
#nativeScrollbackCommitSafeEnd: number | undefined;
|
|
1038
|
-
#nativeScrollbackSnapshotSafeEnd: number | undefined;
|
|
1039
989
|
#fullRedrawCount = 0;
|
|
1040
990
|
// Caps how many inline images render as live graphics; older ones fall back
|
|
1041
991
|
// to text via a purge + full redraw. Cap is configured by the host app.
|
|
@@ -1153,8 +1103,6 @@ export class TUI extends Container {
|
|
|
1153
1103
|
override render(width: number): readonly string[] {
|
|
1154
1104
|
width = Math.max(1, width);
|
|
1155
1105
|
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
1156
|
-
this.#nativeScrollbackCommitSafeEnd = undefined;
|
|
1157
|
-
this.#nativeScrollbackSnapshotSafeEnd = undefined;
|
|
1158
1106
|
const children = this.children;
|
|
1159
1107
|
const previousSegments = this.#frameSegments;
|
|
1160
1108
|
const segments: FrameSegment[] = new Array(children.length);
|
|
@@ -1175,14 +1123,10 @@ export class TUI extends Container {
|
|
|
1175
1123
|
partialRoots !== null && previous !== undefined && previous.component === child && !partialRoots.has(child);
|
|
1176
1124
|
let childLines: readonly string[];
|
|
1177
1125
|
let liveLocalStart: number | undefined;
|
|
1178
|
-
let commitLocalEnd: number | undefined;
|
|
1179
|
-
let snapshotLocalEnd: number | undefined;
|
|
1180
1126
|
let reported: number | undefined;
|
|
1181
1127
|
if (reuse) {
|
|
1182
1128
|
childLines = previous.lines;
|
|
1183
1129
|
liveLocalStart = previous.liveLocalStart;
|
|
1184
|
-
commitLocalEnd = previous.commitLocalEnd;
|
|
1185
|
-
snapshotLocalEnd = previous.snapshotLocalEnd;
|
|
1186
1130
|
} else {
|
|
1187
1131
|
// Feed the engine's committed-row claim (from the previous frame's
|
|
1188
1132
|
// emit) before rendering so the child can skip re-deriving blocks
|
|
@@ -1195,22 +1139,6 @@ export class TUI extends Container {
|
|
|
1195
1139
|
liveLocalStart = Number.isFinite(liveRegionStart)
|
|
1196
1140
|
? Math.max(0, Math.min(childLines.length, Math.trunc(liveRegionStart)))
|
|
1197
1141
|
: childLines.length;
|
|
1198
|
-
const commitSafeEnd = getNativeScrollbackCommitSafeEnd(child);
|
|
1199
|
-
if (commitSafeEnd !== undefined) {
|
|
1200
|
-
commitLocalEnd = Number.isFinite(commitSafeEnd)
|
|
1201
|
-
? Math.max(liveLocalStart, Math.min(childLines.length, Math.trunc(commitSafeEnd)))
|
|
1202
|
-
: childLines.length;
|
|
1203
|
-
}
|
|
1204
|
-
// Durable snapshot end: clamped at/above the byte-stable end (or
|
|
1205
|
-
// the live-region start when none) so a child can never report a
|
|
1206
|
-
// shallower durable boundary than its byte-stable one.
|
|
1207
|
-
const snapshotSafeEnd = getNativeScrollbackSnapshotSafeEnd(child);
|
|
1208
|
-
if (snapshotSafeEnd !== undefined) {
|
|
1209
|
-
const snapshotFloor = commitLocalEnd ?? liveLocalStart;
|
|
1210
|
-
snapshotLocalEnd = Number.isFinite(snapshotSafeEnd)
|
|
1211
|
-
? Math.max(snapshotFloor, Math.min(childLines.length, Math.trunc(snapshotSafeEnd)))
|
|
1212
|
-
: childLines.length;
|
|
1213
|
-
}
|
|
1214
1142
|
}
|
|
1215
1143
|
// Consume the stability report unconditionally for implementers:
|
|
1216
1144
|
// reading re-bases the component's baseline to the state this
|
|
@@ -1221,19 +1149,13 @@ export class TUI extends Container {
|
|
|
1221
1149
|
reported = getRenderStablePrefixRows(child);
|
|
1222
1150
|
}
|
|
1223
1151
|
// Topmost seam wins. Commits are prefix-only: the first child that
|
|
1224
|
-
// reports a live region
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1152
|
+
// reports a live region already bounds everything below it, so a
|
|
1153
|
+
// lower sibling's seam (e.g. a status loader under a streaming
|
|
1154
|
+
// transcript) must never overwrite it — moving the boundary down
|
|
1155
|
+
// would commit the earlier child's still-mutable rows as stale
|
|
1156
|
+
// history.
|
|
1229
1157
|
if (liveLocalStart !== undefined && this.#nativeScrollbackLiveRegionStart === undefined) {
|
|
1230
1158
|
this.#nativeScrollbackLiveRegionStart = offset + liveLocalStart;
|
|
1231
|
-
if (commitLocalEnd !== undefined) {
|
|
1232
|
-
this.#nativeScrollbackCommitSafeEnd = offset + commitLocalEnd;
|
|
1233
|
-
}
|
|
1234
|
-
if (snapshotLocalEnd !== undefined) {
|
|
1235
|
-
this.#nativeScrollbackSnapshotSafeEnd = offset + snapshotLocalEnd;
|
|
1236
|
-
}
|
|
1237
1159
|
}
|
|
1238
1160
|
if (chainStable) {
|
|
1239
1161
|
if (previous !== undefined && previous.component === child && previous.start === offset) {
|
|
@@ -1262,8 +1184,6 @@ export class TUI extends Container {
|
|
|
1262
1184
|
start: offset,
|
|
1263
1185
|
rowCount: childLines.length,
|
|
1264
1186
|
liveLocalStart,
|
|
1265
|
-
commitLocalEnd,
|
|
1266
|
-
snapshotLocalEnd,
|
|
1267
1187
|
};
|
|
1268
1188
|
offset += childLines.length;
|
|
1269
1189
|
}
|
|
@@ -2662,31 +2582,15 @@ export class TUI extends Container {
|
|
|
2662
2582
|
// known. Ascending by frame row.
|
|
2663
2583
|
const cursorMarkers = this.#frameCursorMarkers;
|
|
2664
2584
|
const liveRegionStart = this.#nativeScrollbackLiveRegionStart;
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
//
|
|
2669
|
-
//
|
|
2670
|
-
//
|
|
2671
|
-
//
|
|
2672
|
-
// whatever scrolls above the window is committed — never committed nowhere
|
|
2673
|
-
// AND painted nowhere (the loss bug). The boundaries no longer gate the
|
|
2674
|
-
// commit; they define the audit-exempt span. byteStableBoundary: rows below
|
|
2675
|
-
// it are byte-stable (never re-layout), audited. durableBoundary: rows in
|
|
2676
|
-
// [byteStableBoundary, durableBoundary) are durable — permanent on scroll-off
|
|
2677
|
-
// but may drift in place (a streaming table re-aligning), committed
|
|
2678
|
-
// audit-EXEMPT. Rows at/beyond durableBoundary committed only because they
|
|
2679
|
-
// scrolled above the window (a commit-unstable barrier over a long tail) are
|
|
2680
|
-
// forced-overflow rows: audited, so a later shift/finalize/removal re-anchors
|
|
2681
|
-
// (duplication, never loss) instead of stranding a stale prefix. Built on the
|
|
2682
|
-
// finalized prefix (live-region start); the whole frame when the root reports
|
|
2683
|
-
// no seam (shell semantics: whatever scrolls is final).
|
|
2585
|
+
|
|
2586
|
+
// Exactness boundary (used by the audit-zone math below). Rows below it
|
|
2587
|
+
// are declared FINAL by the component seam: when they commit, they enter
|
|
2588
|
+
// the audited zone (byte-exact, repairable on violation). Rows above it
|
|
2589
|
+
// that scroll off the window commit as frozen visual snapshots (see
|
|
2590
|
+
// #committedPrefixAuditRows). The whole frame is final when the root
|
|
2591
|
+
// reports no seam (shell semantics).
|
|
2684
2592
|
const frameLength = rawFrame.length;
|
|
2685
|
-
const
|
|
2686
|
-
const durableBoundary = Math.max(
|
|
2687
|
-
byteStableBoundary,
|
|
2688
|
-
Math.min(frameLength, snapshotSafeEnd ?? byteStableBoundary),
|
|
2689
|
-
);
|
|
2593
|
+
const finalBoundary = Math.max(0, Math.min(frameLength, liveRegionStart ?? frameLength));
|
|
2690
2594
|
|
|
2691
2595
|
// 2. Transition state captured before any emitter runs.
|
|
2692
2596
|
const prevWindowTop = this.#windowTopRow;
|
|
@@ -2703,49 +2607,67 @@ export class TUI extends Container {
|
|
|
2703
2607
|
(resizeEventOccurred && this.#previousHeight > 0);
|
|
2704
2608
|
const geometryChanged = widthChanged || heightChanged;
|
|
2705
2609
|
|
|
2706
|
-
// Committed-prefix audit
|
|
2707
|
-
//
|
|
2708
|
-
//
|
|
2709
|
-
//
|
|
2710
|
-
//
|
|
2711
|
-
//
|
|
2712
|
-
//
|
|
2713
|
-
//
|
|
2714
|
-
//
|
|
2715
|
-
//
|
|
2716
|
-
//
|
|
2717
|
-
// that provably did not change since the last (aligned) frame cannot
|
|
2718
|
-
// have diverged.
|
|
2610
|
+
// Committed-prefix audit. Rows below the audit mark are hard-verified
|
|
2611
|
+
// exact bytes; rows between the mark and the current exactness boundary
|
|
2612
|
+
// are frozen snapshots whose source JUST became final and must be
|
|
2613
|
+
// verified once (a pending header settling, a barrier clearing above a
|
|
2614
|
+
// shifted tail); rows past the boundary are still-live frozen snapshots,
|
|
2615
|
+
// exempt so a collapsing preview can never spray re-anchors mid-run. A
|
|
2616
|
+
// divergence re-anchors and recommits — duplication, never loss —
|
|
2617
|
+
// instead of silently skipping rows (committed nowhere, painted
|
|
2618
|
+
// nowhere). Skipped on geometry frames (a rewrap legitimately reflows
|
|
2619
|
+
// every row), and skipped when the composed frame's stable prefix
|
|
2620
|
+
// covers every verified row and no rows newly became final.
|
|
2719
2621
|
let committedRowsResynced = false;
|
|
2720
|
-
|
|
2721
|
-
//
|
|
2722
|
-
//
|
|
2723
|
-
//
|
|
2724
|
-
//
|
|
2725
|
-
//
|
|
2726
|
-
//
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
const auditUpper =
|
|
2731
|
-
this.#committedPrefixDurableRows < this.#committedRows ? this.#committedRows : this.#committedPrefixAuditRows;
|
|
2732
|
-
const hardAuditEnd = Math.min(this.#committedRows, durableBoundary);
|
|
2733
|
-
const needHardAudit = this.#committedPrefixDurableRows < hardAuditEnd;
|
|
2622
|
+
const newlyFinalEnd = Math.min(this.#committedRows, finalBoundary);
|
|
2623
|
+
// The exactness boundary can RETREAT (a markdown rewind, a mermaid fence
|
|
2624
|
+
// appearing, a fast-path reset re-opening a block): rows verified under
|
|
2625
|
+
// the old boundary have a live source again. Demote them to frozen
|
|
2626
|
+
// snapshots instead of auditing content that is expected to change —
|
|
2627
|
+
// their committed bytes stay as the visual record, and the next boundary
|
|
2628
|
+
// rise strict-verifies them once like any other frozen row.
|
|
2629
|
+
if (this.#committedPrefixAuditRows > newlyFinalEnd) {
|
|
2630
|
+
this.#committedPrefixAuditRows = newlyFinalEnd;
|
|
2631
|
+
}
|
|
2734
2632
|
const auditRan =
|
|
2735
2633
|
this.#hasEverRendered &&
|
|
2736
2634
|
!geometryChanged &&
|
|
2737
2635
|
!this.#clearScrollbackOnNextRender &&
|
|
2738
|
-
(this.#renderStablePrefixRows <
|
|
2636
|
+
(this.#renderStablePrefixRows < this.#committedPrefixAuditRows ||
|
|
2637
|
+
newlyFinalEnd > this.#committedPrefixAuditRows);
|
|
2739
2638
|
if (auditRan) {
|
|
2740
2639
|
const committedRowsBeforeAudit = this.#committedRows;
|
|
2741
|
-
this.#auditCommittedPrefix(rawFrame,
|
|
2640
|
+
this.#auditCommittedPrefix(rawFrame, newlyFinalEnd);
|
|
2742
2641
|
committedRowsResynced = this.#committedRows !== committedRowsBeforeAudit;
|
|
2743
2642
|
}
|
|
2744
|
-
//
|
|
2745
|
-
//
|
|
2643
|
+
// A frame that shrank below the committed row count collapsed content
|
|
2644
|
+
// that was already recorded (a live suffix collapsing on abort/result).
|
|
2645
|
+
// Re-base the commit index at the first divergence against the recorded
|
|
2646
|
+
// prefix — frozen snapshots included; a collapse is precisely when the
|
|
2647
|
+
// record and the frame part ways — so the surviving exact prefix stays
|
|
2648
|
+
// recognized and is never re-shown or re-committed. Only genuinely new
|
|
2649
|
+
// content repaints below it.
|
|
2650
|
+
if (!geometryChanged && !this.#clearScrollbackOnNextRender && frameLength < this.#committedRows) {
|
|
2651
|
+
const limit = Math.min(this.#committedRows, frameLength);
|
|
2652
|
+
let diverged = limit;
|
|
2653
|
+
for (let i = 0; i < limit; i++) {
|
|
2654
|
+
if (!rowsEquivalent(rawFrame[i]!, this.#committedPrefix[i]!)) {
|
|
2655
|
+
diverged = i;
|
|
2656
|
+
break;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
if (diverged < this.#committedRows) {
|
|
2660
|
+
this.#committedRows = diverged;
|
|
2661
|
+
this.#committedPrefixAuditRows = Math.min(this.#committedPrefixAuditRows, diverged);
|
|
2662
|
+
this.#committedPrefix.length = diverged;
|
|
2663
|
+
committedRowsResynced = true;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
// Committed-prefix state this frame's commit math extends from
|
|
2667
|
+
// (post-audit): drives the audit-mark advance after the emit.
|
|
2746
2668
|
const preCommitRows = this.#committedRows;
|
|
2747
|
-
const
|
|
2748
|
-
|
|
2669
|
+
const preAuditRows = this.#committedPrefixAuditRows;
|
|
2670
|
+
let committedPrefixResliced = false;
|
|
2749
2671
|
|
|
2750
2672
|
// 3. Window and commit math (lengths only; content prepared below).
|
|
2751
2673
|
let hasVisibleOverlay = false;
|
|
@@ -2768,7 +2690,6 @@ export class TUI extends Container {
|
|
|
2768
2690
|
const fullPaint = firstPaint || replaceRequested || geometryRebuild;
|
|
2769
2691
|
let windowTop: number;
|
|
2770
2692
|
let chunkTo: number;
|
|
2771
|
-
let committedPrefixResliced = false;
|
|
2772
2693
|
if (fullPaint) {
|
|
2773
2694
|
committedPrefixResliced = true;
|
|
2774
2695
|
windowTop = Math.max(0, frameLength - height);
|
|
@@ -2789,9 +2710,9 @@ export class TUI extends Container {
|
|
|
2789
2710
|
// stale committed copy stays in native history; duplicating a few rows
|
|
2790
2711
|
// is preferable to a live editor gap and matches the existing
|
|
2791
2712
|
// "duplication, never loss" resync contract.
|
|
2713
|
+
committedPrefixResliced = true;
|
|
2792
2714
|
windowTop = Math.max(0, frameLength - height);
|
|
2793
2715
|
chunkTo = windowTop;
|
|
2794
|
-
committedPrefixResliced = true;
|
|
2795
2716
|
this.#committedRows = chunkTo;
|
|
2796
2717
|
this.#committedPrefix = rawFrame.slice(0, chunkTo);
|
|
2797
2718
|
} else {
|
|
@@ -2802,12 +2723,14 @@ export class TUI extends Container {
|
|
|
2802
2723
|
// multiplexer resize the pane reflowed its own history; committed
|
|
2803
2724
|
// rows keep their old wrap there, same as any shell output.
|
|
2804
2725
|
windowTop = Math.max(this.#committedRows, frameLength - height, 0);
|
|
2805
|
-
//
|
|
2806
|
-
//
|
|
2807
|
-
//
|
|
2808
|
-
//
|
|
2809
|
-
//
|
|
2810
|
-
//
|
|
2726
|
+
// Whatever scrolls above the window commits — the tape is the visual
|
|
2727
|
+
// record; nothing that was painted may vanish. Overlays freeze
|
|
2728
|
+
// commits: composited rows must never enter history, and the hidden
|
|
2729
|
+
// gap backfills via the chunk once the overlay closes. A multiplexer
|
|
2730
|
+
// resize also commits nothing — the pane keeps its own (old-wrap)
|
|
2731
|
+
// history — and re-bases the audit prefix at the new width so the
|
|
2732
|
+
// accepted wrap drift does not read as a violation on the next
|
|
2733
|
+
// ordinary frame.
|
|
2811
2734
|
chunkTo = hasVisibleOverlay || geometryChanged ? this.#committedRows : windowTop;
|
|
2812
2735
|
if (geometryChanged) {
|
|
2813
2736
|
committedPrefixResliced = true;
|
|
@@ -2870,15 +2793,7 @@ export class TUI extends Container {
|
|
|
2870
2793
|
cursorTrackingLineCount,
|
|
2871
2794
|
});
|
|
2872
2795
|
this.#committedPrefix = rawFrame.slice(0, chunkTo);
|
|
2873
|
-
this.#
|
|
2874
|
-
true,
|
|
2875
|
-
preCommitRows,
|
|
2876
|
-
preCommitAuditRows,
|
|
2877
|
-
preCommitDurableRows,
|
|
2878
|
-
byteStableBoundary,
|
|
2879
|
-
durableBoundary,
|
|
2880
|
-
false,
|
|
2881
|
-
);
|
|
2796
|
+
this.#committedPrefixAuditRows = Math.min(chunkTo, finalBoundary);
|
|
2882
2797
|
this.#clearScrollbackOnNextRender = false;
|
|
2883
2798
|
this.#hasEverRendered = true;
|
|
2884
2799
|
if (!firstPaint && frameLength > height) this.#armPostFullPaintSettle();
|
|
@@ -2899,39 +2814,32 @@ export class TUI extends Container {
|
|
|
2899
2814
|
for (let i = this.#committedPrefix.length; i < chunkTo; i++) {
|
|
2900
2815
|
this.#committedPrefix.push(rawFrame[i] ?? "");
|
|
2901
2816
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2817
|
+
// Audit-mark advance. A re-slice re-bases it outright. Otherwise it may
|
|
2818
|
+
// advance to the exactness boundary only when this frame verified the
|
|
2819
|
+
// newly-final span (auditRan hard-scans it) or no such span existed —
|
|
2820
|
+
// rows committed this frame below the boundary are fresh exact bytes.
|
|
2821
|
+
if (committedPrefixResliced || auditRan || preAuditRows >= Math.min(preCommitRows, finalBoundary)) {
|
|
2822
|
+
this.#committedPrefixAuditRows = Math.min(this.#committedRows, finalBoundary);
|
|
2823
|
+
} else {
|
|
2824
|
+
this.#committedPrefixAuditRows = Math.min(preAuditRows, this.#committedRows);
|
|
2825
|
+
}
|
|
2911
2826
|
}
|
|
2912
2827
|
|
|
2913
2828
|
/**
|
|
2914
|
-
* Detect committed-prefix violations
|
|
2915
|
-
*
|
|
2916
|
-
*
|
|
2917
|
-
*
|
|
2918
|
-
* history was always
|
|
2829
|
+
* Detect committed-prefix violations (see {@link findCommittedPrefixResync}
|
|
2830
|
+
* for the zone semantics) and re-anchor the commit index at the first moved
|
|
2831
|
+
* row, so subsequent rows recommit instead of being skipped: the stale copy
|
|
2832
|
+
* stays in history — duplication, never loss. Pure in-place restyles keep
|
|
2833
|
+
* their alignment and are left alone (stale styling in history was always
|
|
2834
|
+
* the accepted artifact).
|
|
2919
2835
|
*/
|
|
2920
|
-
#auditCommittedPrefix(rawFrame: readonly string[],
|
|
2836
|
+
#auditCommittedPrefix(rawFrame: readonly string[], newlyFinalEnd: number): void {
|
|
2921
2837
|
const prefix = this.#committedPrefix;
|
|
2922
2838
|
if (prefix.length === 0) return;
|
|
2923
|
-
const resyncTo = findCommittedPrefixResync(
|
|
2924
|
-
rawFrame,
|
|
2925
|
-
prefix,
|
|
2926
|
-
prefix.length,
|
|
2927
|
-
this.#committedPrefixAuditRows,
|
|
2928
|
-
this.#committedPrefixDurableRows,
|
|
2929
|
-
permanentEnd,
|
|
2930
|
-
);
|
|
2839
|
+
const resyncTo = findCommittedPrefixResync(rawFrame, prefix, this.#committedPrefixAuditRows, newlyFinalEnd);
|
|
2931
2840
|
if (resyncTo < 0) return;
|
|
2932
2841
|
this.#committedRows = resyncTo;
|
|
2933
2842
|
this.#committedPrefixAuditRows = Math.min(this.#committedPrefixAuditRows, resyncTo);
|
|
2934
|
-
this.#committedPrefixDurableRows = Math.min(this.#committedPrefixDurableRows, resyncTo);
|
|
2935
2843
|
prefix.length = resyncTo;
|
|
2936
2844
|
if ($flag("PI_DEBUG_REDRAW")) {
|
|
2937
2845
|
const msg = `[${new Date().toISOString()}] commit resync: committed prefix diverged at row ${resyncTo}; recommitting\n`;
|
|
@@ -2939,46 +2847,6 @@ export class TUI extends Container {
|
|
|
2939
2847
|
}
|
|
2940
2848
|
}
|
|
2941
2849
|
|
|
2942
|
-
/**
|
|
2943
|
-
* Recompute the audit-rows / durable-rows marks after a commit (see the
|
|
2944
|
-
* #committedPrefixAuditRows field doc for the three audit zones).
|
|
2945
|
-
*
|
|
2946
|
-
* auditRows tracks the byte-stable boundary; durableRows the durable snapshot
|
|
2947
|
-
* boundary. A wholesale re-slice (full paint / shrink / geometry) re-bases
|
|
2948
|
-
* each mark from the current frame (min(committed, boundary)). An incremental
|
|
2949
|
-
* extend keeps a mark once a row past it has committed (mark < committed): a
|
|
2950
|
-
* later RISE in a boundary (a table finalizing) must neither pull
|
|
2951
|
-
* already-committed stale snapshots back under the byte-stable cap nor
|
|
2952
|
-
* retroactively exempt forced-overflow rows already audited. durableRows is
|
|
2953
|
-
* floored at auditRows so the exempt window can never invert.
|
|
2954
|
-
*/
|
|
2955
|
-
#updateCommittedAuditRows(
|
|
2956
|
-
resliced: boolean,
|
|
2957
|
-
preCommittedRows: number,
|
|
2958
|
-
preAuditRows: number,
|
|
2959
|
-
preDurableRows: number,
|
|
2960
|
-
byteStableBoundary: number,
|
|
2961
|
-
durableBoundary: number,
|
|
2962
|
-
hardAudited: boolean,
|
|
2963
|
-
): void {
|
|
2964
|
-
const committed = this.#committedRows;
|
|
2965
|
-
const auditRows =
|
|
2966
|
-
resliced || preAuditRows >= preCommittedRows
|
|
2967
|
-
? Math.min(committed, byteStableBoundary)
|
|
2968
|
-
: Math.min(preAuditRows, committed);
|
|
2969
|
-
// durableRows also advances when a hard audit ran this frame: the resync's
|
|
2970
|
-
// full hard scan verified the forced suffix [durableRows, min(committed,
|
|
2971
|
-
// durableBoundary)) (re-anchoring on any divergence), so those rows are now
|
|
2972
|
-
// proven durable and may leave the audited set — otherwise the durable-rise
|
|
2973
|
-
// gate would re-fire the full scan every frame (and spray on later drift).
|
|
2974
|
-
const durableRows =
|
|
2975
|
-
resliced || preDurableRows >= preCommittedRows || hardAudited
|
|
2976
|
-
? Math.min(committed, durableBoundary)
|
|
2977
|
-
: Math.min(preDurableRows, committed);
|
|
2978
|
-
this.#committedPrefixAuditRows = auditRows;
|
|
2979
|
-
this.#committedPrefixDurableRows = Math.max(auditRows, durableRows);
|
|
2980
|
-
}
|
|
2981
|
-
|
|
2982
2850
|
/**
|
|
2983
2851
|
* Prepare the composed frame for emission, in place. Rows below
|
|
2984
2852
|
* `#preparedValidRows` are already prepared against the current frame (the
|
|
@@ -3729,17 +3597,20 @@ export class TUI extends Container {
|
|
|
3729
3597
|
}
|
|
3730
3598
|
}
|
|
3731
3599
|
|
|
3732
|
-
// In-window diff: nothing commits.
|
|
3733
|
-
//
|
|
3734
|
-
//
|
|
3735
|
-
//
|
|
3736
|
-
// row
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
if (
|
|
3600
|
+
// In-window diff: nothing commits. Rewrite in place when the window slid
|
|
3601
|
+
// without a commit — an overlay visible (composited rows must never enter
|
|
3602
|
+
// history), a commit-frozen geometry frame, or the window pulling back
|
|
3603
|
+
// down after a shrink. Overlay cursor-only frames can also leave the
|
|
3604
|
+
// tracked row behind the physical cursor; a relative partial rewrite from
|
|
3605
|
+
// that stale origin can CRLF on the bottom row and scroll native history
|
|
3606
|
+
// without appending to the commit tape, so overlays always take the
|
|
3607
|
+
// top-clamped full rewrite.
|
|
3608
|
+
const inPlaceRewrite = repaintVirtualScrollInPlace || scroll !== 0;
|
|
3609
|
+
if (chunkLength === 0) {
|
|
3610
|
+
if (forceWindowRewrite || inPlaceRewrite) this.#fullRedrawCount += 1;
|
|
3611
|
+
let firstChanged = forceWindowRewrite || inPlaceRewrite ? 0 : -1;
|
|
3612
|
+
let lastChanged = forceWindowRewrite || inPlaceRewrite ? height - 1 : -1;
|
|
3613
|
+
if (!forceWindowRewrite && !inPlaceRewrite) {
|
|
3743
3614
|
const comparable = previousWindow.length === height;
|
|
3744
3615
|
for (let r = 0; r < height; r++) {
|
|
3745
3616
|
if (comparable && (window[r] ?? "") === (previousWindow[r] ?? "")) continue;
|
|
@@ -3755,10 +3626,11 @@ export class TUI extends Container {
|
|
|
3755
3626
|
return;
|
|
3756
3627
|
}
|
|
3757
3628
|
let buffer = this.#paintBeginSequence + purgeSequence;
|
|
3758
|
-
if (
|
|
3759
|
-
// The cursor tracker can be stale after overlay-only frames
|
|
3760
|
-
//
|
|
3761
|
-
//
|
|
3629
|
+
if (inPlaceRewrite) {
|
|
3630
|
+
// The cursor tracker can be stale after overlay-only frames, and
|
|
3631
|
+
// meaningless after an uncommitted slide. A large CUU clamps at the
|
|
3632
|
+
// viewport top without using absolute cursor home, so the following
|
|
3633
|
+
// full-window rewrite cannot overflow the bottom.
|
|
3762
3634
|
if (height > 1) buffer += `\x1b[${height - 1}A`;
|
|
3763
3635
|
} else {
|
|
3764
3636
|
const rowDelta = firstChanged - currentScreenRow;
|
|
@@ -3838,7 +3710,7 @@ export class TUI extends Container {
|
|
|
3838
3710
|
: `fullPaint(clearScrollback=${intent.clearScrollback})`;
|
|
3839
3711
|
const state =
|
|
3840
3712
|
`committed=${this.#committedRows}, windowTop=${this.#windowTopRow}, ` +
|
|
3841
|
-
`lrStart=${this.#nativeScrollbackLiveRegionStart}
|
|
3713
|
+
`lrStart=${this.#nativeScrollbackLiveRegionStart}`;
|
|
3842
3714
|
const msg = `[${new Date().toISOString()}] render: ${detail} (prev=${this.#previousFrameLength}, new=${newLength}, height=${height}, ${state})\n`;
|
|
3843
3715
|
fs.appendFileSync(getDebugLogPath(), msg);
|
|
3844
3716
|
}
|