fix(tui): consume extension slash commands locally

This commit is contained in:
Aleksandr Khaustov
2026-08-03 14:02:05 +04:00
parent 5039b33a11
commit 276ff92afe
3 changed files with 43 additions and 0 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed extension slash commands appearing as user prompts after being handled locally.
## [17.2.5] - 2026-08-03
### Breaking Changes
@@ -752,6 +752,25 @@ export class InputController {
}
}
// Extension commands are local actions. Execute them before the normal
// submission path creates an optimistic user message; otherwise a
// consumed command remains rendered like a prompt sent to the model.
const extensionCommandSpace = text.indexOf(" ");
const isLocalExtensionCommand =
text.startsWith("/") &&
runner?.getCommand(extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace)) !==
undefined;
if (isLocalExtensionCommand) {
this.ctx.editor.addToHistory(text);
try {
await this.ctx.session.prompt(text, { images: inputImages });
} catch (error) {
this.ctx.editor.setText(text);
this.ctx.showError(error instanceof Error ? error.message : String(error));
}
return;
}
// Handle bash command (! for normal, !! for excluded from context)
if (text.startsWith("!")) {
const isExcluded = text.startsWith("!!");
@@ -15,6 +15,7 @@ function makeCtx(isStreaming = false) {
const handleMCPCommand = vi.fn(async () => {});
const followUp = vi.fn(async (_text: string, _images?: ImageContent[]) => {});
const steer = vi.fn(async (_text: string, _images?: ImageContent[]) => {});
const prompt = vi.fn(async () => false);
const onInputCallback = vi.fn();
let text = "";
const editor = {
@@ -44,6 +45,7 @@ function makeCtx(isStreaming = false) {
extensionRunner: undefined,
followUp,
steer,
prompt,
},
focusedAgentId: undefined,
collabGuest: undefined,
@@ -75,6 +77,7 @@ function makeCtx(isStreaming = false) {
onInputCallback,
handleMCPCommand,
showStatus: ctx.showStatus,
prompt,
};
}
@@ -117,6 +120,23 @@ describe("input controller — slash command history (#3148)", () => {
expect(addToHistory).not.toHaveBeenCalled();
});
it("executes extension commands without rendering them as user prompts", async () => {
const { ctx, editor, addToHistory, onInputCallback, prompt } = makeCtx();
Object.defineProperty(ctx.session, "extensionRunner", {
value: {
getCommand: (name: string) => (name === "id" ? { name } : undefined),
hasHandlers: () => false,
},
});
controllerFor(ctx);
await editor.onSubmit?.("/id");
expect(prompt).toHaveBeenCalledWith("/id", { images: undefined });
expect(addToHistory).toHaveBeenCalledWith("/id");
expect(onInputCallback).not.toHaveBeenCalled();
});
it("routes /queue through the yield-only follow-up queue while streaming", async () => {
const { ctx, editor, addToHistory, followUp, showStatus } = makeCtx(true);
controllerFor(ctx);