@gajae-code/tui 0.8.0 → 0.8.2
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 +6 -0
- package/dist/types/autocomplete.d.ts +1 -0
- package/dist/types/components/editor.d.ts +1 -1
- package/package.json +3 -3
- package/src/autocomplete.ts +119 -96
- package/src/components/editor.ts +37 -14
- package/src/tui.ts +21 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-07-06
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
- Fixed backward jump-to-character (`ctrl+alt+]`) matching the character under the cursor when the cursor is at column 0: `lastIndexOf` clamps a negative start position to 0, so the jump stopped in place instead of skipping the cursor position and continuing into earlier lines.
|
|
9
|
+
- GJC-launched psmux panes are now treated as multiplexer sessions even when they do not expose `$TMUX`, so resize and forced redraw paths repaint the live viewport instead of replaying/clearing scrollback and leaving the bottom-pinned composer area above stale blank rows.
|
|
10
|
+
|
|
5
11
|
## [0.8.0] - 2026-07-04
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -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": "@gajae-code/tui",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.2",
|
|
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",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"fmt": "biome format --write ."
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@gajae-code/natives": "0.8.
|
|
42
|
-
"@gajae-code/utils": "0.8.
|
|
41
|
+
"@gajae-code/natives": "0.8.2",
|
|
42
|
+
"@gajae-code/utils": "0.8.2",
|
|
43
43
|
"lru-cache": "11.3.6",
|
|
44
44
|
"marked": "^18.0.3"
|
|
45
45
|
},
|
package/src/autocomplete.ts
CHANGED
|
@@ -189,7 +189,20 @@ function normalizeSlashCommandText(value: string): string {
|
|
|
189
189
|
.trim()
|
|
190
190
|
.replace(/\s+/g, " ");
|
|
191
191
|
}
|
|
192
|
+
const NON_COMMAND_SLASH_PREFIX_PRECEDERS = new Set(["/", "\\", ":", ".", "~"]);
|
|
192
193
|
|
|
194
|
+
export function extractSlashCommandTokenPrefix(text: string): string | null {
|
|
195
|
+
const slashIndex = text.lastIndexOf("/");
|
|
196
|
+
if (slashIndex === -1) return null;
|
|
197
|
+
|
|
198
|
+
const token = text.slice(slashIndex);
|
|
199
|
+
if (/[\s]/.test(token)) return null;
|
|
200
|
+
|
|
201
|
+
const charBeforeSlash = text[slashIndex - 1];
|
|
202
|
+
if (charBeforeSlash && NON_COMMAND_SLASH_PREFIX_PRECEDERS.has(charBeforeSlash)) return null;
|
|
203
|
+
|
|
204
|
+
return token;
|
|
205
|
+
}
|
|
193
206
|
export interface AutocompleteItem {
|
|
194
207
|
value: string;
|
|
195
208
|
label: string;
|
|
@@ -272,6 +285,66 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
272
285
|
this.#basePath = basePath;
|
|
273
286
|
}
|
|
274
287
|
|
|
288
|
+
#getCommandName(cmd: SlashCommand | AutocompleteItem): string {
|
|
289
|
+
return "name" in cmd ? cmd.name : cmd.value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#getSlashCommandNameSuggestions(prefix: string): AutocompleteItem[] {
|
|
293
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
294
|
+
|
|
295
|
+
return this.#commands
|
|
296
|
+
.filter(cmd => {
|
|
297
|
+
const name = this.#getCommandName(cmd);
|
|
298
|
+
if (!name) return false;
|
|
299
|
+
if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
|
|
300
|
+
if (getSlashCommandMatchRank(lowerPrefix, name.toLowerCase()) < 4) return true;
|
|
301
|
+
const desc = cmd.description?.toLowerCase();
|
|
302
|
+
return desc ? fuzzyMatch(lowerPrefix, desc) : false;
|
|
303
|
+
})
|
|
304
|
+
.map((cmd, index) => {
|
|
305
|
+
const name = this.#getCommandName(cmd);
|
|
306
|
+
const lowerName = name?.toLowerCase() ?? "";
|
|
307
|
+
const lowerDesc = cmd.description?.toLowerCase() ?? "";
|
|
308
|
+
const nameScore = fuzzyMatch(lowerPrefix, lowerName) ? fuzzyScore(lowerPrefix, lowerName) : 0;
|
|
309
|
+
const descScore = fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
310
|
+
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
311
|
+
const desc = cmd.description ?? "";
|
|
312
|
+
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
313
|
+
const priority = "priority" in cmd && typeof cmd.priority === "number" ? cmd.priority : 0;
|
|
314
|
+
return {
|
|
315
|
+
value: name,
|
|
316
|
+
label: "name" in cmd ? cmd.name : cmd.label,
|
|
317
|
+
score: Math.max(nameScore, descScore),
|
|
318
|
+
priority,
|
|
319
|
+
matchRank: getSlashCommandMatchRank(lowerPrefix, lowerName),
|
|
320
|
+
index,
|
|
321
|
+
...(fullDesc && { description: fullDesc }),
|
|
322
|
+
} as AutocompleteItem & { score: number; priority: number; matchRank: number; index: number };
|
|
323
|
+
})
|
|
324
|
+
.sort((a, b) => a.matchRank - b.matchRank || b.priority - a.priority || b.score - a.score || a.index - b.index)
|
|
325
|
+
.map(({ score: _score, priority: _priority, matchRank: _matchRank, index: _index, ...rest }) => rest);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#getInlineSlashCommandNameSuggestions(prefix: string): AutocompleteItem[] {
|
|
329
|
+
if (prefix.length === 0) return this.#getSlashCommandNameSuggestions(prefix);
|
|
330
|
+
|
|
331
|
+
const normalizedPrefix = normalizeSlashCommandText(prefix);
|
|
332
|
+
return this.#getSlashCommandNameSuggestions(prefix).filter(item => {
|
|
333
|
+
const lowerValue = item.value.toLowerCase();
|
|
334
|
+
if (lowerValue.startsWith(prefix.toLowerCase())) return true;
|
|
335
|
+
if (!normalizedPrefix) return true;
|
|
336
|
+
return normalizeSlashCommandText(item.value).startsWith(normalizedPrefix);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#extractSlashCommandPrefix(text: string): string | null {
|
|
341
|
+
return extractSlashCommandTokenPrefix(text);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#isKnownCommandItem(item: AutocompleteItem): boolean {
|
|
345
|
+
return this.#commands.some(cmd => this.#getCommandName(cmd) === item.value);
|
|
346
|
+
}
|
|
347
|
+
|
|
275
348
|
async getSuggestions(
|
|
276
349
|
lines: string[],
|
|
277
350
|
cursorLine: number,
|
|
@@ -301,52 +374,14 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
301
374
|
};
|
|
302
375
|
}
|
|
303
376
|
|
|
304
|
-
// Check for slash commands
|
|
377
|
+
// Check for slash commands at the submitted-message start
|
|
305
378
|
if (textBeforeCursor.startsWith("/")) {
|
|
306
379
|
const spaceIndex = textBeforeCursor.indexOf(" ");
|
|
307
380
|
|
|
308
381
|
if (spaceIndex === -1) {
|
|
309
382
|
// No space yet - complete command names
|
|
310
383
|
const prefix = textBeforeCursor.slice(1); // Remove the "/"
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
// Filter commands using fuzzy matching (subsequence match)
|
|
314
|
-
const matches = this.#commands
|
|
315
|
-
.filter(cmd => {
|
|
316
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
317
|
-
if (!name) return false;
|
|
318
|
-
// Match name, normalized slash-name aliases, or description.
|
|
319
|
-
if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
|
|
320
|
-
if (getSlashCommandMatchRank(lowerPrefix, name.toLowerCase()) < 4) return true;
|
|
321
|
-
const desc = cmd.description?.toLowerCase();
|
|
322
|
-
return desc ? fuzzyMatch(lowerPrefix, desc) : false;
|
|
323
|
-
})
|
|
324
|
-
.map((cmd, index) => {
|
|
325
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
326
|
-
const lowerName = name?.toLowerCase() ?? "";
|
|
327
|
-
const lowerDesc = cmd.description?.toLowerCase() ?? "";
|
|
328
|
-
// Score name matches higher than description matches
|
|
329
|
-
const nameScore = fuzzyMatch(lowerPrefix, lowerName) ? fuzzyScore(lowerPrefix, lowerName) : 0;
|
|
330
|
-
const descScore = fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
331
|
-
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
332
|
-
const desc = cmd.description ?? "";
|
|
333
|
-
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
334
|
-
const priority = "priority" in cmd && typeof cmd.priority === "number" ? cmd.priority : 0;
|
|
335
|
-
return {
|
|
336
|
-
value: name,
|
|
337
|
-
label: "name" in cmd ? cmd.name : cmd.label,
|
|
338
|
-
score: Math.max(nameScore, descScore),
|
|
339
|
-
priority,
|
|
340
|
-
matchRank: getSlashCommandMatchRank(lowerPrefix, lowerName),
|
|
341
|
-
index,
|
|
342
|
-
...(fullDesc && { description: fullDesc }),
|
|
343
|
-
};
|
|
344
|
-
})
|
|
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);
|
|
384
|
+
const matches = this.#getSlashCommandNameSuggestions(prefix);
|
|
350
385
|
|
|
351
386
|
if (matches.length === 0) return null;
|
|
352
387
|
|
|
@@ -354,36 +389,54 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
354
389
|
items: matches,
|
|
355
390
|
prefix: textBeforeCursor,
|
|
356
391
|
};
|
|
357
|
-
}
|
|
358
|
-
// Space found - complete command arguments
|
|
359
|
-
const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
|
|
360
|
-
const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
|
|
392
|
+
}
|
|
361
393
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
394
|
+
// Space found - complete command arguments
|
|
395
|
+
const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
|
|
396
|
+
const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
|
|
397
|
+
|
|
398
|
+
const command = this.#commands.find(cmd => this.#getCommandName(cmd) === commandName);
|
|
399
|
+
if (!command || !("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
|
|
400
|
+
return null; // No argument completion for this command
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const argumentSuggestions = await command.getArgumentCompletions(argumentText);
|
|
404
|
+
if (!Array.isArray(argumentSuggestions) || argumentSuggestions.length === 0) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
items: argumentSuggestions,
|
|
410
|
+
prefix: argumentText,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
369
413
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
414
|
+
const pathMatch = this.#extractPathPrefix(textBeforeCursor, false);
|
|
415
|
+
let pathSuggestions: AutocompleteItem[] | null = null;
|
|
416
|
+
const slashPrefix = this.#extractSlashCommandPrefix(textBeforeCursor);
|
|
417
|
+
if (slashPrefix) {
|
|
418
|
+
if (pathMatch === slashPrefix && slashPrefix.startsWith("/")) {
|
|
419
|
+
pathSuggestions = await this.#getFileSuggestions(pathMatch);
|
|
420
|
+
if (pathSuggestions.length > 0) {
|
|
421
|
+
return {
|
|
422
|
+
items: pathSuggestions,
|
|
423
|
+
prefix: pathMatch,
|
|
424
|
+
};
|
|
373
425
|
}
|
|
426
|
+
}
|
|
374
427
|
|
|
428
|
+
const matches = this.#getInlineSlashCommandNameSuggestions(slashPrefix.slice(1));
|
|
429
|
+
if (matches.length > 0) {
|
|
375
430
|
return {
|
|
376
|
-
items:
|
|
377
|
-
prefix:
|
|
431
|
+
items: matches,
|
|
432
|
+
prefix: slashPrefix,
|
|
378
433
|
};
|
|
379
434
|
}
|
|
380
435
|
}
|
|
381
436
|
|
|
382
437
|
// Check for file paths - triggered by Tab or if we detect a path pattern
|
|
383
|
-
const pathMatch = this.#extractPathPrefix(textBeforeCursor, false);
|
|
384
|
-
|
|
385
438
|
if (pathMatch !== null) {
|
|
386
|
-
const suggestions = await this.#getFileSuggestions(pathMatch);
|
|
439
|
+
const suggestions = pathSuggestions ?? (await this.#getFileSuggestions(pathMatch));
|
|
387
440
|
if (suggestions.length === 0) return null;
|
|
388
441
|
|
|
389
442
|
// Check if we have an exact match that is a directory
|
|
@@ -418,9 +471,12 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
418
471
|
const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
|
|
419
472
|
const afterCursor = currentLine.slice(cursorCol);
|
|
420
473
|
|
|
421
|
-
// Check if we're completing a slash command
|
|
422
|
-
//
|
|
423
|
-
const isSlashCommand =
|
|
474
|
+
// Check if we're completing a slash command name. Start-of-line commands
|
|
475
|
+
// execute on submit; inline slash tokens are completed as ordinary text.
|
|
476
|
+
const isSlashCommand =
|
|
477
|
+
prefix.startsWith("/") &&
|
|
478
|
+
!prefix.slice(1).includes("/") &&
|
|
479
|
+
(beforePrefix.trim() === "" || this.#isKnownCommandItem(item));
|
|
424
480
|
if (isSlashCommand) {
|
|
425
481
|
// This is a command name completion
|
|
426
482
|
const newLine = `${beforePrefix}/${item.value} ${afterCursor}`;
|
|
@@ -855,40 +911,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
855
911
|
if (textBeforeCursor.length <= 1) return null; // Bare "/" alone, don't auto-complete
|
|
856
912
|
if (textBeforeCursor.includes(" ")) return null; // Only complete command name, not args
|
|
857
913
|
|
|
858
|
-
const
|
|
859
|
-
const lowerPrefix = prefix.toLowerCase();
|
|
860
|
-
|
|
861
|
-
const matches = this.#commands
|
|
862
|
-
.filter(cmd => {
|
|
863
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
864
|
-
if (!name) return false;
|
|
865
|
-
if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
|
|
866
|
-
if (getSlashCommandMatchRank(lowerPrefix, name.toLowerCase()) < 4) return true;
|
|
867
|
-
const desc = cmd.description?.toLowerCase();
|
|
868
|
-
return desc ? fuzzyMatch(lowerPrefix, desc) : false;
|
|
869
|
-
})
|
|
870
|
-
.map((cmd, index) => {
|
|
871
|
-
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
872
|
-
const lowerName = name?.toLowerCase() ?? "";
|
|
873
|
-
const lowerDesc = cmd.description?.toLowerCase() ?? "";
|
|
874
|
-
const nameScore = fuzzyMatch(lowerPrefix, lowerName) ? fuzzyScore(lowerPrefix, lowerName) : 0;
|
|
875
|
-
const descScore = fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
876
|
-
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
877
|
-
const desc = cmd.description ?? "";
|
|
878
|
-
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
879
|
-
const priority = "priority" in cmd && typeof cmd.priority === "number" ? cmd.priority : 0;
|
|
880
|
-
return {
|
|
881
|
-
value: name,
|
|
882
|
-
label: "name" in cmd ? cmd.name : cmd.label,
|
|
883
|
-
score: Math.max(nameScore, descScore),
|
|
884
|
-
priority,
|
|
885
|
-
matchRank: getSlashCommandMatchRank(lowerPrefix, lowerName),
|
|
886
|
-
index,
|
|
887
|
-
...(fullDesc && { description: fullDesc }),
|
|
888
|
-
} as AutocompleteItem & { score: number; priority: number; matchRank: number; index: number };
|
|
889
|
-
})
|
|
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);
|
|
914
|
+
const matches = this.#getSlashCommandNameSuggestions(textBeforeCursor.slice(1));
|
|
892
915
|
|
|
893
916
|
if (matches.length === 0) return null;
|
|
894
917
|
return { items: matches, prefix: textBeforeCursor };
|
package/src/components/editor.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { getProjectDir, logger } from "@gajae-code/utils";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type AutocompleteProvider,
|
|
4
|
+
type CombinedAutocompleteProvider,
|
|
5
|
+
extractSlashCommandTokenPrefix,
|
|
6
|
+
} from "../autocomplete";
|
|
3
7
|
import { BracketedPasteHandler } from "../bracketed-paste";
|
|
4
8
|
import { getKeybindings, type KeybindingsManager } from "../keybindings";
|
|
5
9
|
import { extractPrintableText, matchesKey } from "../keys";
|
|
@@ -1152,7 +1156,7 @@ export class Editor implements Component, Focusable {
|
|
|
1152
1156
|
// Check for stale autocomplete state due to debounce
|
|
1153
1157
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1154
1158
|
const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1155
|
-
if (currentTextBeforeCursor
|
|
1159
|
+
if (!currentTextBeforeCursor.endsWith(this.#autocompletePrefix)) {
|
|
1156
1160
|
// Autocomplete is stale - cancel and fall through to normal submission
|
|
1157
1161
|
this.#cancelAutocomplete();
|
|
1158
1162
|
} else {
|
|
@@ -1290,8 +1294,8 @@ export class Editor implements Component, Focusable {
|
|
|
1290
1294
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1291
1295
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1292
1296
|
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1297
|
+
(this.#isInSubmittedSlashCommandContext() ||
|
|
1298
|
+
this.#getSlashTokenBeforeCursor()?.startsWith("/skill") === true) &&
|
|
1295
1299
|
this.#autocompleteProvider?.trySyncSlashCompletion
|
|
1296
1300
|
) {
|
|
1297
1301
|
const syncResult = this.#autocompleteProvider.trySyncSlashCompletion(textBeforeCursor);
|
|
@@ -1728,8 +1732,8 @@ export class Editor implements Component, Focusable {
|
|
|
1728
1732
|
|
|
1729
1733
|
// Check if we should trigger or update autocomplete
|
|
1730
1734
|
if (!this.#autocompleteState) {
|
|
1731
|
-
// Auto-trigger for
|
|
1732
|
-
if (char === "/" && this.#isAtStartOfSubmittedMessage()) {
|
|
1735
|
+
// Auto-trigger for slash command tokens.
|
|
1736
|
+
if (char === "/" && (this.#isAtStartOfSubmittedMessage() || this.#isInSlashTokenContext())) {
|
|
1733
1737
|
this.#tryTriggerAutocomplete();
|
|
1734
1738
|
}
|
|
1735
1739
|
// Auto-trigger for "@" file reference (fuzzy search)
|
|
@@ -1750,8 +1754,8 @@ export class Editor implements Component, Focusable {
|
|
|
1750
1754
|
else if (/[a-zA-Z0-9.\-_/]/.test(char)) {
|
|
1751
1755
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1752
1756
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1753
|
-
// Check if we're in a slash command
|
|
1754
|
-
if (this.#isInSubmittedSlashCommandContext()) {
|
|
1757
|
+
// Check if we're in a slash command or inline slash token
|
|
1758
|
+
if (this.#isInSubmittedSlashCommandContext() || this.#isInSlashTokenContext()) {
|
|
1755
1759
|
this.#tryTriggerAutocomplete();
|
|
1756
1760
|
}
|
|
1757
1761
|
// Check if we're in an @ file reference context
|
|
@@ -1946,8 +1950,8 @@ export class Editor implements Component, Focusable {
|
|
|
1946
1950
|
// If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
|
|
1947
1951
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1948
1952
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1949
|
-
// Slash command context
|
|
1950
|
-
if (this.#isInSubmittedSlashCommandContext()) {
|
|
1953
|
+
// Slash command or inline slash token context
|
|
1954
|
+
if (this.#isInSubmittedSlashCommandContext() || this.#isInSlashTokenContext()) {
|
|
1951
1955
|
this.#tryTriggerAutocomplete();
|
|
1952
1956
|
}
|
|
1953
1957
|
// @ file reference context
|
|
@@ -2103,7 +2107,7 @@ export class Editor implements Component, Focusable {
|
|
|
2103
2107
|
} else {
|
|
2104
2108
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2105
2109
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2106
|
-
if (this.#isInSubmittedSlashCommandContext()) {
|
|
2110
|
+
if (this.#isInSubmittedSlashCommandContext() || this.#isInSlashTokenContext()) {
|
|
2107
2111
|
this.#tryTriggerAutocomplete();
|
|
2108
2112
|
} else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
|
|
2109
2113
|
this.#tryTriggerAutocomplete();
|
|
@@ -2417,8 +2421,8 @@ export class Editor implements Component, Focusable {
|
|
|
2417
2421
|
} else {
|
|
2418
2422
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2419
2423
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2420
|
-
// Slash command context
|
|
2421
|
-
if (this.#isInSubmittedSlashCommandContext()) {
|
|
2424
|
+
// Slash command or inline slash token context
|
|
2425
|
+
if (this.#isInSubmittedSlashCommandContext() || this.#isInSlashTokenContext()) {
|
|
2422
2426
|
this.#tryTriggerAutocomplete();
|
|
2423
2427
|
}
|
|
2424
2428
|
// @ file reference context
|
|
@@ -2598,6 +2602,12 @@ export class Editor implements Component, Focusable {
|
|
|
2598
2602
|
: this.#state.cursorCol - 1
|
|
2599
2603
|
: undefined;
|
|
2600
2604
|
|
|
2605
|
+
// Backward from column 0: lastIndexOf clamps a negative position to 0,
|
|
2606
|
+
// which would match the character under the cursor instead of skipping it
|
|
2607
|
+
if (!isForward && searchFrom !== undefined && searchFrom < 0) {
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2601
2611
|
const idx = isForward ? line.indexOf(char, searchFrom) : line.lastIndexOf(char, searchFrom);
|
|
2602
2612
|
|
|
2603
2613
|
if (idx !== -1) {
|
|
@@ -2647,6 +2657,16 @@ export class Editor implements Component, Focusable {
|
|
|
2647
2657
|
return this.#hasOnlyWhitespaceBeforeCursorLine() && beforeCursor.trimStart().startsWith("/");
|
|
2648
2658
|
}
|
|
2649
2659
|
|
|
2660
|
+
#getSlashTokenBeforeCursor(): string | null {
|
|
2661
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2662
|
+
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2663
|
+
return extractSlashCommandTokenPrefix(beforeCursor);
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
#isInSlashTokenContext(): boolean {
|
|
2667
|
+
return this.#getSlashTokenBeforeCursor() !== null;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2650
2670
|
#isSlashCommandNameAutocompleteSelection(): boolean {
|
|
2651
2671
|
if (this.#autocompleteState !== "regular") {
|
|
2652
2672
|
return false;
|
|
@@ -2728,7 +2748,10 @@ export class Editor implements Component, Focusable {
|
|
|
2728
2748
|
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2729
2749
|
|
|
2730
2750
|
// Check if we're in a slash command context
|
|
2731
|
-
if (
|
|
2751
|
+
if (
|
|
2752
|
+
(this.#isInSubmittedSlashCommandContext() && !beforeCursor.trimStart().includes(" ")) ||
|
|
2753
|
+
this.#isInSlashTokenContext()
|
|
2754
|
+
) {
|
|
2732
2755
|
this.#handleSlashCommandCompletion();
|
|
2733
2756
|
} else {
|
|
2734
2757
|
this.#forceFileAutocomplete(true);
|
package/src/tui.ts
CHANGED
|
@@ -139,9 +139,29 @@ function isTermuxSession(): boolean {
|
|
|
139
139
|
return Boolean(process.env.TERMUX_VERSION);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
|
|
143
|
+
const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "no"]);
|
|
144
|
+
|
|
145
|
+
function envIsEnabled(value: string | undefined): boolean {
|
|
146
|
+
const normalized = value?.trim().toLowerCase();
|
|
147
|
+
return normalized !== undefined && normalized.length > 0 && !DISABLED_ENV_VALUES.has(normalized);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function termLooksMultiplexed(value: string | undefined): boolean {
|
|
151
|
+
const term = value?.trim().toLowerCase() ?? "";
|
|
152
|
+
return term.startsWith("tmux") || term.startsWith("screen");
|
|
153
|
+
}
|
|
154
|
+
|
|
142
155
|
/** Detect terminal multiplexers where scrollback clearing and height-change redraws are hostile. */
|
|
143
156
|
function isMultiplexerSession(): boolean {
|
|
144
|
-
return Boolean(
|
|
157
|
+
return Boolean(
|
|
158
|
+
envIsEnabled(Bun.env.TMUX) ||
|
|
159
|
+
envIsEnabled(Bun.env.TMUX_PANE) ||
|
|
160
|
+
envIsEnabled(Bun.env.STY) ||
|
|
161
|
+
envIsEnabled(Bun.env.ZELLIJ) ||
|
|
162
|
+
envIsEnabled(Bun.env[GJC_TMUX_LAUNCHED_ENV]) ||
|
|
163
|
+
termLooksMultiplexed(Bun.env.TERM),
|
|
164
|
+
);
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
function useLegacyMultiplexerFullRender(): boolean {
|