@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?: (SlashCommand | AutocompleteItem)[], basePath?: string);
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.10.11",
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.10.11",
41
- "@oh-my-pi/pi-utils": "15.10.11",
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
  },
@@ -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: (SlashCommand | AutocompleteItem)[];
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: (SlashCommand | AutocompleteItem)[] = [], basePath: string = getProjectDir()) {
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
- // Filter commands using fuzzy matching (subsequence match)
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 };
@@ -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;