@prometheus-ai/tui 0.5.3 → 0.5.8
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/dist/types/autocomplete.d.ts +3 -1
- package/dist/types/components/box.d.ts +1 -1
- package/dist/types/components/editor.d.ts +35 -2
- package/dist/types/components/image.d.ts +22 -3
- package/dist/types/components/input.d.ts +6 -1
- package/dist/types/components/loader.d.ts +9 -2
- package/dist/types/components/markdown.d.ts +3 -1
- package/dist/types/components/scroll-view.d.ts +23 -1
- package/dist/types/components/select-list.d.ts +19 -1
- package/dist/types/components/settings-list.d.ts +87 -7
- package/dist/types/components/spacer.d.ts +1 -1
- package/dist/types/components/tab-bar.d.ts +37 -4
- package/dist/types/components/text.d.ts +2 -2
- package/dist/types/components/truncated-text.d.ts +1 -1
- package/dist/types/fuzzy.d.ts +10 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/keybindings.d.ts +5 -3
- package/dist/types/keys.d.ts +1 -1
- package/dist/types/kill-ring.d.ts +0 -7
- package/dist/types/kitty-graphics.d.ts +16 -31
- package/dist/types/loop-watchdog.d.ts +39 -0
- package/dist/types/mouse.d.ts +41 -0
- package/dist/types/stdin-buffer.d.ts +17 -0
- package/dist/types/terminal-capabilities.d.ts +74 -18
- package/dist/types/terminal.d.ts +34 -36
- package/dist/types/tui.d.ts +191 -79
- package/dist/types/utils.d.ts +5 -2
- package/package.json +4 -4
- package/src/autocomplete.ts +79 -65
- package/src/components/box.ts +43 -63
- package/src/components/editor.ts +471 -136
- package/src/components/image.ts +85 -9
- package/src/components/input.ts +12 -3
- package/src/components/loader.ts +35 -21
- package/src/components/markdown.ts +174 -53
- package/src/components/scroll-view.ts +63 -2
- package/src/components/select-list.ts +233 -38
- package/src/components/settings-list.ts +626 -64
- package/src/components/spacer.ts +9 -5
- package/src/components/tab-bar.ts +153 -28
- package/src/components/text.ts +6 -2
- package/src/components/truncated-text.ts +10 -2
- package/src/fuzzy.ts +214 -59
- package/src/index.ts +3 -1
- package/src/keybindings.ts +72 -14
- package/src/keys.ts +1 -1
- package/src/kill-ring.ts +5 -0
- package/src/kitty-graphics.ts +2 -101
- package/src/loop-watchdog.ts +106 -0
- package/src/mouse.ts +55 -0
- package/src/stdin-buffer.ts +291 -81
- package/src/terminal-capabilities.ts +206 -168
- package/src/terminal.ts +367 -110
- package/src/tui.ts +2102 -1729
- package/src/utils.ts +92 -60
package/dist/types/tui.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ImageBudget } from "./components/image";
|
|
2
|
-
import type
|
|
2
|
+
import { type Terminal } from "./terminal";
|
|
3
3
|
import { visibleWidth } from "./utils";
|
|
4
4
|
type InputListenerResult = {
|
|
5
5
|
consume?: boolean;
|
|
6
6
|
data?: string;
|
|
7
7
|
} | undefined;
|
|
8
8
|
type InputListener = (data: string) => InputListenerResult;
|
|
9
|
+
type StartListener = () => void;
|
|
9
10
|
export interface RenderTimer {
|
|
10
11
|
cancel(): void;
|
|
11
12
|
}
|
|
@@ -17,16 +18,32 @@ export interface RenderScheduler {
|
|
|
17
18
|
export interface TUIOptions {
|
|
18
19
|
renderScheduler?: RenderScheduler;
|
|
19
20
|
}
|
|
21
|
+
export interface TUIStartOptions {
|
|
22
|
+
/** Clear saved native scrollback before the first paint. */
|
|
23
|
+
clearScrollback?: boolean;
|
|
24
|
+
}
|
|
20
25
|
/**
|
|
21
26
|
* Component interface - all components must implement this
|
|
27
|
+
*
|
|
28
|
+
* Render contract: the returned array (and its rows) belongs to the component.
|
|
29
|
+
* Callers MUST NOT mutate it — components are allowed to return a cached array
|
|
30
|
+
* and will return the exact same reference for as long as their rendered
|
|
31
|
+
* content is unchanged. Conversely, a component MUST return a fresh array
|
|
32
|
+
* reference whenever its content changed; reference equality across two
|
|
33
|
+
* render() calls is the engine's proof that the rows are byte-identical
|
|
34
|
+
* (containers memoize their concatenation on it, and the TUI derives the
|
|
35
|
+
* frame's stable prefix from it). A component that mutates a previously
|
|
36
|
+
* returned array in place must implement {@link RenderStablePrefix} to declare
|
|
37
|
+
* which leading rows survived.
|
|
22
38
|
*/
|
|
23
39
|
export interface Component {
|
|
24
40
|
/**
|
|
25
|
-
* Render the component to
|
|
26
|
-
*
|
|
27
|
-
*
|
|
41
|
+
* Render the component to an array of physical rows at the given width.
|
|
42
|
+
* The result is component-owned and `readonly` to the caller; an unchanged
|
|
43
|
+
* component may (and should) return the same array reference it returned
|
|
44
|
+
* last time.
|
|
28
45
|
*/
|
|
29
|
-
render(width: number): string[];
|
|
46
|
+
render(width: number): readonly string[];
|
|
30
47
|
/**
|
|
31
48
|
* Optional handler for keyboard input when component has focus
|
|
32
49
|
*/
|
|
@@ -37,32 +54,108 @@ export interface Component {
|
|
|
37
54
|
*/
|
|
38
55
|
wantsKeyRelease?: boolean;
|
|
39
56
|
/**
|
|
40
|
-
*
|
|
57
|
+
* Optional hook to invalidate any cached rendering state.
|
|
41
58
|
* Called when theme changes or when component needs to re-render from scratch.
|
|
42
59
|
*/
|
|
43
|
-
invalidate(): void;
|
|
60
|
+
invalidate?(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Optional teardown. Called when the component is permanently removed from
|
|
63
|
+
* the live tree (e.g. a transcript reset). Release timers, intervals, and
|
|
64
|
+
* subscriptions here. Must be idempotent. Containers propagate dispose to
|
|
65
|
+
* their children; leaf components without resources may omit it.
|
|
66
|
+
*/
|
|
67
|
+
dispose?(): void;
|
|
44
68
|
}
|
|
45
69
|
/**
|
|
46
|
-
*
|
|
47
|
-
* renders a
|
|
48
|
-
* line index where that suffix begins after each render.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
70
|
+
* Component seam for append-only native-scrollback commits. A component that
|
|
71
|
+
* renders a finalized prefix followed by a live/mutating suffix reports the
|
|
72
|
+
* local line index where that suffix begins after each render. The engine
|
|
73
|
+
* commits rows to native scrollback only up to that boundary; everything
|
|
74
|
+
* below repaints in place inside the visible window and never enters history
|
|
75
|
+
* until it finalizes.
|
|
51
76
|
*
|
|
52
77
|
* `getNativeScrollbackCommitSafeEnd` optionally reports a *deeper* boundary
|
|
53
|
-
* inside
|
|
54
|
-
* append-only (
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
78
|
+
* inside the live suffix: the line index up to which the live region is
|
|
79
|
+
* append-only (earlier rows never re-layout — a streaming assistant message).
|
|
80
|
+
* Rows in `[liveRegionStart, commitSafeEnd)` may commit even though they are
|
|
81
|
+
* technically live, because they will never change. Without it, a single live
|
|
82
|
+
* block that alone overflows the window would hold its scrolled-off head out
|
|
83
|
+
* of history until it finalizes. Volatile live blocks (tool previews that
|
|
84
|
+
* collapse) omit it. Defaults to `liveRegionStart` when absent; a root that
|
|
85
|
+
* reports no seam at all commits everything that scrolls (shell semantics).
|
|
86
|
+
* `getNativeScrollbackSnapshotSafeEnd` optionally reports a still deeper
|
|
87
|
+
* boundary: the line index up to which the live region is *durable* — its rows
|
|
88
|
+
* may still change bytes later (a streaming markdown table re-aligning its
|
|
89
|
+
* columns every row), but their CURRENT snapshot is permanent content, so
|
|
90
|
+
* dropping them when they scroll above the window is forbidden. Unlike
|
|
91
|
+
* `commitSafeEnd` (byte-stable: offered rows are asserted never to re-layout and
|
|
92
|
+
* stay under the committed-prefix audit), rows committed under the snapshot end
|
|
93
|
+
* are audit-EXEMPT once they pass the window top — the engine appends their
|
|
94
|
+
* scroll-off snapshot and never recommits them, so later layout drift becomes a
|
|
95
|
+
* frozen stale row in history (duplication never loss) instead of either a
|
|
96
|
+
* dropped row or an audit re-anchor spray. Provisional live blocks (collapsing
|
|
97
|
+
* tool/edit previews whose head is a throwaway tail window) omit it. Defaults to
|
|
98
|
+
* `commitSafeEnd ?? liveRegionStart` when absent.
|
|
99
|
+
*
|
|
100
|
+
* When several root children report a seam in the same frame, the topmost
|
|
101
|
+
* one (and its commit-safe / snapshot-safe extension) defines the boundary:
|
|
102
|
+
* commits are prefix-only, so everything below the first seam is already
|
|
103
|
+
* excluded.
|
|
62
104
|
*/
|
|
63
105
|
export interface NativeScrollbackLiveRegion {
|
|
64
106
|
getNativeScrollbackLiveRegionStart(): number | undefined;
|
|
65
107
|
getNativeScrollbackCommitSafeEnd?(): number | undefined;
|
|
108
|
+
getNativeScrollbackSnapshotSafeEnd?(): number | undefined;
|
|
109
|
+
}
|
|
110
|
+
export interface NativeScrollbackCommittedRows {
|
|
111
|
+
setNativeScrollbackCommittedRows(rows: number): void;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Opt-in stability report for components that mutate their returned render
|
|
115
|
+
* array in place across frames (instead of returning a fresh array per
|
|
116
|
+
* change). The engine reads it right after the component's `render()` returns:
|
|
117
|
+
* the report counts the leading rows of the just-returned array that are
|
|
118
|
+
* byte-identical to the array state the reader last observed. The engine uses
|
|
119
|
+
* it to reuse the composed frame's prefix — skipping marker extraction, line
|
|
120
|
+
* preparation, and the committed-prefix audit for those rows.
|
|
121
|
+
*
|
|
122
|
+
* Contract:
|
|
123
|
+
* - Reading CONSUMES the report: it re-bases the baseline to the current
|
|
124
|
+
* array state. The accumulated count therefore covers every render since
|
|
125
|
+
* the previous read, so out-of-band `render()` calls between engine frames
|
|
126
|
+
* (an exporter walking the tree) can only lower the report, never inflate
|
|
127
|
+
* it past what the engine actually has.
|
|
128
|
+
* - An implementer that cannot prove stability for a frame must lower the
|
|
129
|
+
* accumulated count to 0 for that render.
|
|
130
|
+
* - Rows at or beyond the report may have been mutated in place; rows before
|
|
131
|
+
* it must be the identical string values at the identical indices.
|
|
132
|
+
*/
|
|
133
|
+
export interface RenderStablePrefix {
|
|
134
|
+
getRenderStablePrefixRows(): number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Opt-in fast path for composing only the visible tail of a tall component
|
|
138
|
+
* during a terminal resize. A drag emits a SIGWINCH burst, and the width
|
|
139
|
+
* changes on every event: a full compose re-lays-out (and, for markdown,
|
|
140
|
+
* re-lexes) the entire transcript per event — O(history) work that is
|
|
141
|
+
* discarded the instant the next event arrives. While the resize is in flight
|
|
142
|
+
* the engine paints only the viewport, so it asks each tall root child for at
|
|
143
|
+
* most `maxRows` rows from the bottom of its render at `width` and skips
|
|
144
|
+
* composing everything above the fold. The authoritative full paint replays
|
|
145
|
+
* once the drag settles (see {@link TUI} resize handling).
|
|
146
|
+
*
|
|
147
|
+
* Contract:
|
|
148
|
+
* - Returns the BOTTOM rows of the component's full render at `width`, in
|
|
149
|
+
* top-to-bottom order, capped at `maxRows` (fewer when the component is
|
|
150
|
+
* shorter). The rows MUST be byte-identical to the corresponding tail of
|
|
151
|
+
* what `render(width)` would have returned, modulo a one-row separator at
|
|
152
|
+
* the very top edge (a transient frame the settle paint overwrites).
|
|
153
|
+
* - MUST NOT mutate any persistent full-compose state: the next `render()`
|
|
154
|
+
* (the settle paint) has to reconcile exactly as if the tail render never
|
|
155
|
+
* happened. Warming pure per-width render caches is fine and desirable.
|
|
156
|
+
*/
|
|
157
|
+
export interface ViewportTailProvider {
|
|
158
|
+
renderViewportTail(width: number, maxRows: number): readonly string[];
|
|
66
159
|
}
|
|
67
160
|
/**
|
|
68
161
|
* Interface for components that can receive focus and display a cursor.
|
|
@@ -85,22 +178,6 @@ export interface Focusable {
|
|
|
85
178
|
export interface RenderRequestOptions {
|
|
86
179
|
/** Clear terminal scrollback for intentional transcript replacement. */
|
|
87
180
|
clearScrollback?: boolean;
|
|
88
|
-
/**
|
|
89
|
-
* Bypass the unknown-Windows-viewport deferral for this render so the
|
|
90
|
-
* caller's intentional live UI mutation reaches the terminal even when
|
|
91
|
-
* `Terminal#isNativeViewportAtBottom()` cannot answer.
|
|
92
|
-
*
|
|
93
|
-
* Use only for renders driven by direct user interaction (autocomplete
|
|
94
|
-
* updates, IME, etc.). Any background/offscreen transcript change that
|
|
95
|
-
* coalesces into the same frame WILL also bypass the deferral and reach
|
|
96
|
-
* native scrollback — that is the trade-off, and the reason ordinary
|
|
97
|
-
* `requestRender()` calls must continue to omit this flag.
|
|
98
|
-
*/
|
|
99
|
-
allowUnknownViewportMutation?: boolean;
|
|
100
|
-
}
|
|
101
|
-
/** Options for deferred native scrollback rebuild checkpoints. Reserved for API stability. */
|
|
102
|
-
export interface NativeScrollbackRefreshOptions {
|
|
103
|
-
allowUnknownViewport?: boolean;
|
|
104
181
|
}
|
|
105
182
|
/** Type guard to check if a component implements Focusable */
|
|
106
183
|
export declare function isFocusable(component: Component | null): component is Component & Focusable;
|
|
@@ -156,6 +233,15 @@ export interface OverlayOptions {
|
|
|
156
233
|
* Called each render cycle with current terminal dimensions.
|
|
157
234
|
*/
|
|
158
235
|
visible?: (termWidth: number, termHeight: number) => boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Borrow the terminal's alternate screen buffer for this overlay's lifetime
|
|
238
|
+
* (vim/less idiom). While the topmost visible overlay sets this, the engine
|
|
239
|
+
* paints only the modal on the alt screen and emits no ED3 / scrollback
|
|
240
|
+
* bytes, so the transcript on the normal screen stays untouched and is not
|
|
241
|
+
* scrollable behind the modal. Defaults off — all other overlays are
|
|
242
|
+
* unchanged and still draw over the transcript on the normal screen.
|
|
243
|
+
*/
|
|
244
|
+
fullscreen?: boolean;
|
|
159
245
|
}
|
|
160
246
|
/**
|
|
161
247
|
* Handle returned by showOverlay for controlling the overlay
|
|
@@ -172,13 +258,40 @@ export interface OverlayHandle {
|
|
|
172
258
|
* Container - a component that contains other components
|
|
173
259
|
*/
|
|
174
260
|
export declare class Container implements Component {
|
|
261
|
+
#private;
|
|
175
262
|
children: Component[];
|
|
176
263
|
addChild(component: Component): void;
|
|
177
264
|
removeChild(component: Component): void;
|
|
178
265
|
clear(): void;
|
|
179
266
|
invalidate(): void;
|
|
180
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Propagate teardown to children. Call when the container's children are
|
|
269
|
+
* being permanently discarded (not when they are detached for reuse — use
|
|
270
|
+
* {@link clear} for that). Idempotent per child via each child's own dispose.
|
|
271
|
+
*/
|
|
272
|
+
dispose(): void;
|
|
273
|
+
render(width: number): readonly string[];
|
|
181
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Decide whether `frame` still aligns with the committed prefix, and where to
|
|
277
|
+
* re-anchor the commit index when it does not. Returns the resync row index,
|
|
278
|
+
* or -1 when no resync is needed.
|
|
279
|
+
*
|
|
280
|
+
* The detector exploits the asymmetry between the two mutation classes: an
|
|
281
|
+
* in-place edit or restyle of committed rows disturbs only the touched rows
|
|
282
|
+
* (alignment below them is intact — the stale copy in history is the
|
|
283
|
+
* long-accepted artifact), while any insertion or deletion shifts EVERY row
|
|
284
|
+
* below it, including the rows just above the commit boundary. So the prefix
|
|
285
|
+
* *tail* is sampled (up to 8 non-blank rows within the last 24, compared
|
|
286
|
+
* SGR-stripped so theme changes stay quiet, tolerating one mismatch for a
|
|
287
|
+
* legitimate single-row edit): aligned ⇒ no resync; misaligned ⇒ resync at
|
|
288
|
+
* the first non-equivalent row, recommitting from there — duplication, never
|
|
289
|
+
* loss. Highly repetitive tails (identical filler rows) can mask a shift, in
|
|
290
|
+
* which case the skipped rows are content-identical to the committed ones —
|
|
291
|
+
* observationally harmless. Exported for the render-stress harness, whose
|
|
292
|
+
* shadow commit ledger must mirror the engine's law exactly.
|
|
293
|
+
*/
|
|
294
|
+
export declare function findCommittedPrefixResync(frame: readonly string[], prefix: readonly string[], auditLimit?: number): number;
|
|
182
295
|
/**
|
|
183
296
|
* TUI - Main class for managing terminal UI with differential rendering
|
|
184
297
|
*/
|
|
@@ -194,8 +307,16 @@ export declare class TUI extends Container {
|
|
|
194
307
|
hidden: boolean;
|
|
195
308
|
}[];
|
|
196
309
|
constructor(terminal: Terminal, showHardwareCursor?: boolean, options?: TUIOptions);
|
|
197
|
-
render(width: number): string[];
|
|
310
|
+
render(width: number): readonly string[];
|
|
198
311
|
get fullRedraws(): number;
|
|
312
|
+
/**
|
|
313
|
+
* Transient viewport-only paints emitted by the non-multiplexer resize fast
|
|
314
|
+
* path. These never touch native scrollback or the commit ledger, so they
|
|
315
|
+
* are counted apart from {@link fullRedraws}.
|
|
316
|
+
*/
|
|
317
|
+
get resizeViewportPaints(): number;
|
|
318
|
+
/** Whether a non-multiplexer resize drag is currently in flight. */
|
|
319
|
+
get resizeViewportActive(): boolean;
|
|
199
320
|
/** Shared budget that caps how many inline images render as live graphics. */
|
|
200
321
|
get imageBudget(): ImageBudget;
|
|
201
322
|
/**
|
|
@@ -206,44 +327,16 @@ export declare class TUI extends Container {
|
|
|
206
327
|
setMaxInlineImages(cap: number): void;
|
|
207
328
|
getShowHardwareCursor(): boolean;
|
|
208
329
|
setShowHardwareCursor(enabled: boolean): void;
|
|
209
|
-
getClearOnShrink(): boolean;
|
|
210
|
-
/**
|
|
211
|
-
* Set whether to trigger full re-render when content shrinks.
|
|
212
|
-
* When true (default), empty rows are cleared when content shrinks.
|
|
213
|
-
* When false, empty rows remain (reduces redraws on slower terminals).
|
|
214
|
-
*/
|
|
215
|
-
setClearOnShrink(enabled: boolean): void;
|
|
216
330
|
/**
|
|
217
331
|
* Whether DEC 2026 synchronized-output wrappers are currently emitted around
|
|
218
|
-
* paints. Starts from conservative terminal/env detection and is
|
|
219
|
-
*
|
|
332
|
+
* paints. Starts from conservative terminal/env detection and is reconciled at
|
|
333
|
+
* runtime against the terminal's DECRQM mode-2026 report — enabled on a
|
|
334
|
+
* positive report, disabled on a negative one.
|
|
220
335
|
*/
|
|
221
336
|
get synchronizedOutput(): boolean;
|
|
222
|
-
/**
|
|
223
|
-
* When enabled, live render frames rebuild native scrollback on offscreen and
|
|
224
|
-
* structural changes even when the viewport position is unobservable (POSIX,
|
|
225
|
-
* where `isNativeViewportAtBottom()` is `undefined`), instead of deferring to a
|
|
226
|
-
* non-destructive repaint. This trades the anti-yank guarantee for a clean,
|
|
227
|
-
* duplicate-free history and is meant for windows where output above the fold
|
|
228
|
-
* is actively re-rendering — e.g. a tool whose result is still streaming and
|
|
229
|
-
* re-laying-out rows that have already scrolled into history. A terminal that
|
|
230
|
-
* reports a *known*-scrolled viewport still defers, as does native Windows
|
|
231
|
-
* (the viewport is never observable there and ConPTY hosts erase host
|
|
232
|
-
* scrollback on ED3 — #1635/#1746); only the unknown POSIX case is forced to
|
|
233
|
-
* rebuild. POSIX hosts known to disturb scrolled readers on xterm ED3
|
|
234
|
-
* (`CSI 3 J`, erase saved lines) also defer the eager opt-in; checkpoint
|
|
235
|
-
* rebuilds are unaffected.
|
|
236
|
-
*
|
|
237
|
-
* Disabling stays active through one already-requested frame: the event batch
|
|
238
|
-
* that ends a foreground stream both removes its UI rows (loader/status
|
|
239
|
-
* teardown — a shrink) and clears this flag before the throttled render timer
|
|
240
|
-
* fires. If the flag dropped immediately, that teardown frame would hit the
|
|
241
|
-
* ED3-risk idle deferral and freeze on screen (stale spinner) until the next
|
|
242
|
-
* keystroke. When no render is pending, disable immediately so a later
|
|
243
|
-
* unrelated content mutation does not inherit foreground-stream privileges.
|
|
244
|
-
*/
|
|
245
|
-
setEagerNativeScrollbackRebuild(enabled: boolean): void;
|
|
246
337
|
setFocus(component: Component | null): void;
|
|
338
|
+
/** Component currently receiving keyboard input, if any. */
|
|
339
|
+
getFocused(): Component | null;
|
|
247
340
|
/**
|
|
248
341
|
* Show an overlay component with configurable positioning and sizing.
|
|
249
342
|
* Returns a handle to control the overlay's visibility.
|
|
@@ -254,22 +347,41 @@ export declare class TUI extends Container {
|
|
|
254
347
|
/** Check if there are any visible overlays */
|
|
255
348
|
hasOverlay(): boolean;
|
|
256
349
|
invalidate(): void;
|
|
257
|
-
start(): void;
|
|
350
|
+
start(options?: TUIStartOptions): void;
|
|
351
|
+
addStartListener(listener: StartListener): () => void;
|
|
258
352
|
addInputListener(listener: InputListener): () => void;
|
|
259
353
|
removeInputListener(listener: InputListener): void;
|
|
260
354
|
stop(): void;
|
|
261
|
-
/**
|
|
262
|
-
* Rebuild native terminal scrollback if live rendering deferred a history rewrite.
|
|
263
|
-
* Callers should only invoke this at checkpoints where the user is expected to be
|
|
264
|
-
* at the terminal bottom, such as after submitting a new prompt.
|
|
265
|
-
*/
|
|
266
|
-
refreshNativeScrollbackIfDirty(_options?: NativeScrollbackRefreshOptions): boolean;
|
|
267
355
|
/**
|
|
268
356
|
* Force an immediate full replay of the current frame, including native
|
|
269
357
|
* scrollback. This is the keyboard-accessible equivalent of the resize reset:
|
|
270
358
|
* no queued diff frame or terminal scrollback probe can downgrade it to a
|
|
271
359
|
* viewport-only repaint.
|
|
360
|
+
*
|
|
361
|
+
* Invalidates every component first so the replay reflects current state. A
|
|
362
|
+
* geometry-driven reset thaws frozen scrollback snapshots implicitly (the new
|
|
363
|
+
* width misses every cached snapshot), but a same-width reset would otherwise
|
|
364
|
+
* replay stale snapshots — leaving host-frozen blocks (e.g. a transcript whose
|
|
365
|
+
* committed rows are immutable on ED3-risk terminals) showing pre-mutation
|
|
366
|
+
* content. Invalidation is the generic signal those containers use to retire
|
|
367
|
+
* their snapshots, which is exactly what a user-driven display reset wants.
|
|
272
368
|
*/
|
|
273
369
|
resetDisplay(): void;
|
|
274
370
|
requestRender(force?: boolean, options?: RenderRequestOptions): void;
|
|
371
|
+
/**
|
|
372
|
+
* Schedule a render on behalf of `component` after a self-contained change
|
|
373
|
+
* (spinner frame, blink) that cannot have affected any other component.
|
|
374
|
+
*
|
|
375
|
+
* When every request since the last frame is component-scoped and the
|
|
376
|
+
* frame is otherwise quiet — no resize or geometry change, no overlays, no
|
|
377
|
+
* live inline images, no forced repaint, unchanged root child list — the
|
|
378
|
+
* next compose re-renders only the root subtrees containing the requesting
|
|
379
|
+
* components and reuses the previous frame's rows (and seam reports) for
|
|
380
|
+
* every other root child, skipping the full component-tree walk that makes
|
|
381
|
+
* long transcripts expensive to repaint at animation rate. Any concurrent
|
|
382
|
+
* full request or unsafe condition downgrades the frame to a normal full
|
|
383
|
+
* compose, so this is never less correct than `requestRender()` — only
|
|
384
|
+
* cheaper.
|
|
385
|
+
*/
|
|
386
|
+
requestComponentRender(component: Component): void;
|
|
275
387
|
}
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -33,9 +33,12 @@ export declare function padding(n: number): string;
|
|
|
33
33
|
* Get the shared grapheme segmenter instance.
|
|
34
34
|
*/
|
|
35
35
|
export declare function getSegmenter(): Intl.Segmenter;
|
|
36
|
-
export declare function visibleWidthRaw(str: string): number;
|
|
37
36
|
/**
|
|
38
|
-
*
|
|
37
|
+
* Visible width of a string in terminal columns, excluding ANSI/OSC escapes.
|
|
38
|
+
*
|
|
39
|
+
* `Bun.stringWidth` does the heavy lifting (UAX#11 width tables + ANSI/OSC
|
|
40
|
+
* stripping); this adds the two corrections it omits — tabs (expanded to
|
|
41
|
+
* `tabWidth` cells) and OSC 66 text-sizing payloads (scaled by `s=`).
|
|
39
42
|
*/
|
|
40
43
|
export declare function visibleWidth(str: string): number;
|
|
41
44
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@prometheus-ai/tui",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.8",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://prometheus.trivlab.com",
|
|
7
7
|
"author": "Uttam Trivedi",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"fmt": "biome format --write ."
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@prometheus-ai/natives": "0.5.
|
|
38
|
-
"@prometheus-ai/utils": "0.5.
|
|
37
|
+
"@prometheus-ai/natives": "0.5.8",
|
|
38
|
+
"@prometheus-ai/utils": "0.5.8",
|
|
39
39
|
"lru-cache": "11.5.1",
|
|
40
|
-
"marked": "^18.0.
|
|
40
|
+
"marked": "^18.0.5"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"chalk": "^5.6.2",
|
package/src/autocomplete.ts
CHANGED
|
@@ -160,6 +160,7 @@ type Awaitable<T> = T | Promise<T>;
|
|
|
160
160
|
|
|
161
161
|
export interface SlashCommand {
|
|
162
162
|
name: string;
|
|
163
|
+
aliases?: string[];
|
|
163
164
|
description?: string;
|
|
164
165
|
argumentHint?: string;
|
|
165
166
|
// Function to get argument completions for this command
|
|
@@ -210,9 +211,81 @@ export interface AutocompleteProvider {
|
|
|
210
211
|
trySyncInlineReplace?(textBeforeCursor: string): { replaceLen: number; insert: string } | null;
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
type CommandEntry = SlashCommand | AutocompleteItem;
|
|
215
|
+
|
|
216
|
+
function getCommandName(cmd: CommandEntry): string | undefined {
|
|
217
|
+
return "name" in cmd ? cmd.name : cmd.value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getCommandAliases(cmd: CommandEntry): string[] {
|
|
221
|
+
if (!("aliases" in cmd) || !Array.isArray(cmd.aliases)) return [];
|
|
222
|
+
return cmd.aliases.filter(alias => typeof alias === "string" && alias.length > 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function commandMatchesNameOrAlias(cmd: CommandEntry, commandName: string): boolean {
|
|
226
|
+
const name = getCommandName(cmd);
|
|
227
|
+
if (name === commandName) return true;
|
|
228
|
+
return getCommandAliases(cmd).includes(commandName);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function scoreCommandTextMatch(lowerPrefix: string, lowerTarget: string): number {
|
|
232
|
+
if (lowerPrefix.length === 0) return 1;
|
|
233
|
+
if (lowerPrefix === lowerTarget) return 1000;
|
|
234
|
+
// Flat score for every prefix match so same-prefix commands keep registry
|
|
235
|
+
// order under the stable sort. A length penalty here would rank the shorter
|
|
236
|
+
// name first (e.g. `/set` → `setup` above `settings`), silently changing the
|
|
237
|
+
// command that the sync-completion path applies on Enter.
|
|
238
|
+
if (lowerTarget.startsWith(lowerPrefix)) return 900;
|
|
239
|
+
return fuzzyMatch(lowerPrefix, lowerTarget) ? fuzzyScore(lowerPrefix, lowerTarget) : 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildSlashCommandCompletions(commands: CommandEntry[], lowerPrefix: string): AutocompleteItem[] {
|
|
243
|
+
return commands
|
|
244
|
+
.flatMap(cmd => {
|
|
245
|
+
const name = getCommandName(cmd);
|
|
246
|
+
if (!name) return [];
|
|
247
|
+
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
248
|
+
const desc = cmd.description ?? "";
|
|
249
|
+
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
250
|
+
const candidates: Array<AutocompleteItem & { score: number }> = [];
|
|
251
|
+
|
|
252
|
+
const nameScore = scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
|
|
253
|
+
const lowerDesc = desc.toLowerCase();
|
|
254
|
+
const descScore =
|
|
255
|
+
lowerDesc && fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
256
|
+
const primaryScore = Math.max(nameScore, descScore);
|
|
257
|
+
if (primaryScore > 0) {
|
|
258
|
+
candidates.push({
|
|
259
|
+
value: name,
|
|
260
|
+
label: "name" in cmd ? cmd.name : cmd.label,
|
|
261
|
+
score: primaryScore,
|
|
262
|
+
...(fullDesc && { description: fullDesc }),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (lowerPrefix.length > 0) {
|
|
267
|
+
for (const alias of getCommandAliases(cmd)) {
|
|
268
|
+
if (alias === name) continue;
|
|
269
|
+
const aliasScore = scoreCommandTextMatch(lowerPrefix, alias.toLowerCase());
|
|
270
|
+
if (aliasScore === 0) continue;
|
|
271
|
+
candidates.push({
|
|
272
|
+
value: alias,
|
|
273
|
+
label: alias,
|
|
274
|
+
score: aliasScore,
|
|
275
|
+
...(fullDesc && { description: fullDesc }),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return candidates;
|
|
281
|
+
})
|
|
282
|
+
.sort((a, b) => b.score - a.score)
|
|
283
|
+
.map(({ score: _, ...rest }) => rest);
|
|
284
|
+
}
|
|
285
|
+
|
|
213
286
|
// Combined provider that handles both slash commands and file paths.
|
|
214
287
|
export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
215
|
-
#commands:
|
|
288
|
+
#commands: CommandEntry[];
|
|
216
289
|
#basePath: string;
|
|
217
290
|
// Intentionally separate from prometheus-natives cache: this cache is a local,
|
|
218
291
|
// per-directory readdir fast-path for prefix completions. Global fuzzy
|
|
@@ -220,7 +293,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
220
293
|
#dirCache: Map<string, { entries: fs.Dirent[]; timestamp: number }> = new Map();
|
|
221
294
|
readonly #DIR_CACHE_TTL = 2000; // 2 seconds
|
|
222
295
|
|
|
223
|
-
constructor(commands:
|
|
296
|
+
constructor(commands: CommandEntry[] = [], basePath: string = getProjectDir()) {
|
|
224
297
|
this.#commands = commands;
|
|
225
298
|
this.#basePath = basePath;
|
|
226
299
|
}
|
|
@@ -274,35 +347,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
274
347
|
const prefix = textBeforeCursor.slice(1); // Remove the "/"
|
|
275
348
|
const lowerPrefix = prefix.toLowerCase();
|
|
276
349
|
|
|
277
|
-
|
|
278
|
-
const matches = this.#commands
|
|
279
|
-
.filter(cmd => {
|
|
280
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
281
|
-
if (!name) return false;
|
|
282
|
-
// Match name or description
|
|
283
|
-
if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
|
|
284
|
-
const desc = cmd.description?.toLowerCase();
|
|
285
|
-
return desc ? fuzzyMatch(lowerPrefix, desc) : false;
|
|
286
|
-
})
|
|
287
|
-
.map(cmd => {
|
|
288
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
289
|
-
const lowerName = name?.toLowerCase() ?? "";
|
|
290
|
-
const lowerDesc = cmd.description?.toLowerCase() ?? "";
|
|
291
|
-
// Score name matches higher than description matches
|
|
292
|
-
const nameScore = fuzzyMatch(lowerPrefix, lowerName) ? fuzzyScore(lowerPrefix, lowerName) : 0;
|
|
293
|
-
const descScore = fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
294
|
-
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
295
|
-
const desc = cmd.description ?? "";
|
|
296
|
-
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
297
|
-
return {
|
|
298
|
-
value: name,
|
|
299
|
-
label: "name" in cmd ? cmd.name : cmd.label,
|
|
300
|
-
score: Math.max(nameScore, descScore),
|
|
301
|
-
...(fullDesc && { description: fullDesc }),
|
|
302
|
-
};
|
|
303
|
-
})
|
|
304
|
-
.sort((a, b) => b.score - a.score)
|
|
305
|
-
.map(({ score: _, ...rest }) => rest);
|
|
350
|
+
const matches = buildSlashCommandCompletions(this.#commands, lowerPrefix);
|
|
306
351
|
|
|
307
352
|
if (matches.length === 0) return null;
|
|
308
353
|
|
|
@@ -315,10 +360,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
315
360
|
const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
|
|
316
361
|
const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
|
|
317
362
|
|
|
318
|
-
const command = this.#commands.find(cmd =>
|
|
319
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
320
|
-
return name === commandName;
|
|
321
|
-
});
|
|
363
|
+
const command = this.#commands.find(cmd => commandMatchesNameOrAlias(cmd, commandName));
|
|
322
364
|
if (!command || !("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
|
|
323
365
|
return null; // No argument completion for this command
|
|
324
366
|
}
|
|
@@ -819,10 +861,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
819
861
|
const commandName = textBeforeCursor.slice(1, spaceIndex);
|
|
820
862
|
const argumentText = textBeforeCursor.slice(spaceIndex + 1);
|
|
821
863
|
|
|
822
|
-
const command = this.#commands.find(cmd =>
|
|
823
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
824
|
-
return name === commandName;
|
|
825
|
-
});
|
|
864
|
+
const command = this.#commands.find(cmd => commandMatchesNameOrAlias(cmd, commandName));
|
|
826
865
|
|
|
827
866
|
if (!command || !("getInlineHint" in command) || !command.getInlineHint) {
|
|
828
867
|
return null;
|
|
@@ -838,32 +877,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
838
877
|
const prefix = textBeforeCursor.slice(1);
|
|
839
878
|
const lowerPrefix = prefix.toLowerCase();
|
|
840
879
|
|
|
841
|
-
const matches = this.#commands
|
|
842
|
-
.filter(cmd => {
|
|
843
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
844
|
-
if (!name) return false;
|
|
845
|
-
if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
|
|
846
|
-
const desc = cmd.description?.toLowerCase();
|
|
847
|
-
return desc ? fuzzyMatch(lowerPrefix, desc) : false;
|
|
848
|
-
})
|
|
849
|
-
.map(cmd => {
|
|
850
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
851
|
-
const lowerName = name?.toLowerCase() ?? "";
|
|
852
|
-
const lowerDesc = cmd.description?.toLowerCase() ?? "";
|
|
853
|
-
const nameScore = fuzzyMatch(lowerPrefix, lowerName) ? fuzzyScore(lowerPrefix, lowerName) : 0;
|
|
854
|
-
const descScore = fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
855
|
-
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
856
|
-
const desc = cmd.description ?? "";
|
|
857
|
-
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
858
|
-
return {
|
|
859
|
-
value: name,
|
|
860
|
-
label: "name" in cmd ? cmd.name : cmd.label,
|
|
861
|
-
score: Math.max(nameScore, descScore),
|
|
862
|
-
...(fullDesc && { description: fullDesc }),
|
|
863
|
-
} as AutocompleteItem & { score: number };
|
|
864
|
-
})
|
|
865
|
-
.sort((a, b) => b.score - a.score)
|
|
866
|
-
.map(({ score: _, ...rest }) => rest);
|
|
880
|
+
const matches = buildSlashCommandCompletions(this.#commands, lowerPrefix);
|
|
867
881
|
|
|
868
882
|
if (matches.length === 0) return null;
|
|
869
883
|
return { items: matches, prefix: textBeforeCursor };
|