fix(tui): clear extension command image drafts

This commit is contained in:
can1357
2026-08-03 14:43:11 +02:00
parent 86a6786dd2
commit d46c58b5c8
2 changed files with 31 additions and 26 deletions
@@ -752,24 +752,6 @@ 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("!")) {
@@ -820,6 +802,20 @@ export class InputController {
this.ctx.queueCompactionMessage(text, "steer", images);
return;
}
// 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.
if (this.#isLocalExtensionCommand(text)) {
this.ctx.editor.clearDraft(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;
}
// If streaming, use prompt() with steer behavior
// This handles extension commands (execute immediately), prompt template expansion, and queueing
@@ -933,14 +929,18 @@ export class InputController {
* Local extension commands are consumed before reaching the shared session
* title gate and must not name the conversation.
*/
#maybeStartTitleGeneration(text: string): void {
const runner = this.ctx.session.extensionRunner;
#isLocalExtensionCommand(text: string): boolean {
const extensionCommandSpace = text.indexOf(" ");
const isLocalExtensionCommand =
return (
text.startsWith("/") &&
runner?.getCommand(extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace)) !==
undefined;
if (isLocalExtensionCommand) {
this.ctx.session.extensionRunner?.getCommand(
extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace),
) !== undefined
);
}
#maybeStartTitleGeneration(text: string): void {
if (this.#isLocalExtensionCommand(text)) {
return;
}
this.ctx.session.maybeStartTitleGeneration(text, () => {
@@ -120,7 +120,7 @@ describe("input controller — slash command history (#3148)", () => {
expect(addToHistory).not.toHaveBeenCalled();
});
it("executes extension commands without rendering them as user prompts", async () => {
it("executes extension commands without rendering them as user prompts or retaining image drafts", async () => {
const { ctx, editor, addToHistory, onInputCallback, prompt } = makeCtx();
Object.defineProperty(ctx.session, "extensionRunner", {
value: {
@@ -128,13 +128,18 @@ describe("input controller — slash command history (#3148)", () => {
hasHandlers: () => false,
},
});
const image: ImageContent = { type: "image", data: "image-data", mimeType: "image/png" };
editor.pendingImages = [image];
editor.pendingImageLinks = ["file:///draft.png"];
controllerFor(ctx);
await editor.onSubmit?.("/id");
expect(prompt).toHaveBeenCalledWith("/id", { images: undefined });
expect(prompt).toHaveBeenCalledWith("/id", { images: [image] });
expect(addToHistory).toHaveBeenCalledWith("/id");
expect(onInputCallback).not.toHaveBeenCalled();
expect(editor.pendingImages).toEqual([]);
expect(editor.pendingImageLinks).toEqual([]);
});
it("routes /queue through the yield-only follow-up queue while streaming", async () => {