@oh-my-pi/pi-tui 16.1.6 → 16.1.7

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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.1.7] - 2026-06-20
6
+
7
+ ### Fixed
8
+
9
+ - Fixed slash command autocomplete, inline hints, and Enter completion when the slash command is preceded by leading whitespace ([#3095](https://github.com/can1357/oh-my-pi/issues/3095)).
10
+ - Fixed empty `/` autocomplete burying user skill commands below every built-in command, so installed skills appear in the initial slash popup ([#2875](https://github.com/can1357/oh-my-pi/issues/2875)).
11
+
5
12
  ## [16.1.0] - 2026-06-19
6
13
 
7
14
  ### Added
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Locate the slash that opens a slash command on the line, allowing leading
3
+ * whitespace. Returns the index of the `/` or `null` when the line is not a
4
+ * slash command. Aligns with `trimStart` semantics so the editor and provider
5
+ * agree on which prefixes count.
6
+ */
7
+ export declare function findLeadingSlashCommandStart(text: string): number | null;
1
8
  export interface AutocompleteItem {
2
9
  value: string;
3
10
  label: string;
@@ -1,4 +1,4 @@
1
- import type { AutocompleteProvider } from "../autocomplete";
1
+ import { type AutocompleteProvider } from "../autocomplete";
2
2
  import type { SymbolTheme } from "../symbols";
3
3
  import { type Component, type Focusable } from "../tui";
4
4
  import { type SelectListTheme } from "./select-list";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "16.1.6",
4
+ "version": "16.1.7",
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": "16.1.6",
41
- "@oh-my-pi/pi-utils": "16.1.6",
40
+ "@oh-my-pi/pi-natives": "16.1.7",
41
+ "@oh-my-pi/pi-utils": "16.1.7",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.5"
44
44
  },
@@ -56,6 +56,18 @@ function isTokenStart(text: string, index: number): boolean {
56
56
  return index === 0 || PATH_DELIMITERS.has(text[index - 1] ?? "");
57
57
  }
58
58
 
59
+ /**
60
+ * Locate the slash that opens a slash command on the line, allowing leading
61
+ * whitespace. Returns the index of the `/` or `null` when the line is not a
62
+ * slash command. Aligns with `trimStart` semantics so the editor and provider
63
+ * agree on which prefixes count.
64
+ */
65
+ export function findLeadingSlashCommandStart(text: string): number | null {
66
+ const trimmed = text.trimStart();
67
+ if (!trimmed.startsWith("/")) return null;
68
+ return text.length - trimmed.length;
69
+ }
70
+
59
71
  function extractQuotedPrefix(text: string): string | null {
60
72
  const quoteStart = findUnclosedQuoteStart(text);
61
73
  if (quoteStart === null) {
@@ -249,7 +261,9 @@ function buildSlashCommandCompletions(commands: CommandEntry[], lowerPrefix: str
249
261
  const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
250
262
  const candidates: Array<AutocompleteItem & { score: number }> = [];
251
263
 
252
- const nameScore = scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
264
+ const isSkillCommand = name.startsWith("skill:");
265
+ const nameScore =
266
+ lowerPrefix.length === 0 && isSkillCommand ? 950 : scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
253
267
  const lowerDesc = desc.toLowerCase();
254
268
  const descScore =
255
269
  lowerDesc && fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
@@ -338,13 +352,14 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
338
352
  };
339
353
  }
340
354
 
341
- // Check for slash commands
342
- if (textBeforeCursor.startsWith("/")) {
343
- const spaceIndex = textBeforeCursor.indexOf(" ");
355
+ const slashStart = findLeadingSlashCommandStart(textBeforeCursor);
356
+ if (slashStart !== null) {
357
+ const commandText = textBeforeCursor.slice(slashStart);
358
+ const spaceIndex = commandText.indexOf(" ");
344
359
 
345
360
  if (spaceIndex === -1) {
346
361
  // No space yet - complete command names
347
- const prefix = textBeforeCursor.slice(1); // Remove the "/"
362
+ const prefix = commandText.slice(1); // Remove the "/"
348
363
  const lowerPrefix = prefix.toLowerCase();
349
364
 
350
365
  const matches = buildSlashCommandCompletions(this.#commands, lowerPrefix);
@@ -353,12 +368,16 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
353
368
 
354
369
  return {
355
370
  items: matches,
371
+ // Preserve the full text-before-cursor (incl. leading
372
+ // whitespace) so the editor's Enter-staleness check
373
+ // (`autocompletePrefix !== currentTextBeforeCursor`)
374
+ // still applies the completion for ` /sk`.
356
375
  prefix: textBeforeCursor,
357
376
  };
358
377
  } else {
359
378
  // Space found - complete command arguments
360
- const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
361
- const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
379
+ const commandName = commandText.slice(1, spaceIndex); // Command without "/"
380
+ const argumentText = commandText.slice(spaceIndex + 1); // Text after space
362
381
 
363
382
  const command = this.#commands.find(cmd => commandMatchesNameOrAlias(cmd, commandName));
364
383
  if (!command || !("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
@@ -416,13 +435,12 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
416
435
  const textBeforeCursor = currentLine.slice(0, cursorCol);
417
436
  const afterCursor = currentLine.slice(cursorCol);
418
437
 
419
- const slashStart = textBeforeCursor.indexOf("/");
420
- const hasOnlyWhitespaceBeforeSlash = slashStart >= 0 && textBeforeCursor.slice(0, slashStart).trim() === "";
438
+ const slashStart = findLeadingSlashCommandStart(textBeforeCursor);
421
439
 
422
440
  // Slash command suggestions can be accepted before the debounced refresh
423
441
  // catches up to newly typed characters. Replace the live command token,
424
442
  // not only the prefix captured when the suggestion list was rendered.
425
- if (prefix.startsWith("/") && hasOnlyWhitespaceBeforeSlash) {
443
+ if (findLeadingSlashCommandStart(prefix) !== null && slashStart !== null) {
426
444
  const slashPrefix = textBeforeCursor.slice(slashStart);
427
445
  if (!slashPrefix.includes(" ") && !slashPrefix.slice(1).includes("/")) {
428
446
  const beforeSlash = currentLine.slice(0, slashStart);
@@ -854,13 +872,15 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
854
872
  const currentLine = lines[cursorLine] || "";
855
873
  const textBeforeCursor = currentLine.slice(0, cursorCol);
856
874
 
857
- if (!textBeforeCursor.startsWith("/")) return null;
875
+ const slashStart = findLeadingSlashCommandStart(textBeforeCursor);
876
+ if (slashStart === null) return null;
858
877
 
859
- const spaceIndex = textBeforeCursor.indexOf(" ");
878
+ const commandText = textBeforeCursor.slice(slashStart);
879
+ const spaceIndex = commandText.indexOf(" ");
860
880
  if (spaceIndex === -1) return null;
861
881
 
862
- const commandName = textBeforeCursor.slice(1, spaceIndex);
863
- const argumentText = textBeforeCursor.slice(spaceIndex + 1);
882
+ const commandName = commandText.slice(1, spaceIndex);
883
+ const argumentText = commandText.slice(spaceIndex + 1);
864
884
 
865
885
  const command = this.#commands.find(cmd => commandMatchesNameOrAlias(cmd, commandName));
866
886
 
@@ -871,16 +891,20 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
871
891
  return command.getInlineHint(argumentText);
872
892
  }
873
893
  trySyncSlashCompletion(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
874
- if (!textBeforeCursor.startsWith("/")) return null;
875
- if (textBeforeCursor.length <= 1) return null; // Bare "/" alone, don't auto-complete
876
- if (textBeforeCursor.includes(" ")) return null; // Only complete command name, not args
894
+ const slashStart = findLeadingSlashCommandStart(textBeforeCursor);
895
+ if (slashStart === null) return null;
896
+ const commandText = textBeforeCursor.slice(slashStart);
897
+ if (commandText.length <= 1) return null; // Bare "/" alone, don't auto-complete
898
+ if (commandText.includes(" ")) return null; // Only complete command name, not args
877
899
 
878
- const prefix = textBeforeCursor.slice(1);
900
+ const prefix = commandText.slice(1);
879
901
  const lowerPrefix = prefix.toLowerCase();
880
902
 
881
903
  const matches = buildSlashCommandCompletions(this.#commands, lowerPrefix);
882
904
 
883
905
  if (matches.length === 0) return null;
906
+ // Mirror `getSuggestions`: preserve leading whitespace so the editor's
907
+ // sync apply path passes the full text-before-cursor through.
884
908
  return { items: matches, prefix: textBeforeCursor };
885
909
  }
886
910
  }
@@ -1,5 +1,9 @@
1
1
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
2
- import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete";
2
+ import {
3
+ type AutocompleteProvider,
4
+ type CombinedAutocompleteProvider,
5
+ findLeadingSlashCommandStart,
6
+ } from "../autocomplete";
3
7
  import { BracketedPasteHandler, decodeReencodedPasteControls } from "../bracketed-paste";
4
8
  import { getKeybindings, type KeybindingsManager } from "../keybindings";
5
9
  import { extractPrintableText, matchesKey } from "../keys";
@@ -1116,7 +1120,10 @@ export class Editor implements Component, Focusable {
1116
1120
  }
1117
1121
 
1118
1122
  // If Enter was pressed on a slash command, apply completion and submit
1119
- if ((kb.matches(data, "tui.input.submit") || data === "\n") && this.#autocompletePrefix.startsWith("/")) {
1123
+ if (
1124
+ (kb.matches(data, "tui.input.submit") || data === "\n") &&
1125
+ findLeadingSlashCommandStart(this.#autocompletePrefix) !== null
1126
+ ) {
1120
1127
  // Check for stale autocomplete state due to debounce
1121
1128
  const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
1122
1129
  const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
@@ -1263,7 +1270,7 @@ export class Editor implements Component, Focusable {
1263
1270
  const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
1264
1271
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1265
1272
  if (
1266
- textBeforeCursor.startsWith("/") &&
1273
+ findLeadingSlashCommandStart(textBeforeCursor) !== null &&
1267
1274
  this.#isInSubmittedSlashCommandContext() &&
1268
1275
  this.#autocompleteProvider?.trySyncSlashCompletion
1269
1276
  ) {