From a6320171946faaa541aaef3bd04e014e2ce487c0 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:40:53 +0800 Subject: [PATCH] fix(tui): keep runtime controls interactive --- tui/src/app.test.ts | 3 +++ tui/src/app.ts | 4 +++- tui/src/picker-menu.ts | 7 +++++++ tui/src/runtime-controls.ts | 16 +++++++--------- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index ba07aafca..728f9b8f1 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -653,6 +653,7 @@ describe("NanobotTui layout", () => { try { await waitUntil(() => (app as unknown as { ready: boolean }).ready) + app.accept({ event: "goal_status", chat_id: "chat", status: "running" }) await setup.flush() await setup.mockMouse.click( ui.runtimeControls.modelText.x + 2, @@ -682,6 +683,8 @@ describe("NanobotTui layout", () => { access_mode: "full", restrict_to_workspace: false, }]) + expect((app as unknown as { activeTurn: boolean }).activeTurn).toBe(true) + app.accept({ event: "goal_status", chat_id: "chat", status: "idle" }) } finally { globalThis.fetch = original } diff --git a/tui/src/app.ts b/tui/src/app.ts index b779c6468..b7bfbcc0b 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -507,7 +507,9 @@ export class NanobotTui { modelPreset: this.modelPreset, workspace: options.workspace, access: options.access, - available: () => ({ ready: this.ready, active: this.activeTurn }), + // Runtime settings are session state. Changing them during a turn is + // safe and takes effect when the next provider call starts. + available: () => this.ready, beforeOpen: () => this.closeTransientMenus(), refreshScope: () => this.refreshSessionMetadata(this.client.activeChatId), onModel: (preset) => this.sendGatewayCommand(`/model ${preset}`, "side_channel", true), diff --git a/tui/src/picker-menu.ts b/tui/src/picker-menu.ts index ae60a3106..e96da8d2d 100644 --- a/tui/src/picker-menu.ts +++ b/tui/src/picker-menu.ts @@ -49,6 +49,12 @@ export class PickerMenu { paddingRight: 1, backgroundColor: RGBA.defaultBackground(), visible: false, + onMouseDown: (event) => { + if (event.button !== 0) return + event.preventDefault() + event.stopPropagation() + this.renderer.clearSelection() + }, }) } @@ -138,6 +144,7 @@ export class PickerMenu { if (event.button !== 0) return event.preventDefault() event.stopPropagation() + this.renderer.clearSelection() this.selected = index this.options.onSelect?.(item) }, diff --git a/tui/src/runtime-controls.ts b/tui/src/runtime-controls.ts index ab3cea82c..cc411bb01 100644 --- a/tui/src/runtime-controls.ts +++ b/tui/src/runtime-controls.ts @@ -27,7 +27,7 @@ interface RuntimeControlsOptions { modelPreset: string workspace: string access: string - available: () => { ready: boolean; active: boolean } + available: () => boolean beforeOpen: () => void refreshScope: () => Promise onModel: (name: string) => void @@ -165,6 +165,7 @@ export class RuntimeControls { if (event.button !== 0) return event.preventDefault() event.stopPropagation() + this.renderer.clearSelection() open() }, }) @@ -199,7 +200,7 @@ export class RuntimeControls { } private async openModel(): Promise { - if (!this.canOpen("Model")) return + if (!this.canOpen()) return try { await this.load(true) } catch (error) { @@ -222,7 +223,7 @@ export class RuntimeControls { } private async openAccess(): Promise { - if (!this.canOpen("Access")) return + if (!this.canOpen()) return try { await Promise.all([this.load(true), this.options.refreshScope()]) } catch (error) { @@ -250,12 +251,9 @@ export class RuntimeControls { this.opened() } - private canOpen(label: string): boolean { - const state = this.options.available() - if (state.ready && !state.active) return true - this.options.onStatus(state.active - ? `${label} can be changed between turns` - : "Preparing chat…") + private canOpen(): boolean { + if (this.options.available()) return true + this.options.onStatus("Preparing chat…") return false }