@gajae-code/tui 0.11.2 → 0.11.3

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,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.3] - 2026-07-19
6
+ ### Fixed
7
+
8
+ - Suppressed slash-command and skill autocomplete inside line-local single-backtick code spans while preserving path completion and ordinary slash matching outside literals (#2619).
9
+
5
10
  ## [0.11.0] - 2026-07-15
6
11
  ### Fixed
7
12
 
@@ -1,4 +1,5 @@
1
1
  export declare function getSlashCommandMatchRank(query: string, commandName: string): number;
2
+ export declare function isInsideInlineCodeSpan(text: string): boolean;
2
3
  export declare function extractSlashCommandTokenPrefix(text: string): string | null;
3
4
  export interface AutocompleteItem {
4
5
  value: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/tui",
4
- "version": "0.11.2",
4
+ "version": "0.11.3",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -36,8 +36,8 @@
36
36
  "fmt": "biome format --write ."
37
37
  },
38
38
  "dependencies": {
39
- "@gajae-code/natives": "0.11.2",
40
- "@gajae-code/utils": "0.11.2",
39
+ "@gajae-code/natives": "0.11.3",
40
+ "@gajae-code/utils": "0.11.3",
41
41
  "lru-cache": "11.3.6",
42
42
  "marked": "18.0.6"
43
43
  },
@@ -190,10 +190,38 @@ function normalizeSlashCommandText(value: string): string {
190
190
  .replace(/\s+/g, " ");
191
191
  }
192
192
  const NON_COMMAND_SLASH_PREFIX_PRECEDERS = new Set(["/", "\\", ":", ".", "~"]);
193
+ function findOpenInlineCodeSpanStart(text: string): number | null {
194
+ let openDelimiter: number | null = null;
195
+
196
+ for (let i = 0; i < text.length; ) {
197
+ if (text[i] !== "`") {
198
+ i += 1;
199
+ continue;
200
+ }
201
+
202
+ let runEnd = i + 1;
203
+ while (text[runEnd] === "`") runEnd += 1;
204
+ if (runEnd - i !== 1) {
205
+ i = runEnd;
206
+ continue;
207
+ }
208
+
209
+ let backslashCount = 0;
210
+ for (let j = i - 1; j >= 0 && text[j] === "\\"; j -= 1) backslashCount += 1;
211
+ if (backslashCount % 2 === 0) openDelimiter = openDelimiter === null ? i : null;
212
+ i = runEnd;
213
+ }
214
+
215
+ return openDelimiter;
216
+ }
217
+ export function isInsideInlineCodeSpan(text: string): boolean {
218
+ return findOpenInlineCodeSpanStart(text) !== null;
219
+ }
193
220
 
194
221
  export function extractSlashCommandTokenPrefix(text: string): string | null {
195
222
  const slashIndex = text.lastIndexOf("/");
196
223
  if (slashIndex === -1) return null;
224
+ if (isInsideInlineCodeSpan(text.slice(0, slashIndex + 1))) return null;
197
225
 
198
226
  const token = text.slice(slashIndex);
199
227
  if (/[\s]/.test(token)) return null;
@@ -556,7 +584,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
556
584
  }
557
585
 
558
586
  const lastDelimiterIndex = findLastDelimiter(text);
559
- const pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);
587
+ const inlineCodeStart = findOpenInlineCodeSpanStart(text);
588
+ const prefixStart = Math.max(lastDelimiterIndex, inlineCodeStart ?? -1);
589
+ const pathPrefix = prefixStart === -1 ? text : text.slice(prefixStart + 1);
560
590
 
561
591
  // For forced extraction (Tab key), always return something
562
592
  if (forceExtract) {
@@ -3,6 +3,7 @@ import {
3
3
  type AutocompleteProvider,
4
4
  type CombinedAutocompleteProvider,
5
5
  extractSlashCommandTokenPrefix,
6
+ isInsideInlineCodeSpan,
6
7
  } from "../autocomplete";
7
8
  import { BracketedPasteHandler } from "../bracketed-paste";
8
9
  import { getKeybindings, type KeybindingsManager } from "../keybindings";
@@ -459,6 +460,7 @@ export class Editor implements Component, Focusable {
459
460
  #autocompleteState: "regular" | "force" | null = null;
460
461
  #autocompletePrefix: string = "";
461
462
  #autocompleteRequestId: number = 0;
463
+ #autocompleteOrigin?: { docVersion: number; cursorLine: number; cursorCol: number };
462
464
  #autocompleteMaxVisible: number = 5;
463
465
  onAutocompleteUpdate?: () => void;
464
466
 
@@ -1201,6 +1203,11 @@ export class Editor implements Component, Focusable {
1201
1203
 
1202
1204
  // If Tab was pressed, always apply the selection
1203
1205
  if (kb.matches(data, "tui.input.tab")) {
1206
+ if (!this.#isAutocompleteSelectionCurrent()) {
1207
+ this.#cancelAutocomplete();
1208
+ this.#handleTabCompletion();
1209
+ return;
1210
+ }
1204
1211
  const selected = this.#autocompleteList.getSelectedItem();
1205
1212
  if (selected && this.#autocompleteProvider) {
1206
1213
  const shouldChainSlashCommandAutocomplete = this.#isSlashCommandNameAutocompleteSelection();
@@ -1234,36 +1241,38 @@ export class Editor implements Component, Focusable {
1234
1241
  }
1235
1242
 
1236
1243
  // If Enter was pressed on a slash command, apply completion and submit
1237
- if ((kb.matches(data, "tui.input.submit") || data === "\n") && this.#autocompletePrefix.startsWith("/")) {
1238
- // Check for stale autocomplete state due to debounce
1239
- const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
1240
- const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1241
- if (!currentTextBeforeCursor.endsWith(this.#autocompletePrefix)) {
1242
- // Autocomplete is stale - cancel and fall through to normal submission
1244
+ if (
1245
+ (kb.matches(data, "tui.input.submit") || data === "\n") &&
1246
+ this.#autocompleteState === "regular" &&
1247
+ this.#autocompletePrefix.startsWith("/")
1248
+ ) {
1249
+ const selected = this.#autocompleteList.getSelectedItem();
1250
+ if (!this.#isAutocompleteSelectionCurrent()) {
1243
1251
  this.#cancelAutocomplete();
1244
- } else {
1245
- const selected = this.#autocompleteList.getSelectedItem();
1246
- if (selected && this.#autocompleteProvider) {
1247
- const result = this.#autocompleteProvider.applyCompletion(
1248
- this.#state.lines,
1249
- this.#state.cursorLine,
1250
- this.#state.cursorCol,
1251
- selected,
1252
- this.#autocompletePrefix,
1253
- );
1252
+ } else if (selected && this.#autocompleteProvider) {
1253
+ const result = this.#autocompleteProvider.applyCompletion(
1254
+ this.#state.lines,
1255
+ this.#state.cursorLine,
1256
+ this.#state.cursorCol,
1257
+ selected,
1258
+ this.#autocompletePrefix,
1259
+ );
1254
1260
 
1255
- this.#state.lines = result.lines;
1256
- this.#bumpDocumentVersion();
1257
- this.#state.cursorLine = result.cursorLine;
1258
- this.#setCursorCol(result.cursorCol);
1259
- result.onApplied?.();
1260
- }
1261
+ this.#state.lines = result.lines;
1262
+ this.#bumpDocumentVersion();
1263
+ this.#state.cursorLine = result.cursorLine;
1264
+ this.#setCursorCol(result.cursorCol);
1265
+ result.onApplied?.();
1261
1266
  this.#cancelAutocomplete();
1262
1267
  }
1263
1268
  // Don't return - fall through to submission logic
1264
1269
  }
1265
1270
  // If Enter was pressed on a file path, apply completion
1266
1271
  else if (kb.matches(data, "tui.input.submit") || data === "\n") {
1272
+ if (!this.#isAutocompleteSelectionCurrent()) {
1273
+ this.#cancelAutocomplete();
1274
+ return;
1275
+ }
1267
1276
  const selected = this.#autocompleteList.getSelectedItem();
1268
1277
  if (selected && this.#autocompleteProvider) {
1269
1278
  const result = this.#autocompleteProvider.applyCompletion(
@@ -1863,9 +1872,17 @@ export class Editor implements Component, Focusable {
1863
1872
 
1864
1873
  // Check if we should trigger or update autocomplete
1865
1874
  if (!this.#autocompleteState) {
1866
- // Auto-trigger for slash command tokens.
1867
- if (char === "/" && (this.#isAtStartOfSubmittedMessage() || this.#isInSlashTokenContext())) {
1868
- this.#tryTriggerAutocomplete();
1875
+ // Auto-trigger slash commands, or path-only completion inside inline code.
1876
+ if (char === "/") {
1877
+ const currentLine = this.#state.lines[this.#state.cursorLine] || "";
1878
+ const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1879
+ if (this.#isInSubmittedSlashCommandContext()) {
1880
+ this.#tryTriggerAutocomplete();
1881
+ } else if (isInsideInlineCodeSpan(textBeforeCursor)) {
1882
+ this.#forceFileAutocomplete();
1883
+ } else if (this.#isInSlashTokenContext()) {
1884
+ this.#tryTriggerAutocomplete();
1885
+ }
1869
1886
  }
1870
1887
  // Auto-trigger for "@" file reference (fuzzy search)
1871
1888
  else if (char === "@") {
@@ -2771,14 +2788,6 @@ export class Editor implements Component, Focusable {
2771
2788
  return true;
2772
2789
  }
2773
2790
 
2774
- // Slash commands execute only when the submitted prompt starts with the command.
2775
- #isAtStartOfSubmittedMessage(): boolean {
2776
- const currentLine = this.#state.lines[this.#state.cursorLine] || "";
2777
- const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
2778
-
2779
- return this.#hasOnlyWhitespaceBeforeCursorLine() && (beforeCursor.trim() === "" || beforeCursor.trim() === "/");
2780
- }
2781
-
2782
2791
  #isInSubmittedSlashCommandContext(): boolean {
2783
2792
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
2784
2793
  const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
@@ -2794,6 +2803,30 @@ export class Editor implements Component, Focusable {
2794
2803
  #isInSlashTokenContext(): boolean {
2795
2804
  return this.#getSlashTokenBeforeCursor() !== null;
2796
2805
  }
2806
+ #isAutocompleteSelectionCurrent(): boolean {
2807
+ if (!this.#isAutocompleteOriginCurrent()) return false;
2808
+ const currentLine = this.#state.lines[this.#state.cursorLine] || "";
2809
+ const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
2810
+ if (!textBeforeCursor.endsWith(this.#autocompletePrefix)) return false;
2811
+ if (this.#autocompleteState !== "regular" || !this.#autocompletePrefix.startsWith("/")) return true;
2812
+ return extractSlashCommandTokenPrefix(textBeforeCursor) === this.#autocompletePrefix;
2813
+ }
2814
+ #captureAutocompleteOrigin(): { docVersion: number; cursorLine: number; cursorCol: number } {
2815
+ return {
2816
+ docVersion: this.#docVersion,
2817
+ cursorLine: this.#state.cursorLine,
2818
+ cursorCol: this.#state.cursorCol,
2819
+ };
2820
+ }
2821
+
2822
+ #isAutocompleteOriginCurrent(origin = this.#autocompleteOrigin): boolean {
2823
+ return (
2824
+ origin !== undefined &&
2825
+ origin.docVersion === this.#docVersion &&
2826
+ origin.cursorLine === this.#state.cursorLine &&
2827
+ origin.cursorCol === this.#state.cursorCol
2828
+ );
2829
+ }
2797
2830
 
2798
2831
  #isSlashCommandNameAutocompleteSelection(): boolean {
2799
2832
  if (this.#autocompleteState !== "regular") {
@@ -2835,6 +2868,7 @@ export class Editor implements Component, Focusable {
2835
2868
  }
2836
2869
  }
2837
2870
 
2871
+ const origin = this.#captureAutocompleteOrigin();
2838
2872
  const requestId = ++this.#autocompleteRequestId;
2839
2873
 
2840
2874
  const suggestions = await this.#autocompleteProvider.getSuggestions(
@@ -2842,12 +2876,13 @@ export class Editor implements Component, Focusable {
2842
2876
  this.#state.cursorLine,
2843
2877
  this.#state.cursorCol,
2844
2878
  );
2845
- if (requestId !== this.#autocompleteRequestId) return;
2879
+ if (requestId !== this.#autocompleteRequestId || !this.#isAutocompleteOriginCurrent(origin)) return;
2846
2880
 
2847
2881
  if (suggestions && Array.isArray(suggestions.items) && suggestions.items.length > 0) {
2848
2882
  this.#autocompletePrefix = suggestions.prefix;
2849
2883
  this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
2850
2884
  this.#autocompleteState = "regular";
2885
+ this.#autocompleteOrigin = origin;
2851
2886
  this.onAutocompleteUpdate?.();
2852
2887
  } else {
2853
2888
  this.#cancelAutocomplete();
@@ -2907,13 +2942,14 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
2907
2942
  return;
2908
2943
  }
2909
2944
 
2945
+ const origin = this.#captureAutocompleteOrigin();
2910
2946
  const requestId = ++this.#autocompleteRequestId;
2911
2947
  const suggestions = await provider.getForceFileSuggestions(
2912
2948
  this.#state.lines,
2913
2949
  this.#state.cursorLine,
2914
2950
  this.#state.cursorCol,
2915
2951
  );
2916
- if (requestId !== this.#autocompleteRequestId) return;
2952
+ if (requestId !== this.#autocompleteRequestId || !this.#isAutocompleteOriginCurrent(origin)) return;
2917
2953
 
2918
2954
  if (suggestions && Array.isArray(suggestions.items) && suggestions.items.length > 0) {
2919
2955
  // If there's exactly one suggestion and this was an explicit Tab press, apply it immediately
@@ -2941,6 +2977,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
2941
2977
  this.#autocompletePrefix = suggestions.prefix;
2942
2978
  this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
2943
2979
  this.#autocompleteState = "force";
2980
+ this.#autocompleteOrigin = origin;
2944
2981
  this.onAutocompleteUpdate?.();
2945
2982
  } else {
2946
2983
  this.#cancelAutocomplete();
@@ -2955,6 +2992,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
2955
2992
  this.#autocompleteRequestId += 1;
2956
2993
  this.#autocompleteState = null;
2957
2994
  this.#autocompleteList = undefined;
2995
+ this.#autocompleteOrigin = undefined;
2958
2996
  this.#autocompletePrefix = "";
2959
2997
  if (notifyCancel && wasAutocompleting) {
2960
2998
  this.onAutocompleteCancel?.();
@@ -2974,6 +3012,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
2974
3012
  return;
2975
3013
  }
2976
3014
 
3015
+ const origin = this.#captureAutocompleteOrigin();
2977
3016
  const requestId = ++this.#autocompleteRequestId;
2978
3017
 
2979
3018
  const suggestions = await this.#autocompleteProvider.getSuggestions(
@@ -2981,12 +3020,13 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
2981
3020
  this.#state.cursorLine,
2982
3021
  this.#state.cursorCol,
2983
3022
  );
2984
- if (requestId !== this.#autocompleteRequestId) return;
3023
+ if (requestId !== this.#autocompleteRequestId || !this.#isAutocompleteOriginCurrent(origin)) return;
2985
3024
 
2986
3025
  if (suggestions && Array.isArray(suggestions.items) && suggestions.items.length > 0) {
2987
3026
  this.#autocompletePrefix = suggestions.prefix;
2988
3027
  // Always create new SelectList to ensure update
2989
3028
  this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
3029
+ this.#autocompleteOrigin = origin;
2990
3030
  this.onAutocompleteUpdate?.();
2991
3031
  } else {
2992
3032
  this.#cancelAutocomplete();