@sayknow-cli/tui 0.3.16 → 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 +6 -7
  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 +184 -18
  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
 
@@ -2292,11 +2444,9 @@ export class TUI extends Container {
2292
2444
  viewportRepaint(`terminal height changed (${this.#previousHeight} -> ${height})`);
2293
2445
  return;
2294
2446
  }
2295
- if (!isTermuxSession() && !isMultiplexerSession()) {
2296
- logRedraw(`terminal height changed (${this.#previousHeight} -> ${height})`);
2297
- fullRender(true, "terminal height changed");
2298
- return;
2299
- }
2447
+ logRedraw(`terminal height changed (${this.#previousHeight} -> ${height})`);
2448
+ fullRender(true, "terminal height changed");
2449
+ return;
2300
2450
  }
2301
2451
 
2302
2452
  // Content shrunk below the previous render and no overlays - re-render to clear empty rows
@@ -2351,8 +2501,8 @@ export class TUI extends Container {
2351
2501
  }
2352
2502
 
2353
2503
  const nextLiveViewportTop = Math.max(0, newLines.length - height);
2354
- if (firstChanged >= newLines.length && nextLiveViewportTop !== prevViewportTop) {
2355
- 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})`);
2356
2506
  return;
2357
2507
  }
2358
2508
  // All changes are in deleted lines (nothing to render, just clear)
@@ -2598,6 +2748,22 @@ export class TUI extends Container {
2598
2748
  return { seq, toRow: targetRow };
2599
2749
  }
2600
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
+
2601
2767
  /**
2602
2768
  * Register an emitter whose escape payload is appended to every render
2603
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
- }
@@ -1,126 +0,0 @@
1
- import { type AutocompleteProvider } from "../autocomplete";
2
- import type { SymbolTheme } from "../symbols";
3
- import { type Component, type Focusable } from "../tui";
4
- import { type SelectListTheme } from "./select-list";
5
- export interface EditorTheme {
6
- borderColor: (str: string) => string;
7
- selectList: SelectListTheme;
8
- symbols: SymbolTheme;
9
- editorPaddingX?: number;
10
- /** Style function for inline hint/ghost text (dim text after cursor) */
11
- hintStyle?: (text: string) => string;
12
- }
13
- export interface EditorTopBorder {
14
- /** The status content (already styled) */
15
- content: string;
16
- /** Visible width of the content */
17
- width: number;
18
- }
19
- export type EditorBorderStyle = "round" | "sharp";
20
- interface HistoryEntry {
21
- prompt: string;
22
- }
23
- interface HistoryStorage {
24
- add(prompt: string, cwd?: string): Promise<void>;
25
- getRecent(limit: number, cwd?: string): HistoryEntry[];
26
- }
27
- /** Test-only performance counters for advisory baseline tests. */
28
- export declare const __editorPerfCounters: {
29
- layoutTextInvocations: number;
30
- layoutLogicalLinesProcessed: number;
31
- visibleWidthMeasurements: number;
32
- reset(): void;
33
- };
34
- export declare class Editor implements Component, Focusable {
35
- #private;
36
- /** Focusable interface - set by TUI when focus changes */
37
- focused: boolean;
38
- /** When set, replaces the normal cursor glyph at end-of-text with this ANSI-styled string. */
39
- cursorOverride: string | undefined;
40
- /** Display width of the cursorOverride glyph (needed because override may contain ANSI escapes). */
41
- cursorOverrideWidth: number | undefined;
42
- borderColor: (str: string) => string;
43
- onAutocompleteUpdate?: () => void;
44
- onSubmit?: (text: string) => void;
45
- onAltEnter?: (text: string) => void;
46
- onChange?: (text: string) => void;
47
- onAutocompleteCancel?: () => void;
48
- onTabDeclined?: (text: string) => void;
49
- /**
50
- * Called before Tab opens/applies autocomplete. Return true to consume Tab
51
- * for app-level behavior (for example, queueing a draft while a turn runs).
52
- */
53
- onTab?: (text: string) => boolean | undefined;
54
- disableSubmit: boolean;
55
- constructor(theme: EditorTheme);
56
- dispose(): void;
57
- setAutocompleteProvider(provider: AutocompleteProvider): void;
58
- getAutocompleteProvider(): AutocompleteProvider | undefined;
59
- /** Whether the autocomplete dropdown is currently open. */
60
- isAutocompleteOpen(): boolean;
61
- /**
62
- * Set custom content for the top border (e.g., status line).
63
- * Pass undefined to use the default plain border.
64
- */
65
- setTopBorder(content: EditorTopBorder | undefined): void;
66
- /**
67
- * Show or hide the editor border chrome.
68
- */
69
- setBorderVisible(borderVisible: boolean): void;
70
- setBorderStyle(borderStyle: EditorBorderStyle): void;
71
- setClosedBorderBox(closedBorderBox: boolean): void;
72
- setPromptGutter(promptGutter: string | undefined): void;
73
- setInputPrefix(inputPrefix: string | undefined): void;
74
- setPlaceholder(placeholder: string | undefined): void;
75
- /**
76
- * Get the available width for top border content given a total terminal width.
77
- * Accounts for right gutter, border characters, and horizontal padding when visible.
78
- */
79
- getTopBorderAvailableWidth(terminalWidth: number): number;
80
- /**
81
- * Use the real terminal cursor instead of rendering a cursor glyph.
82
- */
83
- setUseTerminalCursor(useTerminalCursor: boolean): void;
84
- getUseTerminalCursor(): boolean;
85
- setMaxHeight(maxHeight: number | undefined): void;
86
- setPaddingX(paddingX: number): void;
87
- setRightGutterWidth(width: number): void;
88
- getAutocompleteMaxVisible(): number;
89
- setAutocompleteMaxVisible(maxVisible: number): void;
90
- setHistoryStorage(storage: HistoryStorage): void;
91
- /**
92
- * Add a prompt to history for up/down arrow navigation.
93
- * Called after successful submission.
94
- */
95
- addToHistory(text: string): void;
96
- invalidate(): void;
97
- render(width: number): string[];
98
- handleInput(data: string): void;
99
- /** Test-only seam: current wrap-cache entry count (memory-bound assertions). */
100
- get wrappedLineCacheSize(): number;
101
- getText(): string;
102
- /**
103
- * Get text with paste markers expanded to their actual content.
104
- * Use this when you need the full content (e.g., for external editor).
105
- */
106
- getExpandedText(): string;
107
- getLines(): string[];
108
- getCursor(): {
109
- line: number;
110
- col: number;
111
- };
112
- moveToLineStart(): void;
113
- moveToLineEnd(): void;
114
- moveToMessageStart(): void;
115
- moveToMessageEnd(): void;
116
- /**
117
- * Undo the last meaningful edit while ignoring transient text that is still present at the cursor.
118
- * Used for command-like autocomplete actions whose typed trigger should not count as the edit being undone.
119
- */
120
- undoPastTransientText(transientText: string): void;
121
- setText(text: string): void;
122
- /** Insert text at the current cursor position */
123
- insertText(text: string): void;
124
- isShowingAutocomplete(): boolean;
125
- }
126
- export {};
@@ -1,18 +0,0 @@
1
- import { type ImageDimensions } from "../terminal-capabilities";
2
- import type { Component } from "../tui";
3
- export interface ImageTheme {
4
- fallbackColor: (str: string) => string;
5
- }
6
- export interface ImageOptions {
7
- maxWidthCells?: number;
8
- maxHeightCells?: number;
9
- filename?: string;
10
- refetch?: () => string;
11
- }
12
- export declare class Image implements Component {
13
- #private;
14
- constructor(base64Data: string, mimeType: string, theme: ImageTheme, options?: ImageOptions, dimensions?: ImageDimensions);
15
- invalidate(): void;
16
- get retainedBase64DataForTest(): string | undefined;
17
- render(width: number): string[];
18
- }