diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index f3c8c6bb3..47c654789 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -1674,21 +1674,23 @@ describe("NanobotTui layout", () => { expect(setup.renderer.getSelection()).toBeNull() }) - test("returns text input focus when other TUI content is clicked", async () => { + test("keeps text input focus when the focusable transcript is clicked", async () => { setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" }) const app = mount(setup) const ui = app as unknown as { composer: TextareaRenderable - status: TextRenderable + transcript: { + root: { x: number; y: number; focused: boolean } + } } app.accept({ event: "delta", chat_id: "chat", text: "clickable answer" }) app.accept({ event: "stream_end", chat_id: "chat" }) await setup.flush() - ui.composer.blur() - await setup.mockMouse.click(ui.status.x, ui.status.y) + await setup.mockMouse.click(ui.transcript.root.x + 1, ui.transcript.root.y + 1) expect(ui.composer.focused).toBe(true) + expect(ui.transcript.root.focused).toBe(false) }) test("animates one stable status line while the agent works", async () => { diff --git a/tui/src/app.ts b/tui/src/app.ts index 6bf194454..75d77c8a2 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -511,7 +511,13 @@ export class NanobotTui { backgroundColor: RGBA.defaultBackground(), onMouseDown: (event) => { if (event.button !== 0) return - if (!this.diffViewer.visible) this.composer.focus() + if (!this.diffViewer.visible) { + // OpenTUI applies automatic focus after mouse handlers run. Prevent + // a focusable transcript ancestor from stealing text focus back + // after the composer has been restored here. + event.preventDefault() + this.composer.focus() + } // Runtime pickers are transient popovers. A primary click anywhere // outside their trigger or body dismisses them; hide() restores the // composer focus through the shared visibility callback.