From 276ff92afea5d07d79acc3b7cd315428afa1b5c3 Mon Sep 17 00:00:00 2001 From: Aleksandr Khaustov Date: Mon, 3 Aug 2026 14:02:05 +0400 Subject: [PATCH] fix(tui): consume extension slash commands locally --- packages/coding-agent/CHANGELOG.md | 4 ++++ .../src/modes/controllers/input-controller.ts | 19 ++++++++++++++++++ .../input-controller-slash-history.test.ts | 20 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b20591af8..aa18f05ae 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index d29d2b049..3a841b835 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -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("!!"); diff --git a/packages/coding-agent/test/input-controller-slash-history.test.ts b/packages/coding-agent/test/input-controller-slash-history.test.ts index 9217e5aac..22d9ea5de 100644 --- a/packages/coding-agent/test/input-controller-slash-history.test.ts +++ b/packages/coding-agent/test/input-controller-slash-history.test.ts @@ -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);