jeopi-tui 16.2.24 → 16.2.26

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.2.25] - 2026-07-05
6
+
7
+ ### Added
8
+
9
+ - Added `priority?: number` to `SlashCommand` (ported from gajae-code): `CombinedAutocompleteProvider`'s slash-command ranking now breaks same-score ties by `priority` (higher first) instead of falling through only to registry order. Text-match quality (exact/prefix/contains/subsequence) still ranks first — `priority` only decides ties within the same score tier, most visibly the bare `/` menu where every command scores 1.
10
+
5
11
  ## [16.2.14] - 2026-07-02
6
12
 
7
13
  ### Fixed
@@ -18,6 +18,8 @@ export interface SlashCommand {
18
18
  name: string;
19
19
  aliases?: string[];
20
20
  description?: string;
21
+ /** Ranking boost for the empty-prefix `/` menu and tied fuzzy matches; higher shows first. Commands without a priority default to 0. */
22
+ priority?: number;
21
23
  argumentHint?: string;
22
24
  /** Dynamic display-only description for slash-command autocomplete. Must be synchronous and side-effect free. */
23
25
  getAutocompleteDescription?: () => string | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-tui",
4
- "version": "16.2.24",
4
+ "version": "16.2.26",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "jeopi-natives": "16.2.24",
41
- "jeopi-utils": "16.2.24",
40
+ "jeopi-natives": "16.2.26",
41
+ "jeopi-utils": "16.2.26",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.5"
44
44
  },
@@ -181,6 +181,8 @@ export interface SlashCommand {
181
181
  name: string;
182
182
  aliases?: string[];
183
183
  description?: string;
184
+ /** Ranking boost for the empty-prefix `/` menu and tied fuzzy matches; higher shows first. Commands without a priority default to 0. */
185
+ priority?: number;
184
186
  argumentHint?: string;
185
187
  /** Dynamic display-only description for slash-command autocomplete. Must be synchronous and side-effect free. */
186
188
  getAutocompleteDescription?: () => string | undefined;
@@ -286,63 +288,74 @@ function scoreCommandTextMatch(lowerPrefix: string, lowerTarget: string): number
286
288
  }
287
289
 
288
290
  function buildSlashCommandCompletions(commands: CommandEntry[], lowerPrefix: string): AutocompleteItem[] {
289
- return commands
290
- .flatMap(cmd => {
291
- const name = getCommandName(cmd);
292
- if (!name) return [];
293
- const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
294
- const staticDesc = getStaticCommandDescription(cmd);
295
- let fullDescMemo: string | undefined;
296
- let fullDescComputed = false;
297
- // Resolve the (possibly live) display description lazily, only once a
298
- // candidate actually matches getAutocompleteDescription reads live
299
- // session state and must not run for every command on each keystroke.
300
- const resolveFullDesc = (): string | undefined => {
301
- if (!fullDescComputed) {
302
- const displayDesc = getAutocompleteCommandDescription(cmd);
303
- fullDescMemo = hint ? (displayDesc ? `${hint} - ${displayDesc}` : hint) : displayDesc;
304
- fullDescComputed = true;
305
- }
306
- return fullDescMemo;
307
- };
308
- const candidates: Array<AutocompleteItem & { score: number }> = [];
309
-
310
- const isSkillCommand = name.startsWith("skill:");
311
- const nameScore =
312
- lowerPrefix.length === 0 && isSkillCommand ? 950 : scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
313
- const lowerDesc = staticDesc.toLowerCase();
314
- const descScore =
315
- lowerDesc && fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
316
- const primaryScore = Math.max(nameScore, descScore);
317
- if (primaryScore > 0) {
318
- const fullDesc = resolveFullDesc();
319
- candidates.push({
320
- value: name,
321
- label: "name" in cmd ? cmd.name : cmd.label,
322
- score: primaryScore,
323
- ...(fullDesc && { description: fullDesc }),
324
- });
325
- }
326
-
327
- if (lowerPrefix.length > 0) {
328
- for (const alias of getCommandAliases(cmd)) {
329
- if (alias === name) continue;
330
- const aliasScore = scoreCommandTextMatch(lowerPrefix, alias.toLowerCase());
331
- if (aliasScore === 0) continue;
291
+ return (
292
+ commands
293
+ .flatMap(cmd => {
294
+ const name = getCommandName(cmd);
295
+ if (!name) return [];
296
+ const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
297
+ const staticDesc = getStaticCommandDescription(cmd);
298
+ let fullDescMemo: string | undefined;
299
+ let fullDescComputed = false;
300
+ // Resolve the (possibly live) display description lazily, only once a
301
+ // candidate actually matches getAutocompleteDescription reads live
302
+ // session state and must not run for every command on each keystroke.
303
+ const resolveFullDesc = (): string | undefined => {
304
+ if (!fullDescComputed) {
305
+ const displayDesc = getAutocompleteCommandDescription(cmd);
306
+ fullDescMemo = hint ? (displayDesc ? `${hint} - ${displayDesc}` : hint) : displayDesc;
307
+ fullDescComputed = true;
308
+ }
309
+ return fullDescMemo;
310
+ };
311
+ const candidates: Array<AutocompleteItem & { score: number; priority: number }> = [];
312
+ const priority = "priority" in cmd && typeof cmd.priority === "number" ? cmd.priority : 0;
313
+
314
+ const isSkillCommand = name.startsWith("skill:");
315
+ const nameScore =
316
+ lowerPrefix.length === 0 && isSkillCommand
317
+ ? 950
318
+ : scoreCommandTextMatch(lowerPrefix, name.toLowerCase());
319
+ const lowerDesc = staticDesc.toLowerCase();
320
+ const descScore =
321
+ lowerDesc && fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
322
+ const primaryScore = Math.max(nameScore, descScore);
323
+ if (primaryScore > 0) {
332
324
  const fullDesc = resolveFullDesc();
333
325
  candidates.push({
334
- value: alias,
335
- label: alias,
336
- score: aliasScore,
326
+ value: name,
327
+ label: "name" in cmd ? cmd.name : cmd.label,
328
+ score: primaryScore,
329
+ priority,
337
330
  ...(fullDesc && { description: fullDesc }),
338
331
  });
339
332
  }
340
- }
341
333
 
342
- return candidates;
343
- })
344
- .sort((a, b) => b.score - a.score)
345
- .map(({ score: _, ...rest }) => rest);
334
+ if (lowerPrefix.length > 0) {
335
+ for (const alias of getCommandAliases(cmd)) {
336
+ if (alias === name) continue;
337
+ const aliasScore = scoreCommandTextMatch(lowerPrefix, alias.toLowerCase());
338
+ if (aliasScore === 0) continue;
339
+ const fullDesc = resolveFullDesc();
340
+ candidates.push({
341
+ value: alias,
342
+ label: alias,
343
+ score: aliasScore,
344
+ priority,
345
+ ...(fullDesc && { description: fullDesc }),
346
+ });
347
+ }
348
+ }
349
+
350
+ return candidates;
351
+ })
352
+ // Score ranks fuzzy-match quality first (exact/prefix/contains/subsequence);
353
+ // `priority` only breaks ties within the same score tier — most visibly the
354
+ // empty-prefix `/` menu, where every command scores 1 and priority alone
355
+ // decides display order (e.g. `/help`, `/new`, `/resume` surface first).
356
+ .sort((a, b) => b.score - a.score || b.priority - a.priority)
357
+ .map(({ score: _score, priority: _priority, ...rest }) => rest)
358
+ );
346
359
  }
347
360
 
348
361
  function hasPromptTextBeforeSlash(