@oh-my-pi/pi-tui 17.4.2 → 18.0.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.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/dist/types/autocomplete.d.ts +10 -1
- package/dist/types/components/editor.d.ts +40 -1
- package/dist/types/components/markdown.d.ts +17 -0
- package/dist/types/components/select-list.d.ts +9 -0
- package/dist/types/keybindings.d.ts +5 -0
- package/dist/types/terminal.d.ts +32 -2
- package/dist/types/tui.d.ts +43 -0
- package/dist/types/utils.d.ts +3 -2
- package/package.json +4 -4
- package/src/autocomplete.ts +84 -54
- package/src/components/editor.ts +273 -26
- package/src/components/markdown.ts +111 -36
- package/src/components/select-list.ts +67 -15
- package/src/keybindings.ts +5 -0
- package/src/terminal-capabilities.ts +7 -1
- package/src/terminal.ts +133 -22
- package/src/tui.ts +177 -23
- package/src/utils.ts +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.0.0] - 2026-08-22
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Changed native macOS spelling and completion functions to return Promises.
|
|
10
|
+
- Updated `EditorTextAssistProvider.tryAutocorrect` signature to receive editor state instead of raw text.
|
|
11
|
+
- Updated `Editor.decorateText` signature to provide line and column context instead of raw text.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `EditorTextAssistProvider` with spelling suggestion support (`ctrl+.`), word replacement choices, and async autocorrection handling.
|
|
16
|
+
- Added `Terminal.pendingOutputBytes` and an output-backpressure render gate to drop stale frames on slow terminals.
|
|
17
|
+
- Added `deferInput` startup option and `enableInput()` across `Terminal`, `TUI`, and `TUIStartOptions` to improve startup responsiveness.
|
|
18
|
+
- Added icon support and customizable theming to autocomplete and select lists.
|
|
19
|
+
- Added `MarkdownTheme.createHighlightStream` for incremental syntax highlighting of completed lines in streaming Markdown code blocks.
|
|
20
|
+
- Added `maxDescriptionRows` option to `SelectList` layouts to truncate wrapped descriptions with an ellipsis.
|
|
21
|
+
- Added `commandUsage` ranking callback support to `CombinedAutocompleteProvider` to prioritize frequently used slash commands.
|
|
22
|
+
- Added `Editor.viewportRowsProvider` to constrain autocomplete dropdowns within the available terminal height.
|
|
23
|
+
- Added `Editor.setTheme()` to dynamically change themes without recreating the editor or losing draft content.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Adjusted word completion to skip appending a trailing space when the following character is punctuation.
|
|
28
|
+
- Increased default autocomplete dropdown height from 5 to 10 items.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed TUI freezing during large repaints on slow or occluded terminals by moving stdout writes to an off-thread writer.
|
|
33
|
+
|
|
34
|
+
## [17.4.4] - 2026-08-22
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Added `setResizeScrollback()` / `ResizeScrollbackMode` (`PI_TUI_RESIZE_SCROLLBACK` env initializer) controlling what a settled in-place width resize does to native scrollback, which the host rewraps naively at the old width: `append` replays the transcript at the settled width below the old-wrap history, `rebuild` clears pane history first (ED3) so it holds exactly one current-width copy, and `preserve` repaints the viewport only with zero history growth. The raw engine defaults to `preserve`; the coding agent's `tui.resizeScrollback` setting (default `append`) governs interactive sessions.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- `visibleWidth` now measures APC sequences (Kitty graphics commands, cursor markers) as zero cells instead of counting their payload as printable text, matching the native width engine.
|
|
43
|
+
- Kitty Unicode-placeholder rows with long styled prefixes (e.g. bordered thumbnail cards) are recognized as image lines again, keeping them on the verbatim render path instead of SGR coalescing/truncation.
|
|
44
|
+
- Fixed multiplexer width-epoch resolution failing for every real component tree, which forced the conservative full-transcript replay (and one duplicated transcript copy in pane history) on every settled width resize: leading children without a width-epoch revision are no longer validated by width-dependent row counts (reflow is not mutation — identity plus the revision, when reported, is the stability proof), and `Markdown` now reports a width-independent mutation revision so it can sit above an epoch source ([#8193](https://github.com/can1357/oh-my-pi/issues/8193), [#7026](https://github.com/can1357/oh-my-pi/issues/7026)).
|
|
45
|
+
|
|
5
46
|
## [17.4.2] - 2026-08-21
|
|
6
47
|
|
|
7
48
|
### Added
|
package/README.md
CHANGED
|
@@ -539,7 +539,7 @@ interface Terminal {
|
|
|
539
539
|
**Built-in implementations:**
|
|
540
540
|
|
|
541
541
|
- `ProcessTerminal` - Uses `process.stdin/stdout`
|
|
542
|
-
- `VirtualTerminal` - For testing (uses
|
|
542
|
+
- `VirtualTerminal` - For testing (uses kitty-vt-wasm)
|
|
543
543
|
|
|
544
544
|
## Utilities
|
|
545
545
|
|
|
@@ -10,6 +10,8 @@ export interface AutocompleteItem {
|
|
|
10
10
|
value: string;
|
|
11
11
|
label: string;
|
|
12
12
|
description?: string;
|
|
13
|
+
/** Optional type-indicator glyph rendered in an aligned column before the label */
|
|
14
|
+
icon?: string;
|
|
13
15
|
/** Dim hint text shown inline after cursor when this item is selected */
|
|
14
16
|
hint?: string;
|
|
15
17
|
}
|
|
@@ -18,6 +20,8 @@ export interface SlashCommand {
|
|
|
18
20
|
name: string;
|
|
19
21
|
aliases?: string[];
|
|
20
22
|
description?: string;
|
|
23
|
+
/** Optional type-indicator glyph shown before the command name in autocomplete */
|
|
24
|
+
icon?: string;
|
|
21
25
|
argumentHint?: string;
|
|
22
26
|
/** Whether the command consumes argument text after the command name. False means the full input stays normal prompt text once args are present. */
|
|
23
27
|
allowArgs?: boolean;
|
|
@@ -73,6 +77,11 @@ export interface AutocompleteProvider {
|
|
|
73
77
|
shouldTriggerFileCompletion?(lines: string[], cursorLine: number, cursorCol: number): boolean;
|
|
74
78
|
}
|
|
75
79
|
type CommandEntry = SlashCommand | AutocompleteItem;
|
|
80
|
+
/** Optional behaviors for {@link CombinedAutocompleteProvider}. */
|
|
81
|
+
export interface CombinedAutocompleteOptions {
|
|
82
|
+
/** Usage count per command name; higher counts rank earlier among equal text-match scores. */
|
|
83
|
+
commandUsage?: (name: string) => number;
|
|
84
|
+
}
|
|
76
85
|
export declare function scoreCommandTextMatch(lowerPrefix: string, lowerTarget: string): number;
|
|
77
86
|
/**
|
|
78
87
|
* Whether a mid-prompt slash token (`prose … /tok`) is skill-shaped enough to
|
|
@@ -90,7 +99,7 @@ export declare function scoreCommandTextMatch(lowerPrefix: string, lowerTarget:
|
|
|
90
99
|
export declare function midPromptSkillTokenMatches(lowerToken: string, name: string, description?: string): boolean;
|
|
91
100
|
export declare class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
92
101
|
#private;
|
|
93
|
-
constructor(commands?: CommandEntry[], basePath?: string);
|
|
102
|
+
constructor(commands?: CommandEntry[], basePath?: string, options?: CombinedAutocompleteOptions);
|
|
94
103
|
getSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
95
104
|
items: AutocompleteItem[];
|
|
96
105
|
prefix: string;
|
|
@@ -23,6 +23,38 @@ interface HistoryStorage {
|
|
|
23
23
|
add(prompt: string, cwd?: string): Promise<void>;
|
|
24
24
|
getRecent(limit: number): HistoryEntry[];
|
|
25
25
|
}
|
|
26
|
+
/** A synchronous replacement immediately before the editor cursor. */
|
|
27
|
+
export interface EditorInlineReplacement {
|
|
28
|
+
/** UTF-16 code units to remove immediately before the cursor. */
|
|
29
|
+
replaceLen: number;
|
|
30
|
+
/** Literal text inserted where the removed suffix started. */
|
|
31
|
+
insert: string;
|
|
32
|
+
}
|
|
33
|
+
/** Replacement candidates and the current-line span they replace. */
|
|
34
|
+
export interface EditorWordReplacements {
|
|
35
|
+
line: number;
|
|
36
|
+
startCol: number;
|
|
37
|
+
endCol: number;
|
|
38
|
+
items: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/** Source location for one visual text segment passed to `decorateText`. */
|
|
41
|
+
export interface EditorTextDecorationContext {
|
|
42
|
+
line: number;
|
|
43
|
+
startCol: number;
|
|
44
|
+
endCol: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Optional prose assistance kept separate from command/file autocomplete.
|
|
48
|
+
* Hosts independently decide whether word completion and autocorrection are enabled.
|
|
49
|
+
*/
|
|
50
|
+
export interface EditorTextAssistProvider {
|
|
51
|
+
/** Return ghost-text suffix for the partial word at the cursor, or `null`. */
|
|
52
|
+
getWordCompletion?(lines: string[], cursorLine: number, cursorCol: number): string | null;
|
|
53
|
+
/** Return a correction after one single-character insertion, or `null`. */
|
|
54
|
+
tryAutocorrect?(lines: string[], cursorLine: number, cursorCol: number): EditorInlineReplacement | null | Promise<EditorInlineReplacement | null>;
|
|
55
|
+
/** Return replacement candidates for the misspelled word at the cursor. */
|
|
56
|
+
getWordReplacements?(lines: string[], cursorLine: number, cursorCol: number): EditorWordReplacements | null | Promise<EditorWordReplacements | null>;
|
|
57
|
+
}
|
|
26
58
|
export declare class Editor implements Component, Focusable {
|
|
27
59
|
#private;
|
|
28
60
|
/** Focusable interface - set by TUI when focus changes */
|
|
@@ -34,9 +66,13 @@ export declare class Editor implements Component, Focusable {
|
|
|
34
66
|
/** Optional hook that decorates displayed user text after source-text layout.
|
|
35
67
|
* Width-changing output is allowed on lines without the cursor; it is truncated
|
|
36
68
|
* to the content width rather than reflowed. Cursor glyphs and inline hints are excluded. */
|
|
37
|
-
decorateText: ((text: string) => string) | undefined;
|
|
69
|
+
decorateText: ((text: string, context: EditorTextDecorationContext) => string) | undefined;
|
|
38
70
|
borderColor: (str: string) => string;
|
|
39
71
|
onAutocompleteUpdate?: () => void;
|
|
72
|
+
/** Called after an async text-assist result mutates the document outside an input event, so hosts can schedule a repaint. */
|
|
73
|
+
onTextAssistApplied?: () => void;
|
|
74
|
+
/** Terminal height source for clamping the autocomplete dropdown. Hosts wire this to their Terminal's rows. */
|
|
75
|
+
viewportRowsProvider?: () => number;
|
|
40
76
|
/** Optional pattern matching atomic placeholder tokens (e.g. `[Image #1, 800x600]` or
|
|
41
77
|
* `[Paste #2, +30 lines]`) that the editor treats as indivisible: a backspace or forward-delete
|
|
42
78
|
* landing on any character of a token removes the whole token instead of corrupting it into
|
|
@@ -56,7 +92,10 @@ export declare class Editor implements Component, Focusable {
|
|
|
56
92
|
onAutocompleteCancel?: () => void;
|
|
57
93
|
disableSubmit: boolean;
|
|
58
94
|
constructor(theme: EditorTheme);
|
|
95
|
+
setTheme(theme: EditorTheme): void;
|
|
59
96
|
setAutocompleteProvider(provider: AutocompleteProvider): void;
|
|
97
|
+
/** Install prose assistance without changing command/file autocomplete. */
|
|
98
|
+
setTextAssistProvider(provider: EditorTextAssistProvider | undefined): void;
|
|
60
99
|
/**
|
|
61
100
|
* Set custom content for the top border (e.g., status line).
|
|
62
101
|
* Pass undefined to use the default plain border.
|
|
@@ -26,6 +26,14 @@ export interface DefaultTextStyle {
|
|
|
26
26
|
/** Underline text */
|
|
27
27
|
underline?: boolean;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Stateful incremental code highlighter carrying parser state across pushes.
|
|
31
|
+
* Produced per streaming fence by {@link MarkdownTheme.createHighlightStream}.
|
|
32
|
+
*/
|
|
33
|
+
export interface HighlightStreamSession {
|
|
34
|
+
/** Highlight the next chunk and advance parser state. */
|
|
35
|
+
push(chunk: string): string;
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Theme functions for markdown elements.
|
|
31
39
|
* Each function takes text and returns styled text with ANSI codes.
|
|
@@ -46,6 +54,14 @@ export interface MarkdownTheme {
|
|
|
46
54
|
strikethrough: (text: string) => string;
|
|
47
55
|
underline: (text: string) => string;
|
|
48
56
|
highlightCode?: (code: string, lang?: string) => string[];
|
|
57
|
+
/**
|
|
58
|
+
* Create a stateful incremental highlighter for one streaming code fence.
|
|
59
|
+
* `push` receives newline-terminated complete lines (only the final push
|
|
60
|
+
* may omit the trailing newline) and must return highlighted ANSI text for
|
|
61
|
+
* exactly the pushed chunk, byte-identical to highlighting the concatenated
|
|
62
|
+
* text through `highlightCode`. Return null when `lang` is unsupported.
|
|
63
|
+
*/
|
|
64
|
+
createHighlightStream?: (lang?: string) => HighlightStreamSession | null;
|
|
49
65
|
/**
|
|
50
66
|
* Resolve a mermaid ASCII rendering by fenced block source text.
|
|
51
67
|
* Return null to fall back to fenced code rendering.
|
|
@@ -58,6 +74,7 @@ export declare class Markdown implements Component, NativeScrollbackCommittedRow
|
|
|
58
74
|
setIgnoreTight(ignore: boolean): this;
|
|
59
75
|
constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, codeBlockIndent?: number);
|
|
60
76
|
setText(text: string): boolean;
|
|
77
|
+
getNativeScrollbackWidthEpochRevision(): number;
|
|
61
78
|
invalidate(): void;
|
|
62
79
|
get transientRenderCache(): boolean;
|
|
63
80
|
set transientRenderCache(value: boolean);
|
|
@@ -5,6 +5,8 @@ export interface SelectItem {
|
|
|
5
5
|
value: string;
|
|
6
6
|
label: string;
|
|
7
7
|
description?: string;
|
|
8
|
+
/** Optional type-indicator glyph rendered in an aligned column before the label */
|
|
9
|
+
icon?: string;
|
|
8
10
|
/** Dim hint text shown inline after cursor when this item is selected */
|
|
9
11
|
hint?: string;
|
|
10
12
|
}
|
|
@@ -15,6 +17,8 @@ export interface SelectListTheme {
|
|
|
15
17
|
scrollInfo: (text: string) => string;
|
|
16
18
|
noMatch: (text: string) => string;
|
|
17
19
|
symbols: SymbolTheme;
|
|
20
|
+
/** Style for the type-icon column on unselected rows. Defaults to plain text. */
|
|
21
|
+
icon?: (text: string) => string;
|
|
18
22
|
/** Hover band applied to the full row under the mouse pointer. */
|
|
19
23
|
hovered?: (text: string) => string;
|
|
20
24
|
}
|
|
@@ -39,6 +43,11 @@ export interface SelectListLayoutOptions {
|
|
|
39
43
|
* wrap unevenly.
|
|
40
44
|
*/
|
|
41
45
|
wrapDescription?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Cap wrapped descriptions at this many visual rows; the last kept row is
|
|
48
|
+
* ellipsized. Only meaningful with `wrapDescription`.
|
|
49
|
+
*/
|
|
50
|
+
maxDescriptionRows?: number;
|
|
42
51
|
}
|
|
43
52
|
export declare class SelectList implements Component, MouseRoutable {
|
|
44
53
|
#private;
|
|
@@ -25,6 +25,7 @@ export interface Keybindings {
|
|
|
25
25
|
"tui.editor.yank": true;
|
|
26
26
|
"tui.editor.yankPop": true;
|
|
27
27
|
"tui.editor.undo": true;
|
|
28
|
+
"tui.editor.spellingSuggestions": true;
|
|
28
29
|
"tui.input.newLine": true;
|
|
29
30
|
"tui.input.submit": true;
|
|
30
31
|
"tui.input.tab": true;
|
|
@@ -129,6 +130,10 @@ export declare const TUI_KEYBINDINGS: {
|
|
|
129
130
|
readonly defaultKeys: ["ctrl+-", "ctrl+_"];
|
|
130
131
|
readonly description: "Undo";
|
|
131
132
|
};
|
|
133
|
+
readonly "tui.editor.spellingSuggestions": {
|
|
134
|
+
readonly defaultKeys: "ctrl+.";
|
|
135
|
+
readonly description: "Show spelling replacements";
|
|
136
|
+
};
|
|
132
137
|
readonly "tui.input.newLine": {
|
|
133
138
|
readonly defaultKeys: ["shift+enter", "ctrl+j"];
|
|
134
139
|
readonly description: "Insert newline";
|
package/dist/types/terminal.d.ts
CHANGED
|
@@ -58,10 +58,30 @@ export declare function setAltScreenActive(active: boolean): void;
|
|
|
58
58
|
export declare function emergencyTerminalRestore(): void;
|
|
59
59
|
/** Terminal-reported appearance (dark/light mode). */
|
|
60
60
|
export type TerminalAppearance = "dark" | "light";
|
|
61
|
+
/** Options for {@link Terminal.start}. */
|
|
62
|
+
export interface TerminalStartOptions {
|
|
63
|
+
/**
|
|
64
|
+
* Paint-only start: skip raw mode, stdin ownership, and every probe that
|
|
65
|
+
* elicits a response on stdin. The host tty keeps cooked-mode line editing
|
|
66
|
+
* (kernel echo lands at the hardware cursor), and typed bytes stay queued
|
|
67
|
+
* in the kernel until {@link Terminal.enableInput} takes ownership and
|
|
68
|
+
* replays them through `onInput`. Used for the startup prepaint so typing
|
|
69
|
+
* echoes even while module loading blocks the event loop.
|
|
70
|
+
*/
|
|
71
|
+
deferInput?: boolean;
|
|
72
|
+
}
|
|
61
73
|
/** Identity of an accepted explicit terminal appearance refresh request. */
|
|
62
74
|
export type TerminalAppearanceRequestToken = number;
|
|
63
75
|
export interface Terminal {
|
|
64
|
-
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void): void;
|
|
76
|
+
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void, options?: TerminalStartOptions): void;
|
|
77
|
+
/**
|
|
78
|
+
* Take ownership of stdin after a `deferInput` start: enable raw mode,
|
|
79
|
+
* attach input handlers, and run the capability probes start() skipped.
|
|
80
|
+
* Bytes the user typed in cooked mode meanwhile are replayed through
|
|
81
|
+
* `onInput`. No-op when input was never deferred. Optional so custom
|
|
82
|
+
* Terminals built against older pi-tui versions keep working.
|
|
83
|
+
*/
|
|
84
|
+
enableInput?(): void;
|
|
65
85
|
stop(): void;
|
|
66
86
|
/**
|
|
67
87
|
* Drain stdin before exiting to prevent Kitty key release events from
|
|
@@ -73,6 +93,14 @@ export interface Terminal {
|
|
|
73
93
|
write(data: string): void;
|
|
74
94
|
get columns(): number;
|
|
75
95
|
get rows(): number;
|
|
96
|
+
/**
|
|
97
|
+
* Output bytes accepted but not yet delivered to the terminal, when the
|
|
98
|
+
* implementation can report it. The renderer skips composing new frames
|
|
99
|
+
* while this backlog is deep, so a slow terminal receives only fresh
|
|
100
|
+
* frames instead of a queue of stale ones. Optional so custom Terminals
|
|
101
|
+
* built against older pi-tui versions keep working.
|
|
102
|
+
*/
|
|
103
|
+
readonly pendingOutputBytes?: number;
|
|
76
104
|
get kittyProtocolActive(): boolean;
|
|
77
105
|
get kittyEnableSequence(): string | null;
|
|
78
106
|
readonly keyboardEnhancementEnterSequence?: string | null;
|
|
@@ -158,11 +186,13 @@ export declare class ProcessTerminal implements Terminal {
|
|
|
158
186
|
*/
|
|
159
187
|
refreshAppearance(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void;
|
|
160
188
|
onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void;
|
|
161
|
-
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void): void;
|
|
189
|
+
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void, options?: TerminalStartOptions): void;
|
|
190
|
+
enableInput(): void;
|
|
162
191
|
drainInput(maxMs?: number, idleMs?: number): Promise<void>;
|
|
163
192
|
stop(): void;
|
|
164
193
|
write(data: string): void;
|
|
165
194
|
get columns(): number;
|
|
195
|
+
get pendingOutputBytes(): number;
|
|
166
196
|
get rows(): number;
|
|
167
197
|
moveBy(lines: number): void;
|
|
168
198
|
hideCursor(force?: boolean): void;
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface TUIOptions {
|
|
|
21
21
|
export interface TUIStartOptions {
|
|
22
22
|
/** Clear saved native scrollback before the first paint. */
|
|
23
23
|
clearScrollback?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Paint without owning stdin: the terminal stays in cooked mode (kernel
|
|
26
|
+
* echo + line editing at the hardware cursor) until {@link TUI.enableInput}
|
|
27
|
+
* switches to raw input and replays the kernel-buffered keystrokes.
|
|
28
|
+
*/
|
|
29
|
+
deferInput?: boolean;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* Component interface - all components must implement this
|
|
@@ -200,6 +206,26 @@ export interface RenderRequestOptions {
|
|
|
200
206
|
/** Clear terminal scrollback for intentional transcript replacement. */
|
|
201
207
|
clearScrollback?: boolean;
|
|
202
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* What a settled in-place width resize (multiplexer pane or an in-place-latched
|
|
211
|
+
* direct terminal) does to native scrollback, which the host rewrapped at the
|
|
212
|
+
* old width:
|
|
213
|
+
* - `append`: replay the transcript at the current width below the old-wrap
|
|
214
|
+
* history — one fresh copy per settled resize, nothing destroyed.
|
|
215
|
+
* - `rebuild`: clear native history first (ED3) and replay — history holds the
|
|
216
|
+
* transcript exactly once at the current width. Requires a host that honors
|
|
217
|
+
* an inner ED3 (tmux does; GNU screen ignores it, degrading to `append`),
|
|
218
|
+
* and erases pre-session pane history.
|
|
219
|
+
* - `preserve`: repaint the viewport only — zero history growth; scrollback
|
|
220
|
+
* keeps the old-width wrap until content next scrolls off.
|
|
221
|
+
*
|
|
222
|
+
* The raw engine defaults to `preserve` (append-only native scrollback, the
|
|
223
|
+
* engine's baseline contract); `PI_TUI_RESIZE_SCROLLBACK` overrides that
|
|
224
|
+
* initial value. The coding agent applies its `tui.resizeScrollback` setting
|
|
225
|
+
* (default `append`) on top at startup, so interactive sessions refresh
|
|
226
|
+
* stale-width history out of the box.
|
|
227
|
+
*/
|
|
228
|
+
export type ResizeScrollbackMode = "rebuild" | "append" | "preserve";
|
|
203
229
|
/** Type guard to check if a component implements Focusable */
|
|
204
230
|
export declare function isFocusable(component: Component | null): component is Component & Focusable;
|
|
205
231
|
/**
|
|
@@ -407,6 +433,16 @@ export declare class TUI extends Container {
|
|
|
407
433
|
* duplicate blocks when a block's final form replaces its live preview.
|
|
408
434
|
*/
|
|
409
435
|
setScrollbackRebuild(enabled: boolean): void;
|
|
436
|
+
/**
|
|
437
|
+
* Get how a settled in-place width resize refreshes native scrollback.
|
|
438
|
+
*/
|
|
439
|
+
getResizeScrollback(): ResizeScrollbackMode;
|
|
440
|
+
/**
|
|
441
|
+
* Set how a settled in-place width resize refreshes native scrollback
|
|
442
|
+
* (see {@link ResizeScrollbackMode}; engine default `preserve` — the coding
|
|
443
|
+
* agent applies its `tui.resizeScrollback` setting, default `append`).
|
|
444
|
+
*/
|
|
445
|
+
setResizeScrollback(mode: ResizeScrollbackMode): void;
|
|
410
446
|
getShowHardwareCursor(): boolean;
|
|
411
447
|
setShowHardwareCursor(enabled: boolean): void;
|
|
412
448
|
/**
|
|
@@ -430,6 +466,13 @@ export declare class TUI extends Container {
|
|
|
430
466
|
hasOverlay(): boolean;
|
|
431
467
|
invalidate(): void;
|
|
432
468
|
start(options?: TUIStartOptions): void;
|
|
469
|
+
/**
|
|
470
|
+
* Take ownership of stdin after a `deferInput` start: raw mode, input
|
|
471
|
+
* handlers, and the response-eliciting capability probes start() skipped.
|
|
472
|
+
* Keystrokes typed in cooked mode meanwhile arrive through the normal input
|
|
473
|
+
* path. Idempotent; no-op when input was never deferred.
|
|
474
|
+
*/
|
|
475
|
+
enableInput(): void;
|
|
433
476
|
addStartListener(listener: StartListener): () => void;
|
|
434
477
|
addInputListener(listener: InputListener): () => void;
|
|
435
478
|
removeInputListener(listener: InputListener): void;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -42,8 +42,9 @@ export declare function getSegmenter(): Intl.Segmenter;
|
|
|
42
42
|
* Visible width of a string in terminal columns, excluding ANSI/OSC escapes.
|
|
43
43
|
*
|
|
44
44
|
* `Bun.stringWidth` does the heavy lifting (UAX#11 width tables + ANSI/OSC
|
|
45
|
-
* stripping); this adds the
|
|
46
|
-
* `tabWidth` cells)
|
|
45
|
+
* stripping); this adds the corrections it omits — tabs (expanded to
|
|
46
|
+
* `tabWidth` cells), OSC 66 text-sizing payloads (scaled by `s=`), and APC
|
|
47
|
+
* sequences (counted as printable by Bun, actually zero cells).
|
|
47
48
|
*/
|
|
48
49
|
export declare function visibleWidth(str: string): number;
|
|
49
50
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "18.0.0",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "
|
|
41
|
-
"@oh-my-pi/pi-utils": "
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.0.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.0.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"
|
|
44
|
+
"kitty-vt-wasm": "^0.2.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"bun": ">=1.3.14"
|
package/src/autocomplete.ts
CHANGED
|
@@ -171,6 +171,8 @@ export interface AutocompleteItem {
|
|
|
171
171
|
value: string;
|
|
172
172
|
label: string;
|
|
173
173
|
description?: string;
|
|
174
|
+
/** Optional type-indicator glyph rendered in an aligned column before the label */
|
|
175
|
+
icon?: string;
|
|
174
176
|
/** Dim hint text shown inline after cursor when this item is selected */
|
|
175
177
|
hint?: string;
|
|
176
178
|
}
|
|
@@ -181,6 +183,8 @@ export interface SlashCommand {
|
|
|
181
183
|
name: string;
|
|
182
184
|
aliases?: string[];
|
|
183
185
|
description?: string;
|
|
186
|
+
/** Optional type-indicator glyph shown before the command name in autocomplete */
|
|
187
|
+
icon?: string;
|
|
184
188
|
argumentHint?: string;
|
|
185
189
|
/** Whether the command consumes argument text after the command name. False means the full input stays normal prompt text once args are present. */
|
|
186
190
|
allowArgs?: boolean;
|
|
@@ -249,6 +253,11 @@ export interface AutocompleteProvider {
|
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
type CommandEntry = SlashCommand | AutocompleteItem;
|
|
256
|
+
/** Optional behaviors for {@link CombinedAutocompleteProvider}. */
|
|
257
|
+
export interface CombinedAutocompleteOptions {
|
|
258
|
+
/** Usage count per command name; higher counts rank earlier among equal text-match scores. */
|
|
259
|
+
commandUsage?: (name: string) => number;
|
|
260
|
+
}
|
|
252
261
|
|
|
253
262
|
function getCommandName(cmd: CommandEntry): string | undefined {
|
|
254
263
|
return "name" in cmd ? cmd.name : cmd.value;
|
|
@@ -287,64 +296,79 @@ export function scoreCommandTextMatch(lowerPrefix: string, lowerTarget: string):
|
|
|
287
296
|
return fuzzyMatch(lowerPrefix, lowerTarget) ? fuzzyScore(lowerPrefix, lowerTarget) : 0;
|
|
288
297
|
}
|
|
289
298
|
|
|
290
|
-
function buildSlashCommandCompletions(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const lowerDesc = staticDesc.toLowerCase();
|
|
316
|
-
const descScore =
|
|
317
|
-
lowerDesc && fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
318
|
-
const primaryScore = Math.max(nameScore, descScore);
|
|
319
|
-
if (primaryScore > 0) {
|
|
320
|
-
const fullDesc = resolveFullDesc();
|
|
321
|
-
best = {
|
|
322
|
-
value: name,
|
|
323
|
-
label: "name" in cmd ? cmd.name : cmd.label,
|
|
324
|
-
score: primaryScore,
|
|
325
|
-
...(fullDesc && { description: fullDesc }),
|
|
299
|
+
function buildSlashCommandCompletions(
|
|
300
|
+
commands: CommandEntry[],
|
|
301
|
+
lowerPrefix: string,
|
|
302
|
+
commandUsage?: (name: string) => number,
|
|
303
|
+
): AutocompleteItem[] {
|
|
304
|
+
return (
|
|
305
|
+
commands
|
|
306
|
+
.flatMap(cmd => {
|
|
307
|
+
const name = getCommandName(cmd);
|
|
308
|
+
if (!name) return [];
|
|
309
|
+
const usage = commandUsage?.(name) ?? 0;
|
|
310
|
+
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
311
|
+
const staticDesc = getStaticCommandDescription(cmd);
|
|
312
|
+
let fullDescMemo: string | undefined;
|
|
313
|
+
let fullDescComputed = false;
|
|
314
|
+
// Resolve the (possibly live) display description lazily, only once a
|
|
315
|
+
// candidate actually matches — getAutocompleteDescription reads live
|
|
316
|
+
// session state and must not run for every command on each keystroke.
|
|
317
|
+
const resolveFullDesc = (): string | undefined => {
|
|
318
|
+
if (!fullDescComputed) {
|
|
319
|
+
const displayDesc = getAutocompleteCommandDescription(cmd);
|
|
320
|
+
fullDescMemo = hint ? (displayDesc ? `${hint} - ${displayDesc}` : hint) : displayDesc;
|
|
321
|
+
fullDescComputed = true;
|
|
322
|
+
}
|
|
323
|
+
return fullDescMemo;
|
|
326
324
|
};
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
325
|
+
let best: (AutocompleteItem & { score: number; usage: number }) | undefined;
|
|
326
|
+
|
|
327
|
+
const isSkillCommand = name.startsWith("skill:");
|
|
328
|
+
const nameScore =
|
|
329
|
+
lowerPrefix.length === 0 && isSkillCommand
|
|
330
|
+
? 950
|
|
331
|
+
: scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
|
|
332
|
+
const lowerDesc = staticDesc.toLowerCase();
|
|
333
|
+
const descScore =
|
|
334
|
+
lowerDesc && fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
335
|
+
const primaryScore = Math.max(nameScore, descScore);
|
|
336
|
+
if (primaryScore > 0) {
|
|
334
337
|
const fullDesc = resolveFullDesc();
|
|
335
338
|
best = {
|
|
336
|
-
value:
|
|
337
|
-
label:
|
|
338
|
-
score:
|
|
339
|
+
value: name,
|
|
340
|
+
label: "name" in cmd ? cmd.name : cmd.label,
|
|
341
|
+
score: primaryScore,
|
|
342
|
+
usage,
|
|
343
|
+
...(cmd.icon && { icon: cmd.icon }),
|
|
339
344
|
...(fullDesc && { description: fullDesc }),
|
|
340
345
|
};
|
|
341
346
|
}
|
|
342
|
-
}
|
|
343
347
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
+
if (lowerPrefix.length > 0) {
|
|
349
|
+
for (const alias of getCommandAliases(cmd)) {
|
|
350
|
+
if (alias === name) continue;
|
|
351
|
+
const aliasScore = scoreCommandTextMatch(lowerPrefix, alias.toLowerCase());
|
|
352
|
+
if (aliasScore === 0 || (best && aliasScore <= best.score)) continue;
|
|
353
|
+
const fullDesc = resolveFullDesc();
|
|
354
|
+
best = {
|
|
355
|
+
value: alias,
|
|
356
|
+
label: alias,
|
|
357
|
+
score: aliasScore,
|
|
358
|
+
usage,
|
|
359
|
+
...(cmd.icon && { icon: cmd.icon }),
|
|
360
|
+
...(fullDesc && { description: fullDesc }),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return best ? [best] : [];
|
|
366
|
+
})
|
|
367
|
+
// Equal text-match scores fall back to usage frequency, then to the
|
|
368
|
+
// stable registry order.
|
|
369
|
+
.sort((a, b) => b.score - a.score || b.usage - a.usage)
|
|
370
|
+
.map(({ score: _, usage: _usage, ...rest }) => rest)
|
|
371
|
+
);
|
|
348
372
|
}
|
|
349
373
|
|
|
350
374
|
function hasPromptTextBeforeSlash(
|
|
@@ -401,15 +425,21 @@ function buildMidPromptSkillCompletions(commands: CommandEntry[], lowerPrefix: s
|
|
|
401
425
|
export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
402
426
|
#commands: CommandEntry[];
|
|
403
427
|
#basePath: string;
|
|
428
|
+
#commandUsage?: (name: string) => number;
|
|
404
429
|
// Intentionally separate from pi-natives cache: this cache is a local,
|
|
405
430
|
// per-directory readdir fast-path for prefix completions. Global fuzzy
|
|
406
431
|
// discovery continues to use native fuzzyFind + shared scan cache.
|
|
407
432
|
#dirCache: Map<string, { entries: fs.Dirent[]; timestamp: number }> = new Map();
|
|
408
433
|
readonly #DIR_CACHE_TTL = 2000; // 2 seconds
|
|
409
434
|
|
|
410
|
-
constructor(
|
|
435
|
+
constructor(
|
|
436
|
+
commands: CommandEntry[] = [],
|
|
437
|
+
basePath: string = getProjectDir(),
|
|
438
|
+
options?: CombinedAutocompleteOptions,
|
|
439
|
+
) {
|
|
411
440
|
this.#commands = commands;
|
|
412
441
|
this.#basePath = basePath;
|
|
442
|
+
this.#commandUsage = options?.commandUsage;
|
|
413
443
|
}
|
|
414
444
|
|
|
415
445
|
async getSuggestions(
|
|
@@ -444,7 +474,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
444
474
|
|
|
445
475
|
const matches = isMidPromptSkillLookup
|
|
446
476
|
? buildMidPromptSkillCompletions(this.#commands, lowerPrefix)
|
|
447
|
-
: buildSlashCommandCompletions(this.#commands, lowerPrefix);
|
|
477
|
+
: buildSlashCommandCompletions(this.#commands, lowerPrefix, this.#commandUsage);
|
|
448
478
|
|
|
449
479
|
if (matches.length > 0) {
|
|
450
480
|
return {
|
|
@@ -1069,7 +1099,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
1069
1099
|
const prefix = commandText.slice(1);
|
|
1070
1100
|
const lowerPrefix = prefix.toLowerCase();
|
|
1071
1101
|
|
|
1072
|
-
const matches = buildSlashCommandCompletions(this.#commands, lowerPrefix);
|
|
1102
|
+
const matches = buildSlashCommandCompletions(this.#commands, lowerPrefix, this.#commandUsage);
|
|
1073
1103
|
|
|
1074
1104
|
if (matches.length === 0) return null;
|
|
1075
1105
|
// Mirror `getSuggestions`: preserve leading whitespace so the editor's
|