@gajae-code/tui 0.18.8 → 0.18.10
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 +19 -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 +503 -72
- 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;
|
|
@@ -1138,6 +1152,7 @@ export class TUI extends Container {
|
|
|
1138
1152
|
#fullRedrawCount = 0;
|
|
1139
1153
|
#stopped = false;
|
|
1140
1154
|
#terminalUnavailable = false;
|
|
1155
|
+
#restartNeedsClear = false; // Set when stop() resets viewport state
|
|
1141
1156
|
#bottomPinnedComponent: Component | null = null;
|
|
1142
1157
|
#pendingTerminalCleanup: Array<{ payload: string; onDelivered?: () => void }> = [];
|
|
1143
1158
|
#mouseSelectionStart: MouseSelectionPoint | null = null;
|
|
@@ -1151,6 +1166,14 @@ export class TUI extends Container {
|
|
|
1151
1166
|
#mouseGestureDragged = false;
|
|
1152
1167
|
#mousePressPoint: MouseSelectionPoint | null = null;
|
|
1153
1168
|
#mousePressAt = 0;
|
|
1169
|
+
/** Last screen cell of a held drag; scrolling re-resolves it against the new viewport. */
|
|
1170
|
+
#mouseDragPointer: { x: number; y: number } | null = null;
|
|
1171
|
+
/** Edge scrolling waits until the drag leaves the press row, so a small drag on an edge row stays put. */
|
|
1172
|
+
#mousePressRow = 0;
|
|
1173
|
+
#mouseDragLeftPressRow = false;
|
|
1174
|
+
#mouseAutoScrollTimer?: NodeJS.Timeout;
|
|
1175
|
+
/** Held only while this TUI runs with mouse copying, so other render() consumers stay stock. */
|
|
1176
|
+
#releaseCopyAnnotations: (() => void) | undefined;
|
|
1154
1177
|
#lastClickPoint: MouseSelectionPoint | null = null;
|
|
1155
1178
|
#lastClickAt = 0;
|
|
1156
1179
|
#clickCount = 0;
|
|
@@ -1159,6 +1182,7 @@ export class TUI extends Container {
|
|
|
1159
1182
|
#manualOutputNotice = false;
|
|
1160
1183
|
#manualTranscriptLineCount = 0;
|
|
1161
1184
|
#manualSuffixLineCount = 0;
|
|
1185
|
+
#manualFrontierSpacerLineCount = 0;
|
|
1162
1186
|
#committedTranscriptRows: Array<number | null> = [];
|
|
1163
1187
|
#paintedManualOutputNotice = false;
|
|
1164
1188
|
#rasterGeneration = 0;
|
|
@@ -1330,6 +1354,9 @@ export class TUI extends Container {
|
|
|
1330
1354
|
override dispose(): void {
|
|
1331
1355
|
if (this.#preparationDisposed) return;
|
|
1332
1356
|
this.#preparationDisposed = true;
|
|
1357
|
+
this.#clearMouseSelection();
|
|
1358
|
+
this.#releaseCopyAnnotations?.();
|
|
1359
|
+
this.#releaseCopyAnnotations = undefined;
|
|
1333
1360
|
this.#renderRequested = false;
|
|
1334
1361
|
this.#renderRequestedGeneration = 0;
|
|
1335
1362
|
this.#inputRenderPending = false;
|
|
@@ -1406,6 +1433,11 @@ export class TUI extends Container {
|
|
|
1406
1433
|
this.#clearOnShrink = enabled;
|
|
1407
1434
|
}
|
|
1408
1435
|
|
|
1436
|
+
/** Overlays rely on padded rows, so contraction replays only without them. */
|
|
1437
|
+
#clearOnShrinkReplays(): boolean {
|
|
1438
|
+
return this.#clearOnShrink && this.overlayStack.length === 0;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1409
1441
|
setFocus(component: Component | null): void {
|
|
1410
1442
|
// Clear focused flag on old component
|
|
1411
1443
|
if (isFocusable(this.#focusedComponent)) {
|
|
@@ -1546,6 +1578,11 @@ export class TUI extends Container {
|
|
|
1546
1578
|
getViewportAnchorComponent(): Component | null {
|
|
1547
1579
|
return this.#viewportAnchorComponent;
|
|
1548
1580
|
}
|
|
1581
|
+
#captureManualResumeViewportTop(): void {
|
|
1582
|
+
if (this.#latestRenderedFrontierSpacerLineCount === 0) return;
|
|
1583
|
+
const committedFrontier = this.#scrollbackResumeViewportTop ?? this.#nativeScrollbackViewportTop;
|
|
1584
|
+
this.#manualResumeViewportTop = committedFrontier > 0 ? committedFrontier : undefined;
|
|
1585
|
+
}
|
|
1549
1586
|
|
|
1550
1587
|
/** Clear manual viewport ownership and durable history before replacing the transcript identity. */
|
|
1551
1588
|
resetViewportAnchorIntent(): void {
|
|
@@ -1557,6 +1594,7 @@ export class TUI extends Container {
|
|
|
1557
1594
|
this.#scrollbackResumeViewportTop = undefined;
|
|
1558
1595
|
this.#nativeScrollbackViewportTop = 0;
|
|
1559
1596
|
this.#nativeScrollbackAdmissionPending = false;
|
|
1597
|
+
this.#manualResumeViewportTop = undefined;
|
|
1560
1598
|
this.#transcriptIdentityResetPending = true;
|
|
1561
1599
|
this.#manualOutputNotice = false;
|
|
1562
1600
|
this.#paintedManualOutputNotice = false;
|
|
@@ -1579,6 +1617,8 @@ export class TUI extends Container {
|
|
|
1579
1617
|
|
|
1580
1618
|
/** Allow one semantic-neighbor reconciliation after a definitive same-transcript rebuild. */
|
|
1581
1619
|
prepareViewportAnchorForTranscriptRebuild(): void {
|
|
1620
|
+
this.#transcriptRebuildPending = true;
|
|
1621
|
+
this.#manualResumeViewportTop = undefined;
|
|
1582
1622
|
if (this.#manualViewportAnchor !== null) this.#reconcileMissingViewportAnchor = true;
|
|
1583
1623
|
}
|
|
1584
1624
|
|
|
@@ -1633,6 +1673,7 @@ export class TUI extends Container {
|
|
|
1633
1673
|
this.#suspendRasterLeasesForManualViewport(() => this.revealViewportAnchor(id, alignment))
|
|
1634
1674
|
)
|
|
1635
1675
|
return true;
|
|
1676
|
+
if (this.#manualViewportTop === undefined) this.#captureManualResumeViewportTop();
|
|
1636
1677
|
this.#manualViewportAnchor = {
|
|
1637
1678
|
id: selected.id,
|
|
1638
1679
|
graphemeIndex:
|
|
@@ -1684,6 +1725,7 @@ export class TUI extends Container {
|
|
|
1684
1725
|
const previousManualViewportAnchor = this.#manualViewportAnchor;
|
|
1685
1726
|
const previousManualViewportFallbackAnchors = this.#manualViewportFallbackAnchors;
|
|
1686
1727
|
const previousReconcileMissingViewportAnchor = this.#reconcileMissingViewportAnchor;
|
|
1728
|
+
const previousManualResumeViewportTop = this.#manualResumeViewportTop;
|
|
1687
1729
|
|
|
1688
1730
|
const direction: -1 | 1 = delta < 0 ? -1 : 1;
|
|
1689
1731
|
const pin = options?.pin ?? "stable";
|
|
@@ -1778,6 +1820,7 @@ export class TUI extends Container {
|
|
|
1778
1820
|
this.#manualViewportFallbackAnchors = fallbacks;
|
|
1779
1821
|
}
|
|
1780
1822
|
}
|
|
1823
|
+
if (previousManualViewportTop === undefined) this.#captureManualResumeViewportTop();
|
|
1781
1824
|
this.#manualViewportTop = targetViewportTop;
|
|
1782
1825
|
let contentPainted = false;
|
|
1783
1826
|
const painted = this.#repaintViewportFromLines(
|
|
@@ -1792,13 +1835,15 @@ export class TUI extends Container {
|
|
|
1792
1835
|
contentPainted = true;
|
|
1793
1836
|
this.#manualTranscriptLineCount = this.#latestRenderedTranscriptLineCount;
|
|
1794
1837
|
this.#manualSuffixLineCount = this.#latestRenderedSuffixLineCount;
|
|
1838
|
+
this.#manualFrontierSpacerLineCount = this.#latestRenderedFrontierSpacerLineCount;
|
|
1795
1839
|
},
|
|
1796
1840
|
false,
|
|
1797
1841
|
this.#kittyPlacementSpans,
|
|
1798
1842
|
this.#kittyPlacementSpansForLines(this.#previousLines, this.#latestRenderedPlacementOwners),
|
|
1799
1843
|
{
|
|
1800
|
-
transcriptLineCount: this.#
|
|
1801
|
-
suffixLineCount: this.#
|
|
1844
|
+
transcriptLineCount: this.#manualTranscriptLineCount,
|
|
1845
|
+
suffixLineCount: this.#manualSuffixLineCount,
|
|
1846
|
+
frontierSpacerLineCount: this.#manualFrontierSpacerLineCount,
|
|
1802
1847
|
},
|
|
1803
1848
|
true,
|
|
1804
1849
|
);
|
|
@@ -1807,6 +1852,7 @@ export class TUI extends Container {
|
|
|
1807
1852
|
this.#manualViewportAnchor = previousManualViewportAnchor;
|
|
1808
1853
|
this.#manualViewportFallbackAnchors = previousManualViewportFallbackAnchors;
|
|
1809
1854
|
this.#reconcileMissingViewportAnchor = previousReconcileMissingViewportAnchor;
|
|
1855
|
+
this.#manualResumeViewportTop = previousManualResumeViewportTop;
|
|
1810
1856
|
}
|
|
1811
1857
|
return painted;
|
|
1812
1858
|
}
|
|
@@ -1822,14 +1868,19 @@ export class TUI extends Container {
|
|
|
1822
1868
|
if (this.#manualViewportTop === undefined) return false;
|
|
1823
1869
|
const height = this.terminal.rows;
|
|
1824
1870
|
const width = this.terminal.columns;
|
|
1871
|
+
// Manual rendering can drop transient padding from the latest frame; restore the
|
|
1872
|
+
// frontier captured when manual history began before rebuilding the live viewport.
|
|
1873
|
+
const committedFrontier = this.#manualResumeViewportTop;
|
|
1825
1874
|
const paddedLiveLines = this.#padBeforeBottomPinnedComponent(
|
|
1826
1875
|
this.#latestRenderedLines,
|
|
1827
|
-
height,
|
|
1876
|
+
committedFrontier === undefined ? height : Math.max(height, committedFrontier + height),
|
|
1828
1877
|
this.#latestRenderedSuffixLineCount,
|
|
1829
1878
|
);
|
|
1830
1879
|
const liveLines = paddedLiveLines.lines;
|
|
1831
1880
|
const liveTranscriptLineCount = this.#latestRenderedTranscriptLineCount;
|
|
1832
|
-
const liveSuffixLineCount = this.#latestRenderedSuffixLineCount
|
|
1881
|
+
const liveSuffixLineCount = this.#latestRenderedSuffixLineCount;
|
|
1882
|
+
const liveFrontierSpacerLineCount =
|
|
1883
|
+
this.#latestRenderedFrontierSpacerLineCount + paddedLiveLines.insertedBlankRows;
|
|
1833
1884
|
const liveKittyPlacementSpans = this.#kittyPlacementSpansForLines(liveLines, this.#latestRenderedPlacementOwners);
|
|
1834
1885
|
let liveCursorPosition = this.#lastCursorPosition;
|
|
1835
1886
|
if (liveCursorPosition !== null && liveCursorPosition.row >= paddedLiveLines.insertionRow) {
|
|
@@ -1855,11 +1906,14 @@ export class TUI extends Container {
|
|
|
1855
1906
|
this.#manualOutputNotice = false;
|
|
1856
1907
|
this.#committedTranscriptRows = [];
|
|
1857
1908
|
this.#paintedManualOutputNotice = false;
|
|
1909
|
+
this.#manualResumeViewportTop = undefined;
|
|
1858
1910
|
this.#lastCursorPosition = liveCursorPosition;
|
|
1859
1911
|
this.#previousLines = liveLines;
|
|
1860
1912
|
this.#manualTranscriptLineCount = liveTranscriptLineCount;
|
|
1861
1913
|
this.#manualSuffixLineCount = liveSuffixLineCount;
|
|
1914
|
+
this.#manualFrontierSpacerLineCount = liveFrontierSpacerLineCount;
|
|
1862
1915
|
this.#latestRenderedLines = liveLines.slice();
|
|
1916
|
+
this.#latestRenderedFrontierSpacerLineCount = liveFrontierSpacerLineCount;
|
|
1863
1917
|
if (this.#scrollbackResumeViewportTop === undefined) {
|
|
1864
1918
|
this.#nativeScrollbackViewportTop = liveViewportTop;
|
|
1865
1919
|
}
|
|
@@ -1876,7 +1930,11 @@ export class TUI extends Container {
|
|
|
1876
1930
|
true,
|
|
1877
1931
|
this.#kittyPlacementSpans,
|
|
1878
1932
|
liveKittyPlacementSpans,
|
|
1879
|
-
{
|
|
1933
|
+
{
|
|
1934
|
+
transcriptLineCount: liveTranscriptLineCount,
|
|
1935
|
+
suffixLineCount: liveSuffixLineCount,
|
|
1936
|
+
frontierSpacerLineCount: liveFrontierSpacerLineCount,
|
|
1937
|
+
},
|
|
1880
1938
|
);
|
|
1881
1939
|
}
|
|
1882
1940
|
|
|
@@ -2100,9 +2158,7 @@ export class TUI extends Container {
|
|
|
2100
2158
|
if (!shouldWrite) return { queueId: id, operation: op.type, status: "stale-token" };
|
|
2101
2159
|
}
|
|
2102
2160
|
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
|
-
);
|
|
2161
|
+
const prefixWritten = this.#guardTerminalOperation(() => this.#emit(new TextDecoder().decode(op.prefix)));
|
|
2106
2162
|
if (!prefixWritten) return failed();
|
|
2107
2163
|
const abortBarrier = () => {
|
|
2108
2164
|
// Abort/cursor-restoration bytes are terminal writes: never emit
|
|
@@ -2112,7 +2168,7 @@ export class TUI extends Container {
|
|
|
2112
2168
|
const abortSuffix = op.abortSuffix === undefined ? "" : new TextDecoder().decode(op.abortSuffix);
|
|
2113
2169
|
const cursorVisibility = op.restoreCursorVisibility ? this.#cursorVisibilitySequence() : "";
|
|
2114
2170
|
if (abortSuffix || cursorVisibility)
|
|
2115
|
-
this.#guardTerminalOperation(() => this
|
|
2171
|
+
this.#guardTerminalOperation(() => this.#emit(abortSuffix + cursorVisibility));
|
|
2116
2172
|
};
|
|
2117
2173
|
const flushed = await (this.terminal as Terminal & { flush?: () => Promise<boolean> }).flush?.();
|
|
2118
2174
|
// Async boundary: the terminal may have stopped while we awaited.
|
|
@@ -2166,7 +2222,7 @@ export class TUI extends Container {
|
|
|
2166
2222
|
const dependent = op.type === "generic-render" || op.type === "generic-full-redraw";
|
|
2167
2223
|
const ok = dependent
|
|
2168
2224
|
? this.#writeProtectedRenderIngress(finalBytes)
|
|
2169
|
-
: this.#guardTerminalOperation(() => this
|
|
2225
|
+
: this.#guardTerminalOperation(() => this.#emit(finalBytes));
|
|
2170
2226
|
if (!ok && dependent) {
|
|
2171
2227
|
const rect = (op as { rect: CellRect }).rect;
|
|
2172
2228
|
const blockedBy = [...this.#rasterCleanup.entries()]
|
|
@@ -2191,7 +2247,7 @@ export class TUI extends Container {
|
|
|
2191
2247
|
lease.revoked = true;
|
|
2192
2248
|
this.#rasterLeases.delete(request.token.ownerId);
|
|
2193
2249
|
const erase = this.#cursorGuardedRasterSequence(new TextDecoder().decode(lease.erase));
|
|
2194
|
-
const ok = this.#guardTerminalOperation(() => this
|
|
2250
|
+
const ok = this.#guardTerminalOperation(() => this.#emit(erase));
|
|
2195
2251
|
if (!ok)
|
|
2196
2252
|
this.#rasterCleanup.set(request.token.ownerId, {
|
|
2197
2253
|
token: lease.token,
|
|
@@ -2357,7 +2413,7 @@ export class TUI extends Container {
|
|
|
2357
2413
|
)
|
|
2358
2414
|
)
|
|
2359
2415
|
return false;
|
|
2360
|
-
return this.#guardTerminalOperation(() => this
|
|
2416
|
+
return this.#guardTerminalOperation(() => this.#emit(buffer));
|
|
2361
2417
|
}
|
|
2362
2418
|
#writeProtectedRenderIngress(buffer: string): boolean {
|
|
2363
2419
|
const affected = [...this.#rasterLeases.values()];
|
|
@@ -2371,7 +2427,7 @@ export class TUI extends Container {
|
|
|
2371
2427
|
...affected.map(lease => new TextDecoder().decode(lease.erase)),
|
|
2372
2428
|
].join(""),
|
|
2373
2429
|
);
|
|
2374
|
-
const ok = this.#guardTerminalOperation(() => this
|
|
2430
|
+
const ok = this.#guardTerminalOperation(() => this.#emit(cleanup + buffer));
|
|
2375
2431
|
if (!ok) {
|
|
2376
2432
|
this.#terminalUnavailable = true;
|
|
2377
2433
|
this.#previousLines = [];
|
|
@@ -2454,6 +2510,9 @@ export class TUI extends Container {
|
|
|
2454
2510
|
this.#markTerminalUnavailable();
|
|
2455
2511
|
throw error;
|
|
2456
2512
|
}
|
|
2513
|
+
// Taken only once the terminal started, so a failed start never leaves annotations on.
|
|
2514
|
+
if (this.options.enableMouse === true && this.options.copySelection && !this.#releaseCopyAnnotations)
|
|
2515
|
+
this.#releaseCopyAnnotations = retainCopyAnnotations();
|
|
2457
2516
|
if (this.#pendingTerminalCleanup.length > 0 || this.#rasterCleanup.size > 0) {
|
|
2458
2517
|
void this.notifyTerminalLifecycle({
|
|
2459
2518
|
kind: "availability-restored",
|
|
@@ -2591,8 +2650,13 @@ export class TUI extends Container {
|
|
|
2591
2650
|
this.#clearSixelProbeState();
|
|
2592
2651
|
}
|
|
2593
2652
|
|
|
2653
|
+
/** The only terminal write sink: copy APCs are in-process metadata and never reach the terminal. */
|
|
2654
|
+
#emit(data: string): void {
|
|
2655
|
+
this.terminal.write(stripCopyApcs(data));
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2594
2658
|
#writeTerminal(data: string, deferRenderFailure = false): boolean {
|
|
2595
|
-
return this.#guardTerminalOperation(() => this
|
|
2659
|
+
return this.#guardTerminalOperation(() => this.#emit(data), !deferRenderFailure);
|
|
2596
2660
|
}
|
|
2597
2661
|
|
|
2598
2662
|
#frameSynchronizedOutput(payload: string): string {
|
|
@@ -2631,7 +2695,7 @@ export class TUI extends Container {
|
|
|
2631
2695
|
return false;
|
|
2632
2696
|
}
|
|
2633
2697
|
try {
|
|
2634
|
-
this
|
|
2698
|
+
this.#emit(data);
|
|
2635
2699
|
} catch {
|
|
2636
2700
|
this.#markTerminalUnavailable();
|
|
2637
2701
|
return false;
|
|
@@ -2812,6 +2876,15 @@ export class TUI extends Container {
|
|
|
2812
2876
|
}
|
|
2813
2877
|
|
|
2814
2878
|
stop(): void {
|
|
2879
|
+
this.#stop(false);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/** Temporarily releases terminal modes for foreground handoff and preserves viewport state for restart. */
|
|
2883
|
+
suspend(): void {
|
|
2884
|
+
this.#stop(true);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
#stop(temporary: boolean): void {
|
|
2815
2888
|
this.#invalidatePreparations();
|
|
2816
2889
|
// Invalidate every raster-queue body captured under the running epoch
|
|
2817
2890
|
// before any teardown: nothing queued before stop may write after
|
|
@@ -2823,6 +2896,8 @@ export class TUI extends Container {
|
|
|
2823
2896
|
this.#clearSixelProbeState();
|
|
2824
2897
|
this.#clearMouseSelection();
|
|
2825
2898
|
this.#resetClickCount();
|
|
2899
|
+
this.#releaseCopyAnnotations?.();
|
|
2900
|
+
this.#releaseCopyAnnotations = undefined;
|
|
2826
2901
|
this.#stopped = true;
|
|
2827
2902
|
this.#settleRenderCommitWaiters(false);
|
|
2828
2903
|
if (this.#renderTimer) {
|
|
@@ -2837,17 +2912,26 @@ export class TUI extends Container {
|
|
|
2837
2912
|
// An armed TIMER dies with the session, but a repair already deferred while
|
|
2838
2913
|
// the user was reading scrollback must survive a temporary stop/start
|
|
2839
2914
|
// (Ctrl-Z resume, external editor): manual viewport ownership survives
|
|
2840
|
-
// restart, so followLiveViewport() still needs the pending repair
|
|
2841
|
-
//
|
|
2915
|
+
// restart, so followLiveViewport() still needs the pending repair and saved
|
|
2916
|
+
// frontier because teardown drops its transient spacers. Without manual
|
|
2917
|
+
// ownership the flags are moot — start() issues a forced full render.
|
|
2842
2918
|
if (this.#manualViewportTop === undefined) {
|
|
2843
2919
|
this.#widthSettleRepairPending = false;
|
|
2844
2920
|
this.#tabWidthRepairPending = false;
|
|
2921
|
+
this.#manualResumeViewportTop = undefined;
|
|
2845
2922
|
}
|
|
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
2923
|
const displayedFrameLines = this.#previousLines.length || this.#latestRenderedLines.length;
|
|
2850
|
-
if (displayedFrameLines > 0) {
|
|
2924
|
+
if (displayedFrameLines > 0 && this.#manualViewportTop !== undefined) {
|
|
2925
|
+
// The manual viewport's logical row is not a physical screen coordinate.
|
|
2926
|
+
// Insert a blank bottom handoff row and discard the pinned row rather than
|
|
2927
|
+
// scrolling committed history into native scrollback. start() redraws the
|
|
2928
|
+
// pinned suffix on resume.
|
|
2929
|
+
const bottomRow = Math.max(1, this.terminal.rows);
|
|
2930
|
+
this.#writeTerminal(`\x1b[${bottomRow};1H\x1b[1L`);
|
|
2931
|
+
this.#hardwareCursorRow = bottomRow - 1;
|
|
2932
|
+
} else if (displayedFrameLines > 0) {
|
|
2933
|
+
// The latest logical frame can differ from the last painted frame when a
|
|
2934
|
+
// semantic viewport retains its previous image.
|
|
2851
2935
|
const targetRow = displayedFrameLines; // Line after the last content
|
|
2852
2936
|
const lineDiff = targetRow - this.#hardwareCursorRow;
|
|
2853
2937
|
if (lineDiff > 0) {
|
|
@@ -2856,6 +2940,7 @@ export class TUI extends Container {
|
|
|
2856
2940
|
this.#writeTerminal(`\x1b[${-lineDiff}A`);
|
|
2857
2941
|
}
|
|
2858
2942
|
this.#writeTerminal("\r\n");
|
|
2943
|
+
this.#hardwareCursorRow = targetRow + 1;
|
|
2859
2944
|
}
|
|
2860
2945
|
|
|
2861
2946
|
if (this.#useImeBlockCursor) {
|
|
@@ -2871,7 +2956,9 @@ export class TUI extends Container {
|
|
|
2871
2956
|
// non-manual restart keeps only the durable baseline until its first render:
|
|
2872
2957
|
// that render can admit a raw-prefix-proven append without replaying history.
|
|
2873
2958
|
this.#restartViewportRepaintPending =
|
|
2874
|
-
|
|
2959
|
+
temporary &&
|
|
2960
|
+
this.#manualViewportTop === undefined &&
|
|
2961
|
+
(this.#previousLines.length > 0 || this.#maxLinesRendered > 0);
|
|
2875
2962
|
if (this.#restartViewportRepaintPending) {
|
|
2876
2963
|
this.#restartDurableLineCount = this.#durableLineCount;
|
|
2877
2964
|
this.#restartDurableRenderedLines = this.#durableRenderedLines.slice();
|
|
@@ -2900,6 +2987,17 @@ export class TUI extends Container {
|
|
|
2900
2987
|
this.#lineEmitWidthCache.clear();
|
|
2901
2988
|
this.#previousWidth = 0;
|
|
2902
2989
|
this.#previousHeight = 0;
|
|
2990
|
+
this.#cursorRow = 0;
|
|
2991
|
+
this.#hardwareCursorRow = 0;
|
|
2992
|
+
this.#viewportTopRow = 0;
|
|
2993
|
+
this.#maxLinesRendered = 0;
|
|
2994
|
+
// Mark that the next initial render needs to clear the screen after terminal reset
|
|
2995
|
+
this.#restartNeedsClear = true;
|
|
2996
|
+
this.#forcedRenderPreviousWidth = 0;
|
|
2997
|
+
this.#forcedRenderPreviousHeight = 0;
|
|
2998
|
+
this.#transcriptRebuildPending = false;
|
|
2999
|
+
this.#latestRenderedFrontierSpacerLineCount = 0;
|
|
3000
|
+
this.#manualFrontierSpacerLineCount = 0;
|
|
2903
3001
|
this.#resizeRenderQueued = false;
|
|
2904
3002
|
this.#resizeRenderMutationQueued = false;
|
|
2905
3003
|
this.#renderMutationQueued = false;
|
|
@@ -3206,8 +3304,14 @@ export class TUI extends Container {
|
|
|
3206
3304
|
if (force) {
|
|
3207
3305
|
// A forced full redraw supersedes any queued input-priority render.
|
|
3208
3306
|
this.#inputRenderPending = false;
|
|
3209
|
-
if (!widthSettleRequest) this.#forcedRenderQueued = true;
|
|
3210
3307
|
if (!widthSettleRequest) {
|
|
3308
|
+
if (!this.#forcedRenderQueued) {
|
|
3309
|
+
this.#forcedRenderPreviousWidth =
|
|
3310
|
+
this.#previousWidth < 0 ? this.#forcedRenderPreviousWidth : this.#previousWidth;
|
|
3311
|
+
this.#forcedRenderPreviousHeight =
|
|
3312
|
+
this.#previousHeight < 0 ? this.#forcedRenderPreviousHeight : this.#previousHeight;
|
|
3313
|
+
}
|
|
3314
|
+
this.#forcedRenderQueued = true;
|
|
3211
3315
|
this.#previousWidth = -1; // -1 triggers widthChanged
|
|
3212
3316
|
this.#previousHeight = -1; // -1 triggers heightChanged
|
|
3213
3317
|
}
|
|
@@ -3311,15 +3415,29 @@ export class TUI extends Container {
|
|
|
3311
3415
|
data = current;
|
|
3312
3416
|
}
|
|
3313
3417
|
|
|
3314
|
-
|
|
3418
|
+
let mouse = parseSgrMouseEvent(data);
|
|
3315
3419
|
if (mouse) {
|
|
3420
|
+
// A held selection may leave the window; clamp it to the nearest cell so
|
|
3421
|
+
// the drag keeps extending instead of being discarded.
|
|
3422
|
+
if (this.#mouseButtonDown && (mouse.kind === "drag" || mouse.kind === "release" || mouse.kind === "wheel")) {
|
|
3423
|
+
mouse = {
|
|
3424
|
+
...mouse,
|
|
3425
|
+
x: Math.min(mouse.x, this.terminal.columns),
|
|
3426
|
+
y: Math.min(mouse.y, this.terminal.rows),
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3316
3429
|
// Coordinates outside the current terminal cannot name a visible cell.
|
|
3317
3430
|
if (mouse.x > this.terminal.columns || mouse.y > this.terminal.rows) {
|
|
3318
3431
|
this.#clearMouseSelection();
|
|
3319
3432
|
this.#resetClickCount();
|
|
3320
3433
|
return;
|
|
3321
3434
|
}
|
|
3322
|
-
if (mouse.kind === "wheel") {
|
|
3435
|
+
if (mouse.kind === "wheel" && this.#mouseButtonDown && this.#mouseSelectionAnchor !== null) {
|
|
3436
|
+
// Wheel during a held drag scrolls under the pointer and keeps extending.
|
|
3437
|
+
this.scrollViewportBy(mouse.direction! * DEFAULT_WHEEL_LINES, { pin: "stable" });
|
|
3438
|
+
this.#extendMouseSelectionAt(mouse);
|
|
3439
|
+
this.#updateMouseAutoScroll();
|
|
3440
|
+
} else if (mouse.kind === "wheel") {
|
|
3323
3441
|
this.#clearMouseSelection();
|
|
3324
3442
|
this.#resetClickCount();
|
|
3325
3443
|
this.scrollViewportBy(mouse.direction! * DEFAULT_WHEEL_LINES, { pin: "stable" });
|
|
@@ -3428,6 +3546,100 @@ export class TUI extends Container {
|
|
|
3428
3546
|
: { line, column: mouse.x - 1 };
|
|
3429
3547
|
}
|
|
3430
3548
|
|
|
3549
|
+
/** Screen rows (zero-based) that currently show transcript lines. */
|
|
3550
|
+
#transcriptScreenRows(): { first: number; last: number } | null {
|
|
3551
|
+
this.#ensureLiveSelectionRows();
|
|
3552
|
+
let first = -1;
|
|
3553
|
+
let last = -1;
|
|
3554
|
+
for (let row = 0; row < this.#committedTranscriptRows.length; row++) {
|
|
3555
|
+
const line = this.#committedTranscriptRows[row];
|
|
3556
|
+
if (line === null || line === undefined || line < 0 || line >= this.#manualTranscriptLineCount) continue;
|
|
3557
|
+
if (first < 0) first = row;
|
|
3558
|
+
last = row;
|
|
3559
|
+
}
|
|
3560
|
+
return first < 0 ? null : { first, last };
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
/**
|
|
3564
|
+
* A held drag over editor/status chrome or past the transcript still names a
|
|
3565
|
+
* transcript cell: the nearest visible row, at the pointer column, extended to
|
|
3566
|
+
* the row edge so a downward drag reaches the end of the last line.
|
|
3567
|
+
*/
|
|
3568
|
+
#mouseDragPoint(pointer: { x: number; y: number }): MouseSelectionPoint | null {
|
|
3569
|
+
const rows = this.#transcriptScreenRows();
|
|
3570
|
+
const direct = this.#mouseSelectionPoint({ kind: "drag", x: pointer.x, y: pointer.y });
|
|
3571
|
+
if (direct !== null || rows === null) return direct;
|
|
3572
|
+
const above = pointer.y - 1 < rows.first;
|
|
3573
|
+
const line = this.#committedTranscriptRows[above ? rows.first : rows.last]!;
|
|
3574
|
+
return { line, column: above ? 0 : Math.max(0, this.terminal.columns - 1) };
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
/** Following live clears the screen-row map until the next paint; rebuild it from the live viewport. */
|
|
3578
|
+
#ensureLiveSelectionRows(): void {
|
|
3579
|
+
if (this.#manualViewportTop !== undefined || this.#committedTranscriptRows.length > 0) return;
|
|
3580
|
+
this.#refreshPaintedLiveViewportObservation(this.terminal.rows);
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
#extendMouseSelectionAt(pointer: { x: number; y: number }): void {
|
|
3584
|
+
const anchor = this.#mouseSelectionAnchor;
|
|
3585
|
+
if (this.#mouseSelectionStart === null || anchor === null) return;
|
|
3586
|
+
this.#mouseDragPointer = { x: pointer.x, y: pointer.y };
|
|
3587
|
+
this.#mouseGestureDragged = true;
|
|
3588
|
+
if (pointer.y !== this.#mousePressRow) this.#mouseDragLeftPressRow = true;
|
|
3589
|
+
const point = this.#mouseDragPoint(pointer);
|
|
3590
|
+
if (point === null) return;
|
|
3591
|
+
const forward = point.line > anchor.line || (point.line === anchor.line && point.column >= anchor.column);
|
|
3592
|
+
// The anchor span stays whole while the far end grows, so dragging after a
|
|
3593
|
+
// double click extends word by word instead of collapsing back to one cell.
|
|
3594
|
+
this.#mouseSelectionStart = this.#snapSelectionPoint(anchor, forward ? "start" : "end");
|
|
3595
|
+
this.#mouseSelectionEnd = this.#snapSelectionPoint(point, forward ? "end" : "start");
|
|
3596
|
+
this.#mouseSelectionActive = true;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
/** Edge direction for a held drag: above the first or at/below the last transcript row. */
|
|
3600
|
+
#mouseAutoScrollDirection(pointer: { x: number; y: number }): -1 | 0 | 1 {
|
|
3601
|
+
const rows = this.#transcriptScreenRows();
|
|
3602
|
+
if (rows === null || !this.#mouseDragLeftPressRow) return 0;
|
|
3603
|
+
const row = pointer.y - 1;
|
|
3604
|
+
if (row <= rows.first) return -1;
|
|
3605
|
+
if (row >= rows.last || row >= this.terminal.rows - 1) return 1;
|
|
3606
|
+
return 0;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
#updateMouseAutoScroll(): void {
|
|
3610
|
+
const pointer = this.#mouseDragPointer;
|
|
3611
|
+
const direction = pointer && this.#mouseButtonDown ? this.#mouseAutoScrollDirection(pointer) : 0;
|
|
3612
|
+
if (direction === 0) {
|
|
3613
|
+
this.#stopMouseAutoScroll();
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
if (this.#mouseAutoScrollTimer !== undefined) return;
|
|
3617
|
+
this.#mouseAutoScrollTimer = setInterval(() => {
|
|
3618
|
+
const held = this.#mouseDragPointer;
|
|
3619
|
+
const step = held && this.#mouseButtonDown ? this.#mouseAutoScrollDirection(held) : 0;
|
|
3620
|
+
const before = this.#transcriptScreenRows();
|
|
3621
|
+
const firstLine = before === null ? null : this.#committedTranscriptRows[before.first];
|
|
3622
|
+
if (held === null || step === 0 || !this.scrollViewportBy(step, { pin: "stable" })) {
|
|
3623
|
+
this.#stopMouseAutoScroll();
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
// A boundary (live bottom, history top) reports success without moving.
|
|
3627
|
+
const after = this.#transcriptScreenRows();
|
|
3628
|
+
if (after === null || this.#committedTranscriptRows[after.first] === firstLine) {
|
|
3629
|
+
this.#stopMouseAutoScroll();
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
this.#extendMouseSelectionAt(held);
|
|
3633
|
+
this.requestRender(false, "mouse");
|
|
3634
|
+
}, MOUSE_AUTO_SCROLL_INTERVAL_MS);
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
#stopMouseAutoScroll(): void {
|
|
3638
|
+
if (this.#mouseAutoScrollTimer === undefined) return;
|
|
3639
|
+
clearInterval(this.#mouseAutoScrollTimer);
|
|
3640
|
+
this.#mouseAutoScrollTimer = undefined;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3431
3643
|
/**
|
|
3432
3644
|
* Resolve the selection granularity for this press. SGR mouse reports carry no
|
|
3433
3645
|
* click counter, so repeats are inferred from the previous press: same cell and
|
|
@@ -3523,6 +3735,8 @@ export class TUI extends Container {
|
|
|
3523
3735
|
const mode = this.#nextClickMode(point, pressAt);
|
|
3524
3736
|
this.#mouseButtonDown = true;
|
|
3525
3737
|
this.#mouseGestureDragged = false;
|
|
3738
|
+
this.#mousePressRow = mouse.y;
|
|
3739
|
+
this.#mouseDragLeftPressRow = false;
|
|
3526
3740
|
this.#mousePressPoint = point;
|
|
3527
3741
|
this.#mousePressAt = pressAt;
|
|
3528
3742
|
this.#mouseSelectionMode = mode;
|
|
@@ -3539,17 +3753,8 @@ export class TUI extends Container {
|
|
|
3539
3753
|
this.#resetClickCount();
|
|
3540
3754
|
return;
|
|
3541
3755
|
}
|
|
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;
|
|
3756
|
+
this.#extendMouseSelectionAt(mouse);
|
|
3757
|
+
this.#updateMouseAutoScroll();
|
|
3553
3758
|
}
|
|
3554
3759
|
|
|
3555
3760
|
#finishMouseSelection(mouse: MouseEvent): void {
|
|
@@ -3558,7 +3763,10 @@ export class TUI extends Container {
|
|
|
3558
3763
|
return;
|
|
3559
3764
|
}
|
|
3560
3765
|
this.#mouseButtonDown = false;
|
|
3561
|
-
|
|
3766
|
+
this.#stopMouseAutoScroll();
|
|
3767
|
+
this.#mouseDragPointer = null;
|
|
3768
|
+
const dragged = this.#mouseGestureDragged;
|
|
3769
|
+
const point = dragged ? this.#mouseDragPoint(mouse) : this.#mouseSelectionPoint(mouse);
|
|
3562
3770
|
const pressPoint = this.#mousePressPoint;
|
|
3563
3771
|
const pressAt = this.#mousePressAt;
|
|
3564
3772
|
const completedClick =
|
|
@@ -3587,7 +3795,9 @@ export class TUI extends Container {
|
|
|
3587
3795
|
this.#clearMouseSelection();
|
|
3588
3796
|
return;
|
|
3589
3797
|
}
|
|
3590
|
-
|
|
3798
|
+
// Source-aware rows return original Markdown, which may carry ESC/C0 bytes from
|
|
3799
|
+
// model or tool text; sanitize at the clipboard boundary (\t and \n survive).
|
|
3800
|
+
const text = stripTerminalControls(this.#extractMouseSelection());
|
|
3591
3801
|
if (!text) {
|
|
3592
3802
|
this.#clearMouseSelection();
|
|
3593
3803
|
return;
|
|
@@ -3601,6 +3811,8 @@ export class TUI extends Container {
|
|
|
3601
3811
|
}
|
|
3602
3812
|
|
|
3603
3813
|
#clearMouseSelection(): void {
|
|
3814
|
+
this.#stopMouseAutoScroll();
|
|
3815
|
+
this.#mouseDragPointer = null;
|
|
3604
3816
|
this.#mouseSelectionStart = null;
|
|
3605
3817
|
this.#mouseSelectionEnd = null;
|
|
3606
3818
|
this.#mouseSelectionAnchor = null;
|
|
@@ -3638,23 +3850,182 @@ export class TUI extends Container {
|
|
|
3638
3850
|
#extractMouseSelection(): string {
|
|
3639
3851
|
const selection = this.#orderedMouseSelection();
|
|
3640
3852
|
if (selection === null) return "";
|
|
3641
|
-
|
|
3853
|
+
type SelectedRow = {
|
|
3854
|
+
line: number;
|
|
3855
|
+
fragment: string;
|
|
3856
|
+
sourceId?: string;
|
|
3857
|
+
annotation?: CopyRowAnnotation;
|
|
3858
|
+
covered?: boolean;
|
|
3859
|
+
atContentStart?: boolean;
|
|
3860
|
+
/** Selected text per table cell; empty where the selection misses the cell. */
|
|
3861
|
+
cells?: string[];
|
|
3862
|
+
/** Folded into the row above as the soft-wrapped tail of its cells. */
|
|
3863
|
+
merged?: boolean;
|
|
3864
|
+
};
|
|
3865
|
+
const selected: SelectedRow[] = [];
|
|
3642
3866
|
const selectionLines = this.#selectionSourceLines();
|
|
3643
3867
|
for (let lineIndex = selection.start.line; lineIndex <= selection.end.line; lineIndex++) {
|
|
3644
3868
|
const line = selectionLines[lineIndex];
|
|
3645
3869
|
if (line === undefined || TERMINAL.isImageLine(line)) {
|
|
3646
|
-
selected.push("");
|
|
3870
|
+
selected.push({ line: lineIndex, fragment: "" });
|
|
3647
3871
|
continue;
|
|
3648
3872
|
}
|
|
3649
|
-
|
|
3873
|
+
// Read renderer metadata before stripping APC/ANSI controls. The metadata is
|
|
3874
|
+
// attached to this physical row, so identical text in another component can
|
|
3875
|
+
// never be mistaken for this selection.
|
|
3876
|
+
const copy = extractCopyRowAnnotation(line);
|
|
3877
|
+
const rendered = copy?.line ?? line;
|
|
3878
|
+
const plain = stripTerminalControls(rendered);
|
|
3650
3879
|
const columns = this.#mouseSelectionColumns(lineIndex, plain);
|
|
3651
3880
|
if (columns === null || columns.end <= columns.start) {
|
|
3652
|
-
selected.push("");
|
|
3881
|
+
selected.push({ line: lineIndex, fragment: "" });
|
|
3653
3882
|
continue;
|
|
3654
3883
|
}
|
|
3655
|
-
|
|
3884
|
+
const candidate = copy?.annotation;
|
|
3885
|
+
const annotation =
|
|
3886
|
+
candidate && copy.originColumn + candidate.contentEnd <= visibleWidth(plain) ? candidate : undefined;
|
|
3887
|
+
const localColumns = copy
|
|
3888
|
+
? { start: columns.start - copy.originColumn, end: columns.end - copy.originColumn }
|
|
3889
|
+
: columns;
|
|
3890
|
+
const presentationStart = annotation ? annotation.contentStart + annotation.prefixColumns : localColumns.start;
|
|
3891
|
+
const start = annotation ? Math.max(localColumns.start, presentationStart) : localColumns.start;
|
|
3892
|
+
const end = annotation ? Math.min(localColumns.end, annotation.contentEnd) : localColumns.end;
|
|
3893
|
+
let fragment = end > start ? sliceByColumn(plain, (copy?.originColumn ?? 0) + start, end - start, false) : "";
|
|
3894
|
+
// Cell padding is presentation; Markdown cell text never keeps outer spaces.
|
|
3895
|
+
const cells = annotation?.ranges?.map(([rangeStart, rangeEnd]) => {
|
|
3896
|
+
const overlapStart = Math.max(start, rangeStart);
|
|
3897
|
+
const overlapEnd = Math.min(end, rangeEnd);
|
|
3898
|
+
return overlapEnd > overlapStart
|
|
3899
|
+
? sliceByColumn(plain, (copy?.originColumn ?? 0) + overlapStart, overlapEnd - overlapStart, false).trim()
|
|
3900
|
+
: "";
|
|
3901
|
+
});
|
|
3902
|
+
if (cells) fragment = cells.filter(cell => cell.length > 0).join("\t");
|
|
3903
|
+
if (annotation?.fence) fragment = "";
|
|
3904
|
+
const covered = annotation
|
|
3905
|
+
? localColumns.start <= annotation.contentStart && localColumns.end >= annotation.contentEnd
|
|
3906
|
+
: false;
|
|
3907
|
+
// A fully covered rendered code row, nested or not, restores its row-local
|
|
3908
|
+
// original slice (not the whole logical line), which preserves an original tab.
|
|
3909
|
+
if (covered && annotation?.originalCode) fragment = annotation.source;
|
|
3910
|
+
selected.push({
|
|
3911
|
+
line: lineIndex,
|
|
3912
|
+
fragment,
|
|
3913
|
+
sourceId: copy?.sourceId,
|
|
3914
|
+
annotation,
|
|
3915
|
+
covered,
|
|
3916
|
+
atContentStart: annotation ? localColumns.start <= presentationStart : true,
|
|
3917
|
+
...(cells ? { cells } : {}),
|
|
3918
|
+
});
|
|
3656
3919
|
}
|
|
3657
|
-
|
|
3920
|
+
// Nothing but decoration (quote margins, table borders, fences) was selected.
|
|
3921
|
+
if (selected.every(row => row.fragment.length === 0)) return "";
|
|
3922
|
+
|
|
3923
|
+
const groups = new Map<string, { rows: SelectedRow[]; complete: boolean }>();
|
|
3924
|
+
for (const row of selected) {
|
|
3925
|
+
if (!row.annotation || !row.sourceId) continue;
|
|
3926
|
+
const key = `${row.sourceId}:${row.annotation.token}`;
|
|
3927
|
+
const group = groups.get(key) ?? { rows: [], complete: false };
|
|
3928
|
+
group.rows.push(row);
|
|
3929
|
+
groups.set(key, group);
|
|
3930
|
+
}
|
|
3931
|
+
// A token is complete when every selected row is covered, the selection holds
|
|
3932
|
+
// every rendered row of the token in order (so a preview that clipped the token's
|
|
3933
|
+
// head or tail cannot restore hidden rows), the last of them carries the
|
|
3934
|
+
// whole-token source, and the rows just outside the group belong to something else.
|
|
3935
|
+
const rowKey = (lineIndex: number): string | undefined => {
|
|
3936
|
+
const line = selectionLines[lineIndex];
|
|
3937
|
+
if (line === undefined || TERMINAL.isImageLine(line)) return undefined;
|
|
3938
|
+
const copy = extractCopyRowAnnotation(line);
|
|
3939
|
+
return copy ? `${copy.sourceId}:${copy.annotation.token}` : undefined;
|
|
3940
|
+
};
|
|
3941
|
+
for (const [key, group] of groups) {
|
|
3942
|
+
const first = group.rows[0]!;
|
|
3943
|
+
const last = group.rows[group.rows.length - 1]!;
|
|
3944
|
+
group.complete =
|
|
3945
|
+
group.rows.every(row => row.covered) &&
|
|
3946
|
+
group.rows.every((row, index) => row.annotation?.tokenRow === index) &&
|
|
3947
|
+
last.annotation?.tokenRows === group.rows.length &&
|
|
3948
|
+
last.annotation.tokenSource !== undefined &&
|
|
3949
|
+
last.line - first.line + 1 === group.rows.length &&
|
|
3950
|
+
rowKey(first.line - 1) !== key &&
|
|
3951
|
+
rowKey(last.line + 1) !== key;
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
const allSemanticRowsAreComplete = selected.every(row => {
|
|
3955
|
+
if (!row.annotation || !row.sourceId) return row.fragment.length === 0;
|
|
3956
|
+
return groups.get(`${row.sourceId}:${row.annotation.token}`)?.complete === true;
|
|
3957
|
+
});
|
|
3958
|
+
if (allSemanticRowsAreComplete) {
|
|
3959
|
+
const completed = [...groups.values()];
|
|
3960
|
+
const codeOnly = completed.length === 1 && completed[0]?.rows[0]?.annotation?.kind === "code";
|
|
3961
|
+
if (codeOnly) {
|
|
3962
|
+
const rows = completed[0]!.rows;
|
|
3963
|
+
const raw = rows[rows.length - 1]?.annotation?.tokenSource ?? "";
|
|
3964
|
+
// Token raw keeps the block's trailing newline; drop it so the closing fence is the last line.
|
|
3965
|
+
const lines = raw.replace(/\n+$/u, "").split("\n");
|
|
3966
|
+
if (/^\s*(```|~~~)/u.test(lines[0] ?? "")) {
|
|
3967
|
+
lines.shift();
|
|
3968
|
+
if (/^\s*(```|~~~)/u.test(lines[lines.length - 1] ?? "")) lines.pop();
|
|
3969
|
+
}
|
|
3970
|
+
return lines.join("\n");
|
|
3971
|
+
}
|
|
3972
|
+
let result = "";
|
|
3973
|
+
for (const row of selected) {
|
|
3974
|
+
// Each complete token contributes once, from its final row.
|
|
3975
|
+
if (row.annotation?.tokenSource !== undefined) result += row.annotation.tokenSource;
|
|
3976
|
+
}
|
|
3977
|
+
return result;
|
|
3978
|
+
}
|
|
3979
|
+
// A table row that soft-wraps its cells is one source row: fold each wrapped
|
|
3980
|
+
// row's cells into the row it continues, rejoined with the removed source gap.
|
|
3981
|
+
let head: SelectedRow | undefined;
|
|
3982
|
+
let headEnd = -1;
|
|
3983
|
+
for (const row of selected) {
|
|
3984
|
+
const gaps = row.annotation?.cellGaps;
|
|
3985
|
+
const headCells = head?.cells;
|
|
3986
|
+
if (
|
|
3987
|
+
head &&
|
|
3988
|
+
headCells &&
|
|
3989
|
+
row.cells &&
|
|
3990
|
+
gaps &&
|
|
3991
|
+
head.sourceId === row.sourceId &&
|
|
3992
|
+
head.annotation?.token === row.annotation?.token &&
|
|
3993
|
+
headEnd + 1 === row.line
|
|
3994
|
+
) {
|
|
3995
|
+
for (const [cell, text] of row.cells.entries()) {
|
|
3996
|
+
if (!text) continue;
|
|
3997
|
+
headCells[cell] = headCells[cell] ? `${headCells[cell]}${gaps[cell] ?? ""}${text}` : text;
|
|
3998
|
+
}
|
|
3999
|
+
head.fragment = headCells.filter(cell => cell.length > 0).join("\t");
|
|
4000
|
+
row.merged = true;
|
|
4001
|
+
headEnd = row.line;
|
|
4002
|
+
continue;
|
|
4003
|
+
}
|
|
4004
|
+
head = row.cells ? row : undefined;
|
|
4005
|
+
headEnd = row.line;
|
|
4006
|
+
}
|
|
4007
|
+
let result = "";
|
|
4008
|
+
let previous: SelectedRow | undefined;
|
|
4009
|
+
for (const row of selected) {
|
|
4010
|
+
const annotation = row.annotation;
|
|
4011
|
+
if (row.merged || annotation?.fence || annotation?.ranges?.length === 0) continue;
|
|
4012
|
+
if (previous) {
|
|
4013
|
+
if (
|
|
4014
|
+
annotation?.continuation &&
|
|
4015
|
+
previous.annotation?.token === annotation.token &&
|
|
4016
|
+
previous.sourceId === row.sourceId
|
|
4017
|
+
) {
|
|
4018
|
+
result += annotation.joinGap;
|
|
4019
|
+
} else {
|
|
4020
|
+
result += "\n";
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
if (annotation?.quoteDepth && !annotation.continuation && row.atContentStart)
|
|
4024
|
+
result += "> ".repeat(annotation.quoteDepth);
|
|
4025
|
+
result += row.fragment;
|
|
4026
|
+
previous = row;
|
|
4027
|
+
}
|
|
4028
|
+
return result;
|
|
3658
4029
|
}
|
|
3659
4030
|
|
|
3660
4031
|
#applyMouseSelection(lines: string[]): string[] {
|
|
@@ -3665,17 +4036,28 @@ export class TUI extends Container {
|
|
|
3665
4036
|
for (let lineIndex = selection.start.line; lineIndex <= selection.end.line; lineIndex++) {
|
|
3666
4037
|
const line = highlighted[lineIndex];
|
|
3667
4038
|
if (line === undefined || TERMINAL.isImageLine(line)) continue;
|
|
3668
|
-
|
|
4039
|
+
// Detach copy APCs verbatim (no payload decoding on this per-frame path) and
|
|
4040
|
+
// put them back around the painted row so a later copy still reads them.
|
|
4041
|
+
const {
|
|
4042
|
+
line: rendered,
|
|
4043
|
+
origin: originMarker,
|
|
4044
|
+
originColumn,
|
|
4045
|
+
rows: annotationSuffix,
|
|
4046
|
+
} = splitCopyAnnotations(line);
|
|
4047
|
+
const plain = stripTerminalControls(rendered);
|
|
3669
4048
|
const width = visibleWidth(plain);
|
|
3670
4049
|
const columns = this.#mouseSelectionColumns(lineIndex, plain);
|
|
3671
4050
|
if (columns === null || columns.end <= columns.start) continue;
|
|
3672
|
-
const before = sliceByColumn(
|
|
3673
|
-
const selected = sliceByColumn(
|
|
4051
|
+
const before = columns.start > 0 ? sliceByColumn(rendered, 0, columns.start, false) : "";
|
|
4052
|
+
const selected = sliceByColumn(rendered, columns.start, columns.end - columns.start, false).replace(
|
|
3674
4053
|
/\x1b\[[0-9;]*m/gu,
|
|
3675
4054
|
control => `${control}\x1b[7m`,
|
|
3676
4055
|
);
|
|
3677
|
-
const after = sliceByColumn(
|
|
3678
|
-
|
|
4056
|
+
const after = columns.end < width ? sliceByColumn(rendered, columns.end, width - columns.end, false) : "";
|
|
4057
|
+
const painted = `${before}\x1b[7m${selected}\x1b[27m${after}`;
|
|
4058
|
+
const head = originColumn > 0 ? sliceByColumn(painted, 0, originColumn, false) : "";
|
|
4059
|
+
const tail = originColumn < width ? sliceByColumn(painted, originColumn, width - originColumn, false) : "";
|
|
4060
|
+
highlighted[lineIndex] = `${head}${originMarker}${tail}${annotationSuffix}`;
|
|
3679
4061
|
}
|
|
3680
4062
|
return highlighted;
|
|
3681
4063
|
}
|
|
@@ -4334,14 +4716,14 @@ export class TUI extends Container {
|
|
|
4334
4716
|
|
|
4335
4717
|
#padBeforeBottomPinnedComponent(
|
|
4336
4718
|
lines: string[],
|
|
4337
|
-
|
|
4719
|
+
minimumLineCount: number,
|
|
4338
4720
|
pinnedLineCount: number,
|
|
4339
4721
|
): { lines: string[]; insertionRow: number; insertedBlankRows: number } {
|
|
4340
|
-
if (pinnedLineCount <= 0 || lines.length >=
|
|
4722
|
+
if (pinnedLineCount <= 0 || lines.length >= minimumLineCount) {
|
|
4341
4723
|
return { lines, insertionRow: lines.length, insertedBlankRows: 0 };
|
|
4342
4724
|
}
|
|
4343
4725
|
|
|
4344
|
-
const insertedBlankRows =
|
|
4726
|
+
const insertedBlankRows = minimumLineCount - lines.length;
|
|
4345
4727
|
const insertionRow = Math.max(0, lines.length - pinnedLineCount);
|
|
4346
4728
|
const padded = [...lines];
|
|
4347
4729
|
padded.splice(insertionRow, 0, ...Array.from({ length: insertedBlankRows }, () => ""));
|
|
@@ -4412,12 +4794,16 @@ export class TUI extends Container {
|
|
|
4412
4794
|
paintLive = false,
|
|
4413
4795
|
placementsToClear: KittyPlacementSpan[] = this.#kittyPlacementSpans,
|
|
4414
4796
|
placementsToPaint: KittyPlacementSpan[] = placementsToClear,
|
|
4415
|
-
geometry?: { transcriptLineCount: number; suffixLineCount: number },
|
|
4797
|
+
geometry?: { transcriptLineCount: number; suffixLineCount: number; frontierSpacerLineCount?: number },
|
|
4416
4798
|
avoidScrollback = true,
|
|
4417
4799
|
): boolean {
|
|
4418
4800
|
const paintManual = this.#manualViewportTop !== undefined && !paintLive;
|
|
4419
4801
|
const transcriptLineCount = geometry?.transcriptLineCount ?? this.#manualTranscriptLineCount;
|
|
4420
4802
|
const suffixLineCount = geometry?.suffixLineCount ?? this.#manualSuffixLineCount;
|
|
4803
|
+
const frontierSpacerLineCount =
|
|
4804
|
+
geometry?.frontierSpacerLineCount ??
|
|
4805
|
+
(paintManual ? this.#manualFrontierSpacerLineCount : this.#latestRenderedFrontierSpacerLineCount);
|
|
4806
|
+
const suffixStart = transcriptLineCount + frontierSpacerLineCount;
|
|
4421
4807
|
if (height <= 0 || width <= 0) return false;
|
|
4422
4808
|
const maxViewportTop = Math.max(
|
|
4423
4809
|
0,
|
|
@@ -4442,7 +4828,7 @@ export class TUI extends Container {
|
|
|
4442
4828
|
const emittedRegions: KittyPlacementRegion[] = paintManual
|
|
4443
4829
|
? [
|
|
4444
4830
|
{ top: nextViewportTop, bottom: nextViewportTop + transcriptCapacity },
|
|
4445
|
-
{ top:
|
|
4831
|
+
{ top: suffixStart, bottom: suffixStart + suffixLineCount },
|
|
4446
4832
|
]
|
|
4447
4833
|
: [{ top: nextViewportTop, bottom: nextViewportTop + height }];
|
|
4448
4834
|
const lineForScreenRow = (screenRow: number): string => {
|
|
@@ -4451,7 +4837,7 @@ export class TUI extends Container {
|
|
|
4451
4837
|
return paintManual && screenRow === transcriptCapacity && noticeRows > 0
|
|
4452
4838
|
? "New output — type to follow"
|
|
4453
4839
|
: paintManual && suffixRow >= 0
|
|
4454
|
-
? (lines[
|
|
4840
|
+
? (lines[suffixStart + suffixRow] ?? "")
|
|
4455
4841
|
: paintManual && lineIndex >= transcriptLineCount
|
|
4456
4842
|
? ""
|
|
4457
4843
|
: (lines[lineIndex] ?? "");
|
|
@@ -4542,6 +4928,9 @@ export class TUI extends Container {
|
|
|
4542
4928
|
}
|
|
4543
4929
|
#recordPaintedViewportObservation(viewportTop: number, height: number, paintManual: boolean): void {
|
|
4544
4930
|
const transcriptCapacity = this.#manualTranscriptCapacity(height);
|
|
4931
|
+
const transcriptLineCount = this.#manualTranscriptLineCount;
|
|
4932
|
+
const suffixStart = transcriptLineCount + this.#manualFrontierSpacerLineCount;
|
|
4933
|
+
const noticeRows = this.#manualOutputNotice && height > this.#manualSuffixLineCount ? 1 : 0;
|
|
4545
4934
|
const anchorFrame = this.#viewportAnchorFrame;
|
|
4546
4935
|
const semanticAnchor =
|
|
4547
4936
|
anchorFrame === null
|
|
@@ -4556,9 +4945,10 @@ export class TUI extends Container {
|
|
|
4556
4945
|
const cursor = this.#lastCursorPosition;
|
|
4557
4946
|
let cursorRow: number | null = null;
|
|
4558
4947
|
if (cursor !== null) {
|
|
4559
|
-
if (paintManual && cursor.row >=
|
|
4560
|
-
|
|
4561
|
-
|
|
4948
|
+
if (paintManual && cursor.row >= suffixStart) {
|
|
4949
|
+
cursorRow = transcriptCapacity + noticeRows + (cursor.row - suffixStart);
|
|
4950
|
+
} else if (paintManual && cursor.row >= transcriptLineCount) {
|
|
4951
|
+
cursorRow = null;
|
|
4562
4952
|
} else if (paintManual) {
|
|
4563
4953
|
cursorRow = this.#committedTranscriptRows.indexOf(cursor.row);
|
|
4564
4954
|
} else {
|
|
@@ -4695,6 +5085,10 @@ export class TUI extends Container {
|
|
|
4695
5085
|
const widthSettleRenderQueued = this.#widthSettleRenderQueued;
|
|
4696
5086
|
const tabWidthRepairPending = this.#tabWidthRepairPending;
|
|
4697
5087
|
const forcedRenderQueued = this.#forcedRenderQueued;
|
|
5088
|
+
const frontierPreviousWidth = this.#previousWidth < 0 ? this.#forcedRenderPreviousWidth : this.#previousWidth;
|
|
5089
|
+
const frontierPreviousHeight = this.#previousHeight < 0 ? this.#forcedRenderPreviousHeight : this.#previousHeight;
|
|
5090
|
+
const transcriptRebuildPending = this.#transcriptRebuildPending;
|
|
5091
|
+
this.#transcriptRebuildPending = false;
|
|
4698
5092
|
this.#resizeRenderQueued = false;
|
|
4699
5093
|
this.#resizeRenderMutationQueued = false;
|
|
4700
5094
|
this.#renderMutationQueued = false;
|
|
@@ -4730,6 +5124,8 @@ export class TUI extends Container {
|
|
|
4730
5124
|
this.#manualViewportTop === undefined &&
|
|
4731
5125
|
this.#latestRaw.length > 0 &&
|
|
4732
5126
|
this.#latestRenderedLines.length === this.#latestRaw.length &&
|
|
5127
|
+
// A cached spacer count is not valid after natural rows replace the transient padding.
|
|
5128
|
+
this.#latestRenderedFrontierSpacerLineCount === 0 &&
|
|
4733
5129
|
this.#previousLines === this.#latestRenderedLines;
|
|
4734
5130
|
let previousKittyPlacementSpans = this.#kittyPlacementSpans;
|
|
4735
5131
|
const placementOwners = new Map<string, KittyPlacementOwner>();
|
|
@@ -4843,6 +5239,7 @@ export class TUI extends Container {
|
|
|
4843
5239
|
let diffStart = 0;
|
|
4844
5240
|
let usedWindowNormalize = false;
|
|
4845
5241
|
let stitched = false;
|
|
5242
|
+
let frontierSpacerLineCount = 0;
|
|
4846
5243
|
if (reusedAnchor !== null && layoutPrefixEligible && !layoutPrefixBlocked) {
|
|
4847
5244
|
const reused = this.#reuseCachedLayoutPrefix(
|
|
4848
5245
|
width,
|
|
@@ -4859,6 +5256,7 @@ export class TUI extends Container {
|
|
|
4859
5256
|
cursorPos = reused.cursorPos;
|
|
4860
5257
|
diffStart = reused.diffStart;
|
|
4861
5258
|
usedWindowNormalize = true;
|
|
5259
|
+
frontierSpacerLineCount = this.#latestRenderedFrontierSpacerLineCount;
|
|
4862
5260
|
}
|
|
4863
5261
|
}
|
|
4864
5262
|
if (!stitched) {
|
|
@@ -4880,11 +5278,24 @@ export class TUI extends Container {
|
|
|
4880
5278
|
}
|
|
4881
5279
|
newLines = this.#constrainPinnedSuffix(renderedLines, height, renderedChildren);
|
|
4882
5280
|
if (hasStickySuffix && height > 0 && this.#manualViewportTop === undefined) {
|
|
4883
|
-
|
|
5281
|
+
// Rebuilds start at their new transcript frontier. Forced redraws use the
|
|
5282
|
+
// captured rendered dimensions rather than their -1 invalidation sentinels.
|
|
5283
|
+
// clearOnShrink replays the whole frame on contraction instead; spacer rows
|
|
5284
|
+
// would otherwise stay on screen as a blank gap above the pinned component.
|
|
5285
|
+
const preserveLiveFrontier =
|
|
5286
|
+
!this.#clearOnShrinkReplays() &&
|
|
5287
|
+
!transcriptIdentityReplaced &&
|
|
5288
|
+
!transcriptRebuildPending &&
|
|
5289
|
+
frontierPreviousWidth === width &&
|
|
5290
|
+
(frontierPreviousHeight === height || this.#viewportRepaintHost());
|
|
5291
|
+
const minimumLineCount = preserveLiveFrontier ? prevViewportTop + height : height;
|
|
5292
|
+
const padded = this.#padBeforeBottomPinnedComponent(
|
|
4884
5293
|
newLines,
|
|
4885
|
-
|
|
5294
|
+
minimumLineCount,
|
|
4886
5295
|
newLines.length - sourceTranscriptLineCount,
|
|
4887
|
-
)
|
|
5296
|
+
);
|
|
5297
|
+
newLines = padded.lines;
|
|
5298
|
+
frontierSpacerLineCount = padded.insertedBlankRows;
|
|
4888
5299
|
}
|
|
4889
5300
|
// Composite overlays into the rendered lines (before differential compare)
|
|
4890
5301
|
if (this.overlayStack.length > 0) {
|
|
@@ -4941,7 +5352,15 @@ export class TUI extends Container {
|
|
|
4941
5352
|
}
|
|
4942
5353
|
}
|
|
4943
5354
|
const nextTranscriptLineCount = sourceTranscriptLineCount;
|
|
4944
|
-
|
|
5355
|
+
// Live-only frontier rows must not consume the manual viewport's pinned-suffix capacity.
|
|
5356
|
+
const nextSuffixLineCount = hasStickySuffix
|
|
5357
|
+
? Math.max(0, newLines.length - nextTranscriptLineCount - frontierSpacerLineCount)
|
|
5358
|
+
: 0;
|
|
5359
|
+
const nextFrameGeometry = {
|
|
5360
|
+
transcriptLineCount: nextTranscriptLineCount,
|
|
5361
|
+
suffixLineCount: nextSuffixLineCount,
|
|
5362
|
+
frontierSpacerLineCount,
|
|
5363
|
+
};
|
|
4945
5364
|
this.#lastCursorPosition = cursorPos;
|
|
4946
5365
|
const total = rawLines.length;
|
|
4947
5366
|
if (renderMetrics.enabled) {
|
|
@@ -4958,6 +5377,7 @@ export class TUI extends Container {
|
|
|
4958
5377
|
this.#latestRenderedLines = newLines;
|
|
4959
5378
|
this.#latestRenderedTranscriptLineCount = nextTranscriptLineCount;
|
|
4960
5379
|
this.#latestRenderedSuffixLineCount = nextSuffixLineCount;
|
|
5380
|
+
this.#latestRenderedFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
4961
5381
|
this.#latestRenderedPlacementOwners = placementOwners;
|
|
4962
5382
|
const naturalViewportTop = Math.max(0, newLines.length - height);
|
|
4963
5383
|
const priorLogicalLineCount = Math.max(this.#previousLines.length, this.#maxLinesRendered);
|
|
@@ -5018,11 +5438,12 @@ export class TUI extends Container {
|
|
|
5018
5438
|
this.#previousHeight = height;
|
|
5019
5439
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5020
5440
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5441
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5021
5442
|
},
|
|
5022
5443
|
false,
|
|
5023
5444
|
previousKittyPlacementSpans,
|
|
5024
5445
|
nextKittyPlacementSpans,
|
|
5025
|
-
|
|
5446
|
+
nextFrameGeometry,
|
|
5026
5447
|
true,
|
|
5027
5448
|
);
|
|
5028
5449
|
if (contentPainted) {
|
|
@@ -5074,6 +5495,9 @@ export class TUI extends Container {
|
|
|
5074
5495
|
newLines.length === this.#previousLines.length &&
|
|
5075
5496
|
newLines.every((line, index) => line === this.#previousLines[index])
|
|
5076
5497
|
) {
|
|
5498
|
+
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5499
|
+
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5500
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5077
5501
|
return;
|
|
5078
5502
|
}
|
|
5079
5503
|
this.#manualViewportTop = nextViewportTop;
|
|
@@ -5095,11 +5519,12 @@ export class TUI extends Container {
|
|
|
5095
5519
|
this.#paintedManualOutputNotice = this.#manualOutputNotice;
|
|
5096
5520
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5097
5521
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5522
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5098
5523
|
},
|
|
5099
5524
|
false,
|
|
5100
5525
|
previousKittyPlacementSpans,
|
|
5101
5526
|
nextKittyPlacementSpans,
|
|
5102
|
-
|
|
5527
|
+
nextFrameGeometry,
|
|
5103
5528
|
);
|
|
5104
5529
|
if (!contentPainted) restoreManualIntent();
|
|
5105
5530
|
return;
|
|
@@ -5171,6 +5596,7 @@ export class TUI extends Container {
|
|
|
5171
5596
|
);
|
|
5172
5597
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5173
5598
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5599
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5174
5600
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5175
5601
|
this.#durableLineCount = newLines.length;
|
|
5176
5602
|
this.#durableRenderedLines = newLines.slice();
|
|
@@ -5209,6 +5635,7 @@ export class TUI extends Container {
|
|
|
5209
5635
|
this.#previousHeight = height;
|
|
5210
5636
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5211
5637
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
5638
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5212
5639
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5213
5640
|
this.#latestRenderedLines = newLines.slice();
|
|
5214
5641
|
if (this.#virtualViewport) this.#latestRaw = rawLines.slice();
|
|
@@ -5216,7 +5643,7 @@ export class TUI extends Container {
|
|
|
5216
5643
|
false,
|
|
5217
5644
|
previousKittyPlacementSpans,
|
|
5218
5645
|
nextKittyPlacementSpans,
|
|
5219
|
-
|
|
5646
|
+
nextFrameGeometry,
|
|
5220
5647
|
true,
|
|
5221
5648
|
);
|
|
5222
5649
|
};
|
|
@@ -5376,9 +5803,12 @@ export class TUI extends Container {
|
|
|
5376
5803
|
return;
|
|
5377
5804
|
}
|
|
5378
5805
|
// First render - just output everything without clearing (assumes clean screen)
|
|
5806
|
+
// unless we're restarting after a stop() which leaves stale content
|
|
5379
5807
|
if (initialRender) {
|
|
5380
5808
|
logRedraw("first render");
|
|
5381
|
-
|
|
5809
|
+
const shouldClear = this.#restartNeedsClear;
|
|
5810
|
+
this.#restartNeedsClear = false;
|
|
5811
|
+
fullRender(shouldClear, "first render");
|
|
5382
5812
|
return;
|
|
5383
5813
|
}
|
|
5384
5814
|
|
|
@@ -5436,13 +5866,11 @@ export class TUI extends Container {
|
|
|
5436
5866
|
// Content shrunk below the previous render and no overlays - re-render to clear empty rows
|
|
5437
5867
|
// (overlays need the padding, so only do this when no overlays are active)
|
|
5438
5868
|
// Configurable via setClearOnShrink() or GJC_CLEAR_ON_SHRINK=0 env var
|
|
5439
|
-
|
|
5869
|
+
// Erase native scrollback too: scrolled-off rows cannot be pulled back into the
|
|
5870
|
+
// viewport, and repainting them on screen would duplicate them in history.
|
|
5871
|
+
if (this.#clearOnShrinkReplays() && newLines.length < this.#previousLines.length) {
|
|
5440
5872
|
logRedraw(`clearOnShrink (prev=${this.#previousLines.length}, new=${newLines.length})`);
|
|
5441
|
-
|
|
5442
|
-
viewportRepaint(`clearOnShrink (prev=${this.#previousLines.length}, new=${newLines.length})`);
|
|
5443
|
-
} else {
|
|
5444
|
-
fullRender(true, "clearOnShrink");
|
|
5445
|
-
}
|
|
5873
|
+
fullRender(true, "clearOnShrink", true);
|
|
5446
5874
|
return;
|
|
5447
5875
|
}
|
|
5448
5876
|
|
|
@@ -5638,6 +6066,7 @@ export class TUI extends Container {
|
|
|
5638
6066
|
);
|
|
5639
6067
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5640
6068
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
6069
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5641
6070
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5642
6071
|
})
|
|
5643
6072
|
)
|
|
@@ -5655,6 +6084,7 @@ export class TUI extends Container {
|
|
|
5655
6084
|
this.#viewportTopRow = Math.max(0, newLines.length - height);
|
|
5656
6085
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5657
6086
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
6087
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5658
6088
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5659
6089
|
return;
|
|
5660
6090
|
}
|
|
@@ -5958,6 +6388,7 @@ export class TUI extends Container {
|
|
|
5958
6388
|
);
|
|
5959
6389
|
this.#manualTranscriptLineCount = nextTranscriptLineCount;
|
|
5960
6390
|
this.#manualSuffixLineCount = nextSuffixLineCount;
|
|
6391
|
+
this.#manualFrontierSpacerLineCount = frontierSpacerLineCount;
|
|
5961
6392
|
this.#refreshPaintedLiveViewportObservation(height);
|
|
5962
6393
|
},
|
|
5963
6394
|
preserveRasterLeases,
|