@oh-my-pi/pi-tui 14.8.0 → 14.8.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "14.8.0",
4
+ "version": "14.8.1",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
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": "14.8.0",
41
- "@oh-my-pi/pi-utils": "14.8.0",
40
+ "@oh-my-pi/pi-natives": "14.8.1",
41
+ "@oh-my-pi/pi-utils": "14.8.1",
42
42
  "lru-cache": "11.3.6",
43
43
  "marked": "^18.0.3"
44
44
  },
@@ -193,6 +193,9 @@ export interface AutocompleteProvider {
193
193
 
194
194
  /** Get inline hint text to show as dim ghost text after the cursor */
195
195
  getInlineHint?(lines: string[], cursorLine: number, cursorCol: number): string | null;
196
+ /** Synchronously try to complete a slash command at the start of a line (no async I/O). */
197
+ /** Returns matched items and the full prefix, or null if not applicable. */
198
+ trySyncSlashCompletion?(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null;
196
199
  }
197
200
 
198
201
  // Combined provider that handles both slash commands and file paths.
@@ -777,4 +780,39 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
777
780
 
778
781
  return command.getInlineHint(argumentText);
779
782
  }
783
+ trySyncSlashCompletion(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
784
+ if (!textBeforeCursor.startsWith("/")) return null;
785
+ if (textBeforeCursor.length <= 1) return null; // Bare "/" alone, don't auto-complete
786
+ if (textBeforeCursor.includes(" ")) return null; // Only complete command name, not args
787
+
788
+ const prefix = textBeforeCursor.slice(1);
789
+ const lowerPrefix = prefix.toLowerCase();
790
+
791
+ const matches = this.#commands
792
+ .filter(cmd => {
793
+ const name = "name" in cmd ? cmd.name : cmd.value;
794
+ if (!name) return false;
795
+ if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
796
+ const desc = cmd.description?.toLowerCase();
797
+ return desc ? fuzzyMatch(lowerPrefix, desc) : false;
798
+ })
799
+ .map(cmd => {
800
+ const name = "name" in cmd ? cmd.name : cmd.value;
801
+ const lowerName = name?.toLowerCase() ?? "";
802
+ const lowerDesc = cmd.description?.toLowerCase() ?? "";
803
+ const nameScore = fuzzyMatch(lowerPrefix, lowerName) ? fuzzyScore(lowerPrefix, lowerName) : 0;
804
+ const descScore = fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
805
+ return {
806
+ value: name,
807
+ label: "name" in cmd ? cmd.name : cmd.label,
808
+ score: Math.max(nameScore, descScore),
809
+ ...(cmd.description && { description: cmd.description }),
810
+ } as AutocompleteItem & { score: number };
811
+ })
812
+ .sort((a, b) => b.score - a.score)
813
+ .map(({ score: _, ...rest }) => rest);
814
+
815
+ if (matches.length === 0) return null;
816
+ return { items: matches, prefix: textBeforeCursor };
817
+ }
780
818
  }
@@ -1135,6 +1135,38 @@ export class Editor implements Component, Focusable {
1135
1135
  return;
1136
1136
  }
1137
1137
 
1138
+ // Synchronous slash command completion for the race condition where
1139
+ // async autocomplete hasn't resolved yet (user types /q quickly + Enter).
1140
+ // Match the existing selected-item behavior when autocomplete IS showing.
1141
+ if (!this.#autocompleteState) {
1142
+ const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
1143
+ const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1144
+ if (
1145
+ textBeforeCursor.startsWith("/") &&
1146
+ this.#isInSubmittedSlashCommandContext() &&
1147
+ this.#autocompleteProvider?.trySyncSlashCompletion
1148
+ ) {
1149
+ const syncResult = this.#autocompleteProvider.trySyncSlashCompletion(textBeforeCursor);
1150
+ if (syncResult && syncResult.items.length > 0) {
1151
+ // Invalidate any pending async autocomplete so its stale results are discarded
1152
+ this.#autocompleteRequestId += 1;
1153
+ // Apply the best match and submit the completed command
1154
+ const selected = syncResult.items[0]!;
1155
+ const result = this.#autocompleteProvider.applyCompletion(
1156
+ this.#state.lines,
1157
+ this.#state.cursorLine,
1158
+ this.#state.cursorCol,
1159
+ selected,
1160
+ syncResult.prefix,
1161
+ );
1162
+ this.#state.lines = result.lines;
1163
+ this.#state.cursorLine = result.cursorLine;
1164
+ this.#setCursorCol(result.cursorCol);
1165
+ result.onApplied?.();
1166
+ }
1167
+ }
1168
+ }
1169
+
1138
1170
  this.#submitValue();
1139
1171
  }
1140
1172
  // Backspace (including Shift+Backspace)
@@ -1466,7 +1498,7 @@ export class Editor implements Component, Focusable {
1466
1498
  // Check if we should trigger or update autocomplete
1467
1499
  if (!this.#autocompleteState) {
1468
1500
  // Auto-trigger for "/" at the start of a line (slash commands)
1469
- if (char === "/" && this.#isAtStartOfMessage()) {
1501
+ if (char === "/" && this.#isAtStartOfSubmittedMessage()) {
1470
1502
  this.#tryTriggerAutocomplete();
1471
1503
  }
1472
1504
  // Auto-trigger for "@" file reference (fuzzy search)
@@ -1488,7 +1520,7 @@ export class Editor implements Component, Focusable {
1488
1520
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
1489
1521
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1490
1522
  // Check if we're in a slash command (with or without space for arguments)
1491
- if (textBeforeCursor.trimStart().startsWith("/")) {
1523
+ if (this.#isInSubmittedSlashCommandContext()) {
1492
1524
  this.#tryTriggerAutocomplete();
1493
1525
  }
1494
1526
  // Check if we're in an @ file reference context
@@ -1661,7 +1693,7 @@ export class Editor implements Component, Focusable {
1661
1693
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
1662
1694
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1663
1695
  // Slash command context
1664
- if (textBeforeCursor.trimStart().startsWith("/")) {
1696
+ if (this.#isInSubmittedSlashCommandContext()) {
1665
1697
  this.#tryTriggerAutocomplete();
1666
1698
  }
1667
1699
  // @ file reference context
@@ -1817,7 +1849,7 @@ export class Editor implements Component, Focusable {
1817
1849
  } else {
1818
1850
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
1819
1851
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1820
- if (textBeforeCursor.trimStart().startsWith("/")) {
1852
+ if (this.#isInSubmittedSlashCommandContext()) {
1821
1853
  this.#tryTriggerAutocomplete();
1822
1854
  } else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
1823
1855
  this.#tryTriggerAutocomplete();
@@ -2131,7 +2163,7 @@ export class Editor implements Component, Focusable {
2131
2163
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
2132
2164
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
2133
2165
  // Slash command context
2134
- if (textBeforeCursor.trimStart().startsWith("/")) {
2166
+ if (this.#isInSubmittedSlashCommandContext()) {
2135
2167
  this.#tryTriggerAutocomplete();
2136
2168
  }
2137
2169
  // @ file reference context
@@ -2327,13 +2359,27 @@ export class Editor implements Component, Focusable {
2327
2359
  this.#setCursorCol(moveWordRight(currentLine, this.#state.cursorCol));
2328
2360
  }
2329
2361
 
2330
- // Helper method to check if cursor is at start of message (for slash command detection)
2331
- #isAtStartOfMessage(): boolean {
2362
+ #hasOnlyWhitespaceBeforeCursorLine(): boolean {
2363
+ for (let i = 0; i < this.#state.cursorLine; i++) {
2364
+ if ((this.#state.lines[i] || "").trim() !== "") {
2365
+ return false;
2366
+ }
2367
+ }
2368
+ return true;
2369
+ }
2370
+
2371
+ // Slash commands execute only when the submitted prompt starts with the command.
2372
+ #isAtStartOfSubmittedMessage(): boolean {
2332
2373
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
2333
2374
  const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
2334
2375
 
2335
- // At start if line is empty, only contains whitespace, or is just "/"
2336
- return beforeCursor.trim() === "" || beforeCursor.trim() === "/";
2376
+ return this.#hasOnlyWhitespaceBeforeCursorLine() && (beforeCursor.trim() === "" || beforeCursor.trim() === "/");
2377
+ }
2378
+
2379
+ #isInSubmittedSlashCommandContext(): boolean {
2380
+ const currentLine = this.#state.lines[this.#state.cursorLine] || "";
2381
+ const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
2382
+ return this.#hasOnlyWhitespaceBeforeCursorLine() && beforeCursor.trimStart().startsWith("/");
2337
2383
  }
2338
2384
 
2339
2385
  #isSlashCommandNameAutocompleteSelection(): boolean {
@@ -2343,7 +2389,9 @@ export class Editor implements Component, Focusable {
2343
2389
 
2344
2390
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
2345
2391
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol).trimStart();
2346
- return textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ");
2392
+ return (
2393
+ this.#isInSubmittedSlashCommandContext() && textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")
2394
+ );
2347
2395
  }
2348
2396
 
2349
2397
  #isCompletedSlashCommandAtCursor(): boolean {
@@ -2353,7 +2401,7 @@ export class Editor implements Component, Focusable {
2353
2401
  }
2354
2402
 
2355
2403
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol).trimStart();
2356
- return /^\/\S+ $/.test(textBeforeCursor);
2404
+ return this.#isInSubmittedSlashCommandContext() && /^\/\S+ $/.test(textBeforeCursor);
2357
2405
  }
2358
2406
 
2359
2407
  // Autocomplete methods
@@ -2407,7 +2455,7 @@ export class Editor implements Component, Focusable {
2407
2455
  const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
2408
2456
 
2409
2457
  // Check if we're in a slash command context
2410
- if (beforeCursor.trimStart().startsWith("/") && !beforeCursor.trimStart().includes(" ")) {
2458
+ if (this.#isInSubmittedSlashCommandContext() && !beforeCursor.trimStart().includes(" ")) {
2411
2459
  this.#handleSlashCommandCompletion();
2412
2460
  } else {
2413
2461
  this.#forceFileAutocomplete(true);