@gajae-code/tui 0.18.7 → 0.18.9
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 +13 -0
- package/dist/types/tui.d.ts +2 -0
- package/dist/types/utils.d.ts +70 -0
- package/package.json +3 -3
- package/src/components/markdown.ts +484 -62
- package/src/tui.ts +480 -65
- package/src/utils.ts +196 -0
package/src/tui.ts
CHANGED
|
@@ -21,12 +21,17 @@ import {
|
|
|
21
21
|
TERMINAL,
|
|
22
22
|
} from "./terminal-capabilities";
|
|
23
23
|
import {
|
|
24
|
+
type CopyRowAnnotation,
|
|
24
25
|
Ellipsis,
|
|
26
|
+
extractCopyRowAnnotation,
|
|
25
27
|
extractSegments,
|
|
26
28
|
isPrintableAscii,
|
|
27
29
|
normalizeTerminalOutput,
|
|
30
|
+
retainCopyAnnotations,
|
|
28
31
|
sliceByColumn,
|
|
29
32
|
sliceWithWidth,
|
|
33
|
+
splitCopyAnnotations,
|
|
34
|
+
stripCopyApcs,
|
|
30
35
|
truncateLinesToWidth,
|
|
31
36
|
truncateToWidth,
|
|
32
37
|
visibleWidth,
|
|
@@ -105,6 +110,8 @@ const LINE_TERMINATOR = "\x1b[0m\x1b]8;;\x07";
|
|
|
105
110
|
const MOUSE_SELECTION_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
106
111
|
/** Discrete mouse-wheel notch size in terminal rows (xterm/less-style). */
|
|
107
112
|
export const DEFAULT_WHEEL_LINES = 3;
|
|
113
|
+
/** Row step cadence while a held drag rests on the transcript edge. */
|
|
114
|
+
const MOUSE_AUTO_SCROLL_INTERVAL_MS = 50;
|
|
108
115
|
/**
|
|
109
116
|
* Repeat clicks on the same cell within this window escalate char -> word -> line
|
|
110
117
|
* selection. SGR mouse reports carry no click counter, so the escalation is timed
|
|
@@ -1002,6 +1009,8 @@ export class TUI extends Container {
|
|
|
1002
1009
|
#latestRenderedLines: string[] = [];
|
|
1003
1010
|
#latestRenderedTranscriptLineCount = 0;
|
|
1004
1011
|
#latestRenderedSuffixLineCount = 0;
|
|
1012
|
+
// Transient live rows between transcript content and the pinned suffix.
|
|
1013
|
+
#latestRenderedFrontierSpacerLineCount = 0;
|
|
1005
1014
|
#latestRenderedPlacementOwners = new Map<string, KittyPlacementOwner>();
|
|
1006
1015
|
#kittyPlacementSpans: KittyPlacementSpan[] = [];
|
|
1007
1016
|
#latestRaw: string[] = [];
|
|
@@ -1019,6 +1028,7 @@ export class TUI extends Container {
|
|
|
1019
1028
|
#restartDurableRawLines: string[] = [];
|
|
1020
1029
|
#restartDurableWidth = 0;
|
|
1021
1030
|
#transcriptIdentityReplaced = false;
|
|
1031
|
+
#transcriptRebuildPending = false;
|
|
1022
1032
|
#lineNormalizationCache = new Map<string, LineNormalizationCacheEntry>();
|
|
1023
1033
|
#lineEmitWidthCache = new Map<string, number>();
|
|
1024
1034
|
#lineTruncationCache = new Map<string, string>();
|
|
@@ -1078,6 +1088,9 @@ export class TUI extends Container {
|
|
|
1078
1088
|
#forcedRenderQueued = false;
|
|
1079
1089
|
#restartViewportRepaintPending = false;
|
|
1080
1090
|
#lastObservedWidth = 0;
|
|
1091
|
+
// Preserve the last committed grid before requestRender(true) invalidates diff dimensions.
|
|
1092
|
+
#forcedRenderPreviousWidth = 0;
|
|
1093
|
+
#forcedRenderPreviousHeight = 0;
|
|
1081
1094
|
// Trailing debounce for the settled width repair. Instance-local: taken from
|
|
1082
1095
|
// options.widthSettleMs when provided (deterministic harnesses pass 0 to
|
|
1083
1096
|
// disable), otherwise from GJC_TUI_WIDTH_SETTLE_MS / PI_TUI_WIDTH_SETTLE_MS,
|
|
@@ -1106,6 +1119,7 @@ export class TUI extends Container {
|
|
|
1106
1119
|
#nativeScrollbackAdmissionPending = false;
|
|
1107
1120
|
#transcriptIdentityResetPending = false;
|
|
1108
1121
|
#manualViewportTop: number | undefined;
|
|
1122
|
+
#manualResumeViewportTop: number | undefined; // Preserved through temporary stop/start while manual history is active.
|
|
1109
1123
|
#viewportAnchorComponent: Component | null = null;
|
|
1110
1124
|
#viewportAnchorFrame: ViewportAnchorFrame | null = null;
|
|
1111
1125
|
#manualViewportAnchor: ManualViewportAnchor | null = null;
|
|
@@ -1151,6 +1165,14 @@ export class TUI extends Container {
|
|
|
1151
1165
|
#mouseGestureDragged = false;
|
|
1152
1166
|
#mousePressPoint: MouseSelectionPoint | null = null;
|
|
1153
1167
|
#mousePressAt = 0;
|
|
1168
|
+
/** Last screen cell of a held drag; scrolling re-resolves it against the new viewport. */
|
|
1169
|
+
#mouseDragPointer: { x: number; y: number } | null = null;
|
|
1170
|
+
/** Edge scrolling waits until the drag leaves the press row, so a small drag on an edge row stays put. */
|
|
1171
|
+
#mousePressRow = 0;
|
|
1172
|
+
#mouseDragLeftPressRow = false;
|
|
1173
|
+
#mouseAutoScrollTimer?: NodeJS.Timeout;
|
|
1174
|
+
/** Held only while this TUI runs with mouse copying, so other render() consumers stay stock. */
|
|
1175
|
+
#releaseCopyAnnotations: (() => void) | undefined;
|
|
1154
1176
|
#lastClickPoint: MouseSelectionPoint | null = null;
|
|
1155
1177
|
#lastClickAt = 0;
|
|
1156
1178
|
#clickCount = 0;
|
|
@@ -1159,6 +1181,7 @@ export class TUI extends Container {
|
|
|
1159
1181
|
#manualOutputNotice = false;
|
|
1160
1182
|
#manualTranscriptLineCount = 0;
|
|
1161
1183
|
#manualSuffixLineCount = 0;
|
|
1184
|
+
#manualFrontierSpacerLineCount = 0;
|
|
1162
1185
|
#committedTranscriptRows: Array<number | null> = [];
|
|
1163
1186
|
#paintedManualOutputNotice = false;
|
|
1164
1187
|
#rasterGeneration = 0;
|
|
@@ -1330,6 +1353,9 @@ export class TUI extends Container {
|
|
|
1330
1353
|
override dispose(): void {
|
|
1331
1354
|
if (this.#preparationDisposed) return;
|
|
1332
1355
|
this.#preparationDisposed = true;
|
|
1356
|
+
this.#clearMouseSelection();
|
|
1357
|
+
this.#releaseCopyAnnotations?.();
|
|
1358
|
+
this.#releaseCopyAnnotations = undefined;
|
|
1333
1359
|
this.#renderRequested = false;
|
|
1334
1360
|
this.#renderRequestedGeneration = 0;
|
|
1335
1361
|
this.#inputRenderPending = false;
|
|
@@ -1546,6 +1572,11 @@ export class TUI extends Container {
|
|
|
1546
1572
|
getViewportAnchorComponent(): Component | null {
|
|
1547
1573
|
return this.#viewportAnchorComponent;
|
|
1548
1574
|
}
|
|
1575
|
+
#captureManualResumeViewportTop(): void {
|
|
1576
|
+
if (this.#latestRenderedFrontierSpacerLineCount === 0) return;
|
|
1577
|
+
const committedFrontier = this.#scrollbackResumeViewportTop ?? this.#nativeScrollbackViewportTop;
|
|
1578
|
+
this.#manualResumeViewportTop = committedFrontier > 0 ? committedFrontier : undefined;
|
|
1579
|
+
}
|
|
1549
1580
|
|
|
1550
1581
|
/** Clear manual viewport ownership and durable history before replacing the transcript identity. */
|
|
1551
1582
|
resetViewportAnchorIntent(): void {
|
|
@@ -1557,6 +1588,7 @@ export class TUI extends Container {
|
|
|
1557
1588
|
this.#scrollbackResumeViewportTop = undefined;
|
|
1558
1589
|
this.#nativeScrollbackViewportTop = 0;
|
|
1559
1590
|
this.#nativeScrollbackAdmissionPending = false;
|
|
1591
|
+
this.#manualResumeViewportTop = undefined;
|
|
1560
1592
|
this.#transcriptIdentityResetPending = true;
|
|
1561
1593
|
this.#manualOutputNotice = false;
|
|
1562
1594
|
this.#paintedManualOutputNotice = false;
|
|
@@ -1579,6 +1611,8 @@ export class TUI extends Container {
|
|
|
1579
1611
|
|
|
1580
1612
|
/** Allow one semantic-neighbor reconciliation after a definitive same-transcript rebuild. */
|
|
1581
1613
|
prepareViewportAnchorForTranscriptRebuild(): void {
|
|
1614
|
+
this.#transcriptRebuildPending = true;
|
|
1615
|
+
this.#manualResumeViewportTop = undefined;
|
|
1582
1616
|
if (this.#manualViewportAnchor !== null) this.#reconcileMissingViewportAnchor = true;
|
|
1583
1617
|
}
|
|
1584
1618
|
|
|
@@ -1633,6 +1667,7 @@ export class TUI extends Container {
|
|
|
1633
1667
|
this.#suspendRasterLeasesForManualViewport(() => this.revealViewportAnchor(id, alignment))
|
|
1634
1668
|
)
|
|
1635
1669
|
return true;
|
|
1670
|
+
if (this.#manualViewportTop === undefined) this.#captureManualResumeViewportTop();
|
|
1636
1671
|
this.#manualViewportAnchor = {
|
|
1637
1672
|
id: selected.id,
|
|
1638
1673
|
graphemeIndex:
|
|
@@ -1684,6 +1719,7 @@ export class TUI extends Container {
|
|
|
1684
1719
|
const previousManualViewportAnchor = this.#manualViewportAnchor;
|
|
1685
1720
|
const previousManualViewportFallbackAnchors = this.#manualViewportFallbackAnchors;
|
|
1686
1721
|
const previousReconcileMissingViewportAnchor = this.#reconcileMissingViewportAnchor;
|
|
1722
|
+
const previousManualResumeViewportTop = this.#manualResumeViewportTop;
|
|
1687
1723
|
|
|
1688
1724
|
const direction: -1 | 1 = delta < 0 ? -1 : 1;
|
|
1689
1725
|
const pin = options?.pin ?? "stable";
|
|
@@ -1778,6 +1814,7 @@ export class TUI extends Container {
|
|
|
1778
1814
|
this.#manualViewportFallbackAnchors = fallbacks;
|
|
1779
1815
|
}
|
|
1780
1816
|
}
|
|
1817
|
+
if (previousManualViewportTop === undefined) this.#captureManualResumeViewportTop();
|
|
1781
1818
|
this.#manualViewportTop = targetViewportTop;
|
|
1782
1819
|
let contentPainted = false;
|
|
1783
1820
|
const painted = this.#repaintViewportFromLines(
|
|
@@ -1792,13 +1829,15 @@ export class TUI extends Container {
|
|
|
1792
1829
|
contentPainted = true;
|
|
1793
1830
|
this.#manualTranscriptLineCount = this.#latestRenderedTranscriptLineCount;
|
|
1794
1831
|
this.#manualSuffixLineCount = this.#latestRenderedSuffixLineCount;
|
|
1832
|
+
this.#manualFrontierSpacerLineCount = this.#latestRenderedFrontierSpacerLineCount;
|
|
1795
1833
|
},
|
|
1796
1834
|
false,
|
|
1797
1835
|
this.#kittyPlacementSpans,
|
|
1798
1836
|
this.#kittyPlacementSpansForLines(this.#previousLines, this.#latestRenderedPlacementOwners),
|
|
1799
1837
|
{
|
|
1800
|
-
transcriptLineCount: this.#
|
|
1801
|
-
suffixLineCount: this.#
|
|
1838
|
+
transcriptLineCount: this.#manualTranscriptLineCount,
|
|
1839
|
+
suffixLineCount: this.#manualSuffixLineCount,
|
|
1840
|
+
frontierSpacerLineCount: this.#manualFrontierSpacerLineCount,
|
|
1802
1841
|
},
|
|
1803
1842
|
true,
|
|
1804
1843
|
);
|
|
@@ -1807,6 +1846,7 @@ export class TUI extends Container {
|
|
|
1807
1846
|
this.#manualViewportAnchor = previousManualViewportAnchor;
|
|
1808
1847
|
this.#manualViewportFallbackAnchors = previousManualViewportFallbackAnchors;
|
|
1809
1848
|
this.#reconcileMissingViewportAnchor = previousReconcileMissingViewportAnchor;
|
|
1849
|
+
this.#manualResumeViewportTop = previousManualResumeViewportTop;
|
|
1810
1850
|
}
|
|
1811
1851
|
return painted;
|
|
1812
1852
|
}
|
|
@@ -1822,14 +1862,19 @@ export class TUI extends Container {
|
|
|
1822
1862
|
if (this.#manualViewportTop === undefined) return false;
|
|
1823
1863
|
const height = this.terminal.rows;
|
|
1824
1864
|
const width = this.terminal.columns;
|
|
1865
|
+
// Manual rendering can drop transient padding from the latest frame; restore the
|
|
1866
|
+
// frontier captured when manual history began before rebuilding the live viewport.
|
|
1867
|
+
const committedFrontier = this.#manualResumeViewportTop;
|
|
1825
1868
|
const paddedLiveLines = this.#padBeforeBottomPinnedComponent(
|
|
1826
1869
|
this.#latestRenderedLines,
|
|
1827
|
-
height,
|
|
1870
|
+
committedFrontier === undefined ? height : Math.max(height, committedFrontier + height),
|
|
1828
1871
|
this.#latestRenderedSuffixLineCount,
|
|
1829
1872
|
);
|
|
1830
1873
|
const liveLines = paddedLiveLines.lines;
|
|
1831
1874
|
const liveTranscriptLineCount = this.#latestRenderedTranscriptLineCount;
|
|
1832
|
-
const liveSuffixLineCount = this.#latestRenderedSuffixLineCount
|
|
1875
|
+
const liveSuffixLineCount = this.#latestRenderedSuffixLineCount;
|
|
1876
|
+
const liveFrontierSpacerLineCount =
|
|
1877
|
+
this.#latestRenderedFrontierSpacerLineCount + paddedLiveLines.insertedBlankRows;
|
|
1833
1878
|
const liveKittyPlacementSpans = this.#kittyPlacementSpansForLines(liveLines, this.#latestRenderedPlacementOwners);
|
|
1834
1879
|
let liveCursorPosition = this.#lastCursorPosition;
|
|
1835
1880
|
if (liveCursorPosition !== null && liveCursorPosition.row >= paddedLiveLines.insertionRow) {
|
|
@@ -1855,11 +1900,14 @@ export class TUI extends Container {
|
|
|
1855
1900
|
this.#manualOutputNotice = false;
|
|
1856
1901
|
this.#committedTranscriptRows = [];
|
|
1857
1902
|
this.#paintedManualOutputNotice = false;
|
|
1903
|
+
this.#manualResumeViewportTop = undefined;
|
|
1858
1904
|
this.#lastCursorPosition = liveCursorPosition;
|
|
1859
1905
|
this.#previousLines = liveLines;
|
|
1860
1906
|
this.#manualTranscriptLineCount = liveTranscriptLineCount;
|
|
1861
1907
|
this.#manualSuffixLineCount = liveSuffixLineCount;
|
|
1908
|
+
this.#manualFrontierSpacerLineCount = liveFrontierSpacerLineCount;
|
|
1862
1909
|
this.#latestRenderedLines = liveLines.slice();
|
|
1910
|
+
this.#latestRenderedFrontierSpacerLineCount = liveFrontierSpacerLineCount;
|
|
1863
1911
|
if (this.#scrollbackResumeViewportTop === undefined) {
|
|
1864
1912
|
this.#nativeScrollbackViewportTop = liveViewportTop;
|
|
1865
1913
|
}
|
|
@@ -1876,7 +1924,11 @@ export class TUI extends Container {
|
|
|
1876
1924
|
true,
|
|
1877
1925
|
this.#kittyPlacementSpans,
|
|
1878
1926
|
liveKittyPlacementSpans,
|
|
1879
|
-
{
|
|
1927
|
+
{
|
|
1928
|
+
transcriptLineCount: liveTranscriptLineCount,
|
|
1929
|
+
suffixLineCount: liveSuffixLineCount,
|
|
1930
|
+
frontierSpacerLineCount: liveFrontierSpacerLineCount,
|
|
1931
|
+
},
|
|
1880
1932
|
);
|
|
1881
1933
|
}
|
|
1882
1934
|
|
|
@@ -2100,9 +2152,7 @@ export class TUI extends Container {
|
|
|
2100
2152
|
if (!shouldWrite) return { queueId: id, operation: op.type, status: "stale-token" };
|
|
2101
2153
|
}
|
|
2102
2154
|
if (op.type === "raster-multipart-batch" && op.prefix !== undefined && op.afterPrefix !== undefined) {
|
|
2103
|
-
const prefixWritten = this.#guardTerminalOperation(() =>
|
|
2104
|
-
this.terminal.write(new TextDecoder().decode(op.prefix)),
|
|
2105
|
-
);
|
|
2155
|
+
const prefixWritten = this.#guardTerminalOperation(() => this.#emit(new TextDecoder().decode(op.prefix)));
|
|
2106
2156
|
if (!prefixWritten) return failed();
|
|
2107
2157
|
const abortBarrier = () => {
|
|
2108
2158
|
// Abort/cursor-restoration bytes are terminal writes: never emit
|
|
@@ -2112,7 +2162,7 @@ export class TUI extends Container {
|
|
|
2112
2162
|
const abortSuffix = op.abortSuffix === undefined ? "" : new TextDecoder().decode(op.abortSuffix);
|
|
2113
2163
|
const cursorVisibility = op.restoreCursorVisibility ? this.#cursorVisibilitySequence() : "";
|
|
2114
2164
|
if (abortSuffix || cursorVisibility)
|
|
2115
|
-
this.#guardTerminalOperation(() => this
|
|
2165
|
+
this.#guardTerminalOperation(() => this.#emit(abortSuffix + cursorVisibility));
|
|
2116
2166
|
};
|
|
2117
2167
|
const flushed = await (this.terminal as Terminal & { flush?: () => Promise<boolean> }).flush?.();
|
|
2118
2168
|
// Async boundary: the terminal may have stopped while we awaited.
|
|
@@ -2166,7 +2216,7 @@ export class TUI extends Container {
|
|
|
2166
2216
|
const dependent = op.type === "generic-render" || op.type === "generic-full-redraw";
|
|
2167
2217
|
const ok = dependent
|
|
2168
2218
|
? this.#writeProtectedRenderIngress(finalBytes)
|
|
2169
|
-
: this.#guardTerminalOperation(() => this
|
|
2219
|
+
: this.#guardTerminalOperation(() => this.#emit(finalBytes));
|
|
2170
2220
|
if (!ok && dependent) {
|
|
2171
2221
|
const rect = (op as { rect: CellRect }).rect;
|
|
2172
2222
|
const blockedBy = [...this.#rasterCleanup.entries()]
|
|
@@ -2191,7 +2241,7 @@ export class TUI extends Container {
|
|
|
2191
2241
|
lease.revoked = true;
|
|
2192
2242
|
this.#rasterLeases.delete(request.token.ownerId);
|
|
2193
2243
|
const erase = this.#cursorGuardedRasterSequence(new TextDecoder().decode(lease.erase));
|
|
2194
|
-
const ok = this.#guardTerminalOperation(() => this
|
|
2244
|
+
const ok = this.#guardTerminalOperation(() => this.#emit(erase));
|
|
2195
2245
|
if (!ok)
|
|
2196
2246
|
this.#rasterCleanup.set(request.token.ownerId, {
|
|
2197
2247
|
token: lease.token,
|
|
@@ -2357,7 +2407,7 @@ export class TUI extends Container {
|
|
|
2357
2407
|
)
|
|
2358
2408
|
)
|
|
2359
2409
|
return false;
|
|
2360
|
-
return this.#guardTerminalOperation(() => this
|
|
2410
|
+
return this.#guardTerminalOperation(() => this.#emit(buffer));
|
|
2361
2411
|
}
|
|
2362
2412
|
#writeProtectedRenderIngress(buffer: string): boolean {
|
|
2363
2413
|
const affected = [...this.#rasterLeases.values()];
|
|
@@ -2371,7 +2421,7 @@ export class TUI extends Container {
|
|
|
2371
2421
|
...affected.map(lease => new TextDecoder().decode(lease.erase)),
|
|
2372
2422
|
].join(""),
|
|
2373
2423
|
);
|
|
2374
|
-
const ok = this.#guardTerminalOperation(() => this
|
|
2424
|
+
const ok = this.#guardTerminalOperation(() => this.#emit(cleanup + buffer));
|
|
2375
2425
|
if (!ok) {
|
|
2376
2426
|
this.#terminalUnavailable = true;
|
|
2377
2427
|
this.#previousLines = [];
|
|
@@ -2454,6 +2504,9 @@ export class TUI extends Container {
|
|
|
2454
2504
|
this.#markTerminalUnavailable();
|
|
2455
2505
|
throw error;
|
|
2456
2506
|
}
|
|
2507
|
+
// Taken only once the terminal started, so a failed start never leaves annotations on.
|
|
2508
|
+
if (this.options.enableMouse === true && this.options.copySelection && !this.#releaseCopyAnnotations)
|
|
2509
|
+
this.#releaseCopyAnnotations = retainCopyAnnotations();
|
|
2457
2510
|
if (this.#pendingTerminalCleanup.length > 0 || this.#rasterCleanup.size > 0) {
|
|
2458
2511
|
void this.notifyTerminalLifecycle({
|
|
2459
2512
|
kind: "availability-restored",
|
|
@@ -2591,8 +2644,13 @@ export class TUI extends Container {
|
|
|
2591
2644
|
this.#clearSixelProbeState();
|
|
2592
2645
|
}
|
|
2593
2646
|
|
|
2647
|
+
/** The only terminal write sink: copy APCs are in-process metadata and never reach the terminal. */
|
|
2648
|
+
#emit(data: string): void {
|
|
2649
|
+
this.terminal.write(stripCopyApcs(data));
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2594
2652
|
#writeTerminal(data: string, deferRenderFailure = false): boolean {
|
|
2595
|
-
return this.#guardTerminalOperation(() => this
|
|
2653
|
+
return this.#guardTerminalOperation(() => this.#emit(data), !deferRenderFailure);
|
|
2596
2654
|
}
|
|
2597
2655
|
|
|
2598
2656
|
#frameSynchronizedOutput(payload: string): string {
|
|
@@ -2631,7 +2689,7 @@ export class TUI extends Container {
|
|
|
2631
2689
|
return false;
|
|
2632
2690
|
}
|
|
2633
2691
|
try {
|
|
2634
|
-
this
|
|
2692
|
+
this.#emit(data);
|
|
2635
2693
|
} catch {
|
|
2636
2694
|
this.#markTerminalUnavailable();
|
|
2637
2695
|
return false;
|
|
@@ -2812,6 +2870,15 @@ export class TUI extends Container {
|
|
|
2812
2870
|
}
|
|
2813
2871
|
|
|
2814
2872
|
stop(): void {
|
|
2873
|
+
this.#stop(false);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
/** Temporarily releases terminal modes for foreground handoff and preserves viewport state for restart. */
|
|
2877
|
+
suspend(): void {
|
|
2878
|
+
this.#stop(true);
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
#stop(temporary: boolean): void {
|
|
2815
2882
|
this.#invalidatePreparations();
|
|
2816
2883
|
// Invalidate every raster-queue body captured under the running epoch
|
|
2817
2884
|
// before any teardown: nothing queued before stop may write after
|
|
@@ -2823,6 +2890,8 @@ export class TUI extends Container {
|
|
|
2823
2890
|
this.#clearSixelProbeState();
|
|
2824
2891
|
this.#clearMouseSelection();
|
|
2825
2892
|
this.#resetClickCount();
|
|
2893
|
+
this.#releaseCopyAnnotations?.();
|
|
2894
|
+
this.#releaseCopyAnnotations = undefined;
|
|
2826
2895
|
this.#stopped = true;
|
|
2827
2896
|
this.#settleRenderCommitWaiters(false);
|
|
2828
2897
|
if (this.#renderTimer) {
|
|
@@ -2837,17 +2906,26 @@ export class TUI extends Container {
|
|
|
2837
2906
|
// An armed TIMER dies with the session, but a repair already deferred while
|
|
2838
2907
|
// the user was reading scrollback must survive a temporary stop/start
|
|
2839
2908
|
// (Ctrl-Z resume, external editor): manual viewport ownership survives
|
|
2840
|
-
// restart, so followLiveViewport() still needs the pending repair
|
|
2841
|
-
//
|
|
2909
|
+
// restart, so followLiveViewport() still needs the pending repair and saved
|
|
2910
|
+
// frontier because teardown drops its transient spacers. Without manual
|
|
2911
|
+
// ownership the flags are moot — start() issues a forced full render.
|
|
2842
2912
|
if (this.#manualViewportTop === undefined) {
|
|
2843
2913
|
this.#widthSettleRepairPending = false;
|
|
2844
2914
|
this.#tabWidthRepairPending = false;
|
|
2915
|
+
this.#manualResumeViewportTop = undefined;
|
|
2845
2916
|
}
|
|
2846
|
-
// Move the cursor after the frame actually displayed to prevent
|
|
2847
|
-
// overwriting/artifacts on exit. The latest logical frame can differ while
|
|
2848
|
-
// a semantic viewport retains the previously painted frame.
|
|
2849
2917
|
const displayedFrameLines = this.#previousLines.length || this.#latestRenderedLines.length;
|
|
2850
|
-
if (displayedFrameLines > 0) {
|
|
2918
|
+
if (displayedFrameLines > 0 && this.#manualViewportTop !== undefined) {
|
|
2919
|
+
// The manual viewport's logical row is not a physical screen coordinate.
|
|
2920
|
+
// Insert a blank bottom handoff row and discard the pinned row rather than
|
|
2921
|
+
// scrolling committed history into native scrollback. start() redraws the
|
|
2922
|
+
// pinned suffix on resume.
|
|
2923
|
+
const bottomRow = Math.max(1, this.terminal.rows);
|
|
2924
|
+
this.#writeTerminal(`\x1b[${bottomRow};1H\x1b[1L`);
|
|
2925
|
+
this.#hardwareCursorRow = bottomRow - 1;
|
|
2926
|
+
} else if (displayedFrameLines > 0) {
|
|
2927
|
+
// The latest logical frame can differ from the last painted frame when a
|
|
2928
|
+
// semantic viewport retains its previous image.
|
|
2851
2929
|
const targetRow = displayedFrameLines; // Line after the last content
|
|
2852
2930
|
const lineDiff = targetRow - this.#hardwareCursorRow;
|
|
2853
2931
|
if (lineDiff > 0) {
|
|
@@ -2856,6 +2934,7 @@ export class TUI extends Container {
|
|
|
2856
2934
|
this.#writeTerminal(`\x1b[${-lineDiff}A`);
|
|
2857
2935
|
}
|
|
2858
2936
|
this.#writeTerminal("\r\n");
|
|
2937
|
+
this.#hardwareCursorRow = targetRow + 1;
|
|
2859
2938
|
}
|
|
2860
2939
|
|
|
2861
2940
|
if (this.#useImeBlockCursor) {
|
|
@@ -2871,7 +2950,9 @@ export class TUI extends Container {
|
|
|
2871
2950
|
// non-manual restart keeps only the durable baseline until its first render:
|
|
2872
2951
|
// that render can admit a raw-prefix-proven append without replaying history.
|
|
2873
2952
|
this.#restartViewportRepaintPending =
|
|
2874
|
-
|
|
2953
|
+
temporary &&
|
|
2954
|
+
this.#manualViewportTop === undefined &&
|
|
2955
|
+
(this.#previousLines.length > 0 || this.#maxLinesRendered > 0);
|
|
2875
2956
|
if (this.#restartViewportRepaintPending) {
|
|
2876
2957
|
this.#restartDurableLineCount = this.#durableLineCount;
|
|
2877
2958
|
this.#restartDurableRenderedLines = this.#durableRenderedLines.slice();
|
|
@@ -2900,6 +2981,11 @@ export class TUI extends Container {
|
|
|
2900
2981
|
this.#lineEmitWidthCache.clear();
|
|
2901
2982
|
this.#previousWidth = 0;
|
|
2902
2983
|
this.#previousHeight = 0;
|
|
2984
|
+
this.#forcedRenderPreviousWidth = 0;
|
|
2985
|
+
this.#forcedRenderPreviousHeight = 0;
|
|
2986
|
+
this.#transcriptRebuildPending = false;
|
|
2987
|
+
this.#latestRenderedFrontierSpacerLineCount = 0;
|
|
2988
|
+
this.#manualFrontierSpacerLineCount = 0;
|
|
2903
2989
|
this.#resizeRenderQueued = false;
|
|
2904
2990
|
this.#resizeRenderMutationQueued = false;
|
|
2905
2991
|
this.#renderMutationQueued = false;
|
|
@@ -3206,8 +3292,14 @@ export class TUI extends Container {
|
|
|
3206
3292
|
if (force) {
|
|
3207
3293
|
// A forced full redraw supersedes any queued input-priority render.
|
|
3208
3294
|
this.#inputRenderPending = false;
|
|
3209
|
-
if (!widthSettleRequest) this.#forcedRenderQueued = true;
|
|
3210
3295
|
if (!widthSettleRequest) {
|
|
3296
|
+
if (!this.#forcedRenderQueued) {
|
|
3297
|
+
this.#forcedRenderPreviousWidth =
|
|
3298
|
+
this.#previousWidth < 0 ? this.#forcedRenderPreviousWidth : this.#previousWidth;
|
|
3299
|
+
this.#forcedRenderPreviousHeight =
|
|
3300
|
+
this.#previousHeight < 0 ? this.#forcedRenderPreviousHeight : this.#previousHeight;
|
|
3301
|
+
}
|
|
3302
|
+
this.#forcedRenderQueued = true;
|
|
3211
3303
|
this.#previousWidth = -1; // -1 triggers widthChanged
|
|
3212
3304
|
this.#previousHeight = -1; // -1 triggers heightChanged
|
|
3213
3305
|
}
|
|
@@ -3311,15 +3403,29 @@ export class TUI extends Container {
|
|
|
3311
3403
|
data = current;
|
|
3312
3404
|
}
|
|
3313
3405
|
|
|
3314
|
-
|
|
3406
|
+
let mouse = parseSgrMouseEvent(data);
|
|
3315
3407
|
if (mouse) {
|
|
3408
|
+
// A held selection may leave the window; clamp it to the nearest cell so
|
|
3409
|
+
// the drag keeps extending instead of being discarded.
|
|
3410
|
+
if (this.#mouseButtonDown && (mouse.kind === "drag" || mouse.kind === "release" || mouse.kind === "wheel")) {
|
|
3411
|
+
mouse = {
|
|
3412
|
+
...mouse,
|
|
3413
|
+
x: Math.min(mouse.x, this.terminal.columns),
|
|
3414
|
+
y: Math.min(mouse.y, this.terminal.rows),
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3316
3417
|
// Coordinates outside the current terminal cannot name a visible cell.
|
|
3317
3418
|
if (mouse.x > this.terminal.columns || mouse.y > this.terminal.rows) {
|
|
3318
3419
|
this.#clearMouseSelection();
|
|
3319
3420
|
this.#resetClickCount();
|
|
3320
3421
|
return;
|
|
3321
3422
|
}
|
|
3322
|
-
if (mouse.kind === "wheel") {
|
|
3423
|
+
if (mouse.kind === "wheel" && this.#mouseButtonDown && this.#mouseSelectionAnchor !== null) {
|
|
3424
|
+
// Wheel during a held drag scrolls under the pointer and keeps extending.
|
|
3425
|
+
this.scrollViewportBy(mouse.direction! * DEFAULT_WHEEL_LINES, { pin: "stable" });
|
|
3426
|
+
this.#extendMouseSelectionAt(mouse);
|
|
3427
|
+
this.#updateMouseAutoScroll();
|
|
3428
|
+
} else if (mouse.kind === "wheel") {
|
|
3323
3429
|
this.#clearMouseSelection();
|
|
3324
3430
|
this.#resetClickCount();
|
|
3325
3431
|
this.scrollViewportBy(mouse.direction! * DEFAULT_WHEEL_LINES, { pin: "stable" });
|
|
@@ -3428,6 +3534,100 @@ export class TUI extends Container {
|
|
|
3428
3534
|
: { line, column: mouse.x - 1 };
|
|
3429
3535
|
}
|
|
3430
3536
|
|
|
3537
|
+
/** Screen rows (zero-based) that currently show transcript lines. */
|
|
3538
|
+
#transcriptScreenRows(): { first: number; last: number } | null {
|
|
3539
|
+
this.#ensureLiveSelectionRows();
|
|
3540
|
+
let first = -1;
|
|
3541
|
+
let last = -1;
|
|
3542
|
+
for (let row = 0; row < this.#committedTranscriptRows.length; row++) {
|
|
3543
|
+
const line = this.#committedTranscriptRows[row];
|
|
3544
|
+
if (line === null || line === undefined || line < 0 || line >= this.#manualTranscriptLineCount) continue;
|
|
3545
|
+
if (first < 0) first = row;
|
|
3546
|
+
last = row;
|
|
3547
|
+
}
|
|
3548
|
+
return first < 0 ? null : { first, last };
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
/**
|
|
3552
|
+
* A held drag over editor/status chrome or past the transcript still names a
|
|
3553
|
+
* transcript cell: the nearest visible row, at the pointer column, extended to
|
|
3554
|
+
* the row edge so a downward drag reaches the end of the last line.
|
|
3555
|
+
*/
|
|
3556
|
+
#mouseDragPoint(pointer: { x: number; y: number }): MouseSelectionPoint | null {
|
|
3557
|
+
const rows = this.#transcriptScreenRows();
|
|
3558
|
+
const direct = this.#mouseSelectionPoint({ kind: "drag", x: pointer.x, y: pointer.y });
|
|
3559
|
+
if (direct !== null || rows === null) return direct;
|
|
3560
|
+
const above = pointer.y - 1 < rows.first;
|
|
3561
|
+
const line = this.#committedTranscriptRows[above ? rows.first : rows.last]!;
|
|
3562
|
+
return { line, column: above ? 0 : Math.max(0, this.terminal.columns - 1) };
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
/** Following live clears the screen-row map until the next paint; rebuild it from the live viewport. */
|
|
3566
|
+
#ensureLiveSelectionRows(): void {
|
|
3567
|
+
if (this.#manualViewportTop !== undefined || this.#committedTranscriptRows.length > 0) return;
|
|
3568
|
+
this.#refreshPaintedLiveViewportObservation(this.terminal.rows);
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
#extendMouseSelectionAt(pointer: { x: number; y: number }): void {
|
|
3572
|
+
const anchor = this.#mouseSelectionAnchor;
|
|
3573
|
+
if (this.#mouseSelectionStart === null || anchor === null) return;
|
|
3574
|
+
this.#mouseDragPointer = { x: pointer.x, y: pointer.y };
|
|
3575
|
+
this.#mouseGestureDragged = true;
|
|
3576
|
+
if (pointer.y !== this.#mousePressRow) this.#mouseDragLeftPressRow = true;
|
|
3577
|
+
const point = this.#mouseDragPoint(pointer);
|
|
3578
|
+
if (point === null) return;
|
|
3579
|
+
const forward = point.line > anchor.line || (point.line === anchor.line && point.column >= anchor.column);
|
|
3580
|
+
// The anchor span stays whole while the far end grows, so dragging after a
|
|
3581
|
+
// double click extends word by word instead of collapsing back to one cell.
|
|
3582
|
+
this.#mouseSelectionStart = this.#snapSelectionPoint(anchor, forward ? "start" : "end");
|
|
3583
|
+
this.#mouseSelectionEnd = this.#snapSelectionPoint(point, forward ? "end" : "start");
|
|
3584
|
+
this.#mouseSelectionActive = true;
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
/** Edge direction for a held drag: above the first or at/below the last transcript row. */
|
|
3588
|
+
#mouseAutoScrollDirection(pointer: { x: number; y: number }): -1 | 0 | 1 {
|
|
3589
|
+
const rows = this.#transcriptScreenRows();
|
|
3590
|
+
if (rows === null || !this.#mouseDragLeftPressRow) return 0;
|
|
3591
|
+
const row = pointer.y - 1;
|
|
3592
|
+
if (row <= rows.first) return -1;
|
|
3593
|
+
if (row >= rows.last || row >= this.terminal.rows - 1) return 1;
|
|
3594
|
+
return 0;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
#updateMouseAutoScroll(): void {
|
|
3598
|
+
const pointer = this.#mouseDragPointer;
|
|
3599
|
+
const direction = pointer && this.#mouseButtonDown ? this.#mouseAutoScrollDirection(pointer) : 0;
|
|
3600
|
+
if (direction === 0) {
|
|
3601
|
+
this.#stopMouseAutoScroll();
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
if (this.#mouseAutoScrollTimer !== undefined) return;
|
|
3605
|
+
this.#mouseAutoScrollTimer = setInterval(() => {
|
|
3606
|
+
const held = this.#mouseDragPointer;
|
|
3607
|
+
const step = held && this.#mouseButtonDown ? this.#mouseAutoScrollDirection(held) : 0;
|
|
3608
|
+
const before = this.#transcriptScreenRows();
|
|
3609
|
+
const firstLine = before === null ? null : this.#committedTranscriptRows[before.first];
|
|
3610
|
+
if (held === null || step === 0 || !this.scrollViewportBy(step, { pin: "stable" })) {
|
|
3611
|
+
this.#stopMouseAutoScroll();
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
// A boundary (live bottom, history top) reports success without moving.
|
|
3615
|
+
const after = this.#transcriptScreenRows();
|
|
3616
|
+
if (after === null || this.#committedTranscriptRows[after.first] === firstLine) {
|
|
3617
|
+
this.#stopMouseAutoScroll();
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
this.#extendMouseSelectionAt(held);
|
|
3621
|
+
this.requestRender(false, "mouse");
|
|
3622
|
+
}, MOUSE_AUTO_SCROLL_INTERVAL_MS);
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
#stopMouseAutoScroll(): void {
|
|
3626
|
+
if (this.#mouseAutoScrollTimer === undefined) return;
|
|
3627
|
+
clearInterval(this.#mouseAutoScrollTimer);
|
|
3628
|
+
this.#mouseAutoScrollTimer = undefined;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3431
3631
|
/**
|
|
3432
3632
|
* Resolve the selection granularity for this press. SGR mouse reports carry no
|
|
3433
3633
|
* click counter, so repeats are inferred from the previous press: same cell and
|
|
@@ -3523,6 +3723,8 @@ export class TUI extends Container {
|
|
|
3523
3723
|
const mode = this.#nextClickMode(point, pressAt);
|
|
3524
3724
|
this.#mouseButtonDown = true;
|
|
3525
3725
|
this.#mouseGestureDragged = false;
|
|
3726
|
+
this.#mousePressRow = mouse.y;
|
|
3727
|
+
this.#mouseDragLeftPressRow = false;
|
|
3526
3728
|
this.#mousePressPoint = point;
|
|
3527
3729
|
this.#mousePressAt = pressAt;
|
|
3528
3730
|
this.#mouseSelectionMode = mode;
|
|
@@ -3539,17 +3741,8 @@ export class TUI extends Container {
|
|
|
3539
3741
|
this.#resetClickCount();
|
|
3540
3742
|
return;
|
|
3541
3743
|
}
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
const point = this.#mouseSelectionPoint(mouse);
|
|
3545
|
-
this.#mouseGestureDragged = true;
|
|
3546
|
-
if (point === null) return;
|
|
3547
|
-
const forward = point.line > anchor.line || (point.line === anchor.line && point.column >= anchor.column);
|
|
3548
|
-
// The anchor span stays whole while the far end grows, so dragging after a
|
|
3549
|
-
// double click extends word by word instead of collapsing back to one cell.
|
|
3550
|
-
this.#mouseSelectionStart = this.#snapSelectionPoint(anchor, forward ? "start" : "end");
|
|
3551
|
-
this.#mouseSelectionEnd = this.#snapSelectionPoint(point, forward ? "end" : "start");
|
|
3552
|
-
this.#mouseSelectionActive = true;
|
|
3744
|
+
this.#extendMouseSelectionAt(mouse);
|
|
3745
|
+
this.#updateMouseAutoScroll();
|
|
3553
3746
|
}
|
|
3554
3747
|
|
|
3555
3748
|
#finishMouseSelection(mouse: MouseEvent): void {
|
|
@@ -3558,7 +3751,10 @@ export class TUI extends Container {
|
|
|
3558
3751
|
return;
|
|
3559
3752
|
}
|
|
3560
3753
|
this.#mouseButtonDown = false;
|
|
3561
|
-
|
|
3754
|
+
this.#stopMouseAutoScroll();
|
|
3755
|
+
this.#mouseDragPointer = null;
|
|
3756
|
+
const dragged = this.#mouseGestureDragged;
|
|
3757
|
+
const point = dragged ? this.#mouseDragPoint(mouse) : this.#mouseSelectionPoint(mouse);
|
|
3562
3758
|
const pressPoint = this.#mousePressPoint;
|
|
3563
3759
|
const pressAt = this.#mousePressAt;
|
|
3564
3760
|
const completedClick =
|
|
@@ -3587,7 +3783,9 @@ export class TUI extends Container {
|
|
|
3587
3783
|
this.#clearMouseSelection();
|
|
3588
3784
|
return;
|
|
3589
3785
|
}
|
|
3590
|
-
|
|
3786
|
+
// Source-aware rows return original Markdown, which may carry ESC/C0 bytes from
|
|
3787
|
+
// model or tool text; sanitize at the clipboard boundary (\t and \n survive).
|
|
3788
|
+
const text = stripTerminalControls(this.#extractMouseSelection());
|
|
3591
3789
|
if (!text) {
|
|
3592
3790
|
this.#clearMouseSelection();
|
|
3593
3791
|
return;
|
|
@@ -3601,6 +3799,8 @@ export class TUI extends Container {
|
|
|
3601
3799
|
}
|
|
3602
3800
|
|
|
3603
3801
|
#clearMouseSelection(): void {
|
|
3802
|
+
this.#stopMouseAutoScroll();
|
|
3803
|
+
this.#mouseDragPointer = null;
|
|
3604
3804
|
this.#mouseSelectionStart = null;
|
|
3605
3805
|
this.#mouseSelectionEnd = null;
|
|
3606
3806
|
this.#mouseSelectionAnchor = null;
|
|
@@ -3638,23 +3838,182 @@ export class TUI extends Container {
|
|
|
3638
3838
|
#extractMouseSelection(): string {
|
|
3639
3839
|
const selection = this.#orderedMouseSelection();
|
|
3640
3840
|
if (selection === null) return "";
|
|
3641
|
-
|
|
3841
|
+
type SelectedRow = {
|
|
3842
|
+
line: number;
|
|
3843
|
+
fragment: string;
|
|
3844
|
+
sourceId?: string;
|
|
3845
|
+
annotation?: CopyRowAnnotation;
|
|
3846
|
+
covered?: boolean;
|
|
3847
|
+
atContentStart?: boolean;
|
|
3848
|
+
/** Selected text per table cell; empty where the selection misses the cell. */
|
|
3849
|
+
cells?: string[];
|
|
3850
|
+
/** Folded into the row above as the soft-wrapped tail of its cells. */
|
|
3851
|
+
merged?: boolean;
|
|
3852
|
+
};
|
|
3853
|
+
const selected: SelectedRow[] = [];
|
|
3642
3854
|
const selectionLines = this.#selectionSourceLines();
|
|
3643
3855
|
for (let lineIndex = selection.start.line; lineIndex <= selection.end.line; lineIndex++) {
|
|
3644
3856
|
const line = selectionLines[lineIndex];
|
|
3645
3857
|
if (line === undefined || TERMINAL.isImageLine(line)) {
|
|
3646
|
-
selected.push("");
|
|
3858
|
+
selected.push({ line: lineIndex, fragment: "" });
|
|
3647
3859
|
continue;
|
|
3648
3860
|
}
|
|
3649
|
-
|
|
3861
|
+
// Read renderer metadata before stripping APC/ANSI controls. The metadata is
|
|
3862
|
+
// attached to this physical row, so identical text in another component can
|
|
3863
|
+
// never be mistaken for this selection.
|
|
3864
|
+
const copy = extractCopyRowAnnotation(line);
|
|
3865
|
+
const rendered = copy?.line ?? line;
|
|
3866
|
+
const plain = stripTerminalControls(rendered);
|
|
3650
3867
|
const columns = this.#mouseSelectionColumns(lineIndex, plain);
|
|
3651
3868
|
if (columns === null || columns.end <= columns.start) {
|
|
3652
|
-
selected.push("");
|
|
3869
|
+
selected.push({ line: lineIndex, fragment: "" });
|
|
3653
3870
|
continue;
|
|
3654
3871
|
}
|
|
3655
|
-
|
|
3872
|
+
const candidate = copy?.annotation;
|
|
3873
|
+
const annotation =
|
|
3874
|
+
candidate && copy.originColumn + candidate.contentEnd <= visibleWidth(plain) ? candidate : undefined;
|
|
3875
|
+
const localColumns = copy
|
|
3876
|
+
? { start: columns.start - copy.originColumn, end: columns.end - copy.originColumn }
|
|
3877
|
+
: columns;
|
|
3878
|
+
const presentationStart = annotation ? annotation.contentStart + annotation.prefixColumns : localColumns.start;
|
|
3879
|
+
const start = annotation ? Math.max(localColumns.start, presentationStart) : localColumns.start;
|
|
3880
|
+
const end = annotation ? Math.min(localColumns.end, annotation.contentEnd) : localColumns.end;
|
|
3881
|
+
let fragment = end > start ? sliceByColumn(plain, (copy?.originColumn ?? 0) + start, end - start, false) : "";
|
|
3882
|
+
// Cell padding is presentation; Markdown cell text never keeps outer spaces.
|
|
3883
|
+
const cells = annotation?.ranges?.map(([rangeStart, rangeEnd]) => {
|
|
3884
|
+
const overlapStart = Math.max(start, rangeStart);
|
|
3885
|
+
const overlapEnd = Math.min(end, rangeEnd);
|
|
3886
|
+
return overlapEnd > overlapStart
|
|
3887
|
+
? sliceByColumn(plain, (copy?.originColumn ?? 0) + overlapStart, overlapEnd - overlapStart, false).trim()
|
|
3888
|
+
: "";
|
|
3889
|
+
});
|
|
3890
|
+
if (cells) fragment = cells.filter(cell => cell.length > 0).join("\t");
|
|
3891
|
+
if (annotation?.fence) fragment = "";
|
|
3892
|
+
const covered = annotation
|
|
3893
|
+
? localColumns.start <= annotation.contentStart && localColumns.end >= annotation.contentEnd
|
|
3894
|
+
: false;
|
|
3895
|
+
// A fully covered rendered code row, nested or not, restores its row-local
|
|
3896
|
+
// original slice (not the whole logical line), which preserves an original tab.
|
|
3897
|
+
if (covered && annotation?.originalCode) fragment = annotation.source;
|
|
3898
|
+
selected.push({
|
|
3899
|
+
line: lineIndex,
|
|
3900
|
+
fragment,
|
|
3901
|
+
sourceId: copy?.sourceId,
|
|
3902
|
+
annotation,
|
|
3903
|
+
covered,
|
|
3904
|
+
atContentStart: annotation ? localColumns.start <= presentationStart : true,
|
|
3905
|
+
...(cells ? { cells } : {}),
|
|
3906
|
+
});
|
|
3656
3907
|
}
|
|
3657
|
-
|
|
3908
|
+
// Nothing but decoration (quote margins, table borders, fences) was selected.
|
|
3909
|
+
if (selected.every(row => row.fragment.length === 0)) return "";
|
|
3910
|
+
|
|
3911
|
+
const groups = new Map<string, { rows: SelectedRow[]; complete: boolean }>();
|
|
3912
|
+
for (const row of selected) {
|
|
3913
|
+
if (!row.annotation || !row.sourceId) continue;
|
|
3914
|
+
const key = `${row.sourceId}:${row.annotation.token}`;
|
|
3915
|
+
const group = groups.get(key) ?? { rows: [], complete: false };
|
|
3916
|
+
group.rows.push(row);
|
|
3917
|
+
groups.set(key, group);
|
|
3918
|
+
}
|
|
3919
|
+
// A token is complete when every selected row is covered, the selection holds
|
|
3920
|
+
// every rendered row of the token in order (so a preview that clipped the token's
|
|
3921
|
+
// head or tail cannot restore hidden rows), the last of them carries the
|
|
3922
|
+
// whole-token source, and the rows just outside the group belong to something else.
|
|
3923
|
+
const rowKey = (lineIndex: number): string | undefined => {
|
|
3924
|
+
const line = selectionLines[lineIndex];
|
|
3925
|
+
if (line === undefined || TERMINAL.isImageLine(line)) return undefined;
|
|
3926
|
+
const copy = extractCopyRowAnnotation(line);
|
|
3927
|
+
return copy ? `${copy.sourceId}:${copy.annotation.token}` : undefined;
|
|
3928
|
+
};
|
|
3929
|
+
for (const [key, group] of groups) {
|
|
3930
|
+
const first = group.rows[0]!;
|
|
3931
|
+
const last = group.rows[group.rows.length - 1]!;
|
|
3932
|
+
group.complete =
|
|
3933
|
+
group.rows.every(row => row.covered) &&
|
|
3934
|
+
group.rows.every((row, index) => row.annotation?.tokenRow === index) &&
|
|
3935
|
+
last.annotation?.tokenRows === group.rows.length &&
|
|
3936
|
+
last.annotation.tokenSource !== undefined &&
|
|
3937
|
+
last.line - first.line + 1 === group.rows.length &&
|
|
3938
|
+
rowKey(first.line - 1) !== key &&
|
|
3939
|
+
rowKey(last.line + 1) !== key;
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
const allSemanticRowsAreComplete = selected.every(row => {
|
|
3943
|
+
if (!row.annotation || !row.sourceId) return row.fragment.length === 0;
|
|
3944
|
+
return groups.get(`${row.sourceId}:${row.annotation.token}`)?.complete === true;
|
|
3945
|
+
});
|
|
3946
|
+
if (allSemanticRowsAreComplete) {
|
|
3947
|
+
const completed = [...groups.values()];
|
|
3948
|
+
const codeOnly = completed.length === 1 && completed[0]?.rows[0]?.annotation?.kind === "code";
|
|
3949
|
+
if (codeOnly) {
|
|
3950
|
+
const rows = completed[0]!.rows;
|
|
3951
|
+
const raw = rows[rows.length - 1]?.annotation?.tokenSource ?? "";
|
|
3952
|
+
// Token raw keeps the block's trailing newline; drop it so the closing fence is the last line.
|
|
3953
|
+
const lines = raw.replace(/\n+$/u, "").split("\n");
|
|
3954
|
+
if (/^\s*(```|~~~)/u.test(lines[0] ?? "")) {
|
|
3955
|
+
lines.shift();
|
|
3956
|
+
if (/^\s*(```|~~~)/u.test(lines[lines.length - 1] ?? "")) lines.pop();
|
|
3957
|
+
}
|
|
3958
|
+
return lines.join("\n");
|
|
3959
|
+
}
|
|
3960
|
+
let result = "";
|
|
3961
|
+
for (const row of selected) {
|
|
3962
|
+
// Each complete token contributes once, from its final row.
|
|
3963
|
+
if (row.annotation?.tokenSource !== undefined) result += row.annotation.tokenSource;
|
|
3964
|
+
}
|
|
3965
|
+
return result;
|
|
3966
|
+
}
|
|
3967
|
+
// A table row that soft-wraps its cells is one source row: fold each wrapped
|
|
3968
|
+
// row's cells into the row it continues, rejoined with the removed source gap.
|
|
3969
|
+
let head: SelectedRow | undefined;
|
|
3970
|
+
let headEnd = -1;
|
|
3971
|
+
for (const row of selected) {
|
|
3972
|
+
const gaps = row.annotation?.cellGaps;
|
|
3973
|
+
const headCells = head?.cells;
|
|
3974
|
+
if (
|
|
3975
|
+
head &&
|
|
3976
|
+
headCells &&
|
|
3977
|
+
row.cells &&
|
|
3978
|
+
gaps &&
|
|
3979
|
+
head.sourceId === row.sourceId &&
|
|
3980
|
+
head.annotation?.token === row.annotation?.token &&
|
|
3981
|
+
headEnd + 1 === row.line
|
|
3982
|
+
) {
|
|
3983
|
+
for (const [cell, text] of row.cells.entries()) {
|
|
3984
|
+
if (!text) continue;
|
|
3985
|
+
headCells[cell] = headCells[cell] ? `${headCells[cell]}${gaps[cell] ?? ""}${text}` : text;
|
|
3986
|
+
}
|
|
3987
|
+
head.fragment = headCells.filter(cell => cell.length > 0).join("\t");
|
|
3988
|
+
row.merged = true;
|
|
3989
|
+
headEnd = row.line;
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
head = row.cells ? row : undefined;
|
|
3993
|
+
headEnd = row.line;
|
|
3994
|
+
}
|
|
3995
|
+
let result = "";
|
|
3996
|
+
let previous: SelectedRow | undefined;
|
|
3997
|
+
for (const row of selected) {
|
|
3998
|
+
const annotation = row.annotation;
|
|
3999
|
+
if (row.merged || annotation?.fence || annotation?.ranges?.length === 0) continue;
|
|
4000
|
+
if (previous) {
|
|
4001
|
+
if (
|
|
4002
|
+
annotation?.continuation &&
|
|
4003
|
+
previous.annotation?.token === annotation.token &&
|
|
4004
|
+
previous.sourceId === row.sourceId
|
|
4005
|
+
) {
|
|
4006
|
+
result += annotation.joinGap;
|
|
4007
|
+
} else {
|
|
4008
|
+
result += "\n";
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
if (annotation?.quoteDepth && !annotation.continuation && row.atContentStart)
|
|
4012
|
+
result += "> ".repeat(annotation.quoteDepth);
|
|
4013
|
+
result += row.fragment;
|
|
4014
|
+
previous = row;
|
|
4015
|
+
}
|
|
4016
|
+
return result;
|
|
3658
4017
|
}
|
|
3659
4018
|
|
|
3660
4019
|
#applyMouseSelection(lines: string[]): string[] {
|
|
@@ -3665,17 +4024,28 @@ export class TUI extends Container {
|
|
|
3665
4024
|
for (let lineIndex = selection.start.line; lineIndex <= selection.end.line; lineIndex++) {
|
|
3666
4025
|
const line = highlighted[lineIndex];
|
|
3667
4026
|
if (line === undefined || TERMINAL.isImageLine(line)) continue;
|
|
3668
|
-
|
|
4027
|
+
// Detach copy APCs verbatim (no payload decoding on this per-frame path) and
|
|
4028
|
+
// put them back around the painted row so a later copy still reads them.
|
|
4029
|
+
const {
|
|
4030
|
+
line: rendered,
|
|
4031
|
+
origin: originMarker,
|
|
4032
|
+
originColumn,
|
|
4033
|
+
rows: annotationSuffix,
|
|
4034
|
+
} = splitCopyAnnotations(line);
|
|
4035
|
+
const plain = stripTerminalControls(rendered);
|
|
3669
4036
|
const width = visibleWidth(plain);
|
|
3670
4037
|
const columns = this.#mouseSelectionColumns(lineIndex, plain);
|
|
3671
4038
|
if (columns === null || columns.end <= columns.start) continue;
|
|
3672
|
-
const before = sliceByColumn(
|
|
3673
|
-
const selected = sliceByColumn(
|
|
4039
|
+
const before = columns.start > 0 ? sliceByColumn(rendered, 0, columns.start, false) : "";
|
|
4040
|
+
const selected = sliceByColumn(rendered, columns.start, columns.end - columns.start, false).replace(
|
|
3674
4041
|
/\x1b\[[0-9;]*m/gu,
|
|
3675
4042
|
control => `${control}\x1b[7m`,
|
|
3676
4043
|
);
|
|
3677
|
-
const after = sliceByColumn(
|
|
3678
|
-
|
|
4044
|
+
const after = columns.end < width ? sliceByColumn(rendered, columns.end, width - columns.end, false) : "";
|
|
4045
|
+
const painted = `${before}\x1b[7m${selected}\x1b[27m${after}`;
|
|
4046
|
+
const head = originColumn > 0 ? sliceByColumn(painted, 0, originColumn, false) : "";
|
|
4047
|
+
const tail = originColumn < width ? sliceByColumn(painted, originColumn, width - originColumn, false) : "";
|
|
4048
|
+
highlighted[lineIndex] = `${head}${originMarker}${tail}${annotationSuffix}`;
|
|
3679
4049
|
}
|
|
3680
4050
|
return highlighted;
|
|
3681
4051
|
}
|
|
@@ -4334,14 +4704,14 @@ export class TUI extends Container {
|
|
|
4334
4704
|
|
|
4335
4705
|
#padBeforeBottomPinnedComponent(
|
|
4336
4706
|
lines: string[],
|
|
4337
|
-
|
|
4707
|
+
minimumLineCount: number,
|
|
4338
4708
|
pinnedLineCount: number,
|
|
4339
4709
|
): { lines: string[]; insertionRow: number; insertedBlankRows: number } {
|
|
4340
|
-
if (pinnedLineCount <= 0 || lines.length >=
|
|
4710
|
+
if (pinnedLineCount <= 0 || lines.length >= minimumLineCount) {
|
|
4341
4711
|
return { lines, insertionRow: lines.length, insertedBlankRows: 0 };
|
|
4342
4712
|
}
|
|
4343
4713
|
|
|
4344
|
-
const insertedBlankRows =
|
|
4714
|
+
const insertedBlankRows = minimumLineCount - lines.length;
|
|
4345
4715
|
const insertionRow = Math.max(0, lines.length - pinnedLineCount);
|
|
4346
4716
|
const padded = [...lines];
|
|
4347
4717
|
padded.splice(insertionRow, 0, ...Array.from({ length: insertedBlankRows }, () => ""));
|
|
@@ -4412,12 +4782,16 @@ export class TUI extends Container {
|
|
|
4412
4782
|
paintLive = false,
|
|
4413
4783
|
placementsToClear: KittyPlacementSpan[] = this.#kittyPlacementSpans,
|
|
4414
4784
|
placementsToPaint: KittyPlacementSpan[] = placementsToClear,
|
|
4415
|
-
geometry?: { transcriptLineCount: number; suffixLineCount: number },
|
|
4785
|
+
geometry?: { transcriptLineCount: number; suffixLineCount: number; frontierSpacerLineCount?: number },
|
|
4416
4786
|
avoidScrollback = true,
|
|
4417
4787
|
): boolean {
|
|
4418
4788
|
const paintManual = this.#manualViewportTop !== undefined && !paintLive;
|
|
4419
4789
|
const transcriptLineCount = geometry?.transcriptLineCount ?? this.#manualTranscriptLineCount;
|
|
4420
4790
|
const suffixLineCount = geometry?.suffixLineCount ?? this.#manualSuffixLineCount;
|
|
4791
|
+
const frontierSpacerLineCount =
|
|
4792
|
+
geometry?.frontierSpacerLineCount ??
|
|
4793
|
+
(paintManual ? this.#manualFrontierSpacerLineCount : this.#latestRenderedFrontierSpacerLineCount);
|
|
4794
|
+
const suffixStart = transcriptLineCount + frontierSpacerLineCount;
|
|
4421
4795
|
if (height <= 0 || width <= 0) return false;
|
|
4422
4796
|
const maxViewportTop = Math.max(
|
|
4423
4797
|
0,
|
|
@@ -4442,7 +4816,7 @@ export class TUI extends Container {
|
|
|
4442
4816
|
const emittedRegions: KittyPlacementRegion[] = paintManual
|
|
4443
4817
|
? [
|
|
4444
4818
|
{ top: nextViewportTop, bottom: nextViewportTop + transcriptCapacity },
|
|
4445
|
-
{ top:
|
|
4819
|
+
{ top: suffixStart, bottom: suffixStart + suffixLineCount },
|
|
4446
4820
|
]
|
|
4447
4821
|
: [{ top: nextViewportTop, bottom: nextViewportTop + height }];
|
|
4448
4822
|
const lineForScreenRow = (screenRow: number): string => {
|
|
@@ -4451,7 +4825,7 @@ export class TUI extends Container {
|
|
|
4451
4825
|
return paintManual && screenRow === transcriptCapacity && noticeRows > 0
|
|
4452
4826
|
? "New output — type to follow"
|
|
4453
4827
|
: paintManual && suffixRow >= 0
|
|
4454
|
-
? (lines[
|
|
4828
|
+
? (lines[suffixStart + suffixRow] ?? "")
|
|
4455
4829
|
: paintManual && lineIndex >= transcriptLineCount
|
|
4456
4830
|
? ""
|
|
4457
4831
|
: (lines[lineIndex] ?? "");
|
|
@@ -4542,6 +4916,9 @@ export class TUI extends Container {
|
|
|
4542
4916
|
}
|
|
4543
4917
|
#recordPaintedViewportObservation(viewportTop: number, height: number, paintManual: boolean): void {
|
|
4544
4918
|
const transcriptCapacity = this.#manualTranscriptCapacity(height);
|
|
4919
|
+
const transcriptLineCount = this.#manualTranscriptLineCount;
|
|
4920
|
+
const suffixStart = transcriptLineCount + this.#manualFrontierSpacerLineCount;
|
|
4921
|
+
const noticeRows = this.#manualOutputNotice && height > this.#manualSuffixLineCount ? 1 : 0;
|
|
4545
4922
|
const anchorFrame = this.#viewportAnchorFrame;
|
|
4546
4923
|
const semanticAnchor =
|
|
4547
4924
|
anchorFrame === null
|
|
@@ -4556,9 +4933,10 @@ export class TUI extends Container {
|
|
|
4556
4933
|
const cursor = this.#lastCursorPosition;
|
|
4557
4934
|
let cursorRow: number | null = null;
|
|
4558
4935
|
if (cursor !== null) {
|
|
4559
|
-
if (paintManual && cursor.row >=
|
|
4560
|
-
|
|
4561
|
-
|
|
4936
|
+
if (paintManual && cursor.row >= suffixStart) {
|
|
4937
|
+
cursorRow = transcriptCapacity + noticeRows + (cursor.row - suffixStart);
|
|
4938
|
+
} else if (paintManual && cursor.row >= transcriptLineCount) {
|
|
4939
|
+
cursorRow = null;
|
|
4562
4940
|
} else if (paintManual) {
|
|
4563
4941
|
cursorRow = this.#committedTranscriptRows.indexOf(cursor.row);
|
|
4564
4942
|
} else {
|
|
@@ -4695,6 +5073,10 @@ export class TUI extends Container {
|
|
|
4695
5073
|
const widthSettleRenderQueued = this.#widthSettleRenderQueued;
|
|
4696
5074
|
const tabWidthRepairPending = this.#tabWidthRepairPending;
|
|
4697
5075
|
const forcedRenderQueued = this.#forcedRenderQueued;
|
|
5076
|
+
const frontierPreviousWidth = this.#previousWidth < 0 ? this.#forcedRenderPreviousWidth : this.#previousWidth;
|
|
5077
|
+
const frontierPreviousHeight = this.#previousHeight < 0 ? this.#forcedRenderPreviousHeight : this.#previousHeight;
|
|
5078
|
+
const transcriptRebuildPending = this.#transcriptRebuildPending;
|
|
5079
|
+
this.#transcriptRebuildPending = false;
|
|
4698
5080
|
this.#resizeRenderQueued = false;
|
|
4699
5081
|
this.#resizeRenderMutationQueued = false;
|
|
4700
5082
|
this.#renderMutationQueued = false;
|
|
@@ -4730,6 +5112,8 @@ export class TUI extends Container {
|
|
|
4730
5112
|
this.#manualViewportTop === undefined &&
|
|
4731
5113
|
this.#latestRaw.length > 0 &&
|
|
4732
5114
|
this.#latestRenderedLines.length === this.#latestRaw.length &&
|
|
5115
|
+
// A cached spacer count is not valid after natural rows replace the transient padding.
|
|
5116
|
+
this.#latestRenderedFrontierSpacerLineCount === 0 &&
|
|
4733
5117
|
this.#previousLines === this.#latestRenderedLines;
|
|
4734
5118
|
let previousKittyPlacementSpans = this.#kittyPlacementSpans;
|
|
4735
5119
|
const placementOwners = new Map<string, KittyPlacementOwner>();
|
|
@@ -4843,6 +5227,7 @@ export class TUI extends Container {
|
|
|
4843
5227
|
let diffStart = 0;
|
|
4844
5228
|
let usedWindowNormalize = false;
|
|
4845
5229
|
let stitched = false;
|
|
5230
|
+
let frontierSpacerLineCount = 0;
|
|
4846
5231
|
if (reusedAnchor !== null && layoutPrefixEligible && !layoutPrefixBlocked) {
|
|
4847
5232
|
const reused = this.#reuseCachedLayoutPrefix(
|
|
4848
5233
|
width,
|
|
@@ -4859,6 +5244,7 @@ export class TUI extends Container {
|
|
|
4859
5244
|
cursorPos = reused.cursorPos;
|
|
4860
5245
|
diffStart = reused.diffStart;
|
|
4861
5246
|
usedWindowNormalize = true;
|
|
5247
|
+
frontierSpacerLineCount = this.#latestRenderedFrontierSpacerLineCount;
|
|
4862
5248
|
}
|
|
4863
5249
|
}
|
|
4864
5250
|
if (!stitched) {
|
|
@@ -4880,11 +5266,21 @@ export class TUI extends Container {
|
|
|
4880
5266
|
}
|
|
4881
5267
|
newLines = this.#constrainPinnedSuffix(renderedLines, height, renderedChildren);
|
|
4882
5268
|
if (hasStickySuffix && height > 0 && this.#manualViewportTop === undefined) {
|
|
4883
|
-
|
|
5269
|
+
// Rebuilds start at their new transcript frontier. Forced redraws use the
|
|
5270
|
+
// captured rendered dimensions rather than their -1 invalidation sentinels.
|
|
5271
|
+
const preserveLiveFrontier =
|
|
5272
|
+
!transcriptIdentityReplaced &&
|
|
5273
|
+
!transcriptRebuildPending &&
|
|
5274
|
+
frontierPreviousWidth === width &&
|
|
5275
|
+
(frontierPreviousHeight === height || this.#viewportRepaintHost());
|
|
5276
|
+
const minimumLineCount = preserveLiveFrontier ? prevViewportTop + height : height;
|
|
5277
|
+
const padded = this.#padBeforeBottomPinnedComponent(
|
|
4884
5278
|
newLines,
|
|
4885
|
-
|
|
5279
|
+
minimumLineCount,
|
|
4886
5280
|
newLines.length - sourceTranscriptLineCount,
|
|
4887
|
-
)
|
|
5281
|
+
);
|
|
5282
|
+
newLines = padded.lines;
|
|
5283
|
+
frontierSpacerLineCount = padded.insertedBlankRows;
|
|
4888
5284
|
}
|
|
4889
5285
|
// Composite overlays into the rendered lines (before differential compare)
|
|
4890
5286
|
if (this.overlayStack.length > 0) {
|
|
@@ -4941,7 +5337,15 @@ export class TUI extends Container {
|
|
|
4941
5337
|
}
|
|
4942
5338
|
}
|
|
4943
5339
|
const nextTranscriptLineCount = sourceTranscriptLineCount;
|
|
4944
|
-
|
|
5340
|
+
// Live-only frontier rows must not consume the manual viewport's pinned-suffix capacity.
|
|
5341
|
+
const nextSuffixLineCount = hasStickySuffix
|
|
5342
|
+
? Math.max(0, newLines.length - nextTranscriptLineCount - frontierSpacerLineCount)
|
|
5343
|
+
: 0;
|
|
5344
|
+
const nextFrameGeometry = {
|
|
5345
|
+
transcriptLineCount: nextTranscriptLineCount,
|
|
5346
|
+
suffixLineCount: nextSuffixLineCount,
|
|
5347
|
+
frontierSpacerLineCount,
|
|
5348
|
+
};
|
|
4945
5349
|
this.#lastCursorPosition = cursorPos;
|
|
4946
5350
|
const total = rawLines.length;
|
|
4947
5351
|
if (renderMetrics.enabled) {
|
|
@@ -4958,6 +5362,7 @@ export class TUI extends Container {
|
|
|
4958
5362
|
this.#latestRenderedLines = newLines;
|
|
4959
5363
|
this.#latestRenderedTranscriptLineCount = nextTranscriptLineCount;
|
|
4960
5364
|
this.#latestRenderedSuffixLineCount = nextSuffixLineCount;
|
|
5365
|
+
this.#latestRenderedFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
4961
5366
|
this.#latestRenderedPlacementOwners = placementOwners;
|
|
4962
5367
|
const naturalViewportTop = Math.max(0, newLines.length - height);
|
|
4963
5368
|
const priorLogicalLineCount = Math.max(this.#previousLines.length, this.#maxLinesRendered);
|
|
@@ -5018,11 +5423,12 @@ export class TUI extends Container {
|
|
|
5018
5423
|
this.#previousHeight = height;
|
|
5019
5424
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5020
5425
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5426
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5021
5427
|
},
|
|
5022
5428
|
false,
|
|
5023
5429
|
previousKittyPlacementSpans,
|
|
5024
5430
|
nextKittyPlacementSpans,
|
|
5025
|
-
|
|
5431
|
+
nextFrameGeometry,
|
|
5026
5432
|
true,
|
|
5027
5433
|
);
|
|
5028
5434
|
if (contentPainted) {
|
|
@@ -5074,6 +5480,9 @@ export class TUI extends Container {
|
|
|
5074
5480
|
newLines.length === this.#previousLines.length &&
|
|
5075
5481
|
newLines.every((line, index) => line === this.#previousLines[index])
|
|
5076
5482
|
) {
|
|
5483
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5484
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5485
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5077
5486
|
return;
|
|
5078
5487
|
}
|
|
5079
5488
|
this.#manualViewportTop = nextViewportTop;
|
|
@@ -5095,11 +5504,12 @@ export class TUI extends Container {
|
|
|
5095
5504
|
this.#paintedManualOutputNotice = this.#manualOutputNotice;
|
|
5096
5505
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5097
5506
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5507
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5098
5508
|
},
|
|
5099
5509
|
false,
|
|
5100
5510
|
previousKittyPlacementSpans,
|
|
5101
5511
|
nextKittyPlacementSpans,
|
|
5102
|
-
|
|
5512
|
+
nextFrameGeometry,
|
|
5103
5513
|
);
|
|
5104
5514
|
if (!contentPainted) restoreManualIntent();
|
|
5105
5515
|
return;
|
|
@@ -5171,6 +5581,7 @@ export class TUI extends Container {
|
|
|
5171
5581
|
);
|
|
5172
5582
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5173
5583
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5584
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5174
5585
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5175
5586
|
this.#durableLineCount = newLines.length;
|
|
5176
5587
|
this.#durableRenderedLines = newLines.slice();
|
|
@@ -5209,6 +5620,7 @@ export class TUI extends Container {
|
|
|
5209
5620
|
this.#previousHeight = height;
|
|
5210
5621
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5211
5622
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5623
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5212
5624
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5213
5625
|
this.#latestRenderedLines = newLines.slice();
|
|
5214
5626
|
if (this.#virtualViewport) this.#latestRaw = rawLines.slice();
|
|
@@ -5216,7 +5628,7 @@ export class TUI extends Container {
|
|
|
5216
5628
|
false,
|
|
5217
5629
|
previousKittyPlacementSpans,
|
|
5218
5630
|
nextKittyPlacementSpans,
|
|
5219
|
-
|
|
5631
|
+
nextFrameGeometry,
|
|
5220
5632
|
true,
|
|
5221
5633
|
);
|
|
5222
5634
|
};
|
|
@@ -5638,6 +6050,7 @@ export class TUI extends Container {
|
|
|
5638
6050
|
);
|
|
5639
6051
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5640
6052
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
6053
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5641
6054
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5642
6055
|
})
|
|
5643
6056
|
)
|
|
@@ -5655,6 +6068,7 @@ export class TUI extends Container {
|
|
|
5655
6068
|
this.#viewportTopRow = Math.max(0, newLines.length - height);
|
|
5656
6069
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5657
6070
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
6071
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5658
6072
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5659
6073
|
return;
|
|
5660
6074
|
}
|
|
@@ -5958,6 +6372,7 @@ export class TUI extends Container {
|
|
|
5958
6372
|
);
|
|
5959
6373
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5960
6374
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
6375
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5961
6376
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5962
6377
|
},
|
|
5963
6378
|
preserveRasterLeases,
|