fix(tui): preserve input typed after submit

This commit is contained in:
chengyongru
2026-09-01 16:21:18 +08:00
committed by chengyongru
parent 22cbfc2fb5
commit eebe3ca292
2 changed files with 39 additions and 7 deletions
+17
View File
@@ -253,6 +253,23 @@ describe("NanobotTui layout", () => {
expect(sent).toEqual(["你好"]) 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 () => { test("inserts newlines with Shift+Enter and the universal Ctrl+J fallback", async () => {
const sent: string[] = [] const sent: string[] = []
setup = await createRenderer({ setup = await createRenderer({
+19 -4
View File
@@ -733,7 +733,10 @@ export class NanobotTui {
// IMEs may commit their final composed glyph after Enter. Matching the // IMEs may commit their final composed glyph after Enter. Matching the
// OpenCode/OpenTUI integration, defer twice before reading plainText. // OpenCode/OpenTUI integration, defer twice before reading plainText.
onSubmit: () => this.deferSubmit(), onSubmit: () => this.deferSubmit(),
onPaste: (event) => this.handlePaste(event), onPaste: (event) => {
this.flushSubmit()
if (!this.composer.isDestroyed) this.handlePaste(event)
},
}) })
this.status = new TextRenderable(renderer, { this.status = new TextRenderable(renderer, {
id: "nanobot-tui-status", id: "nanobot-tui-status",
@@ -855,12 +858,15 @@ export class NanobotTui {
if (this.submitPending) return if (this.submitPending) return
this.submitPending = true this.submitPending = true
const generation = ++this.submitGeneration const generation = ++this.submitGeneration
setTimeout(() => setTimeout(() => { setTimeout(() => setTimeout(() => this.flushSubmit(generation), 0), 0)
if (generation !== this.submitGeneration) return }
private flushSubmit(generation = this.submitGeneration): void {
if (!this.submitPending || generation !== this.submitGeneration) return
this.submitPending = false this.submitPending = false
this.submitGeneration += 1
if (this.composer.isDestroyed) return if (this.composer.isDestroyed) return
this.submit() this.submit()
}, 0), 0)
} }
private submit(): void { private submit(): void {
@@ -1575,6 +1581,15 @@ export class NanobotTui {
} }
private handleKey = (key: KeyEvent): void => { 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 (this.diffViewer.visible) {
if (key.ctrl && key.name === "c") { if (key.ctrl && key.name === "c") {
const selected = this.renderer.getSelection()?.getSelectedText() const selected = this.renderer.getSelection()?.getSelectedText()