@sayknow-cli/tui 0.3.15 → 0.4.0

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.
Files changed (44) hide show
  1. package/README.md +18 -1
  2. package/package.json +8 -9
  3. package/src/autocomplete.ts +31 -1
  4. package/src/bracketed-paste.ts +106 -29
  5. package/src/components/editor.ts +81 -37
  6. package/src/components/input.ts +5 -1
  7. package/src/components/secret-input.ts +489 -0
  8. package/src/components/select-list.ts +138 -33
  9. package/src/index.ts +1 -0
  10. package/src/keybindings.ts +11 -3
  11. package/src/stdin-buffer.ts +153 -20
  12. package/src/terminal.ts +38 -2
  13. package/src/tui.ts +210 -26
  14. package/dist/types/animation-scheduler.d.ts +0 -13
  15. package/dist/types/autocomplete.d.ts +0 -83
  16. package/dist/types/bracketed-paste.d.ts +0 -26
  17. package/dist/types/components/box.d.ts +0 -20
  18. package/dist/types/components/cancellable-loader.d.ts +0 -21
  19. package/dist/types/components/editor.d.ts +0 -126
  20. package/dist/types/components/image.d.ts +0 -18
  21. package/dist/types/components/input.d.ts +0 -16
  22. package/dist/types/components/loader.d.ts +0 -23
  23. package/dist/types/components/markdown.d.ts +0 -87
  24. package/dist/types/components/sayknow-pet.d.ts +0 -128
  25. package/dist/types/components/select-list.d.ts +0 -46
  26. package/dist/types/components/settings-list.d.ts +0 -39
  27. package/dist/types/components/spacer.d.ts +0 -11
  28. package/dist/types/components/tab-bar.d.ts +0 -56
  29. package/dist/types/components/text.d.ts +0 -22
  30. package/dist/types/components/truncated-text.d.ts +0 -10
  31. package/dist/types/editor-component.d.ts +0 -36
  32. package/dist/types/fuzzy.d.ts +0 -15
  33. package/dist/types/index.d.ts +0 -28
  34. package/dist/types/keybindings.d.ts +0 -201
  35. package/dist/types/keys.d.ts +0 -208
  36. package/dist/types/kill-ring.d.ts +0 -27
  37. package/dist/types/metrics.d.ts +0 -85
  38. package/dist/types/stdin-buffer.d.ts +0 -50
  39. package/dist/types/symbols.d.ts +0 -23
  40. package/dist/types/terminal-capabilities.d.ts +0 -187
  41. package/dist/types/terminal.d.ts +0 -90
  42. package/dist/types/ttyid.d.ts +0 -9
  43. package/dist/types/tui.d.ts +0 -269
  44. package/dist/types/utils.d.ts +0 -110
package/src/tui.ts CHANGED
@@ -45,6 +45,44 @@ type InputListener = (data: string) => InputListenerResult;
45
45
  /**
46
46
  * Component interface - all components must implement this
47
47
  */
48
+ export type MouseEvent = {
49
+ kind: "wheel" | "click";
50
+ direction?: -1 | 1;
51
+ button?: 0;
52
+ /** Terminal cell coordinates, one-based. */
53
+ x: number;
54
+ y: number;
55
+ /** Focused-overlay cell coordinates, one-based when dispatched to an overlay. */
56
+ localX?: number;
57
+ localY?: number;
58
+ };
59
+
60
+ type OverlayMouseBounds = {
61
+ row: number;
62
+ col: number;
63
+ width: number;
64
+ height: number;
65
+ termWidth: number;
66
+ termHeight: number;
67
+ };
68
+
69
+ /** Parse xterm SGR mouse reports. Drag and button-release reports are ignored. */
70
+ export function parseSgrMouseEvent(data: string): MouseEvent | undefined {
71
+ const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
72
+ if (!match) return undefined;
73
+ const button = Number(match[1]);
74
+ const x = Number(match[2]);
75
+ const y = Number(match[3]);
76
+ const terminator = match[4];
77
+ if (![button, x, y].every(Number.isSafeInteger) || x < 1 || y < 1) return undefined;
78
+
79
+ if (button & 32 || terminator === "m") return undefined;
80
+ if (button === 64) return { kind: "wheel", direction: -1, x, y };
81
+ if (button === 65) return { kind: "wheel", direction: 1, x, y };
82
+ if (button === 0) return { kind: "click", button, x, y };
83
+ return undefined;
84
+ }
85
+
48
86
  export interface Component {
49
87
  /**
50
88
  * Render the component to lines for the given viewport width
@@ -58,6 +96,9 @@ export interface Component {
58
96
  */
59
97
  handleInput?(data: string): void;
60
98
 
99
+ /** Optional handler for terminal mouse events when component has focus. */
100
+ handleMouse?(event: MouseEvent): void;
101
+
61
102
  /**
62
103
  * If true, component receives key release events (Kitty protocol).
63
104
  * Default is false - release events are filtered out.
@@ -279,31 +320,41 @@ function isViewportSensitiveHost(
279
320
  env: Record<string, string | undefined>,
280
321
  platform: NodeJS.Platform,
281
322
  includeNativeWindows: boolean,
323
+ includeProcessTerminal: boolean,
282
324
  ): boolean {
283
- return isMultiplexerSession(env) || isWindowsTerminalSession(env) || (includeNativeWindows && platform === "win32");
325
+ return (
326
+ isMultiplexerSession(env) ||
327
+ isWindowsTerminalSession(env) ||
328
+ includeProcessTerminal ||
329
+ (includeNativeWindows && platform === "win32")
330
+ );
284
331
  }
285
332
  /**
286
333
  * True when repainting only the live viewport is safer than clearing/replaying
287
- * the full transcript. Native Windows console hosts are included even when
288
- * WT_SESSION is absent because PowerShell/ConPTY launch chains can drop terminal
289
- * identity variables while keeping the same scroll-jump behavior.
334
+ * the full transcript. Real process terminals are viewport-sensitive because
335
+ * their native scrollback position is not observable by the renderer. Native
336
+ * Windows console hosts are also recognized from platform identity when that
337
+ * process-terminal capability is unavailable.
290
338
  */
291
339
  export function shouldUseViewportRepaintForHost(
292
340
  env: Record<string, string | undefined> = Bun.env,
293
341
  platform: NodeJS.Platform = process.platform,
294
- options: { includeNativeWindows?: boolean } = {},
342
+ options: { includeNativeWindows?: boolean; includeProcessTerminal?: boolean } = {},
295
343
  ): boolean {
296
344
  const multiplexed = isMultiplexerSession(env);
297
345
  const includeNativeWindows = options.includeNativeWindows ?? true;
346
+ const includeProcessTerminal = options.includeProcessTerminal ?? false;
298
347
  return (
299
- isViewportSensitiveHost(env, platform, includeNativeWindows) &&
348
+ isViewportSensitiveHost(env, platform, includeNativeWindows, includeProcessTerminal) &&
300
349
  !(multiplexed && useLegacyMultiplexerFullRender(env))
301
350
  );
302
351
  }
303
352
 
304
353
  function useViewportRepaintPath(terminal: Terminal): boolean {
354
+ if (terminal.isProcessTerminal !== true) return false;
305
355
  return shouldUseViewportRepaintForHost(Bun.env, process.platform, {
306
- includeNativeWindows: terminal.isProcessTerminal === true,
356
+ includeNativeWindows: true,
357
+ includeProcessTerminal: true,
307
358
  });
308
359
  }
309
360
 
@@ -319,7 +370,8 @@ function allowsHostNeutralOverflowRepaint(
319
370
  }
320
371
 
321
372
  function shouldPreserveScrollbackOnFullClear(terminal: Terminal): boolean {
322
- return isViewportSensitiveHost(Bun.env, process.platform, terminal.isProcessTerminal === true);
373
+ if (terminal.isProcessTerminal !== true) return false;
374
+ return isViewportSensitiveHost(Bun.env, process.platform, true, true);
323
375
  }
324
376
 
325
377
  /**
@@ -618,6 +670,7 @@ export class TUI extends Container {
618
670
  #stopped = false;
619
671
  #terminalUnavailable = false;
620
672
  #bottomPinnedComponent: Component | null = null;
673
+ #pendingTerminalCleanup: Array<{ payload: string; onDelivered?: () => void }> = [];
621
674
 
622
675
  #unsubscribeTabWidthChange?: () => void;
623
676
  static #renderCounters: TuiRenderCounterSnapshot = {
@@ -661,9 +714,14 @@ export class TUI extends Container {
661
714
  options?: OverlayOptions;
662
715
  preFocus: Component | null;
663
716
  hidden: boolean;
717
+ mouseBounds?: OverlayMouseBounds;
664
718
  }[] = [];
665
719
 
666
- constructor(terminal: Terminal, showHardwareCursor?: boolean) {
720
+ constructor(
721
+ terminal: Terminal,
722
+ showHardwareCursor?: boolean,
723
+ private readonly options: { enableMouse?: boolean } = {},
724
+ ) {
667
725
  super();
668
726
  this.terminal = terminal;
669
727
  if (showHardwareCursor !== undefined) {
@@ -758,6 +816,50 @@ export class TUI extends Container {
758
816
  if (this.#manualViewportAnchor !== null) this.#reconcileMissingViewportAnchor = true;
759
817
  }
760
818
 
819
+ /** Reveal a semantic viewport anchor without changing the rendered content width. */
820
+ revealViewportAnchor(id: ViewportAnchorId, alignment: "top" | "center" | "bottom"): boolean {
821
+ const height = this.terminal.rows;
822
+ const width = this.terminal.columns;
823
+ const frame = this.#viewportAnchorFrame;
824
+ if (height <= 0 || width <= 0 || this.#previousLines.length === 0 || frame === null) return false;
825
+
826
+ const selectedRow = frame.anchors.findIndex(anchor => anchor?.id === id);
827
+ const selected = selectedRow < 0 ? null : frame.anchors[selectedRow];
828
+ if (selected === null) return false;
829
+
830
+ const desiredScreenRow = alignment === "top" ? 0 : alignment === "center" ? Math.floor(height / 2) : height - 1;
831
+ const targetViewportTop = Math.max(0, frame.startRow + selectedRow - desiredScreenRow);
832
+ this.#manualViewportAnchor = {
833
+ id: selected.id,
834
+ graphemeIndex: selected.graphemeStart,
835
+ cellOffset: selected.cellStart,
836
+ desiredScreenRow,
837
+ };
838
+ const firstCandidateRow = Math.max(0, targetViewportTop - frame.startRow);
839
+ const lastCandidateRow = Math.min(frame.anchors.length, targetViewportTop + height - frame.startRow);
840
+ const fallbacks: ManualViewportAnchor[] = [];
841
+ for (let row = firstCandidateRow; row < lastCandidateRow; row++) {
842
+ const anchor = frame.anchors[row];
843
+ if (anchor === null || row === selectedRow) continue;
844
+ fallbacks.push({
845
+ id: anchor.id,
846
+ graphemeIndex: anchor.graphemeStart,
847
+ cellOffset: anchor.cellStart,
848
+ desiredScreenRow: row + frame.startRow - targetViewportTop,
849
+ });
850
+ }
851
+ fallbacks.sort(
852
+ (a, b) =>
853
+ Math.abs(a.desiredScreenRow - this.#manualViewportAnchor!.desiredScreenRow) -
854
+ Math.abs(b.desiredScreenRow - this.#manualViewportAnchor!.desiredScreenRow),
855
+ );
856
+ this.#manualViewportFallbackAnchors = fallbacks;
857
+ this.#manualViewportTop = this.#viewportTopRow;
858
+ this.#reconcileMissingViewportAnchor = false;
859
+ this.requestRender();
860
+ return true;
861
+ }
862
+
761
863
  scrollViewportPages(direction: -1 | 1): boolean {
762
864
  const height = this.terminal.rows;
763
865
  const width = this.terminal.columns;
@@ -866,7 +968,8 @@ export class TUI extends Container {
866
968
  * Returns a handle to control the overlay's visibility.
867
969
  */
868
970
  showOverlay(component: Component, options?: OverlayOptions): OverlayHandle {
869
- const entry = { component, options, preFocus: this.#focusedComponent, hidden: false };
971
+ const entry = { component, options, preFocus: this.#focusedComponent, hidden: false, mouseBounds: undefined };
972
+
870
973
  this.overlayStack.push(entry);
871
974
  // Only focus if overlay is actually visible
872
975
  if (this.#isOverlayVisible(entry)) {
@@ -880,6 +983,8 @@ export class TUI extends Container {
880
983
  hide: () => {
881
984
  const index = this.overlayStack.indexOf(entry);
882
985
  if (index !== -1) {
986
+ entry.mouseBounds = undefined;
987
+
883
988
  this.overlayStack.splice(index, 1);
884
989
  // Restore focus if this overlay had focus
885
990
  if (this.#focusedComponent === component) {
@@ -893,6 +998,8 @@ export class TUI extends Container {
893
998
  setHidden: (hidden: boolean) => {
894
999
  if (entry.hidden === hidden) return;
895
1000
  entry.hidden = hidden;
1001
+ entry.mouseBounds = undefined;
1002
+
896
1003
  // Update focus when hiding/showing
897
1004
  if (hidden) {
898
1005
  // If this overlay had focus, move focus to next visible or preFocus
@@ -916,6 +1023,7 @@ export class TUI extends Container {
916
1023
  hideOverlay(): void {
917
1024
  const overlay = this.overlayStack.pop();
918
1025
  if (!overlay) return;
1026
+ overlay.mouseBounds = undefined;
919
1027
  // Find topmost visible overlay, or fall back to preFocus
920
1028
  const topVisible = this.#getTopmostVisibleOverlay();
921
1029
  this.setFocus(topVisible?.component ?? overlay.preFocus);
@@ -950,11 +1058,13 @@ export class TUI extends Container {
950
1058
  override invalidate(): void {
951
1059
  super.invalidate();
952
1060
  for (const overlay of this.overlayStack) overlay.component.invalidate?.();
1061
+ for (const overlay of this.overlayStack) overlay.mouseBounds = undefined;
953
1062
  }
954
1063
 
955
1064
  start(): void {
956
1065
  this.#stopped = false;
957
1066
  this.#terminalUnavailable = false;
1067
+ this.terminal.setMouseEnabled?.(this.options.enableMouse === true);
958
1068
  this.terminal.start(
959
1069
  data => this.#handleInput(data),
960
1070
  () => {
@@ -962,6 +1072,7 @@ export class TUI extends Container {
962
1072
  this.requestResizeRender();
963
1073
  },
964
1074
  );
1075
+ this.flushTerminalCleanup();
965
1076
  this.#hideCursor();
966
1077
  this.#querySixelSupport();
967
1078
  this.#queryCellSize();
@@ -1171,6 +1282,7 @@ export class TUI extends Container {
1171
1282
  }
1172
1283
 
1173
1284
  stop(): void {
1285
+ this.flushTerminalCleanup();
1174
1286
  this.#clearSixelProbeState();
1175
1287
  this.#stopped = true;
1176
1288
  if (this.#renderTimer) {
@@ -1229,7 +1341,7 @@ export class TUI extends Container {
1229
1341
  * `PI_TUI_LEGACY_MULTIPLEXER_FULL_RENDER=1` to restore the legacy tmux redraw.
1230
1342
  */
1231
1343
  requestResizeRender(): void {
1232
- this.requestRender(!useViewportRepaintPath(this.terminal) && !isTermuxSession(), "resize");
1344
+ this.requestRender(!useViewportRepaintPath(this.terminal), "resize");
1233
1345
  }
1234
1346
 
1235
1347
  requestRender(force = false, source = "unknown"): void {
@@ -1357,6 +1469,44 @@ export class TUI extends Container {
1357
1469
  data = current;
1358
1470
  }
1359
1471
 
1472
+ const mouse = parseSgrMouseEvent(data);
1473
+ if (mouse) {
1474
+ // Coordinates outside the current terminal cannot name a visible cell.
1475
+ if (mouse.x > this.terminal.columns || mouse.y > this.terminal.rows) return;
1476
+ if (mouse.kind === "wheel") this.scrollViewportPages(mouse.direction!);
1477
+ else {
1478
+ const focusedOverlay = this.overlayStack.find(o => o.component === this.#focusedComponent);
1479
+ if (focusedOverlay) {
1480
+ if (!this.#isOverlayVisible(focusedOverlay)) {
1481
+ focusedOverlay.mouseBounds = undefined;
1482
+ return;
1483
+ }
1484
+ const bounds = focusedOverlay.mouseBounds;
1485
+ if (bounds?.termWidth !== this.terminal.columns || bounds.termHeight !== this.terminal.rows) {
1486
+ return;
1487
+ }
1488
+
1489
+ if (
1490
+ !bounds ||
1491
+ mouse.x < bounds.col + 1 ||
1492
+ mouse.x > bounds.col + bounds.width ||
1493
+ mouse.y < bounds.row + 1 ||
1494
+ mouse.y > bounds.row + bounds.height
1495
+ )
1496
+ return;
1497
+ this.#focusedComponent?.handleMouse?.({
1498
+ ...mouse,
1499
+ localX: mouse.x - bounds.col,
1500
+ localY: mouse.y - bounds.row,
1501
+ });
1502
+ } else this.#focusedComponent?.handleMouse?.(mouse);
1503
+ }
1504
+ this.requestRender(false, "mouse");
1505
+ return;
1506
+ }
1507
+ // SGR-looking reports, including malformed reports, are terminal controls.
1508
+ if (data.startsWith("\x1b[<")) return;
1509
+
1360
1510
  // Consume terminal cell size responses without blocking unrelated input.
1361
1511
  if (this.#consumeCellSizeResponse(data)) {
1362
1512
  return;
@@ -1556,6 +1706,7 @@ export class TUI extends Container {
1556
1706
  #compositeOverlays(lines: string[], termWidth: number, termHeight: number): string[] {
1557
1707
  if (this.overlayStack.length === 0) return lines;
1558
1708
  const result = [...lines];
1709
+ for (const entry of this.overlayStack) entry.mouseBounds = undefined;
1559
1710
 
1560
1711
  // Pre-render all visible overlays and calculate positions
1561
1712
  const rendered: { overlayLines: string[]; row: number; col: number; w: number }[] = [];
@@ -1583,6 +1734,7 @@ export class TUI extends Container {
1583
1734
  const { row, col } = this.#resolveOverlayLayout(options, overlayLines.length, termWidth, termHeight);
1584
1735
 
1585
1736
  rendered.push({ overlayLines, row, col, w: width });
1737
+ entry.mouseBounds = { row, col, width, height: overlayLines.length, termWidth, termHeight };
1586
1738
  minLinesNeeded = Math.max(minLinesNeeded, row + overlayLines.length);
1587
1739
  }
1588
1740
 
@@ -2182,16 +2334,25 @@ export class TUI extends Container {
2182
2334
  this.#previousHeight = height;
2183
2335
  };
2184
2336
 
2185
- const viewportRepaint = (reason: string): void => {
2337
+ const viewportRepaint = (reason: string, absoluteClear = false): void => {
2186
2338
  this.#fullRedrawCount += 1;
2187
2339
  if (renderMetrics.enabled) renderMetrics.recordFullRedraw(reason);
2188
2340
  const nextViewportTop = Math.max(0, newLines.length - height);
2189
- const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
2190
2341
  let buffer = "\x1b[?2026h";
2191
- if (currentScreenRow > 0) {
2192
- buffer += `\x1b[${currentScreenRow}A`;
2342
+ if (absoluteClear) {
2343
+ // A width reflow under a terminal multiplexer re-wraps the on-screen rows to
2344
+ // the new width before SIGWINCH fires, so cursor-relative row math (from the
2345
+ // old width) lands on the wrong physical row and corrupts the repaint. Clear
2346
+ // the visible screen and repaint from absolute home; skip 3J so scrollback
2347
+ // history is preserved.
2348
+ buffer += "\x1b[2J\x1b[H";
2349
+ } else {
2350
+ const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
2351
+ if (currentScreenRow > 0) {
2352
+ buffer += `\x1b[${currentScreenRow}A`;
2353
+ }
2354
+ buffer += "\r";
2193
2355
  }
2194
- buffer += "\r";
2195
2356
  for (let screenRow = 0; screenRow < height; screenRow++) {
2196
2357
  if (screenRow > 0) buffer += "\r\n";
2197
2358
  buffer += "\x1b[2K";
@@ -2253,13 +2414,22 @@ export class TUI extends Container {
2253
2414
 
2254
2415
  // Width changes always need a full re-render because wrapping changes.
2255
2416
  if (widthChanged) {
2417
+ // A forced render (requestRender(true)) resets #previousWidth to -1: that is a
2418
+ // *fake* width change (the terminal never reflowed), so it keeps the cheap
2419
+ // cursor-relative repaint. A *real* width change (a valid prior width that
2420
+ // differs) means the multiplexer already re-wrapped the on-screen rows,
2421
+ // invalidating cursor-relative row math, so it needs the absolute clear.
2422
+ const realWidthChange = this.#previousWidth > 0 && this.#previousWidth !== width;
2256
2423
  logRedraw(`terminal width changed (${this.#previousWidth} -> ${width})`);
2257
2424
  if (useViewportRepaintPath(this.terminal)) {
2258
2425
  // In viewport-repaint sessions a full replay can either pile the transcript
2259
2426
  // back onto scrollback (tmux/screen) or visibly jump to the transcript top
2260
- // (Windows Terminal). Repaint the viewport only, mirroring the height-change
2261
- // branch and neutralizing fake width changes from requestRender(true).
2262
- viewportRepaint(`terminal width changed (${this.#previousWidth} -> ${width})`);
2427
+ // (Windows Terminal). For a real width change the reflow invalidates
2428
+ // cursor-relative math, so clear the visible screen and repaint the viewport
2429
+ // from absolute home (absoluteClear) fixing the corruption without replaying
2430
+ // the whole transcript (no scrollback storm). A fake (force) width change
2431
+ // keeps the in-place relative repaint.
2432
+ viewportRepaint(`terminal width changed (${this.#previousWidth} -> ${width})`, realWidthChange);
2263
2433
  } else {
2264
2434
  fullRender(true, "terminal width changed");
2265
2435
  }
@@ -2274,11 +2444,9 @@ export class TUI extends Container {
2274
2444
  viewportRepaint(`terminal height changed (${this.#previousHeight} -> ${height})`);
2275
2445
  return;
2276
2446
  }
2277
- if (!isTermuxSession() && !isMultiplexerSession()) {
2278
- logRedraw(`terminal height changed (${this.#previousHeight} -> ${height})`);
2279
- fullRender(true, "terminal height changed");
2280
- return;
2281
- }
2447
+ logRedraw(`terminal height changed (${this.#previousHeight} -> ${height})`);
2448
+ fullRender(true, "terminal height changed");
2449
+ return;
2282
2450
  }
2283
2451
 
2284
2452
  // Content shrunk below the previous render and no overlays - re-render to clear empty rows
@@ -2333,8 +2501,8 @@ export class TUI extends Container {
2333
2501
  }
2334
2502
 
2335
2503
  const nextLiveViewportTop = Math.max(0, newLines.length - height);
2336
- if (firstChanged >= newLines.length && nextLiveViewportTop !== prevViewportTop) {
2337
- viewportRepaint(`tail shrink changed viewport top (${prevViewportTop} -> ${nextLiveViewportTop})`);
2504
+ if (newLines.length < this.#previousLines.length && nextLiveViewportTop !== prevViewportTop) {
2505
+ viewportRepaint(`content contraction changed viewport top (${prevViewportTop} -> ${nextLiveViewportTop})`);
2338
2506
  return;
2339
2507
  }
2340
2508
  // All changes are in deleted lines (nothing to render, just clear)
@@ -2580,6 +2748,22 @@ export class TUI extends Container {
2580
2748
  return { seq, toRow: targetRow };
2581
2749
  }
2582
2750
 
2751
+ /** Retain terminal cleanup until a write succeeds, even after its component is disposed. */
2752
+ queueTerminalCleanup(payload: string, onDelivered?: () => void): void {
2753
+ this.#pendingTerminalCleanup.push({ payload, onDelivered });
2754
+ this.flushTerminalCleanup();
2755
+ }
2756
+
2757
+ /** Retry queued terminal cleanup after terminal recovery or before shutdown. */
2758
+ flushTerminalCleanup(): void {
2759
+ while (this.#pendingTerminalCleanup.length > 0) {
2760
+ const pending = this.#pendingTerminalCleanup[0];
2761
+ if (!this.#writeTerminal(pending.payload)) return;
2762
+ this.#pendingTerminalCleanup.shift();
2763
+ pending.onDelivered?.();
2764
+ }
2765
+ }
2766
+
2583
2767
  /**
2584
2768
  * Register an emitter whose escape payload is appended to every render
2585
2769
  * write (inside its own synchronized-output block, cursor saved/restored).
@@ -1,13 +0,0 @@
1
- export type AnimationCadence = 16 | 80;
2
- type AnimationCallback = (now: number) => void;
3
- export interface AnimationRegistration {
4
- unregister(): void;
5
- }
6
- export declare function registerAnimationCallback(callback: AnimationCallback, cadence?: AnimationCadence): AnimationRegistration;
7
- export declare const __animationSchedulerTestHooks: {
8
- getActiveTimerCount(cadence?: AnimationCadence): number;
9
- getRegistrantCount(cadence?: AnimationCadence): number;
10
- getStartedTimerCount(cadence?: AnimationCadence): number;
11
- reset(): void;
12
- };
13
- export {};
@@ -1,83 +0,0 @@
1
- export declare function getSlashCommandMatchRank(query: string, commandName: string): number;
2
- export declare function extractSlashCommandTokenPrefix(text: string): string | null;
3
- export interface AutocompleteItem {
4
- value: string;
5
- label: string;
6
- description?: string;
7
- /** Dim hint text shown inline after cursor when this item is selected */
8
- hint?: string;
9
- }
10
- type Awaitable<T> = T | Promise<T>;
11
- export interface SlashCommand {
12
- name: string;
13
- description?: string;
14
- argumentHint?: string;
15
- /**
16
- * Higher values surface first in autocomplete, ahead of fuzzy-score ordering.
17
- * Use this to pin first-class commands (e.g. bundled SKC skills) to the top.
18
- */
19
- priority?: number;
20
- getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
21
- /** Return inline hint text for the current argument state (shown as dim ghost text after cursor) */
22
- getInlineHint?(argumentText: string): string | null;
23
- }
24
- export interface AutocompleteProvider {
25
- /** Get autocomplete suggestions for current text/cursor position */
26
- getSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
27
- items: AutocompleteItem[];
28
- prefix: string;
29
- } | null>;
30
- /** Apply the selected item and return new text + cursor position */
31
- applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
32
- lines: string[];
33
- cursorLine: number;
34
- cursorCol: number;
35
- onApplied?: () => void;
36
- };
37
- /** Get inline hint text to show as dim ghost text after the cursor */
38
- getInlineHint?(lines: string[], cursorLine: number, cursorCol: number): string | null;
39
- /** Synchronously try to complete a slash command at the start of a line (no async I/O). */
40
- /** Returns matched items and the full prefix, or null if not applicable. */
41
- trySyncSlashCompletion?(textBeforeCursor: string): {
42
- items: AutocompleteItem[];
43
- prefix: string;
44
- } | null;
45
- /**
46
- * Synchronously try to expand text immediately before the cursor (no async I/O).
47
- * Called after every single-character insert. Implementations MUST cheaply
48
- * early-return when the trailing context cannot trigger them.
49
- * Returns the number of characters to delete immediately before the cursor
50
- * and the literal string to insert in their place, or null to leave the
51
- * buffer untouched.
52
- */
53
- trySyncInlineReplace?(textBeforeCursor: string): {
54
- replaceLen: number;
55
- insert: string;
56
- } | null;
57
- }
58
- export declare class CombinedAutocompleteProvider implements AutocompleteProvider {
59
- #private;
60
- constructor(commands?: (SlashCommand | AutocompleteItem)[], basePath?: string);
61
- getSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
62
- items: AutocompleteItem[];
63
- prefix: string;
64
- } | null>;
65
- applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
66
- lines: string[];
67
- cursorLine: number;
68
- cursorCol: number;
69
- };
70
- invalidateDirCache(dir?: string): void;
71
- getForceFileSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
72
- items: AutocompleteItem[];
73
- prefix: string;
74
- } | null>;
75
- shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean;
76
- /** Get inline hint text for slash commands with subcommand hints */
77
- getInlineHint(lines: string[], cursorLine: number, cursorCol: number): string | null;
78
- trySyncSlashCompletion(textBeforeCursor: string): {
79
- items: AutocompleteItem[];
80
- prefix: string;
81
- } | null;
82
- }
83
- export {};
@@ -1,26 +0,0 @@
1
- export type PasteResult = {
2
- handled: false;
3
- } | {
4
- handled: true;
5
- pasteContent?: string;
6
- remaining: string;
7
- };
8
- /**
9
- * Handles bracketed paste mode buffering for terminal input components.
10
- *
11
- * Bracketed paste mode wraps pasted content between start (\x1b[200~) and
12
- * end (\x1b[201~) markers, which may arrive split across multiple chunks.
13
- * This class buffers incoming data and assembles complete paste payloads.
14
- */
15
- export declare class BracketedPasteHandler {
16
- #private;
17
- /**
18
- * Process incoming terminal data for bracketed paste sequences.
19
- *
20
- * @returns `{ handled: false }` if the data contains no paste sequence and
21
- * should be processed normally. `{ handled: true }` if the data was
22
- * consumed by paste buffering — `pasteContent` is set when a complete
23
- * paste has been assembled; omitted when still buffering.
24
- */
25
- process(data: string): PasteResult;
26
- }
@@ -1,20 +0,0 @@
1
- import type { Component } from "../tui";
2
- /**
3
- * Box component - a container that applies padding and background to all children
4
- */
5
- export declare class Box implements Component {
6
- #private;
7
- children: Component[];
8
- constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string);
9
- addChild(component: Component): void;
10
- removeChild(component: Component): void;
11
- /** Remove a child without disposing it (for detach-then-readd reuse). */
12
- detachChild(component: Component): void;
13
- clear(): void;
14
- /** Remove all children without disposing them (for detach-then-readd reuse). */
15
- detachAll(): void;
16
- dispose(): void;
17
- setBgFn(bgFn?: (text: string) => string): void;
18
- invalidate(): void;
19
- render(width: number): string[];
20
- }
@@ -1,21 +0,0 @@
1
- import { Loader } from "./loader";
2
- /**
3
- * Loader that can be cancelled with Escape.
4
- * Extends Loader with an AbortSignal for cancelling async operations.
5
- *
6
- * @example
7
- * const loader = new CancellableLoader(tui, cyan, dim, "Working...");
8
- * loader.onAbort = () => done(null);
9
- * doWork(loader.signal).then(done);
10
- */
11
- export declare class CancellableLoader extends Loader {
12
- #private;
13
- /** Called when user presses Escape */
14
- onAbort?: () => void;
15
- /** AbortSignal that is aborted when user presses Escape */
16
- get signal(): AbortSignal;
17
- /** Whether the loader was aborted */
18
- get aborted(): boolean;
19
- handleInput(data: string): void;
20
- dispose(): void;
21
- }