@oh-my-pi/pi-tui 16.1.5 → 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 +7 -0
- package/dist/types/autocomplete.d.ts +7 -0
- package/dist/types/components/editor.d.ts +1 -1
- package/package.json +3 -3
- package/src/autocomplete.ts +42 -18
- package/src/components/editor.ts +10 -3
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
|
|
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.
|
|
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.
|
|
41
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
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
|
},
|
package/src/autocomplete.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
342
|
-
if (
|
|
343
|
-
const
|
|
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 =
|
|
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 =
|
|
361
|
-
const argumentText =
|
|
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
|
|
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
|
|
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
|
-
|
|
875
|
+
const slashStart = findLeadingSlashCommandStart(textBeforeCursor);
|
|
876
|
+
if (slashStart === null) return null;
|
|
858
877
|
|
|
859
|
-
const
|
|
878
|
+
const commandText = textBeforeCursor.slice(slashStart);
|
|
879
|
+
const spaceIndex = commandText.indexOf(" ");
|
|
860
880
|
if (spaceIndex === -1) return null;
|
|
861
881
|
|
|
862
|
-
const commandName =
|
|
863
|
-
const argumentText =
|
|
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
|
-
|
|
875
|
-
if (
|
|
876
|
-
|
|
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 =
|
|
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
|
}
|
package/src/components/editor.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import
|
|
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 (
|
|
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
|
|
1273
|
+
findLeadingSlashCommandStart(textBeforeCursor) !== null &&
|
|
1267
1274
|
this.#isInSubmittedSlashCommandContext() &&
|
|
1268
1275
|
this.#autocompleteProvider?.trySyncSlashCompletion
|
|
1269
1276
|
) {
|