feat(tui): enhance autocomplete functionality and UI interactions
This commit is contained in:
@@ -35,7 +35,7 @@ export const COMMANDS: string[] = [
|
||||
"/help",
|
||||
"/quit",
|
||||
"/clear",
|
||||
"/edit <path>",
|
||||
"/edit",
|
||||
"/mcp",
|
||||
"/model",
|
||||
"/config",
|
||||
@@ -57,6 +57,8 @@ export class InputState {
|
||||
autocompleteVisible = false;
|
||||
autocompleteKind: AutocompleteKind = AutocompleteKind.Command;
|
||||
mentionStart = 0;
|
||||
/** Start index of the command token being completed (for in-place replace). */
|
||||
autocompleteTokenStart = 0;
|
||||
historyFile: string | null = null;
|
||||
|
||||
/** Hide the autocomplete dropdown and clear its state. */
|
||||
@@ -66,6 +68,7 @@ export class InputState {
|
||||
this.autocompleteIdx = 0;
|
||||
this.autocompleteKind = AutocompleteKind.Command;
|
||||
this.mentionStart = 0;
|
||||
this.autocompleteTokenStart = 0;
|
||||
}
|
||||
|
||||
/** Open or refresh the autocomplete dropdown by filtering COMMANDS. */
|
||||
@@ -75,11 +78,28 @@ export class InputState {
|
||||
this.closeAutocomplete();
|
||||
return;
|
||||
}
|
||||
const prefix = trimmed.toLowerCase();
|
||||
this.autocompleteCandidates = COMMANDS.filter((c) => c.startsWith(prefix));
|
||||
// Filter on the *command token* (the word before any space), so typing
|
||||
// "/edit foo" still matches "/edit". A trailing/embedded space must not
|
||||
// kill the dropdown.
|
||||
const word = trimmed.split(/\s+/)[0] ?? "";
|
||||
const prefix = word.toLowerCase();
|
||||
if (!prefix.startsWith("/")) {
|
||||
this.closeAutocomplete();
|
||||
return;
|
||||
}
|
||||
// Prefer exact-match-first so a fully-typed command jumps to the top.
|
||||
this.autocompleteCandidates = COMMANDS.filter((c) => c.startsWith(prefix)).sort(
|
||||
(a, b) => Number(a === word) - Number(b === word),
|
||||
);
|
||||
// Record where the command token starts in the *raw buffer* (not the
|
||||
// trimmed word) so selectAutocomplete can replace just that token.
|
||||
this.autocompleteTokenStart = this.buffer.indexOf("/");
|
||||
this.autocompleteKind = AutocompleteKind.Command;
|
||||
this.autocompleteIdx = 0;
|
||||
this.autocompleteVisible = this.autocompleteCandidates.length > 0;
|
||||
// A fully-typed command needs no dropdown — entering closes it so Enter
|
||||
// runs the command immediately instead of "selecting" the same string.
|
||||
const exact = this.autocompleteCandidates.includes(word);
|
||||
this.autocompleteVisible = this.autocompleteCandidates.length > 0 && !exact;
|
||||
}
|
||||
|
||||
/** Find the `@mention` token (if any) immediately before the cursor. */
|
||||
@@ -130,13 +150,26 @@ export class InputState {
|
||||
}
|
||||
}
|
||||
|
||||
/** Index just past the command token (stops at a space or end of buffer). */
|
||||
private endOfToken(): number {
|
||||
const start = this.autocompleteTokenStart;
|
||||
if (start < 0 || start > this.buffer.length) return start;
|
||||
const rest = this.buffer.slice(start);
|
||||
const space = rest.search(/\s/);
|
||||
return space === -1 ? this.buffer.length : start + space;
|
||||
}
|
||||
|
||||
/** Accept the currently selected autocomplete candidate. Returns true on success. */
|
||||
selectAutocomplete(): boolean {
|
||||
const candidate = this.autocompleteCandidates[this.autocompleteIdx];
|
||||
if (candidate === undefined || candidate === null) return false;
|
||||
if (this.autocompleteKind === AutocompleteKind.Command) {
|
||||
this.buffer = candidate;
|
||||
this.cursor = this.buffer.length;
|
||||
// Replace only the command token (e.g. "/mo" → "/model"), keeping any
|
||||
// text the user already typed after it (e.g. "/edit path" stays).
|
||||
const start = this.autocompleteTokenStart;
|
||||
const end = this.endOfToken();
|
||||
this.buffer = this.buffer.slice(0, start) + candidate + this.buffer.slice(end);
|
||||
this.cursor = start + candidate.length;
|
||||
} else {
|
||||
if (this.cursor < this.mentionStart || this.mentionStart > this.buffer.length) {
|
||||
this.closeAutocomplete();
|
||||
|
||||
Reference in New Issue
Block a user