fix(tui): await async slash command completions closes #2719
This commit is contained in:
@@ -966,7 +966,7 @@ export interface RegisteredCommand {
|
|||||||
name: string;
|
name: string;
|
||||||
sourceInfo: SourceInfo;
|
sourceInfo: SourceInfo;
|
||||||
description?: string;
|
description?: string;
|
||||||
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null;
|
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>;
|
||||||
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed slash-command argument autocomplete to await async `getArgumentCompletions()` results and ignore invalid return values, preventing crashes when extension commands provide asynchronous completions ([#2719](https://github.com/badlogic/pi-mono/issues/2719))
|
||||||
|
|
||||||
## [0.64.0] - 2026-03-29
|
## [0.64.0] - 2026-03-29
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -218,12 +218,14 @@ export interface AutocompleteItem {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Awaitable<T> = T | Promise<T>;
|
||||||
|
|
||||||
export interface SlashCommand {
|
export interface SlashCommand {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
// Function to get argument completions for this command
|
// Function to get argument completions for this command
|
||||||
// Returns null if no argument completion is available
|
// Returns null if no argument completion is available
|
||||||
getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;
|
getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutocompleteSuggestions {
|
export interface AutocompleteSuggestions {
|
||||||
@@ -332,8 +334,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const argumentSuggestions = command.getArgumentCompletions(argumentText);
|
const argumentSuggestions = await command.getArgumentCompletions(argumentText);
|
||||||
if (!argumentSuggestions || argumentSuggestions.length === 0) {
|
if (!Array.isArray(argumentSuggestions) || argumentSuggestions.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2141,7 +2141,7 @@ export class Editor implements Component, Focusable {
|
|||||||
|
|
||||||
this.autocompleteAbort = undefined;
|
this.autocompleteAbort = undefined;
|
||||||
|
|
||||||
if (!suggestions || suggestions.items.length === 0) {
|
if (!suggestions || !Array.isArray(suggestions.items) || suggestions.items.length === 0) {
|
||||||
this.cancelAutocomplete();
|
this.cancelAutocomplete();
|
||||||
this.tui.requestRender();
|
this.tui.requestRender();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2458,6 +2458,48 @@ describe("Editor component", () => {
|
|||||||
assert.strictEqual(editor.getText(), "/model gpt-4o-mini");
|
assert.strictEqual(editor.getText(), "/model gpt-4o-mini");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("awaits async slash command argument completions", async () => {
|
||||||
|
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||||
|
const provider = new CombinedAutocompleteProvider([
|
||||||
|
{
|
||||||
|
name: "load-skills",
|
||||||
|
description: "Load skills",
|
||||||
|
getArgumentCompletions: async (prefix) =>
|
||||||
|
prefix.startsWith("s") ? [{ value: "skill-a", label: "skill-a" }] : null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
editor.setAutocompleteProvider(provider);
|
||||||
|
editor.setText("/load-skills ");
|
||||||
|
|
||||||
|
editor.handleInput("s");
|
||||||
|
await flushAutocomplete();
|
||||||
|
assert.strictEqual(editor.isShowingAutocomplete(), true);
|
||||||
|
|
||||||
|
editor.handleInput("\t");
|
||||||
|
assert.strictEqual(editor.getText(), "/load-skills skill-a");
|
||||||
|
assert.strictEqual(editor.isShowingAutocomplete(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores invalid slash command argument completion results", async () => {
|
||||||
|
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||||
|
const provider = new CombinedAutocompleteProvider([
|
||||||
|
{
|
||||||
|
name: "load-skills",
|
||||||
|
description: "Load skills",
|
||||||
|
getArgumentCompletions: (() => "not-an-array") as unknown as (
|
||||||
|
argumentPrefix: string,
|
||||||
|
) => Promise<{ value: string; label: string }[] | null>,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
editor.setAutocompleteProvider(provider);
|
||||||
|
editor.setText("/load-skills ");
|
||||||
|
|
||||||
|
editor.handleInput("s");
|
||||||
|
await flushAutocomplete();
|
||||||
|
assert.strictEqual(editor.isShowingAutocomplete(), false);
|
||||||
|
assert.strictEqual(editor.getText(), "/load-skills s");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not show argument completions when command has no argument completer", async () => {
|
it("does not show argument completions when command has no argument completer", async () => {
|
||||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||||
const provider = new CombinedAutocompleteProvider([
|
const provider = new CombinedAutocompleteProvider([
|
||||||
|
|||||||
Reference in New Issue
Block a user