fix(tui): await async slash command completions closes #2719

This commit is contained in:
Mario Zechner
2026-03-31 14:41:26 +02:00
parent 5ce0d15b66
commit a1e107897d
5 changed files with 53 additions and 5 deletions

View File

@@ -966,7 +966,7 @@ export interface RegisteredCommand {
name: string;
sourceInfo: SourceInfo;
description?: string;
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null;
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>;
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
}

View File

@@ -2,6 +2,10 @@
## [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
### Fixed

View File

@@ -218,12 +218,14 @@ export interface AutocompleteItem {
description?: string;
}
type Awaitable<T> = T | Promise<T>;
export interface SlashCommand {
name: string;
description?: string;
// Function to get argument completions for this command
// Returns null if no argument completion is available
getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;
getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
}
export interface AutocompleteSuggestions {
@@ -332,8 +334,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
return null;
}
const argumentSuggestions = command.getArgumentCompletions(argumentText);
if (!argumentSuggestions || argumentSuggestions.length === 0) {
const argumentSuggestions = await command.getArgumentCompletions(argumentText);
if (!Array.isArray(argumentSuggestions) || argumentSuggestions.length === 0) {
return null;
}

View File

@@ -2141,7 +2141,7 @@ export class Editor implements Component, Focusable {
this.autocompleteAbort = undefined;
if (!suggestions || suggestions.items.length === 0) {
if (!suggestions || !Array.isArray(suggestions.items) || suggestions.items.length === 0) {
this.cancelAutocomplete();
this.tui.requestRender();
return;

View File

@@ -2458,6 +2458,48 @@ describe("Editor component", () => {
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 () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
const provider = new CombinedAutocompleteProvider([