@gajae-code/tui 0.12.0 → 0.12.1
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 +10 -0
- package/dist/types/stdin-buffer.d.ts +6 -0
- package/dist/types/terminal-capabilities.d.ts +9 -0
- package/dist/types/terminal.d.ts +16 -0
- package/package.json +3 -3
- package/src/autocomplete.ts +4 -0
- package/src/stdin-buffer.ts +99 -11
- package/src/terminal-capabilities.ts +63 -1
- package/src/terminal.ts +98 -11
- package/src/tui.ts +406 -90
package/src/tui.ts
CHANGED
|
@@ -10,9 +10,12 @@ import { isKeyRelease } from "./keys";
|
|
|
10
10
|
import { renderMetrics } from "./metrics";
|
|
11
11
|
import type { Terminal } from "./terminal";
|
|
12
12
|
import {
|
|
13
|
+
encodeKittyPlacementDelete,
|
|
14
|
+
extractKittyPlacementReferences,
|
|
13
15
|
ImageProtocol,
|
|
14
16
|
isImageProtocolForced,
|
|
15
17
|
isUnderTerminalMultiplexer,
|
|
18
|
+
type KittyPlacementReference,
|
|
16
19
|
setCellDimensions,
|
|
17
20
|
setTerminalImageProtocol,
|
|
18
21
|
TERMINAL,
|
|
@@ -42,6 +45,9 @@ const MOUSE_SELECTION_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "
|
|
|
42
45
|
/** Discrete mouse-wheel notch size in terminal rows (xterm/less-style). */
|
|
43
46
|
export const DEFAULT_WHEEL_LINES = 3;
|
|
44
47
|
|
|
48
|
+
/** DA1 (`CSI ? … c`) and XTSMGRAPHICS (`CSI ? … S`) replies to the sixel probe. */
|
|
49
|
+
const DEVICE_REPORT_PATTERN = /^\x1b\[\?[\d;]*[cS]$/u;
|
|
50
|
+
|
|
45
51
|
function stripTerminalControls(text: string): string {
|
|
46
52
|
return Bun.stripANSI(text)
|
|
47
53
|
.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/gu, "")
|
|
@@ -635,6 +641,23 @@ type RenderCommitWaiter = {
|
|
|
635
641
|
timer: NodeJS.Timeout;
|
|
636
642
|
};
|
|
637
643
|
|
|
644
|
+
type KittyPlacementOwner = "transcript" | "suffix" | "overlay";
|
|
645
|
+
|
|
646
|
+
type KittyPlacementSpan = KittyPlacementReference & {
|
|
647
|
+
row: number;
|
|
648
|
+
owner: KittyPlacementOwner;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
type KittyPlacementRegion = {
|
|
652
|
+
top: number;
|
|
653
|
+
bottom: number;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
type KittyPlacementDeletePlan = {
|
|
657
|
+
deletedKeys: Set<string>;
|
|
658
|
+
output: string;
|
|
659
|
+
};
|
|
660
|
+
|
|
638
661
|
/**
|
|
639
662
|
* TUI - Main class for managing terminal UI with differential rendering
|
|
640
663
|
*/
|
|
@@ -642,6 +665,9 @@ export class TUI extends Container {
|
|
|
642
665
|
terminal: Terminal;
|
|
643
666
|
#previousLines: string[] = [];
|
|
644
667
|
#latestRenderedLines: string[] = [];
|
|
668
|
+
#latestRenderedTranscriptLineCount = 0;
|
|
669
|
+
#latestRenderedSuffixLineCount = 0;
|
|
670
|
+
#latestRenderedPlacementOwners = new Map<string, KittyPlacementOwner>();
|
|
645
671
|
/**
|
|
646
672
|
* Raw (pre-normalization) lines from the previous frame, kept only when the
|
|
647
673
|
* virtual-viewport flag is on. Used to detect whether the off-screen prefix is
|
|
@@ -649,6 +675,7 @@ export class TUI extends Container {
|
|
|
649
675
|
* return stable string instances) so its normalized form can be reused (bounded normalize).
|
|
650
676
|
*/
|
|
651
677
|
#previousRaw: string[] = [];
|
|
678
|
+
#kittyPlacementSpans: KittyPlacementSpan[] = [];
|
|
652
679
|
#lineNormalizationCache = new Map<string, LineNormalizationCacheEntry>();
|
|
653
680
|
#lineEmitWidthCache = new Map<string, number>();
|
|
654
681
|
#lineTruncationCache = new Map<string, string>();
|
|
@@ -1026,6 +1053,10 @@ export class TUI extends Container {
|
|
|
1026
1053
|
if (!Number.isFinite(deltaRows)) return false;
|
|
1027
1054
|
const delta = Math.trunc(deltaRows);
|
|
1028
1055
|
if (delta === 0) return false;
|
|
1056
|
+
const previousManualViewportTop = this.#manualViewportTop;
|
|
1057
|
+
const previousManualViewportAnchor = this.#manualViewportAnchor;
|
|
1058
|
+
const previousManualViewportFallbackAnchors = this.#manualViewportFallbackAnchors;
|
|
1059
|
+
const previousReconcileMissingViewportAnchor = this.#reconcileMissingViewportAnchor;
|
|
1029
1060
|
|
|
1030
1061
|
const direction: -1 | 1 = delta < 0 ? -1 : 1;
|
|
1031
1062
|
const pin = options?.pin ?? "stable";
|
|
@@ -1116,7 +1147,8 @@ export class TUI extends Container {
|
|
|
1116
1147
|
}
|
|
1117
1148
|
}
|
|
1118
1149
|
this.#manualViewportTop = targetViewportTop;
|
|
1119
|
-
|
|
1150
|
+
let contentPainted = false;
|
|
1151
|
+
const painted = this.#repaintViewportFromLines(
|
|
1120
1152
|
this.#previousLines,
|
|
1121
1153
|
width,
|
|
1122
1154
|
height,
|
|
@@ -1124,7 +1156,26 @@ export class TUI extends Container {
|
|
|
1124
1156
|
null,
|
|
1125
1157
|
"manual viewport scroll",
|
|
1126
1158
|
this.#manualViewportAnchor !== null,
|
|
1159
|
+
() => {
|
|
1160
|
+
contentPainted = true;
|
|
1161
|
+
this.#manualTranscriptLineCount = this.#latestRenderedTranscriptLineCount;
|
|
1162
|
+
this.#manualSuffixLineCount = this.#latestRenderedSuffixLineCount;
|
|
1163
|
+
},
|
|
1164
|
+
false,
|
|
1165
|
+
this.#kittyPlacementSpans,
|
|
1166
|
+
this.#kittyPlacementSpansForLines(this.#previousLines, this.#latestRenderedPlacementOwners),
|
|
1167
|
+
{
|
|
1168
|
+
transcriptLineCount: this.#latestRenderedTranscriptLineCount,
|
|
1169
|
+
suffixLineCount: this.#latestRenderedSuffixLineCount,
|
|
1170
|
+
},
|
|
1127
1171
|
);
|
|
1172
|
+
if (!contentPainted) {
|
|
1173
|
+
this.#manualViewportTop = previousManualViewportTop;
|
|
1174
|
+
this.#manualViewportAnchor = previousManualViewportAnchor;
|
|
1175
|
+
this.#manualViewportFallbackAnchors = previousManualViewportFallbackAnchors;
|
|
1176
|
+
this.#reconcileMissingViewportAnchor = previousReconcileMissingViewportAnchor;
|
|
1177
|
+
}
|
|
1178
|
+
return painted;
|
|
1128
1179
|
}
|
|
1129
1180
|
|
|
1130
1181
|
scrollViewportPages(direction: -1 | 1): boolean {
|
|
@@ -1141,9 +1192,12 @@ export class TUI extends Container {
|
|
|
1141
1192
|
const paddedLiveLines = this.#padBeforeBottomPinnedComponent(
|
|
1142
1193
|
this.#latestRenderedLines,
|
|
1143
1194
|
height,
|
|
1144
|
-
this.#
|
|
1195
|
+
this.#latestRenderedSuffixLineCount,
|
|
1145
1196
|
);
|
|
1146
1197
|
const liveLines = paddedLiveLines.lines;
|
|
1198
|
+
const liveTranscriptLineCount = this.#latestRenderedTranscriptLineCount;
|
|
1199
|
+
const liveSuffixLineCount = this.#latestRenderedSuffixLineCount + paddedLiveLines.insertedBlankRows;
|
|
1200
|
+
const liveKittyPlacementSpans = this.#kittyPlacementSpansForLines(liveLines, this.#latestRenderedPlacementOwners);
|
|
1147
1201
|
let liveCursorPosition = this.#lastCursorPosition;
|
|
1148
1202
|
if (liveCursorPosition !== null && liveCursorPosition.row >= paddedLiveLines.insertionRow) {
|
|
1149
1203
|
liveCursorPosition = {
|
|
@@ -1170,6 +1224,8 @@ export class TUI extends Container {
|
|
|
1170
1224
|
this.#paintedManualOutputNotice = false;
|
|
1171
1225
|
this.#lastCursorPosition = liveCursorPosition;
|
|
1172
1226
|
this.#previousLines = liveLines;
|
|
1227
|
+
this.#manualTranscriptLineCount = liveTranscriptLineCount;
|
|
1228
|
+
this.#manualSuffixLineCount = liveSuffixLineCount;
|
|
1173
1229
|
if (this.#scrollbackResumeViewportTop === undefined) {
|
|
1174
1230
|
this.#nativeScrollbackViewportTop = liveViewportTop;
|
|
1175
1231
|
}
|
|
@@ -1183,6 +1239,9 @@ export class TUI extends Container {
|
|
|
1183
1239
|
}
|
|
1184
1240
|
},
|
|
1185
1241
|
true,
|
|
1242
|
+
this.#kittyPlacementSpans,
|
|
1243
|
+
liveKittyPlacementSpans,
|
|
1244
|
+
{ transcriptLineCount: liveTranscriptLineCount, suffixLineCount: liveSuffixLineCount },
|
|
1186
1245
|
);
|
|
1187
1246
|
}
|
|
1188
1247
|
|
|
@@ -1561,6 +1620,8 @@ export class TUI extends Container {
|
|
|
1561
1620
|
|
|
1562
1621
|
stop(): void {
|
|
1563
1622
|
this.flushTerminalCleanup();
|
|
1623
|
+
const placementCleanup = this.#kittyPlacementDeletePlan(this.#kittyPlacementSpans, [], [], true).output;
|
|
1624
|
+
if (placementCleanup.length > 0 && this.#writeTerminal(placementCleanup)) this.#kittyPlacementSpans = [];
|
|
1564
1625
|
this.#clearSixelProbeState();
|
|
1565
1626
|
this.#stopped = true;
|
|
1566
1627
|
this.#settleRenderCommitWaiters(false);
|
|
@@ -1612,6 +1673,7 @@ export class TUI extends Container {
|
|
|
1612
1673
|
this.#previousLines = [];
|
|
1613
1674
|
this.#latestRenderedLines = [];
|
|
1614
1675
|
this.#previousRaw = [];
|
|
1676
|
+
this.#kittyPlacementSpans = [];
|
|
1615
1677
|
this.#lineNormalizationCache.clear();
|
|
1616
1678
|
this.#lineTruncationCache.clear();
|
|
1617
1679
|
this.#lineEmitWidthCache.clear();
|
|
@@ -1715,8 +1777,6 @@ export class TUI extends Container {
|
|
|
1715
1777
|
}
|
|
1716
1778
|
if (renderMetrics.enabled) renderMetrics.recordRequest(source);
|
|
1717
1779
|
if (force) {
|
|
1718
|
-
const preserveViewportCursor =
|
|
1719
|
-
useViewportRepaintPath(this.terminal) || shouldPreserveScrollbackOnFullClear(this.terminal);
|
|
1720
1780
|
// A forced full redraw supersedes any queued input-priority render.
|
|
1721
1781
|
this.#inputRenderPending = false;
|
|
1722
1782
|
this.#previousLines = [];
|
|
@@ -1729,12 +1789,6 @@ export class TUI extends Container {
|
|
|
1729
1789
|
this.#previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
|
|
1730
1790
|
this.#lineNormalizationCacheLimit = 0;
|
|
1731
1791
|
this.#lineTruncationCacheLimit = 0;
|
|
1732
|
-
if (!preserveViewportCursor) {
|
|
1733
|
-
this.#cursorRow = 0;
|
|
1734
|
-
this.#hardwareCursorRow = 0;
|
|
1735
|
-
this.#viewportTopRow = 0;
|
|
1736
|
-
this.#maxLinesRendered = 0;
|
|
1737
|
-
}
|
|
1738
1792
|
if (this.#renderTimer) {
|
|
1739
1793
|
clearTimeout(this.#renderTimer);
|
|
1740
1794
|
this.#renderTimer = undefined;
|
|
@@ -1898,6 +1952,15 @@ export class TUI extends Container {
|
|
|
1898
1952
|
return;
|
|
1899
1953
|
}
|
|
1900
1954
|
|
|
1955
|
+
// DA1 and XTSMGRAPHICS replies belong to the sixel probe, whose listener runs
|
|
1956
|
+
// above. Reaching this point means the probe already finished, timed out, or
|
|
1957
|
+
// was cleared by a stop()/start() cycle while the terminal still owed the
|
|
1958
|
+
// reply. These are terminal-to-host reports, never user input, so drop them
|
|
1959
|
+
// instead of typing them into the focused component.
|
|
1960
|
+
if (DEVICE_REPORT_PATTERN.test(data)) {
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1901
1964
|
// Global debug key handler (registry: tui.global.debug, default Shift+Ctrl+D)
|
|
1902
1965
|
if (getKeybindings().matches(data, "tui.global.debug") && this.onDebug) {
|
|
1903
1966
|
this.onDebug();
|
|
@@ -2217,7 +2280,12 @@ export class TUI extends Container {
|
|
|
2217
2280
|
}
|
|
2218
2281
|
|
|
2219
2282
|
/** Composite all overlays into content lines (in stack order, later = on top). */
|
|
2220
|
-
#compositeOverlays(
|
|
2283
|
+
#compositeOverlays(
|
|
2284
|
+
lines: string[],
|
|
2285
|
+
termWidth: number,
|
|
2286
|
+
termHeight: number,
|
|
2287
|
+
placementOwners?: Map<string, KittyPlacementOwner>,
|
|
2288
|
+
): string[] {
|
|
2221
2289
|
if (this.overlayStack.length === 0) return lines;
|
|
2222
2290
|
const result = [...lines];
|
|
2223
2291
|
for (const entry of this.overlayStack) entry.mouseBounds = undefined;
|
|
@@ -2278,6 +2346,11 @@ export class TUI extends Container {
|
|
|
2278
2346
|
// (components should already respect width, but this ensures it)
|
|
2279
2347
|
const truncatedOverlayLine =
|
|
2280
2348
|
visibleWidth(overlayLines[i]) > w ? sliceByColumn(overlayLines[i], 0, w, true) : overlayLines[i];
|
|
2349
|
+
if (placementOwners !== undefined) {
|
|
2350
|
+
for (const placement of extractKittyPlacementReferences(truncatedOverlayLine)) {
|
|
2351
|
+
placementOwners.set(this.#kittyPlacementKey(placement), "overlay");
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2281
2354
|
result[idx] = this.#compositeLineAt(result[idx], truncatedOverlayLine, col, w, termWidth);
|
|
2282
2355
|
modifiedLines.add(idx);
|
|
2283
2356
|
}
|
|
@@ -2495,6 +2568,92 @@ export class TUI extends Container {
|
|
|
2495
2568
|
return lines;
|
|
2496
2569
|
}
|
|
2497
2570
|
|
|
2571
|
+
#kittyPlacementKey(reference: KittyPlacementReference): string {
|
|
2572
|
+
return `${reference.imageId}:${reference.placementId}`;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
#kittyPlacementSpansForLines(
|
|
2576
|
+
lines: string[],
|
|
2577
|
+
owners: ReadonlyMap<string, KittyPlacementOwner>,
|
|
2578
|
+
): KittyPlacementSpan[] {
|
|
2579
|
+
const placements: KittyPlacementSpan[] = [];
|
|
2580
|
+
for (let row = 0; row < lines.length; row++) {
|
|
2581
|
+
for (const placement of extractKittyPlacementReferences(lines[row])) {
|
|
2582
|
+
placements.push({
|
|
2583
|
+
...placement,
|
|
2584
|
+
row,
|
|
2585
|
+
owner: owners.get(this.#kittyPlacementKey(placement)) ?? "transcript",
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
return placements;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
#kittyPlacementIntersectsRegion(placement: KittyPlacementSpan, region: KittyPlacementRegion): boolean {
|
|
2593
|
+
return placement.row < region.bottom && placement.row + placement.rows > region.top;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
#kittyPlacementDeletePlan(
|
|
2597
|
+
previous: KittyPlacementSpan[],
|
|
2598
|
+
next: KittyPlacementSpan[],
|
|
2599
|
+
overwrittenRegions: KittyPlacementRegion[],
|
|
2600
|
+
deleteAll = false,
|
|
2601
|
+
overwrittenOwners: KittyPlacementOwner[] = [],
|
|
2602
|
+
): KittyPlacementDeletePlan {
|
|
2603
|
+
const deletedKeys = new Set<string>();
|
|
2604
|
+
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return { deletedKeys, output: "" };
|
|
2605
|
+
const nextByKey = new Map(next.map(placement => [this.#kittyPlacementKey(placement), placement]));
|
|
2606
|
+
let output = "";
|
|
2607
|
+
for (const placement of previous) {
|
|
2608
|
+
const key = this.#kittyPlacementKey(placement);
|
|
2609
|
+
if (deletedKeys.has(key)) continue;
|
|
2610
|
+
const candidate = nextByKey.get(key);
|
|
2611
|
+
const changed =
|
|
2612
|
+
candidate === undefined || candidate.row !== placement.row || candidate.rows !== placement.rows;
|
|
2613
|
+
const overwritten =
|
|
2614
|
+
deleteAll ||
|
|
2615
|
+
overwrittenOwners.includes(placement.owner) ||
|
|
2616
|
+
overwrittenRegions.some(region => this.#kittyPlacementIntersectsRegion(placement, region));
|
|
2617
|
+
if (!changed && !overwritten) continue;
|
|
2618
|
+
deletedKeys.add(key);
|
|
2619
|
+
output += encodeKittyPlacementDelete(placement);
|
|
2620
|
+
}
|
|
2621
|
+
return { deletedKeys, output };
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
#kittyCommittedPlacementsAfterPaint(
|
|
2625
|
+
previous: KittyPlacementSpan[],
|
|
2626
|
+
next: KittyPlacementSpan[],
|
|
2627
|
+
deletePlan: KittyPlacementDeletePlan,
|
|
2628
|
+
emittedRegions: KittyPlacementRegion[],
|
|
2629
|
+
): KittyPlacementSpan[] {
|
|
2630
|
+
const committed = new Map<string, KittyPlacementSpan>();
|
|
2631
|
+
for (const placement of previous) {
|
|
2632
|
+
const key = this.#kittyPlacementKey(placement);
|
|
2633
|
+
if (!deletePlan.deletedKeys.has(key)) committed.set(key, placement);
|
|
2634
|
+
}
|
|
2635
|
+
for (const placement of next) {
|
|
2636
|
+
if (!emittedRegions.some(region => placement.row >= region.top && placement.row < region.bottom)) continue;
|
|
2637
|
+
committed.set(this.#kittyPlacementKey(placement), placement);
|
|
2638
|
+
}
|
|
2639
|
+
return [...committed.values()];
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
#kittyViewportTopIncludingPlacementAnchors(viewportTop: number, placements: KittyPlacementSpan[]): number {
|
|
2643
|
+
let resolvedTop = viewportTop;
|
|
2644
|
+
let changed: boolean;
|
|
2645
|
+
do {
|
|
2646
|
+
const priorTop = resolvedTop;
|
|
2647
|
+
for (const placement of placements) {
|
|
2648
|
+
if (placement.row < resolvedTop && placement.row + placement.rows > resolvedTop) {
|
|
2649
|
+
resolvedTop = placement.row;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
changed = resolvedTop !== priorTop;
|
|
2653
|
+
} while (changed);
|
|
2654
|
+
return resolvedTop;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2498
2657
|
#pinnedChildLines(component: Component, renderedChildren: Map<Component, string[]>): string[] {
|
|
2499
2658
|
const lines = renderedChildren.get(component);
|
|
2500
2659
|
if (lines === undefined) throw new Error("Missing rendered direct child for pinned suffix");
|
|
@@ -2594,9 +2753,9 @@ export class TUI extends Container {
|
|
|
2594
2753
|
padded.splice(insertionRow, 0, ...Array.from({ length: insertedBlankRows }, () => ""));
|
|
2595
2754
|
return { lines: padded, insertionRow, insertedBlankRows };
|
|
2596
2755
|
}
|
|
2597
|
-
#manualTranscriptCapacity(height: number): number {
|
|
2598
|
-
const noticeRows = this.#manualOutputNotice && height >
|
|
2599
|
-
return Math.max(0, height -
|
|
2756
|
+
#manualTranscriptCapacity(height: number, suffixLineCount = this.#manualSuffixLineCount): number {
|
|
2757
|
+
const noticeRows = this.#manualOutputNotice && height > suffixLineCount ? 1 : 0;
|
|
2758
|
+
return Math.max(0, height - suffixLineCount - noticeRows);
|
|
2600
2759
|
}
|
|
2601
2760
|
#resolveManualAnchor(frame: ViewportAnchorFrame): number | null {
|
|
2602
2761
|
const anchor = this.#manualViewportAnchor;
|
|
@@ -2657,8 +2816,13 @@ export class TUI extends Container {
|
|
|
2657
2816
|
allowPastLiveBottom = false,
|
|
2658
2817
|
onPainted?: () => void,
|
|
2659
2818
|
paintLive = false,
|
|
2819
|
+
placementsToClear: KittyPlacementSpan[] = this.#kittyPlacementSpans,
|
|
2820
|
+
placementsToPaint: KittyPlacementSpan[] = placementsToClear,
|
|
2821
|
+
geometry?: { transcriptLineCount: number; suffixLineCount: number },
|
|
2660
2822
|
): boolean {
|
|
2661
2823
|
const paintManual = this.#manualViewportTop !== undefined && !paintLive;
|
|
2824
|
+
const transcriptLineCount = geometry?.transcriptLineCount ?? this.#manualTranscriptLineCount;
|
|
2825
|
+
const suffixLineCount = geometry?.suffixLineCount ?? this.#manualSuffixLineCount;
|
|
2662
2826
|
if (height <= 0 || width <= 0) return false;
|
|
2663
2827
|
const maxViewportTop = Math.max(
|
|
2664
2828
|
0,
|
|
@@ -2666,18 +2830,32 @@ export class TUI extends Container {
|
|
|
2666
2830
|
? lines.length - (allowPastLiveBottom ? 1 : height)
|
|
2667
2831
|
: allowPastLiveBottom
|
|
2668
2832
|
? lines.length - 1
|
|
2669
|
-
:
|
|
2833
|
+
: transcriptLineCount - this.#manualTranscriptCapacity(height, suffixLineCount),
|
|
2670
2834
|
);
|
|
2671
|
-
|
|
2835
|
+
let nextViewportTop = Math.max(0, Math.min(maxViewportTop, viewportTop));
|
|
2836
|
+
if (paintManual)
|
|
2837
|
+
nextViewportTop = this.#kittyViewportTopIncludingPlacementAnchors(nextViewportTop, placementsToPaint);
|
|
2672
2838
|
const currentScreenRow = Math.max(0, Math.min(height - 1, this.#hardwareCursorRow - this.#viewportTopRow));
|
|
2673
|
-
|
|
2839
|
+
const transcriptCapacity = paintManual ? this.#manualTranscriptCapacity(height, suffixLineCount) : height;
|
|
2840
|
+
const noticeRows = paintManual && this.#manualOutputNotice && height > suffixLineCount ? 1 : 0;
|
|
2841
|
+
const deletePlan = this.#kittyPlacementDeletePlan(
|
|
2842
|
+
placementsToClear,
|
|
2843
|
+
placementsToPaint,
|
|
2844
|
+
[{ top: this.#viewportTopRow, bottom: this.#viewportTopRow + height }],
|
|
2845
|
+
false,
|
|
2846
|
+
paintManual ? ["suffix", "overlay"] : [],
|
|
2847
|
+
);
|
|
2848
|
+
const emittedRegions: KittyPlacementRegion[] = paintManual
|
|
2849
|
+
? [
|
|
2850
|
+
{ top: nextViewportTop, bottom: nextViewportTop + transcriptCapacity },
|
|
2851
|
+
{ top: transcriptLineCount, bottom: transcriptLineCount + suffixLineCount },
|
|
2852
|
+
]
|
|
2853
|
+
: [{ top: nextViewportTop, bottom: nextViewportTop + height }];
|
|
2854
|
+
let buffer = `\x1b[?2026h${deletePlan.output}`;
|
|
2674
2855
|
if (currentScreenRow > 0) {
|
|
2675
2856
|
buffer += `\x1b[${currentScreenRow}A`;
|
|
2676
2857
|
}
|
|
2677
2858
|
buffer += "\r";
|
|
2678
|
-
|
|
2679
|
-
const transcriptCapacity = paintManual ? this.#manualTranscriptCapacity(height) : height;
|
|
2680
|
-
const noticeRows = paintManual && this.#manualOutputNotice && height > this.#manualSuffixLineCount ? 1 : 0;
|
|
2681
2859
|
const committedTranscriptRows: Array<number | null> = [];
|
|
2682
2860
|
for (let screenRow = 0; screenRow < height; screenRow++) {
|
|
2683
2861
|
if (screenRow > 0) buffer += "\r\n";
|
|
@@ -2688,12 +2866,12 @@ export class TUI extends Container {
|
|
|
2688
2866
|
paintManual && screenRow === transcriptCapacity && noticeRows > 0
|
|
2689
2867
|
? "New output — type to follow"
|
|
2690
2868
|
: paintManual && suffixRow >= 0
|
|
2691
|
-
? (lines[
|
|
2692
|
-
: paintManual && lineIndex >=
|
|
2869
|
+
? (lines[transcriptLineCount + suffixRow] ?? "")
|
|
2870
|
+
: paintManual && lineIndex >= transcriptLineCount
|
|
2693
2871
|
? ""
|
|
2694
2872
|
: (lines[lineIndex] ?? "");
|
|
2695
2873
|
committedTranscriptRows.push(
|
|
2696
|
-
screenRow < transcriptCapacity && lineIndex <
|
|
2874
|
+
screenRow < transcriptCapacity && lineIndex < transcriptLineCount ? lineIndex : null,
|
|
2697
2875
|
);
|
|
2698
2876
|
const isImage = TERMINAL.isImageLine(line);
|
|
2699
2877
|
if (!isImage && this.#visibleWidthForDifferentialGuard(line) > width) {
|
|
@@ -2715,23 +2893,30 @@ export class TUI extends Container {
|
|
|
2715
2893
|
}
|
|
2716
2894
|
buffer += cursorSeq;
|
|
2717
2895
|
buffer += "\x1b[?2026l";
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2896
|
+
let contentWritten = false;
|
|
2897
|
+
const writeSucceeded = this.#writeRenderBufferAndReanchorImeCursor(buffer, cursorPos, lines.length, () => {
|
|
2898
|
+
contentWritten = true;
|
|
2899
|
+
this.#hardwareCursorRow = cursorToRow;
|
|
2900
|
+
this.#committedTranscriptRows = committedTranscriptRows;
|
|
2901
|
+
this.#cursorRow = Math.max(0, lines.length - 1);
|
|
2902
|
+
this.#maxLinesRendered = lines.length;
|
|
2903
|
+
this.#viewportTopRow = nextViewportTop;
|
|
2904
|
+
if (paintManual) this.#manualViewportTop = nextViewportTop;
|
|
2905
|
+
this.#kittyPlacementSpans = this.#kittyCommittedPlacementsAfterPaint(
|
|
2906
|
+
placementsToClear,
|
|
2907
|
+
placementsToPaint,
|
|
2908
|
+
deletePlan,
|
|
2909
|
+
emittedRegions,
|
|
2910
|
+
);
|
|
2911
|
+
onPainted?.();
|
|
2912
|
+
});
|
|
2913
|
+
if (!contentWritten) return false;
|
|
2729
2914
|
|
|
2730
2915
|
if (this.#debugRedraw) {
|
|
2731
2916
|
const msg = `[${new Date().toISOString()}] viewportRepaint: ${reason} (lines=${lines.length}, height=${height}, viewportTop=${nextViewportTop})\n`;
|
|
2732
2917
|
this.#appendDebugRedrawLog(msg);
|
|
2733
2918
|
}
|
|
2734
|
-
return
|
|
2919
|
+
return writeSucceeded;
|
|
2735
2920
|
}
|
|
2736
2921
|
|
|
2737
2922
|
#doRender(): void {
|
|
@@ -2752,23 +2937,27 @@ export class TUI extends Container {
|
|
|
2752
2937
|
const renderedLines: string[] = [];
|
|
2753
2938
|
const renderedChildren = new Map<Component, string[]>();
|
|
2754
2939
|
let anchorFrame: ViewportAnchorFrame | null = null;
|
|
2940
|
+
let previousKittyPlacementSpans = this.#kittyPlacementSpans;
|
|
2941
|
+
const placementOwners = new Map<string, KittyPlacementOwner>();
|
|
2942
|
+
const pinnedChildIndex =
|
|
2943
|
+
this.#bottomPinnedComponent === null ? -1 : this.children.indexOf(this.#bottomPinnedComponent);
|
|
2944
|
+
const hasStickySuffix = pinnedChildIndex >= 0;
|
|
2755
2945
|
const anchorRenderFailureCountBefore = viewportAnchorRenderFailureCount;
|
|
2756
|
-
for (
|
|
2946
|
+
for (let childIndex = 0; childIndex < this.children.length; childIndex++) {
|
|
2947
|
+
const child = this.children[childIndex];
|
|
2757
2948
|
const rendered = safeRenderComponentWithViewportAnchors(child, width, "tui-child");
|
|
2758
2949
|
renderedChildren.set(child, rendered.lines);
|
|
2759
2950
|
if (child === this.#viewportAnchorComponent && rendered.anchors.some(anchor => anchor !== null)) {
|
|
2760
2951
|
anchorFrame = { startRow: renderedLines.length, anchors: rendered.anchors };
|
|
2761
2952
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
break;
|
|
2953
|
+
const owner: KittyPlacementOwner = hasStickySuffix && childIndex >= pinnedChildIndex ? "suffix" : "transcript";
|
|
2954
|
+
for (const line of rendered.lines) {
|
|
2955
|
+
for (const placement of extractKittyPlacementReferences(line)) {
|
|
2956
|
+
placementOwners.set(this.#kittyPlacementKey(placement), owner);
|
|
2957
|
+
}
|
|
2958
|
+
renderedLines.push(line);
|
|
2769
2959
|
}
|
|
2770
2960
|
}
|
|
2771
|
-
const hasStickySuffix = pinnedChildIndex >= 0;
|
|
2772
2961
|
const sourceTranscriptLineCount = hasStickySuffix
|
|
2773
2962
|
? this.children
|
|
2774
2963
|
.slice(0, pinnedChildIndex)
|
|
@@ -2786,10 +2975,12 @@ export class TUI extends Container {
|
|
|
2786
2975
|
newLines.length - sourceTranscriptLineCount,
|
|
2787
2976
|
).lines;
|
|
2788
2977
|
}
|
|
2978
|
+
const nextTranscriptLineCount = sourceTranscriptLineCount;
|
|
2979
|
+
const nextSuffixLineCount = hasStickySuffix ? Math.max(0, newLines.length - nextTranscriptLineCount) : 0;
|
|
2789
2980
|
|
|
2790
2981
|
// Composite overlays into the rendered lines (before differential compare)
|
|
2791
2982
|
if (this.overlayStack.length > 0) {
|
|
2792
|
-
newLines = this.#compositeOverlays(newLines, width, height);
|
|
2983
|
+
newLines = this.#compositeOverlays(newLines, width, height, placementOwners);
|
|
2793
2984
|
}
|
|
2794
2985
|
|
|
2795
2986
|
// Extract cursor position (marker must be found before diff comparison)
|
|
@@ -2857,9 +3048,11 @@ export class TUI extends Container {
|
|
|
2857
3048
|
renderMetrics.recordLineCount("measured", total - diffStart);
|
|
2858
3049
|
if (usedWindowNormalize) renderMetrics.recordLineCount("offscreenScan", diffStart);
|
|
2859
3050
|
}
|
|
3051
|
+
const nextKittyPlacementSpans = this.#kittyPlacementSpansForLines(newLines, placementOwners);
|
|
2860
3052
|
this.#latestRenderedLines = newLines;
|
|
2861
|
-
this.#
|
|
2862
|
-
this.#
|
|
3053
|
+
this.#latestRenderedTranscriptLineCount = nextTranscriptLineCount;
|
|
3054
|
+
this.#latestRenderedSuffixLineCount = nextSuffixLineCount;
|
|
3055
|
+
this.#latestRenderedPlacementOwners = placementOwners;
|
|
2863
3056
|
const naturalViewportTop = Math.max(0, newLines.length - height);
|
|
2864
3057
|
const priorLogicalLineCount = Math.max(this.#previousLines.length, this.#maxLinesRendered);
|
|
2865
3058
|
if (this.#transcriptIdentityResetPending) {
|
|
@@ -2875,6 +3068,17 @@ export class TUI extends Container {
|
|
|
2875
3068
|
}
|
|
2876
3069
|
|
|
2877
3070
|
if (this.#manualViewportTop !== undefined) {
|
|
3071
|
+
const committedManualViewportTop = this.#manualViewportTop;
|
|
3072
|
+
const committedManualViewportAnchor = this.#manualViewportAnchor;
|
|
3073
|
+
const committedManualViewportFallbackAnchors = this.#manualViewportFallbackAnchors;
|
|
3074
|
+
const committedReconcileMissingViewportAnchor = this.#reconcileMissingViewportAnchor;
|
|
3075
|
+
const restoreManualIntent = (): void => {
|
|
3076
|
+
this.#manualViewportTop = committedManualViewportTop;
|
|
3077
|
+
this.#manualViewportAnchor = committedManualViewportAnchor;
|
|
3078
|
+
this.#manualViewportFallbackAnchors = committedManualViewportFallbackAnchors;
|
|
3079
|
+
this.#reconcileMissingViewportAnchor = committedReconcileMissingViewportAnchor;
|
|
3080
|
+
};
|
|
3081
|
+
let contentPainted = false;
|
|
2878
3082
|
let resolvedAnchorTop = anchorFrame === null ? null : this.#resolveManualAnchor(anchorFrame);
|
|
2879
3083
|
if (
|
|
2880
3084
|
this.#manualViewportAnchor !== null &&
|
|
@@ -2892,6 +3096,7 @@ export class TUI extends Container {
|
|
|
2892
3096
|
if (anchorRenderFailed) {
|
|
2893
3097
|
// Keep semantic intent armed for recovery, but render the diagnostic frame
|
|
2894
3098
|
// instead of masking a provider failure behind stale transcript content.
|
|
3099
|
+
contentPainted = false;
|
|
2895
3100
|
this.#repaintViewportFromLines(
|
|
2896
3101
|
newLines,
|
|
2897
3102
|
width,
|
|
@@ -2900,16 +3105,27 @@ export class TUI extends Container {
|
|
|
2900
3105
|
null,
|
|
2901
3106
|
"failed semantic viewport render",
|
|
2902
3107
|
true,
|
|
3108
|
+
() => {
|
|
3109
|
+
contentPainted = true;
|
|
3110
|
+
this.#previousLines = newLines;
|
|
3111
|
+
this.#previousWidth = width;
|
|
3112
|
+
this.#previousHeight = height;
|
|
3113
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
3114
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
3115
|
+
},
|
|
3116
|
+
false,
|
|
3117
|
+
previousKittyPlacementSpans,
|
|
3118
|
+
nextKittyPlacementSpans,
|
|
3119
|
+
{ transcriptLineCount: nextTranscriptLineCount, suffixLineCount: nextSuffixLineCount },
|
|
2903
3120
|
);
|
|
2904
|
-
|
|
2905
|
-
this.#previousWidth = width;
|
|
2906
|
-
this.#previousHeight = height;
|
|
3121
|
+
if (!contentPainted) restoreManualIntent();
|
|
2907
3122
|
return;
|
|
2908
3123
|
}
|
|
2909
3124
|
// A formerly valid semantic target is temporarily absent (provider removal,
|
|
2910
3125
|
// replacement, eviction, or object deletion). Keep the last resolved frame
|
|
2911
3126
|
// instead of silently reinterpreting manual intent as a numeric viewport.
|
|
2912
3127
|
const retainedLines = this.#previousLines.length > 0 ? this.#previousLines : newLines;
|
|
3128
|
+
contentPainted = false;
|
|
2913
3129
|
this.#repaintViewportFromLines(
|
|
2914
3130
|
retainedLines,
|
|
2915
3131
|
width,
|
|
@@ -2918,9 +3134,15 @@ export class TUI extends Container {
|
|
|
2918
3134
|
null,
|
|
2919
3135
|
"unresolved semantic viewport render",
|
|
2920
3136
|
true,
|
|
3137
|
+
() => {
|
|
3138
|
+
contentPainted = true;
|
|
3139
|
+
this.#previousWidth = width;
|
|
3140
|
+
this.#previousHeight = height;
|
|
3141
|
+
},
|
|
3142
|
+
false,
|
|
3143
|
+
previousKittyPlacementSpans,
|
|
2921
3144
|
);
|
|
2922
|
-
|
|
2923
|
-
this.#previousHeight = height;
|
|
3145
|
+
if (!contentPainted) restoreManualIntent();
|
|
2924
3146
|
return;
|
|
2925
3147
|
}
|
|
2926
3148
|
const nextViewportTop = resolvedAnchorTop ?? this.#manualViewportTop;
|
|
@@ -2936,26 +3158,34 @@ export class TUI extends Container {
|
|
|
2936
3158
|
}
|
|
2937
3159
|
this.#manualViewportTop = nextViewportTop;
|
|
2938
3160
|
this.#reconcileMissingViewportAnchor = false;
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
)
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3161
|
+
contentPainted = false;
|
|
3162
|
+
this.#repaintViewportFromLines(
|
|
3163
|
+
newLines,
|
|
3164
|
+
width,
|
|
3165
|
+
height,
|
|
3166
|
+
nextViewportTop,
|
|
3167
|
+
null,
|
|
3168
|
+
"manual viewport render",
|
|
3169
|
+
this.#manualViewportAnchor !== null,
|
|
3170
|
+
() => {
|
|
3171
|
+
contentPainted = true;
|
|
3172
|
+
this.#previousLines = newLines;
|
|
3173
|
+
this.#previousWidth = width;
|
|
3174
|
+
this.#previousHeight = height;
|
|
3175
|
+
this.#paintedManualOutputNotice = this.#manualOutputNotice;
|
|
3176
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
3177
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
3178
|
+
},
|
|
3179
|
+
false,
|
|
3180
|
+
previousKittyPlacementSpans,
|
|
3181
|
+
nextKittyPlacementSpans,
|
|
3182
|
+
{ transcriptLineCount: nextTranscriptLineCount, suffixLineCount: nextSuffixLineCount },
|
|
3183
|
+
);
|
|
3184
|
+
if (!contentPainted) restoreManualIntent();
|
|
2955
3185
|
return;
|
|
2956
3186
|
}
|
|
2957
3187
|
// Helper to clear scrollback and viewport and render all new lines
|
|
2958
|
-
let viewportRepaint: (reason: string, targetViewportTop?: number) =>
|
|
3188
|
+
let viewportRepaint: (reason: string, targetViewportTop?: number) => boolean;
|
|
2959
3189
|
const fullRender = (clear: boolean, reason = "full render", forceScrollbackClear = false): void => {
|
|
2960
3190
|
if (
|
|
2961
3191
|
clear &&
|
|
@@ -2968,7 +3198,13 @@ export class TUI extends Container {
|
|
|
2968
3198
|
}
|
|
2969
3199
|
this.#fullRedrawCount += 1;
|
|
2970
3200
|
if (renderMetrics.enabled) renderMetrics.recordFullRedraw(reason);
|
|
2971
|
-
|
|
3201
|
+
const deletePlan = this.#kittyPlacementDeletePlan(
|
|
3202
|
+
previousKittyPlacementSpans,
|
|
3203
|
+
nextKittyPlacementSpans,
|
|
3204
|
+
[],
|
|
3205
|
+
clear,
|
|
3206
|
+
);
|
|
3207
|
+
let buffer = `\x1b[?2026h${deletePlan.output}`; // Begin synchronized output
|
|
2972
3208
|
// Skip clearing scrollback (3J) in hosts where clear/replay can snap the
|
|
2973
3209
|
// native viewport away from the live prompt (tmux/screen, Windows ConPTY) —
|
|
2974
3210
|
// unless the caller explicitly needs history erased (the settled width
|
|
@@ -3004,17 +3240,28 @@ export class TUI extends Container {
|
|
|
3004
3240
|
this.#previousLines = newLines;
|
|
3005
3241
|
this.#previousWidth = width;
|
|
3006
3242
|
this.#previousHeight = height;
|
|
3243
|
+
this.#kittyPlacementSpans = this.#kittyCommittedPlacementsAfterPaint(
|
|
3244
|
+
previousKittyPlacementSpans,
|
|
3245
|
+
nextKittyPlacementSpans,
|
|
3246
|
+
deletePlan,
|
|
3247
|
+
[{ top: Number.NEGATIVE_INFINITY, bottom: Number.POSITIVE_INFINITY }],
|
|
3248
|
+
);
|
|
3249
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
3250
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
3007
3251
|
})
|
|
3008
3252
|
)
|
|
3009
3253
|
return;
|
|
3010
3254
|
};
|
|
3011
3255
|
|
|
3012
|
-
viewportRepaint = (reason: string, targetViewportTop = Math.max(0, newLines.length - height)):
|
|
3256
|
+
viewportRepaint = (reason: string, targetViewportTop = Math.max(0, newLines.length - height)): boolean => {
|
|
3013
3257
|
this.#fullRedrawCount += 1;
|
|
3014
3258
|
if (renderMetrics.enabled) renderMetrics.recordFullRedraw(reason);
|
|
3015
3259
|
const nextViewportTop = targetViewportTop;
|
|
3016
3260
|
const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
|
|
3017
|
-
|
|
3261
|
+
const deletePlan = this.#kittyPlacementDeletePlan(previousKittyPlacementSpans, nextKittyPlacementSpans, [
|
|
3262
|
+
{ top: prevViewportTop, bottom: prevViewportTop + height },
|
|
3263
|
+
]);
|
|
3264
|
+
let buffer = `\x1b[?2026h${deletePlan.output}`;
|
|
3018
3265
|
if (currentScreenRow > 0) {
|
|
3019
3266
|
buffer += `\x1b[${currentScreenRow}A`;
|
|
3020
3267
|
}
|
|
@@ -3045,23 +3292,32 @@ export class TUI extends Container {
|
|
|
3045
3292
|
}
|
|
3046
3293
|
buffer += cursorSeq;
|
|
3047
3294
|
buffer += "\x1b[?2026l";
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3295
|
+
let contentWritten = false;
|
|
3296
|
+
this.#writeRenderBufferAndReanchorImeCursor(buffer, cursorPos, newLines.length, () => {
|
|
3297
|
+
contentWritten = true;
|
|
3298
|
+
this.#hardwareCursorRow = cursorToRow;
|
|
3299
|
+
this.#cursorRow = Math.max(0, newLines.length - 1);
|
|
3300
|
+
this.#maxLinesRendered = newLines.length;
|
|
3301
|
+
this.#viewportTopRow = nextViewportTop;
|
|
3302
|
+
this.#previousLines = newLines;
|
|
3303
|
+
this.#previousWidth = width;
|
|
3304
|
+
this.#previousHeight = height;
|
|
3305
|
+
this.#kittyPlacementSpans = this.#kittyCommittedPlacementsAfterPaint(
|
|
3306
|
+
previousKittyPlacementSpans,
|
|
3307
|
+
nextKittyPlacementSpans,
|
|
3308
|
+
deletePlan,
|
|
3309
|
+
[{ top: nextViewportTop, bottom: nextViewportTop + height }],
|
|
3310
|
+
);
|
|
3311
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
3312
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
3313
|
+
});
|
|
3314
|
+
if (!contentWritten) return false;
|
|
3060
3315
|
|
|
3061
3316
|
if (this.#debugRedraw) {
|
|
3062
3317
|
const msg = `[${new Date().toISOString()}] viewportRepaint: ${reason} (prev=${this.#previousLines.length}, new=${newLines.length}, height=${height}, viewportTop=${nextViewportTop})\n`;
|
|
3063
3318
|
this.#appendDebugRedrawLog(msg);
|
|
3064
3319
|
}
|
|
3320
|
+
return true;
|
|
3065
3321
|
};
|
|
3066
3322
|
|
|
3067
3323
|
const debugRedraw = this.#debugRedraw;
|
|
@@ -3161,6 +3417,22 @@ export class TUI extends Container {
|
|
|
3161
3417
|
lastChanged = newLines.length - 1;
|
|
3162
3418
|
}
|
|
3163
3419
|
let appendStart = appendedLines && firstChanged === this.#previousLines.length && firstChanged > 0;
|
|
3420
|
+
if (firstChanged >= 0) {
|
|
3421
|
+
const changedTop = firstChanged;
|
|
3422
|
+
let expanded: boolean;
|
|
3423
|
+
do {
|
|
3424
|
+
const priorTop = firstChanged;
|
|
3425
|
+
const priorBottom = lastChanged + 1;
|
|
3426
|
+
for (const placement of previousKittyPlacementSpans) {
|
|
3427
|
+
const placementBottom = placement.row + placement.rows;
|
|
3428
|
+
if (placement.row >= priorBottom || placementBottom <= priorTop) continue;
|
|
3429
|
+
firstChanged = Math.min(firstChanged, placement.row);
|
|
3430
|
+
lastChanged = Math.max(lastChanged, placementBottom - 1);
|
|
3431
|
+
}
|
|
3432
|
+
expanded = firstChanged !== priorTop || lastChanged + 1 !== priorBottom;
|
|
3433
|
+
} while (expanded);
|
|
3434
|
+
if (firstChanged !== changedTop) appendStart = false;
|
|
3435
|
+
}
|
|
3164
3436
|
|
|
3165
3437
|
// No changes - but still need to update hardware cursor position if it moved
|
|
3166
3438
|
if (firstChanged === -1) {
|
|
@@ -3174,6 +3446,21 @@ export class TUI extends Container {
|
|
|
3174
3446
|
viewportRepaint(`content contraction changed viewport top (${prevViewportTop} -> ${nextLiveViewportTop})`);
|
|
3175
3447
|
return;
|
|
3176
3448
|
}
|
|
3449
|
+
if (
|
|
3450
|
+
appendedLines &&
|
|
3451
|
+
nextLiveViewportTop > prevViewportTop &&
|
|
3452
|
+
previousKittyPlacementSpans.some(placement =>
|
|
3453
|
+
this.#kittyPlacementIntersectsRegion(placement, {
|
|
3454
|
+
top: prevViewportTop,
|
|
3455
|
+
bottom: prevViewportTop + height,
|
|
3456
|
+
}),
|
|
3457
|
+
)
|
|
3458
|
+
) {
|
|
3459
|
+
viewportRepaint(
|
|
3460
|
+
`content append moved a Kitty placement viewport (${prevViewportTop} -> ${nextLiveViewportTop})`,
|
|
3461
|
+
);
|
|
3462
|
+
return;
|
|
3463
|
+
}
|
|
3177
3464
|
if (appendedLines && this.#scrollbackResumeViewportTop !== undefined && nextLiveViewportTop > prevViewportTop) {
|
|
3178
3465
|
const resumeViewportTop = this.#scrollbackResumeViewportTop;
|
|
3179
3466
|
if (nextLiveViewportTop <= resumeViewportTop) {
|
|
@@ -3186,10 +3473,15 @@ export class TUI extends Container {
|
|
|
3186
3473
|
const previousLines = this.#previousLines;
|
|
3187
3474
|
const previousWidth = this.#previousWidth;
|
|
3188
3475
|
const previousHeight = this.#previousHeight;
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3476
|
+
if (
|
|
3477
|
+
!viewportRepaint(
|
|
3478
|
+
`staging committed scrollback frontier before resumed admission (${prevViewportTop} -> ${resumeViewportTop} -> ${nextLiveViewportTop})`,
|
|
3479
|
+
resumeViewportTop,
|
|
3480
|
+
)
|
|
3481
|
+
) {
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
previousKittyPlacementSpans = this.#kittyPlacementSpans;
|
|
3193
3485
|
this.#previousLines = previousLines;
|
|
3194
3486
|
this.#previousWidth = previousWidth;
|
|
3195
3487
|
this.#previousHeight = previousHeight;
|
|
@@ -3203,7 +3495,10 @@ export class TUI extends Container {
|
|
|
3203
3495
|
// All changes are in deleted lines (nothing to render, just clear)
|
|
3204
3496
|
if (firstChanged >= newLines.length) {
|
|
3205
3497
|
if (this.#previousLines.length > newLines.length) {
|
|
3206
|
-
|
|
3498
|
+
const deletePlan = this.#kittyPlacementDeletePlan(previousKittyPlacementSpans, nextKittyPlacementSpans, [
|
|
3499
|
+
{ top: firstChanged, bottom: lastChanged + 1 },
|
|
3500
|
+
]);
|
|
3501
|
+
let buffer = `\x1b[?2026h${deletePlan.output}`;
|
|
3207
3502
|
// Move to end of new content (clamp to 0 for empty content)
|
|
3208
3503
|
const targetRow = Math.max(0, newLines.length - 1);
|
|
3209
3504
|
const lineDiff = computeLineDiff(targetRow);
|
|
@@ -3245,6 +3540,14 @@ export class TUI extends Container {
|
|
|
3245
3540
|
this.#previousHeight = height;
|
|
3246
3541
|
this.#maxLinesRendered = newLines.length;
|
|
3247
3542
|
this.#viewportTopRow = Math.max(0, newLines.length - height);
|
|
3543
|
+
this.#kittyPlacementSpans = this.#kittyCommittedPlacementsAfterPaint(
|
|
3544
|
+
previousKittyPlacementSpans,
|
|
3545
|
+
nextKittyPlacementSpans,
|
|
3546
|
+
deletePlan,
|
|
3547
|
+
[],
|
|
3548
|
+
);
|
|
3549
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
3550
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
3248
3551
|
})
|
|
3249
3552
|
)
|
|
3250
3553
|
return;
|
|
@@ -3254,6 +3557,8 @@ export class TUI extends Container {
|
|
|
3254
3557
|
this.#previousHeight = height;
|
|
3255
3558
|
this.#maxLinesRendered = newLines.length;
|
|
3256
3559
|
this.#viewportTopRow = Math.max(0, newLines.length - height);
|
|
3560
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
3561
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
3257
3562
|
return;
|
|
3258
3563
|
}
|
|
3259
3564
|
|
|
@@ -3280,7 +3585,10 @@ export class TUI extends Container {
|
|
|
3280
3585
|
|
|
3281
3586
|
// Render from first changed line to end
|
|
3282
3587
|
// Build buffer with all updates wrapped in synchronized output
|
|
3283
|
-
|
|
3588
|
+
const deletePlan = this.#kittyPlacementDeletePlan(previousKittyPlacementSpans, nextKittyPlacementSpans, [
|
|
3589
|
+
{ top: firstChanged, bottom: lastChanged + 1 },
|
|
3590
|
+
]);
|
|
3591
|
+
let buffer = `\x1b[?2026h${deletePlan.output}`; // Begin synchronized output
|
|
3284
3592
|
const prevViewportBottom = prevViewportTop + height - 1;
|
|
3285
3593
|
const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
|
|
3286
3594
|
if (moveTargetRow > prevViewportBottom) {
|
|
@@ -3408,6 +3716,14 @@ export class TUI extends Container {
|
|
|
3408
3716
|
this.#previousLines = newLines;
|
|
3409
3717
|
this.#previousWidth = width;
|
|
3410
3718
|
this.#previousHeight = height;
|
|
3719
|
+
this.#kittyPlacementSpans = this.#kittyCommittedPlacementsAfterPaint(
|
|
3720
|
+
previousKittyPlacementSpans,
|
|
3721
|
+
nextKittyPlacementSpans,
|
|
3722
|
+
deletePlan,
|
|
3723
|
+
[{ top: firstChanged, bottom: renderEnd + 1 }],
|
|
3724
|
+
);
|
|
3725
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
3726
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
3411
3727
|
})
|
|
3412
3728
|
)
|
|
3413
3729
|
return;
|