@gajae-code/tui 0.3.1 → 0.4.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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.4.0] - 2026-06-06
6
+
7
+ ### Changed
8
+
9
+ - Ranked slash command-name suggestions by stronger prefix/token matches before fuzzy fallback ordering, while keeping priority as an intra-tier tie-breaker.
10
+
5
11
  ## [0.3.1] - 2026-06-05
6
12
 
7
13
  ### Changed
@@ -1,3 +1,4 @@
1
+ export declare function getSlashCommandMatchRank(query: string, commandName: string): number;
1
2
  export interface AutocompleteItem {
2
3
  value: string;
3
4
  label: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/tui",
4
- "version": "0.3.1",
4
+ "version": "0.4.0",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -38,8 +38,8 @@
38
38
  "fmt": "biome format --write ."
39
39
  },
40
40
  "dependencies": {
41
- "@gajae-code/natives": "0.3.1",
42
- "@gajae-code/utils": "0.3.1",
41
+ "@gajae-code/natives": "0.4.0",
42
+ "@gajae-code/utils": "0.4.0",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
@@ -147,6 +147,48 @@ function fuzzyScore(query: string, target: string): number {
147
147
  // Base score 40 for subsequence, minus penalty for gaps
148
148
  return Math.max(1, 40 - gaps * 5);
149
149
  }
150
+ export function getSlashCommandMatchRank(query: string, commandName: string): number {
151
+ const normalizedQuery = normalizeSlashCommandText(query);
152
+ if (normalizedQuery.length === 0) return 4;
153
+
154
+ const normalizedName = normalizeSlashCommandText(commandName);
155
+ if (commandName.toLowerCase().startsWith(query.toLowerCase()) || normalizedName.startsWith(normalizedQuery)) {
156
+ return 0;
157
+ }
158
+
159
+ const queryTokens = normalizedQuery.split(" ").filter(Boolean);
160
+ const nameTokens = normalizedName.split(" ").filter(Boolean);
161
+ if (queryTokens.length === 1 && nameTokens.includes(queryTokens[0]!)) {
162
+ return 1;
163
+ }
164
+
165
+ if (
166
+ queryTokens.length > 1 &&
167
+ queryTokens.every((token, index) => {
168
+ const nameToken = nameTokens[index];
169
+ return nameToken ? nameToken.startsWith(token) : false;
170
+ })
171
+ ) {
172
+ return 2;
173
+ }
174
+
175
+ if (
176
+ queryTokens.length === 1 &&
177
+ nameTokens.some(token => token.startsWith(queryTokens[0]!) || token.includes(queryTokens[0]!))
178
+ ) {
179
+ return 3;
180
+ }
181
+
182
+ return 4;
183
+ }
184
+
185
+ function normalizeSlashCommandText(value: string): string {
186
+ return value
187
+ .toLowerCase()
188
+ .replace(/[^a-z0-9]+/g, " ")
189
+ .trim()
190
+ .replace(/\s+/g, " ");
191
+ }
150
192
 
151
193
  export interface AutocompleteItem {
152
194
  value: string;
@@ -273,12 +315,13 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
273
315
  .filter(cmd => {
274
316
  const name = "name" in cmd ? cmd.name : cmd.value;
275
317
  if (!name) return false;
276
- // Match name or description
318
+ // Match name, normalized slash-name aliases, or description.
277
319
  if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
320
+ if (getSlashCommandMatchRank(lowerPrefix, name.toLowerCase()) < 4) return true;
278
321
  const desc = cmd.description?.toLowerCase();
279
322
  return desc ? fuzzyMatch(lowerPrefix, desc) : false;
280
323
  })
281
- .map(cmd => {
324
+ .map((cmd, index) => {
282
325
  const name = "name" in cmd ? cmd.name : cmd.value;
283
326
  const lowerName = name?.toLowerCase() ?? "";
284
327
  const lowerDesc = cmd.description?.toLowerCase() ?? "";
@@ -294,11 +337,16 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
294
337
  label: "name" in cmd ? cmd.name : cmd.label,
295
338
  score: Math.max(nameScore, descScore),
296
339
  priority,
340
+ matchRank: getSlashCommandMatchRank(lowerPrefix, lowerName),
341
+ index,
297
342
  ...(fullDesc && { description: fullDesc }),
298
343
  };
299
344
  })
300
- .sort((a, b) => b.priority - a.priority || b.score - a.score)
301
- .map(({ score: _score, priority: _priority, ...rest }) => rest);
345
+ .sort(
346
+ (a, b) =>
347
+ a.matchRank - b.matchRank || b.priority - a.priority || b.score - a.score || a.index - b.index,
348
+ )
349
+ .map(({ score: _score, priority: _priority, matchRank: _matchRank, index: _index, ...rest }) => rest);
302
350
 
303
351
  if (matches.length === 0) return null;
304
352
 
@@ -815,10 +863,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
815
863
  const name = "name" in cmd ? cmd.name : cmd.value;
816
864
  if (!name) return false;
817
865
  if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
866
+ if (getSlashCommandMatchRank(lowerPrefix, name.toLowerCase()) < 4) return true;
818
867
  const desc = cmd.description?.toLowerCase();
819
868
  return desc ? fuzzyMatch(lowerPrefix, desc) : false;
820
869
  })
821
- .map(cmd => {
870
+ .map((cmd, index) => {
822
871
  const name = "name" in cmd ? cmd.name : cmd.value;
823
872
  const lowerName = name?.toLowerCase() ?? "";
824
873
  const lowerDesc = cmd.description?.toLowerCase() ?? "";
@@ -833,11 +882,13 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
833
882
  label: "name" in cmd ? cmd.name : cmd.label,
834
883
  score: Math.max(nameScore, descScore),
835
884
  priority,
885
+ matchRank: getSlashCommandMatchRank(lowerPrefix, lowerName),
886
+ index,
836
887
  ...(fullDesc && { description: fullDesc }),
837
- } as AutocompleteItem & { score: number; priority: number };
888
+ } as AutocompleteItem & { score: number; priority: number; matchRank: number; index: number };
838
889
  })
839
- .sort((a, b) => b.priority - a.priority || b.score - a.score)
840
- .map(({ score: _score, priority: _priority, ...rest }) => rest);
890
+ .sort((a, b) => a.matchRank - b.matchRank || b.priority - a.priority || b.score - a.score || a.index - b.index)
891
+ .map(({ score: _score, priority: _priority, matchRank: _matchRank, index: _index, ...rest }) => rest);
841
892
 
842
893
  if (matches.length === 0) return null;
843
894
  return { items: matches, prefix: textBeforeCursor };