@oh-my-pi/pi-tui 15.10.11 → 15.11.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
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.11.0] - 2026-06-10
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added support for asynchronous `onSubmit` handlers by allowing the callback to return a `Promise<void>`
|
|
9
|
+
|
|
5
10
|
## [15.10.11] - 2026-06-10
|
|
6
11
|
|
|
7
12
|
### Added
|
|
@@ -1273,4 +1278,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
|
|
|
1273
1278
|
|
|
1274
1279
|
### Fixed
|
|
1275
1280
|
|
|
1276
|
-
- **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
|
|
1281
|
+
- **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
|
|
@@ -8,6 +8,7 @@ export interface AutocompleteItem {
|
|
|
8
8
|
type Awaitable<T> = T | Promise<T>;
|
|
9
9
|
export interface SlashCommand {
|
|
10
10
|
name: string;
|
|
11
|
+
aliases?: string[];
|
|
11
12
|
description?: string;
|
|
12
13
|
argumentHint?: string;
|
|
13
14
|
getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
|
|
@@ -48,9 +49,10 @@ export interface AutocompleteProvider {
|
|
|
48
49
|
insert: string;
|
|
49
50
|
} | null;
|
|
50
51
|
}
|
|
52
|
+
type CommandEntry = SlashCommand | AutocompleteItem;
|
|
51
53
|
export declare class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
52
54
|
#private;
|
|
53
|
-
constructor(commands?:
|
|
55
|
+
constructor(commands?: CommandEntry[], basePath?: string);
|
|
54
56
|
getSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
55
57
|
items: AutocompleteItem[];
|
|
56
58
|
prefix: string;
|
|
@@ -43,7 +43,7 @@ export declare class Editor implements Component, Focusable {
|
|
|
43
43
|
* stray text. MUST be a global regex; the editor recompiles a private copy so its `lastIndex`
|
|
44
44
|
* is never shared with the caller. */
|
|
45
45
|
atomicTokenPattern: RegExp | undefined;
|
|
46
|
-
onSubmit?: (text: string) => void
|
|
46
|
+
onSubmit?: (text: string) => void | Promise<void>;
|
|
47
47
|
onAltEnter?: (text: string) => void;
|
|
48
48
|
onChange?: (text: string) => void;
|
|
49
49
|
onAutocompleteCancel?: () => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.11.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": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.11.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.11.0",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.4"
|
|
44
44
|
},
|
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 pi-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 };
|
package/src/components/editor.ts
CHANGED
|
@@ -450,7 +450,7 @@ export class Editor implements Component, Focusable {
|
|
|
450
450
|
// Debounce timer for autocomplete updates
|
|
451
451
|
#autocompleteTimeout?: NodeJS.Timeout;
|
|
452
452
|
|
|
453
|
-
onSubmit?: (text: string) => void
|
|
453
|
+
onSubmit?: (text: string) => void | Promise<void>;
|
|
454
454
|
onAltEnter?: (text: string) => void;
|
|
455
455
|
onChange?: (text: string) => void;
|
|
456
456
|
onAutocompleteCancel?: () => void;
|