diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index fcebb8019..09cde1a29 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -736,6 +736,27 @@ describe("NanobotTui layout", () => { access_mode: "full", restrict_to_workspace: false, }]) + + await setup.mockMouse.click( + ui.runtimeControls.modelText.x + 2, + ui.runtimeControls.modelText.y, + ) + await waitUntil(() => ui.runtimeControls.visible) + ui.composer.blur() + await setup.mockMouse.click(ui.status.x, ui.status.y) + expect(ui.runtimeControls.visible).toBe(false) + expect(ui.composer.focused).toBe(true) + + await setup.mockMouse.click( + ui.runtimeControls.accessText.x + 2, + ui.runtimeControls.accessText.y, + ) + await waitUntil(() => ui.runtimeControls.visible) + ui.composer.blur() + await setup.mockMouse.click(ui.status.x, ui.status.y) + expect(ui.runtimeControls.visible).toBe(false) + expect(ui.composer.focused).toBe(true) + expect((app as unknown as { activeTurn: boolean }).activeTurn).toBe(true) app.accept({ event: "goal_status", chat_id: "chat", status: "idle" }) } finally { diff --git a/tui/src/app.ts b/tui/src/app.ts index 906867abb..5cce93ff1 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -487,10 +487,15 @@ export class NanobotTui { flexDirection: "column", backgroundColor: RGBA.defaultBackground(), onMouseDown: (event) => { + if (event.button !== 0) return + // 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. + this.dismissRuntimeControls() // Selection belongs to transcript/input content, never to empty chrome. // Clearing it here prevents default-background cells from lingering as // opaque blocks in terminals with differential repainting. - if (event.button === 0 && event.target && !event.target.selectable) { + if (event.target && !event.target.selectable) { this.renderer.clearSelection() } }, @@ -1732,6 +1737,10 @@ export class NanobotTui { this.activeMentionQuery = null } + private dismissRuntimeControls(): void { + if (this.runtimeControls.visible) this.runtimeControls.hide() + } + private applyWorkspaceScope(scope: WorkspaceScopePayload): void { if (scope.project_path) { this.hostWorkspace = scope.project_path