@oh-my-pi/pi-tui 15.10.4 → 15.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/types/components/editor.d.ts +6 -0
- package/dist/types/components/input.d.ts +3 -0
- package/dist/types/tui.d.ts +2 -0
- package/package.json +3 -3
- package/src/components/editor.ts +90 -28
- package/src/components/input.ts +6 -0
- package/src/stdin-buffer.ts +47 -37
- package/src/tui.ts +18 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.10.6] - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `TUI.getFocused()` accessor and `Input.pasteText(text)` method so callers consuming non-bracketed paste transports (e.g. kitty's OSC 5522 enhanced clipboard) can route a paste payload to the currently focused modal Input rather than always to the primary editor. Mirrors the existing `Editor.pasteText` semantics: newlines stripped, tabs normalized, NFC normalization applied. ([#2127](https://github.com/can1357/oh-my-pi/issues/2127))
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed tmux/screen/zellij rewind/branch (`requestRender(true, { clearScrollback: true })`) permanently anchoring the input box to the pane top and overlaying scrollback after a streamed reply had grown past the viewport. `#emitFullPaint` only reset `#scrollbackHighWater` inside the `clearScrollback` branch and otherwise raised it monotonically, so inside multiplexers (where `\x1b[3J` is a no-op and `clearScrollback` is forced off) the streaming peak survived the rewind; on the next frame `#planLiveRegionPinnedRender` saw the stale high-water and anchored `renderViewportTop` past the actual content, repainting every visible row blank and parking the cursor at screen row 0 for the rest of the session. A full repaint with `clearViewport: true` re-emits the entire transcript from row 0, so `#scrollbackHighWater` is now assigned (not max-clamped) to the natural push count regardless of whether ED 3 was issued ([#2130](https://github.com/can1357/oh-my-pi/issues/2130)).
|
|
13
|
+
|
|
14
|
+
## [15.10.5] - 2026-06-08
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added `atomicTokenPattern` to `Editor`: when set to a global regex matching placeholder tokens such as `[Image #1, 800x600]` or `[Paste #2, +30 lines]`, a single backspace or forward-delete landing anywhere on a token removes the whole token instead of corrupting it into stray text.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Changed the large-paste placeholder label from `[paste #N +X lines]`/`[paste #N Y chars]` to `[Paste #N, +X lines]`/`[Paste #N, Y chars]`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed pasting large text lagging the prompt for hundreds of milliseconds before the `[paste #N …]` placeholder appeared. `StdinBuffer` assembled bracketed pastes by re-concatenating and re-scanning the entire accumulated buffer on every incoming stdin chunk (`#pasteBuffer += chunk; indexOf(END)`), which is O(n²) in the paste size and dominates when the terminal/PTY delivers the paste in many small reads (SSH, tmux, slow hosts) — a 1 MB paste at 1 KB chunks cost ~33 ms and 5 MB ~740 ms. Chunks are now collected in an array and joined once when the end marker arrives, with a short overlap tail carried across chunk boundaries so a marker split between two reads is still detected without rescanning, making assembly O(n) (~1 ms for 5 MB). The `Editor` paste cleaner also dropped its `split("").filter().join("")` per-code-unit array allocation in favor of a single control-character regex pass (~20× faster on large pastes).
|
|
27
|
+
|
|
5
28
|
## [15.10.4] - 2026-06-08
|
|
6
29
|
|
|
7
30
|
### Fixed
|
|
@@ -37,6 +37,12 @@ export declare class Editor implements Component, Focusable {
|
|
|
37
37
|
decorateText: ((text: string) => string) | undefined;
|
|
38
38
|
borderColor: (str: string) => string;
|
|
39
39
|
onAutocompleteUpdate?: () => void;
|
|
40
|
+
/** Optional pattern matching atomic placeholder tokens (e.g. `[Image #1, 800x600]` or
|
|
41
|
+
* `[Paste #2, +30 lines]`) that the editor treats as indivisible: a backspace or forward-delete
|
|
42
|
+
* landing on any character of a token removes the whole token instead of corrupting it into
|
|
43
|
+
* stray text. MUST be a global regex; the editor recompiles a private copy so its `lastIndex`
|
|
44
|
+
* is never shared with the caller. */
|
|
45
|
+
atomicTokenPattern: RegExp | undefined;
|
|
40
46
|
onSubmit?: (text: string) => void;
|
|
41
47
|
onAltEnter?: (text: string) => void;
|
|
42
48
|
onChange?: (text: string) => void;
|
|
@@ -13,6 +13,9 @@ export declare class Input implements Component, Focusable {
|
|
|
13
13
|
setUseTerminalCursor(useTerminalCursor: boolean): void;
|
|
14
14
|
getUseTerminalCursor(): boolean;
|
|
15
15
|
handleInput(data: string): void;
|
|
16
|
+
/** Apply terminal paste semantics to text from non-bracketed paste transports
|
|
17
|
+
* (e.g. kitty's OSC 5522 enhanced clipboard read). Mirrors `Editor.pasteText`. */
|
|
18
|
+
pasteText(text: string): void;
|
|
16
19
|
invalidate(): void;
|
|
17
20
|
render(width: number): string[];
|
|
18
21
|
}
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -271,6 +271,8 @@ export declare class TUI extends Container {
|
|
|
271
271
|
*/
|
|
272
272
|
setEagerNativeScrollbackRebuild(enabled: boolean): void;
|
|
273
273
|
setFocus(component: Component | null): void;
|
|
274
|
+
/** Component currently receiving keyboard input, if any. */
|
|
275
|
+
getFocused(): Component | null;
|
|
274
276
|
/**
|
|
275
277
|
* Show an overlay component with configurable positioning and sizing.
|
|
276
278
|
* Returns a handle to control the overlay's visibility.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "15.10.
|
|
4
|
+
"version": "15.10.6",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.10.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.10.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.10.6",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.10.6",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.4"
|
|
44
44
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -368,6 +368,15 @@ export class Editor implements Component, Focusable {
|
|
|
368
368
|
#pastes: Map<number, string> = new Map();
|
|
369
369
|
#pasteCounter: number = 0;
|
|
370
370
|
|
|
371
|
+
/** Optional pattern matching atomic placeholder tokens (e.g. `[Image #1, 800x600]` or
|
|
372
|
+
* `[Paste #2, +30 lines]`) that the editor treats as indivisible: a backspace or forward-delete
|
|
373
|
+
* landing on any character of a token removes the whole token instead of corrupting it into
|
|
374
|
+
* stray text. MUST be a global regex; the editor recompiles a private copy so its `lastIndex`
|
|
375
|
+
* is never shared with the caller. */
|
|
376
|
+
atomicTokenPattern: RegExp | undefined;
|
|
377
|
+
#atomicTokenSource: string | undefined;
|
|
378
|
+
#atomicTokenRe: RegExp | undefined;
|
|
379
|
+
|
|
371
380
|
// Bracketed paste mode buffering
|
|
372
381
|
#pasteHandler = new BracketedPasteHandler();
|
|
373
382
|
|
|
@@ -1389,7 +1398,7 @@ export class Editor implements Component, Focusable {
|
|
|
1389
1398
|
#expandPasteMarkers(text: string): string {
|
|
1390
1399
|
let result = text;
|
|
1391
1400
|
for (const [pasteId, pasteContent] of this.#pastes) {
|
|
1392
|
-
const markerRegex = new RegExp(`\\[
|
|
1401
|
+
const markerRegex = new RegExp(`\\[Paste #${pasteId}(?:, (?:\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
1393
1402
|
result = result.replace(markerRegex, () => pasteContent);
|
|
1394
1403
|
}
|
|
1395
1404
|
return result;
|
|
@@ -1627,11 +1636,10 @@ export class Editor implements Component, Focusable {
|
|
|
1627
1636
|
// Convert tabs to spaces (4 spaces per tab)
|
|
1628
1637
|
const tabExpandedText = cleanText.replace(/\t/g, " ");
|
|
1629
1638
|
|
|
1630
|
-
//
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
.join("");
|
|
1639
|
+
// Strip control characters except newline (tabs already expanded above,
|
|
1640
|
+
// CRs already normalized). Single regex pass instead of split/filter/join
|
|
1641
|
+
// to avoid allocating a per-code-unit array for large pastes.
|
|
1642
|
+
let filteredText = tabExpandedText.replace(/[\x00-\x09\x0B-\x1F]/g, "");
|
|
1635
1643
|
|
|
1636
1644
|
// If pasting a file path (starts with /, ~, or .) and the character before
|
|
1637
1645
|
// the cursor is a word character, prepend a space for better readability
|
|
@@ -1654,11 +1662,11 @@ export class Editor implements Component, Focusable {
|
|
|
1654
1662
|
const pasteId = this.#pasteCounter;
|
|
1655
1663
|
this.#pastes.set(pasteId, filteredText);
|
|
1656
1664
|
|
|
1657
|
-
// Insert marker like "[
|
|
1665
|
+
// Insert marker like "[Paste #1, +123 lines]" or "[Paste #1, 1234 chars]"
|
|
1658
1666
|
const marker =
|
|
1659
1667
|
pastedLines.length > 10
|
|
1660
|
-
? `[
|
|
1661
|
-
: `[
|
|
1668
|
+
? `[Paste #${pasteId}, +${pastedLines.length} lines]`
|
|
1669
|
+
: `[Paste #${pasteId}, ${totalChars} chars]`;
|
|
1662
1670
|
this.#insertTextAtCursor(marker);
|
|
1663
1671
|
|
|
1664
1672
|
return;
|
|
@@ -1727,26 +1735,72 @@ export class Editor implements Component, Focusable {
|
|
|
1727
1735
|
if (this.onSubmit) this.onSubmit(result);
|
|
1728
1736
|
}
|
|
1729
1737
|
|
|
1738
|
+
/** Resolve the compiled, global copy of `atomicTokenPattern`, rebuilt only when the source changes. */
|
|
1739
|
+
#getAtomicTokenRe(): RegExp | undefined {
|
|
1740
|
+
const pattern = this.atomicTokenPattern;
|
|
1741
|
+
if (pattern === undefined) {
|
|
1742
|
+
this.#atomicTokenSource = undefined;
|
|
1743
|
+
this.#atomicTokenRe = undefined;
|
|
1744
|
+
return undefined;
|
|
1745
|
+
}
|
|
1746
|
+
if (pattern.source !== this.#atomicTokenSource) {
|
|
1747
|
+
this.#atomicTokenSource = pattern.source;
|
|
1748
|
+
this.#atomicTokenRe = new RegExp(
|
|
1749
|
+
pattern.source,
|
|
1750
|
+
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`,
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
return this.#atomicTokenRe;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/** Find an atomic token on `line` whose span contains column `col` (`start <= col < end`). */
|
|
1757
|
+
#atomicTokenAt(line: string, col: number): { start: number; end: number } | undefined {
|
|
1758
|
+
const re = this.#getAtomicTokenRe();
|
|
1759
|
+
if (re === undefined) return undefined;
|
|
1760
|
+
re.lastIndex = 0;
|
|
1761
|
+
for (;;) {
|
|
1762
|
+
const match = re.exec(line);
|
|
1763
|
+
if (match === null) break;
|
|
1764
|
+
if (match[0].length === 0) {
|
|
1765
|
+
re.lastIndex = match.index + 1;
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
const start = match.index;
|
|
1769
|
+
const end = start + match[0].length;
|
|
1770
|
+
if (col < start) break;
|
|
1771
|
+
if (col < end) return { start, end };
|
|
1772
|
+
}
|
|
1773
|
+
return undefined;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1730
1776
|
#handleBackspace(): void {
|
|
1731
1777
|
this.#historyIndex = -1; // Exit history browsing mode
|
|
1732
1778
|
this.#resetKillSequence();
|
|
1733
1779
|
this.#recordUndoState();
|
|
1734
1780
|
|
|
1735
1781
|
if (this.#state.cursorCol > 0) {
|
|
1736
|
-
// Delete grapheme before cursor (handles emojis, combining characters, etc.)
|
|
1737
1782
|
const line = this.#state.lines[this.#state.cursorLine] || "";
|
|
1738
|
-
|
|
1783
|
+
// An atomic placeholder token (image/paste marker) deletes as a unit, so a single
|
|
1784
|
+
// backspace never leaves a half-eaten `[Paste #1, +30 lines` behind as stray text.
|
|
1785
|
+
const token = this.#atomicTokenAt(line, this.#state.cursorCol - 1);
|
|
1786
|
+
if (token !== undefined) {
|
|
1787
|
+
this.#state.lines[this.#state.cursorLine] = line.slice(0, token.start) + line.slice(token.end);
|
|
1788
|
+
this.#setCursorCol(token.start);
|
|
1789
|
+
} else {
|
|
1790
|
+
// Delete grapheme before cursor (handles emojis, combining characters, etc.)
|
|
1791
|
+
const beforeCursor = line.slice(0, this.#state.cursorCol);
|
|
1739
1792
|
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1793
|
+
// Find the last grapheme in the text before cursor
|
|
1794
|
+
const graphemes = [...segmenter.segment(beforeCursor)];
|
|
1795
|
+
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1796
|
+
const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
1744
1797
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1798
|
+
const before = line.slice(0, this.#state.cursorCol - graphemeLength);
|
|
1799
|
+
const after = line.slice(this.#state.cursorCol);
|
|
1747
1800
|
|
|
1748
|
-
|
|
1749
|
-
|
|
1801
|
+
this.#state.lines[this.#state.cursorLine] = before + after;
|
|
1802
|
+
this.#setCursorCol(this.#state.cursorCol - graphemeLength);
|
|
1803
|
+
}
|
|
1750
1804
|
} else if (this.#state.cursorLine > 0) {
|
|
1751
1805
|
// Merge with previous line
|
|
1752
1806
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
@@ -2218,17 +2272,25 @@ export class Editor implements Component, Focusable {
|
|
|
2218
2272
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2219
2273
|
|
|
2220
2274
|
if (this.#state.cursorCol < currentLine.length) {
|
|
2221
|
-
//
|
|
2222
|
-
const
|
|
2275
|
+
// An atomic placeholder token (image/paste marker) deletes as a unit.
|
|
2276
|
+
const token = this.#atomicTokenAt(currentLine, this.#state.cursorCol);
|
|
2277
|
+
if (token !== undefined) {
|
|
2278
|
+
this.#state.lines[this.#state.cursorLine] =
|
|
2279
|
+
currentLine.slice(0, token.start) + currentLine.slice(token.end);
|
|
2280
|
+
this.#setCursorCol(token.start);
|
|
2281
|
+
} else {
|
|
2282
|
+
// Delete grapheme at cursor position (handles emojis, combining characters, etc.)
|
|
2283
|
+
const afterCursor = currentLine.slice(this.#state.cursorCol);
|
|
2223
2284
|
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2285
|
+
// Find the first grapheme at cursor
|
|
2286
|
+
const graphemes = [...segmenter.segment(afterCursor)];
|
|
2287
|
+
const firstGrapheme = graphemes[0];
|
|
2288
|
+
const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
2228
2289
|
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2290
|
+
const before = currentLine.slice(0, this.#state.cursorCol);
|
|
2291
|
+
const after = currentLine.slice(this.#state.cursorCol + graphemeLength);
|
|
2292
|
+
this.#state.lines[this.#state.cursorLine] = before + after;
|
|
2293
|
+
}
|
|
2232
2294
|
} else if (this.#state.cursorLine < this.#state.lines.length - 1) {
|
|
2233
2295
|
// At end of line - merge with next line
|
|
2234
2296
|
const nextLine = this.#state.lines[this.#state.cursorLine + 1] || "";
|
package/src/components/input.ts
CHANGED
|
@@ -187,6 +187,12 @@ export class Input implements Component, Focusable {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/** Apply terminal paste semantics to text from non-bracketed paste transports
|
|
191
|
+
* (e.g. kitty's OSC 5522 enhanced clipboard read). Mirrors `Editor.pasteText`. */
|
|
192
|
+
pasteText(text: string): void {
|
|
193
|
+
this.#handlePaste(text);
|
|
194
|
+
}
|
|
195
|
+
|
|
190
196
|
#insertCharacter(text: string): void {
|
|
191
197
|
const isWordChunk = [...segmenter.segment(text)].every(seg => getWordNavKind(seg.segment) !== "whitespace");
|
|
192
198
|
// Undo coalescing: consecutive word typing coalesces into one undo unit.
|
package/src/stdin-buffer.ts
CHANGED
|
@@ -265,7 +265,8 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
265
265
|
#timeout?: NodeJS.Timeout;
|
|
266
266
|
readonly #timeoutMs: number;
|
|
267
267
|
#pasteMode: boolean = false;
|
|
268
|
-
#
|
|
268
|
+
#pasteChunks: string[] = [];
|
|
269
|
+
#pasteOverlap: string = "";
|
|
269
270
|
#pendingKittyPrintableCodepoint: number | undefined;
|
|
270
271
|
|
|
271
272
|
constructor(options: StdinBufferOptions = {}) {
|
|
@@ -302,24 +303,9 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
302
303
|
this.#buffer += str;
|
|
303
304
|
|
|
304
305
|
if (this.#pasteMode) {
|
|
305
|
-
|
|
306
|
+
const chunk = this.#buffer;
|
|
306
307
|
this.#buffer = "";
|
|
307
|
-
|
|
308
|
-
const endIndex = this.#pasteBuffer.indexOf(BRACKETED_PASTE_END);
|
|
309
|
-
if (endIndex !== -1) {
|
|
310
|
-
const pastedContent = this.#pasteBuffer.slice(0, endIndex);
|
|
311
|
-
const remaining = this.#pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);
|
|
312
|
-
|
|
313
|
-
this.#pasteMode = false;
|
|
314
|
-
this.#pasteBuffer = "";
|
|
315
|
-
this.#pendingKittyPrintableCodepoint = undefined;
|
|
316
|
-
|
|
317
|
-
this.emit("paste", pastedContent);
|
|
318
|
-
|
|
319
|
-
if (remaining.length > 0) {
|
|
320
|
-
this.process(remaining);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
308
|
+
this.#consumePasteChunk(chunk);
|
|
323
309
|
return;
|
|
324
310
|
}
|
|
325
311
|
|
|
@@ -335,25 +321,12 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
335
321
|
|
|
336
322
|
this.#pendingKittyPrintableCodepoint = undefined;
|
|
337
323
|
this.#buffer = this.#buffer.slice(startIndex + BRACKETED_PASTE_START.length);
|
|
338
|
-
|
|
339
|
-
this.#pasteBuffer = this.#buffer;
|
|
324
|
+
const firstChunk = this.#buffer;
|
|
340
325
|
this.#buffer = "";
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const remaining = this.#pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);
|
|
346
|
-
|
|
347
|
-
this.#pasteMode = false;
|
|
348
|
-
this.#pasteBuffer = "";
|
|
349
|
-
this.#pendingKittyPrintableCodepoint = undefined;
|
|
350
|
-
|
|
351
|
-
this.emit("paste", pastedContent);
|
|
352
|
-
|
|
353
|
-
if (remaining.length > 0) {
|
|
354
|
-
this.process(remaining);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
326
|
+
this.#pasteMode = true;
|
|
327
|
+
this.#pasteChunks = [];
|
|
328
|
+
this.#pasteOverlap = "";
|
|
329
|
+
this.#consumePasteChunk(firstChunk);
|
|
357
330
|
return;
|
|
358
331
|
}
|
|
359
332
|
|
|
@@ -375,6 +348,42 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
375
348
|
}
|
|
376
349
|
}
|
|
377
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Consume one chunk of paste-mode input. Chunks are accumulated in an array
|
|
353
|
+
* and only joined once the end marker arrives, so a large paste delivered in
|
|
354
|
+
* many small terminal reads stays O(total) instead of the O(total^2) cost of
|
|
355
|
+
* re-concatenating and rescanning the whole buffer on every chunk. A short
|
|
356
|
+
* overlap tail (end-marker length - 1) is carried across chunk boundaries so
|
|
357
|
+
* a marker split between two reads is still detected without rescanning.
|
|
358
|
+
*/
|
|
359
|
+
#consumePasteChunk(chunk: string): void {
|
|
360
|
+
const probe = this.#pasteOverlap + chunk;
|
|
361
|
+
if (probe.indexOf(BRACKETED_PASTE_END) === -1) {
|
|
362
|
+
this.#pasteChunks.push(chunk);
|
|
363
|
+
const keep = BRACKETED_PASTE_END.length - 1;
|
|
364
|
+
this.#pasteOverlap = probe.length > keep ? probe.slice(probe.length - keep) : probe;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// End marker arrived: join once and split at its first occurrence,
|
|
369
|
+
// matching the prior indexOf-from-start semantics exactly.
|
|
370
|
+
const flat = this.#pasteChunks.length > 0 ? `${this.#pasteChunks.join("")}${chunk}` : chunk;
|
|
371
|
+
const endIndex = flat.indexOf(BRACKETED_PASTE_END);
|
|
372
|
+
const pastedContent = flat.slice(0, endIndex);
|
|
373
|
+
const remaining = flat.slice(endIndex + BRACKETED_PASTE_END.length);
|
|
374
|
+
|
|
375
|
+
this.#pasteMode = false;
|
|
376
|
+
this.#pasteChunks = [];
|
|
377
|
+
this.#pasteOverlap = "";
|
|
378
|
+
this.#pendingKittyPrintableCodepoint = undefined;
|
|
379
|
+
|
|
380
|
+
this.emit("paste", pastedContent);
|
|
381
|
+
|
|
382
|
+
if (remaining.length > 0) {
|
|
383
|
+
this.process(remaining);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
378
387
|
#emitDataSequence(sequence: string): void {
|
|
379
388
|
const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined;
|
|
380
389
|
if (rawCodepoint !== undefined && rawCodepoint === this.#pendingKittyPrintableCodepoint) {
|
|
@@ -409,7 +418,8 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
409
418
|
}
|
|
410
419
|
this.#buffer = "";
|
|
411
420
|
this.#pasteMode = false;
|
|
412
|
-
this.#
|
|
421
|
+
this.#pasteChunks = [];
|
|
422
|
+
this.#pasteOverlap = "";
|
|
413
423
|
this.#pendingKittyPrintableCodepoint = undefined;
|
|
414
424
|
}
|
|
415
425
|
|
package/src/tui.ts
CHANGED
|
@@ -776,6 +776,11 @@ export class TUI extends Container {
|
|
|
776
776
|
}
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
+
/** Component currently receiving keyboard input, if any. */
|
|
780
|
+
getFocused(): Component | null {
|
|
781
|
+
return this.#focusedComponent;
|
|
782
|
+
}
|
|
783
|
+
|
|
779
784
|
/**
|
|
780
785
|
* Show an overlay component with configurable positioning and sizing.
|
|
781
786
|
* Returns a handle to control the overlay's visibility.
|
|
@@ -3063,13 +3068,22 @@ export class TUI extends Container {
|
|
|
3063
3068
|
|
|
3064
3069
|
this.#maxLinesRendered = options.clearViewport ? lines.length : Math.max(this.#maxLinesRendered, lines.length);
|
|
3065
3070
|
if (options.clearScrollback) {
|
|
3066
|
-
this.#scrollbackHighWater = 0;
|
|
3067
3071
|
this.#suppressNextSuffixScroll = lines.length > height;
|
|
3068
3072
|
}
|
|
3069
3073
|
const pushedNow = Math.max(0, lines.length - height);
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3074
|
+
// A full repaint physically re-emits the entire transcript from row 0, so
|
|
3075
|
+
// the rows committed to native scrollback by this paint are exactly
|
|
3076
|
+
// `pushedNow`. Outside multiplexers an `\x1b[3J` above wiped pre-paint
|
|
3077
|
+
// scrollback as well, so the assignment matches reality. Inside
|
|
3078
|
+
// multiplexers `\x1b[3J` is a no-op and stale pre-paint rows remain in
|
|
3079
|
+
// pane history, but those belong to the previous logical transcript and
|
|
3080
|
+
// must drop out of the renderer's bookkeeping: leaving a stale
|
|
3081
|
+
// `#scrollbackHighWater` above `pushedNow` mis-anchors the pinned emitter
|
|
3082
|
+
// on every subsequent frame, pinning the input box to the top of the pane
|
|
3083
|
+
// after a rewind/branch shrinks the transcript (issue #2130). When
|
|
3084
|
+
// `clearViewport` is false (no current caller), keep the monotonic
|
|
3085
|
+
// behavior so deferred paints do not lower the tracker.
|
|
3086
|
+
this.#scrollbackHighWater = options.clearViewport ? pushedNow : Math.max(this.#scrollbackHighWater, pushedNow);
|
|
3073
3087
|
this.#commit(lines, width, height, Math.max(0, this.#maxLinesRendered - height), cursorControl);
|
|
3074
3088
|
}
|
|
3075
3089
|
|