@oh-my-pi/pi-tui 18.0.0 → 18.0.3
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 +37 -0
- package/dist/types/autocomplete.d.ts +7 -5
- package/dist/types/components/editor.d.ts +2 -4
- package/dist/types/components/image.d.ts +2 -5
- package/dist/types/components/markdown.d.ts +2 -25
- package/dist/types/components/text.d.ts +0 -1
- package/dist/types/terminal-capabilities.d.ts +10 -3
- package/dist/types/terminal.d.ts +10 -0
- package/dist/types/tui.d.ts +47 -245
- package/package.json +3 -3
- package/src/autocomplete.ts +66 -10
- package/src/components/editor.ts +137 -134
- package/src/components/image.ts +4 -6
- package/src/components/loader.ts +1 -9
- package/src/components/markdown.ts +48 -407
- package/src/components/text.ts +8 -18
- package/src/fuzzy.ts +63 -6
- package/src/stdin-buffer.ts +130 -20
- package/src/terminal-capabilities.ts +13 -2
- package/src/terminal.ts +14 -0
- package/src/tui.ts +567 -3599
package/src/autocomplete.ts
CHANGED
|
@@ -9,6 +9,7 @@ const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
|
|
|
9
9
|
function buildAutocompleteFuzzyDiscoveryProfile(
|
|
10
10
|
query: string,
|
|
11
11
|
basePath: string,
|
|
12
|
+
signal?: AbortSignal,
|
|
12
13
|
): {
|
|
13
14
|
query: string;
|
|
14
15
|
path: string;
|
|
@@ -16,6 +17,7 @@ function buildAutocompleteFuzzyDiscoveryProfile(
|
|
|
16
17
|
hidden: boolean;
|
|
17
18
|
gitignore: boolean;
|
|
18
19
|
cache: boolean;
|
|
20
|
+
signal?: AbortSignal;
|
|
19
21
|
} {
|
|
20
22
|
return {
|
|
21
23
|
query,
|
|
@@ -24,6 +26,7 @@ function buildAutocompleteFuzzyDiscoveryProfile(
|
|
|
24
26
|
hidden: true,
|
|
25
27
|
gitignore: true,
|
|
26
28
|
cache: true,
|
|
29
|
+
...(signal ? { signal } : {}),
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -198,11 +201,12 @@ export interface SlashCommand {
|
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
export interface AutocompleteProvider {
|
|
201
|
-
/** Get autocomplete suggestions for current text/cursor position */
|
|
204
|
+
/** Get autocomplete suggestions for current text/cursor position. Expensive providers SHOULD stop when `signal` aborts. */
|
|
202
205
|
getSuggestions(
|
|
203
206
|
lines: string[],
|
|
204
207
|
cursorLine: number,
|
|
205
208
|
cursorCol: number,
|
|
209
|
+
signal?: AbortSignal,
|
|
206
210
|
): Promise<{
|
|
207
211
|
items: AutocompleteItem[];
|
|
208
212
|
prefix: string; // What we're matching against (e.g., "/" or "src/")
|
|
@@ -241,11 +245,13 @@ export interface AutocompleteProvider {
|
|
|
241
245
|
* Force file-path completion (called on Tab). Returns matched items plus the
|
|
242
246
|
* full prefix, or null when no path token sits before the cursor. Present on
|
|
243
247
|
* file-aware providers; absent on slash-only ones.
|
|
248
|
+
* Expensive providers SHOULD stop when `signal` aborts.
|
|
244
249
|
*/
|
|
245
250
|
getForceFileSuggestions?(
|
|
246
251
|
lines: string[],
|
|
247
252
|
cursorLine: number,
|
|
248
253
|
cursorCol: number,
|
|
254
|
+
signal?: AbortSignal,
|
|
249
255
|
): Promise<{ items: AutocompleteItem[]; prefix: string } | null>;
|
|
250
256
|
|
|
251
257
|
/** Whether a Tab press should attempt file completion at the cursor. */
|
|
@@ -324,7 +330,7 @@ function buildSlashCommandCompletions(
|
|
|
324
330
|
};
|
|
325
331
|
let best: (AutocompleteItem & { score: number; usage: number }) | undefined;
|
|
326
332
|
|
|
327
|
-
const isSkillCommand = name.startsWith(
|
|
333
|
+
const isSkillCommand = name.startsWith(SKILL_NAMESPACE);
|
|
328
334
|
const nameScore =
|
|
329
335
|
lowerPrefix.length === 0 && isSkillCommand
|
|
330
336
|
? 950
|
|
@@ -383,7 +389,35 @@ function hasPromptTextBeforeSlash(
|
|
|
383
389
|
return textBeforeCursor.slice(0, slashStart).trim() !== "";
|
|
384
390
|
}
|
|
385
391
|
|
|
386
|
-
const SKILL_NAMESPACE = "skill:";
|
|
392
|
+
export const SKILL_NAMESPACE = "skill:";
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Collapse `skill:*` commands into a single `/skill:` namespace row while the
|
|
396
|
+
* typed prefix has not committed to the namespace. Until the prefix starts
|
|
397
|
+
* with `skill:`, individual skills never list — a lone group entry (shown only
|
|
398
|
+
* while the prefix is still a prefix of `skill:`) keeps the `/` popup
|
|
399
|
+
* readable. Accepting the group inserts `/skill:` without a trailing space so
|
|
400
|
+
* the reopened popup expands to the individual skills.
|
|
401
|
+
*/
|
|
402
|
+
function collapseSkillNamespace(commands: CommandEntry[], lowerPrefix: string): CommandEntry[] {
|
|
403
|
+
if (lowerPrefix.startsWith(SKILL_NAMESPACE)) return commands;
|
|
404
|
+
let skillCount = 0;
|
|
405
|
+
let skillIcon: string | undefined;
|
|
406
|
+
const rest = commands.filter(cmd => {
|
|
407
|
+
if (!getCommandName(cmd)?.startsWith(SKILL_NAMESPACE)) return true;
|
|
408
|
+
skillCount += 1;
|
|
409
|
+
skillIcon ??= cmd.icon;
|
|
410
|
+
return false;
|
|
411
|
+
});
|
|
412
|
+
if (skillCount === 0) return commands;
|
|
413
|
+
if (!SKILL_NAMESPACE.startsWith(lowerPrefix)) return rest;
|
|
414
|
+
rest.push({
|
|
415
|
+
name: SKILL_NAMESPACE,
|
|
416
|
+
description: `${skillCount} skill${skillCount === 1 ? "" : "s"}`,
|
|
417
|
+
...(skillIcon && { icon: skillIcon }),
|
|
418
|
+
});
|
|
419
|
+
return rest;
|
|
420
|
+
}
|
|
387
421
|
|
|
388
422
|
/**
|
|
389
423
|
* Whether a mid-prompt slash token (`prose … /tok`) is skill-shaped enough to
|
|
@@ -446,7 +480,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
446
480
|
lines: string[],
|
|
447
481
|
cursorLine: number,
|
|
448
482
|
cursorCol: number,
|
|
483
|
+
signal?: AbortSignal,
|
|
449
484
|
): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
|
|
485
|
+
if (signal?.aborted) return null;
|
|
450
486
|
const currentLine = lines[cursorLine] || "";
|
|
451
487
|
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
452
488
|
|
|
@@ -474,7 +510,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
474
510
|
|
|
475
511
|
const matches = isMidPromptSkillLookup
|
|
476
512
|
? buildMidPromptSkillCompletions(this.#commands, lowerPrefix)
|
|
477
|
-
: buildSlashCommandCompletions(
|
|
513
|
+
: buildSlashCommandCompletions(
|
|
514
|
+
collapseSkillNamespace(this.#commands, lowerPrefix),
|
|
515
|
+
lowerPrefix,
|
|
516
|
+
this.#commandUsage,
|
|
517
|
+
);
|
|
478
518
|
|
|
479
519
|
if (matches.length > 0) {
|
|
480
520
|
return {
|
|
@@ -538,7 +578,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
538
578
|
}
|
|
539
579
|
const suggestions =
|
|
540
580
|
rawPrefix.length > 0
|
|
541
|
-
? await this.#getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix })
|
|
581
|
+
? await this.#getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix, signal })
|
|
542
582
|
: await this.#getFileSuggestions("@");
|
|
543
583
|
if (suggestions.length === 0 && rawPrefix.length > 0) {
|
|
544
584
|
const fallback = await this.#getFileSuggestions(atPrefix);
|
|
@@ -631,14 +671,18 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
631
671
|
const slashPrefix = textBeforeCursor.slice(leadingSlashStart);
|
|
632
672
|
if (!slashPrefix.includes(" ") && !slashPrefix.slice(1).includes("/")) {
|
|
633
673
|
const beforeSlash = currentLine.slice(0, leadingSlashStart);
|
|
634
|
-
|
|
674
|
+
// The collapsed `/skill:` namespace row completes to the namespace
|
|
675
|
+
// itself: no trailing space, so completion continues with the
|
|
676
|
+
// individual skills instead of finishing a command token.
|
|
677
|
+
const insert = item.value === SKILL_NAMESPACE ? `/${item.value}` : `/${item.value} `;
|
|
678
|
+
const newLine = `${beforeSlash}${insert}${afterCursor}`;
|
|
635
679
|
const newLines = [...lines];
|
|
636
680
|
newLines[cursorLine] = newLine;
|
|
637
681
|
|
|
638
682
|
return {
|
|
639
683
|
lines: newLines,
|
|
640
684
|
cursorLine,
|
|
641
|
-
cursorCol: beforeSlash.length +
|
|
685
|
+
cursorCol: beforeSlash.length + insert.length,
|
|
642
686
|
};
|
|
643
687
|
}
|
|
644
688
|
}
|
|
@@ -981,12 +1025,16 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
981
1025
|
}
|
|
982
1026
|
}
|
|
983
1027
|
|
|
984
|
-
async #getFuzzyFileSuggestions(
|
|
1028
|
+
async #getFuzzyFileSuggestions(
|
|
1029
|
+
query: string,
|
|
1030
|
+
options: { isQuotedPrefix: boolean; signal?: AbortSignal },
|
|
1031
|
+
): Promise<AutocompleteItem[]> {
|
|
985
1032
|
try {
|
|
986
1033
|
const scopedQuery = await this.#resolveScopedFuzzyQuery(query);
|
|
1034
|
+
if (options.signal?.aborted) return [];
|
|
987
1035
|
const searchPath = scopedQuery?.baseDir ?? this.#basePath;
|
|
988
1036
|
const fuzzyQuery = scopedQuery?.query ?? query;
|
|
989
|
-
const result = await fuzzyFind(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath));
|
|
1037
|
+
const result = await fuzzyFind(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath, options.signal));
|
|
990
1038
|
const lowerQuery = fuzzyQuery.toLowerCase();
|
|
991
1039
|
const filteredMatches = result.matches.filter(entry => {
|
|
992
1040
|
const p = entry.path.endsWith("/") ? entry.path.slice(0, -1) : entry.path;
|
|
@@ -1029,7 +1077,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
1029
1077
|
lines: string[],
|
|
1030
1078
|
cursorLine: number,
|
|
1031
1079
|
cursorCol: number,
|
|
1080
|
+
signal?: AbortSignal,
|
|
1032
1081
|
): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
|
|
1082
|
+
if (signal?.aborted) return null;
|
|
1033
1083
|
const currentLine = lines[cursorLine] || "";
|
|
1034
1084
|
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
1035
1085
|
|
|
@@ -1099,7 +1149,13 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
1099
1149
|
const prefix = commandText.slice(1);
|
|
1100
1150
|
const lowerPrefix = prefix.toLowerCase();
|
|
1101
1151
|
|
|
1102
|
-
|
|
1152
|
+
// The `/skill:` namespace row is excluded here: the sync path submits
|
|
1153
|
+
// immediately after applying, and the bare namespace is not a command.
|
|
1154
|
+
const matches = buildSlashCommandCompletions(
|
|
1155
|
+
collapseSkillNamespace(this.#commands, lowerPrefix),
|
|
1156
|
+
lowerPrefix,
|
|
1157
|
+
this.#commandUsage,
|
|
1158
|
+
).filter(item => item.value !== SKILL_NAMESPACE);
|
|
1103
1159
|
|
|
1104
1160
|
if (matches.length === 0) return null;
|
|
1105
1161
|
// Mirror `getSuggestions`: preserve leading whitespace so the editor's
|