diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index 5d7a115ee..cc3c76f8c 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -253,6 +253,23 @@ describe("NanobotTui layout", () => { expect(sent).toEqual(["你好"]) }) + test("keeps input typed immediately after Enter in the next draft", async () => { + const sent: string[] = [] + setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" }) + const app = mount(setup, sent) + app.accept({ event: "attached", chat_id: "chat" }) + await Bun.sleep(1) + const composer = (app as unknown as { composer: TextareaRenderable }).composer + + composer.setText("first") + setup.mockInput.pressEnter() + for (const key of "next") setup.mockInput.pressKey(key) + await waitUntil(() => sent.length > 0) + + expect(sent).toEqual(["first"]) + expect(composer.plainText).toBe("next") + }) + test("inserts newlines with Shift+Enter and the universal Ctrl+J fallback", async () => { const sent: string[] = [] setup = await createRenderer({ diff --git a/tui/src/app.ts b/tui/src/app.ts index 53856a157..15427c464 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -733,7 +733,10 @@ export class NanobotTui { // IMEs may commit their final composed glyph after Enter. Matching the // OpenCode/OpenTUI integration, defer twice before reading plainText. onSubmit: () => this.deferSubmit(), - onPaste: (event) => this.handlePaste(event), + onPaste: (event) => { + this.flushSubmit() + if (!this.composer.isDestroyed) this.handlePaste(event) + }, }) this.status = new TextRenderable(renderer, { id: "nanobot-tui-status", @@ -855,12 +858,15 @@ export class NanobotTui { if (this.submitPending) return this.submitPending = true const generation = ++this.submitGeneration - setTimeout(() => setTimeout(() => { - if (generation !== this.submitGeneration) return - this.submitPending = false - if (this.composer.isDestroyed) return - this.submit() - }, 0), 0) + setTimeout(() => setTimeout(() => this.flushSubmit(generation), 0), 0) + } + + private flushSubmit(generation = this.submitGeneration): void { + if (!this.submitPending || generation !== this.submitGeneration) return + this.submitPending = false + this.submitGeneration += 1 + if (this.composer.isDestroyed) return + this.submit() } private submit(): void { @@ -1575,6 +1581,15 @@ export class NanobotTui { } private handleKey = (key: KeyEvent): void => { + // The app receives keypresses before the focused Textarea. Seal the pending + // submission first so this key is inserted into the next draft. + if (this.submitPending) { + this.flushSubmit() + if (this.quitting || this.composer.isDestroyed) { + key.preventDefault() + return + } + } if (this.diffViewer.visible) { if (key.ctrl && key.name === "c") { const selected = this.renderer.getSelection()?.getSelectedText()