From a1e107897d56292d987cecb5816979acae6fc6aa Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 31 Mar 2026 14:41:26 +0200 Subject: [PATCH] fix(tui): await async slash command completions closes #2719 --- .../coding-agent/src/core/extensions/types.ts | 2 +- packages/tui/CHANGELOG.md | 4 ++ packages/tui/src/autocomplete.ts | 8 ++-- packages/tui/src/components/editor.ts | 2 +- packages/tui/test/editor.test.ts | 42 +++++++++++++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 6d5f176d..bf05f991 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -966,7 +966,7 @@ export interface RegisteredCommand { name: string; sourceInfo: SourceInfo; description?: string; - getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null; + getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null | Promise; handler: (args: string, ctx: ExtensionCommandContext) => Promise; } diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 2c4e7bc9..dfc539f9 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -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 diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index e2dd4c3e..792f119b 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -218,12 +218,14 @@ export interface AutocompleteItem { description?: string; } +type Awaitable = T | Promise; + 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; } 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; } diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index b2139d5e..d0dbc17d 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -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; diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index 39cbc60f..960378fc 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -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([