fix(tui): rejected stale mid-prompt skill autocomplete on non-matching tokens
The staleness guard previously accepted any clean trailing slash token, so the 100 ms debounce window let a bare-`/` skill popup rewrite a subsequently typed `/tmp` to `/skill:…`. Score the live token against the selected skill's value and reject the branch when it would no longer surface — Tab then falls through to file completion. Fixes #4619
This commit is contained in:
@@ -276,7 +276,7 @@ function commandMatchesNameOrAlias(cmd: CommandEntry, commandName: string): bool
|
||||
return getCommandAliases(cmd).includes(commandName);
|
||||
}
|
||||
|
||||
function scoreCommandTextMatch(lowerPrefix: string, lowerTarget: string): number {
|
||||
export function scoreCommandTextMatch(lowerPrefix: string, lowerTarget: string): number {
|
||||
if (lowerPrefix.length === 0) return 1;
|
||||
if (lowerPrefix === lowerTarget) return 1000;
|
||||
// Flat score for every prefix match so same-prefix commands keep registry
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type AutocompleteProvider,
|
||||
findLeadingSlashCommandStart,
|
||||
findTrailingSlashCommandStart,
|
||||
scoreCommandTextMatch,
|
||||
} from "../autocomplete";
|
||||
import { BracketedPasteHandler, decodeReencodedPasteControls } from "../bracketed-paste";
|
||||
import { getKeybindings, type KeybindingsManager } from "../keybindings";
|
||||
@@ -2902,8 +2903,17 @@ export class Editor implements Component, Focusable {
|
||||
const currentTrailingStart = findTrailingSlashCommandStart(currentTextBeforeCursor);
|
||||
if (currentTrailingStart !== null) {
|
||||
const token = currentTextBeforeCursor.slice(currentTrailingStart);
|
||||
if (!token.includes(" ") && !token.slice(1).includes("/")) return true;
|
||||
if (!token.includes(" ") && !token.slice(1).includes("/")) {
|
||||
// Guard the timing window where the popup was built for an earlier
|
||||
// query (e.g. bare `/`) and the user typed further characters before
|
||||
// the 100 ms debounced refresh fired: accept the stale skill only
|
||||
// when the current query would still surface it. `tmp` after a bare
|
||||
// slash therefore falls through to file completion instead of
|
||||
// rewriting the user's `/tmp` to `/skill:…`.
|
||||
if (scoreCommandTextMatch(token.slice(1).toLowerCase(), item.value.toLowerCase()) > 0) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (findLeadingSlashCommandStart(this.#autocompletePrefix) !== null) {
|
||||
|
||||
@@ -188,6 +188,19 @@ describe("Editor Enter handler sync slash completion", () => {
|
||||
expect(editor.isShowingAutocomplete()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not apply a stale mid-prompt skill suggestion when the live token stops matching", async () => {
|
||||
const editor = createSkillEditor();
|
||||
|
||||
await openMidPromptSkillAutocomplete(editor, "see ");
|
||||
// Race the 100 ms debounce: type a non-skill token before the popup refreshes.
|
||||
editor.handleInput("tmp");
|
||||
editor.handleInput("\t");
|
||||
|
||||
// The stale `skill:security-scan` popup must not rewrite `/tmp` to `/skill:…`.
|
||||
expect(editor.getText()).toBe("see /tmp");
|
||||
expect(editor.isShowingAutocomplete()).toBe(false);
|
||||
});
|
||||
|
||||
it("opens mid-prompt skill autocomplete and inserts the skill token without wiping the draft on Tab", async () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
editor.setAutocompleteProvider(
|
||||
|
||||
Reference in New Issue
Block a user