From 40aa99f45638bad5231651bd01ad9914e9285c2d Mon Sep 17 00:00:00 2001 From: chengyongru Date: Tue, 18 Aug 2026 16:57:21 +0800 Subject: [PATCH] fix(tui): keep composer visible and focused --- tui/src/app.test.ts | 33 +++++++++++++++++++++++++++++---- tui/src/app.ts | 8 ++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index a6c90b5b4..f3c8c6bb3 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -1350,7 +1350,7 @@ describe("NanobotTui layout", () => { expect(markdown?.syntaxStyle).not.toBe(darkSyntax) }) - test("uses a quiet surface instead of a boxed composer", async () => { + test("distinguishes the composer with a quiet focus edge", async () => { setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" }) const app = NanobotTui.mount( setup.renderer, @@ -1362,14 +1362,20 @@ describe("NanobotTui layout", () => { ready: boolean composerFrame: { border: boolean | string[] + borderColor: { toInts(): number[] } backgroundColor: { toInts(): number[] } } - composer: { backgroundColor: { toInts(): number[] } } + composer: { + backgroundColor: { toInts(): number[] } + placeholderColor: { toInts(): number[] } + } } - expect(internals.composerFrame.border).toBeFalse() + expect(internals.composerFrame.border).toEqual(["left"]) + expect(internals.composerFrame.borderColor.toInts().slice(0, 3)).toEqual([185, 77, 11]) expect(internals.composerFrame.backgroundColor.toInts().slice(0, 3)).toEqual([240, 240, 240]) expect(internals.composer.backgroundColor.toInts().slice(0, 3)).toEqual([240, 240, 240]) + expect(internals.composer.placeholderColor.toInts().slice(0, 3)).toEqual([111, 111, 120]) app.accept({ event: "attached", chat_id: "chat" }) await waitUntil(() => internals.ready) @@ -1377,7 +1383,9 @@ describe("NanobotTui layout", () => { const composerLine = setup.captureCharFrame().split("\n") .find((line) => line.includes("Ask nanobot anything")) || "" - expect(composerLine).not.toContain("│") + expect(composerLine).toContain("│") + expect(composerLine).not.toContain("┌") + expect(composerLine).not.toContain("┐") }) test("uses asymmetric roles instead of chat bubbles", async () => { @@ -1666,6 +1674,23 @@ describe("NanobotTui layout", () => { expect(setup.renderer.getSelection()).toBeNull() }) + test("returns text input focus when other TUI content 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 + } + 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) + + expect(ui.composer.focused).toBe(true) + }) + test("animates one stable status line while the agent works", async () => { setup = await createRenderer({ width: 88, height: 24, screenMode: "alternate-screen" }) const app = mount(setup) diff --git a/tui/src/app.ts b/tui/src/app.ts index 15da7eec4..6bf194454 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -511,6 +511,7 @@ export class NanobotTui { backgroundColor: RGBA.defaultBackground(), onMouseDown: (event) => { if (event.button !== 0) return + if (!this.diffViewer.visible) 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. @@ -602,7 +603,8 @@ export class NanobotTui { width: "100%", minHeight: 1, flexShrink: 0, - border: false, + border: ["left"], + borderColor: this.palette.accent, paddingLeft: 1, paddingRight: 1, backgroundColor: composerSurface, @@ -614,7 +616,7 @@ export class NanobotTui { maxHeight: 8, wrapMode: "word", placeholder: COMPOSER_PLACEHOLDER, - placeholderColor: this.palette.faint, + placeholderColor: this.palette.muted, textColor: this.palette.text, focusedTextColor: this.palette.text, backgroundColor: composerSurface, @@ -1693,8 +1695,10 @@ export class NanobotTui { private updateComposerAppearance(): void { const surface = this.composerSurface() this.composerFrame.backgroundColor = surface + this.composerFrame.borderColor = this.palette.accent this.composer.backgroundColor = surface this.composer.focusedBackgroundColor = surface + this.composer.placeholderColor = this.palette.muted } private syncComposerPlaceholder(): void {