From edd1212200b7cb37db5bdb98e5f33e260a01fd04 Mon Sep 17 00:00:00 2001 From: Michael Yu Date: Fri, 29 May 2026 23:03:03 +0800 Subject: [PATCH] fix(coding-agent): buffer early input before prompt loop --- .../src/modes/interactive/interactive-mode.ts | 8 +++ .../interactive-mode-startup-input.test.ts | 72 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 packages/coding-agent/test/interactive-mode-startup-input.test.ts diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 901e12451..f3a2ba6ad 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -277,6 +277,7 @@ export class InteractiveMode { private version: string; private isInitialized = false; private onInputCallback?: (text: string) => void; + private pendingUserInputs: string[] = []; private loadingAnimation: Loader | undefined = undefined; private workingMessage: string | undefined = undefined; private workingVisible = true; @@ -2660,6 +2661,8 @@ export class InteractiveMode { if (this.onInputCallback) { this.onInputCallback(text); + } else { + this.pendingUserInputs.push(text); } this.editor.addToHistory?.(text); }; @@ -3231,6 +3234,11 @@ export class InteractiveMode { } async getUserInput(): Promise { + const queuedInput = this.pendingUserInputs.shift(); + if (queuedInput !== undefined) { + return queuedInput; + } + return new Promise((resolve) => { this.onInputCallback = (text: string) => { this.onInputCallback = undefined; diff --git a/packages/coding-agent/test/interactive-mode-startup-input.test.ts b/packages/coding-agent/test/interactive-mode-startup-input.test.ts new file mode 100644 index 000000000..b784f3769 --- /dev/null +++ b/packages/coding-agent/test/interactive-mode-startup-input.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from "vitest"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; + +type SubmitContext = { + defaultEditor: { onSubmit?: (text: string) => void }; + editor: { + addToHistory?: (text: string) => void; + setText: (text: string) => void; + }; + session: { + isCompacting: boolean; + isStreaming: boolean; + isBashRunning: boolean; + prompt: (text: string, options?: unknown) => Promise; + }; + flushPendingBashComponents: () => void; + onInputCallback?: (text: string) => void; + pendingUserInputs: string[]; +}; + +type InputContext = { + onInputCallback?: (text: string) => void; + pendingUserInputs: string[]; +}; + +type InteractiveModePrivate = { + setupEditorSubmitHandler(this: SubmitContext): void; + getUserInput(this: InputContext): Promise; +}; + +const interactiveModePrototype = InteractiveMode.prototype as unknown as InteractiveModePrivate; + +function createSubmitContext(): SubmitContext { + return { + defaultEditor: {}, + editor: { + addToHistory: vi.fn(), + setText: vi.fn(), + }, + session: { + isCompacting: false, + isStreaming: false, + isBashRunning: false, + prompt: vi.fn(async () => {}), + }, + flushPendingBashComponents: vi.fn(), + pendingUserInputs: [], + }; +} + +describe("InteractiveMode startup input", () => { + it("queues a normal prompt submitted before the input callback is installed", async () => { + const context = createSubmitContext(); + interactiveModePrototype.setupEditorSubmitHandler.call(context); + + await context.defaultEditor.onSubmit?.(" early prompt "); + + expect(context.pendingUserInputs).toEqual(["early prompt"]); + expect(context.flushPendingBashComponents).toHaveBeenCalledTimes(1); + expect(context.editor.addToHistory).toHaveBeenCalledWith("early prompt"); + }); + + it("returns queued startup input before installing a new input callback", async () => { + const context: InputContext = { + pendingUserInputs: ["queued prompt"], + }; + + await expect(interactiveModePrototype.getUserInput.call(context)).resolves.toBe("queued prompt"); + expect(context.onInputCallback).toBeUndefined(); + expect(context.pendingUserInputs).toEqual([]); + }); +});