fix(tui): keep runtime controls interactive

This commit is contained in:
Xubin Ren
2026-08-17 20:56:10 +08:00
parent 03d982023a
commit a632017194
4 changed files with 20 additions and 10 deletions
+3
View File
@@ -653,6 +653,7 @@ describe("NanobotTui layout", () => {
try { try {
await waitUntil(() => (app as unknown as { ready: boolean }).ready) await waitUntil(() => (app as unknown as { ready: boolean }).ready)
app.accept({ event: "goal_status", chat_id: "chat", status: "running" })
await setup.flush() await setup.flush()
await setup.mockMouse.click( await setup.mockMouse.click(
ui.runtimeControls.modelText.x + 2, ui.runtimeControls.modelText.x + 2,
@@ -682,6 +683,8 @@ describe("NanobotTui layout", () => {
access_mode: "full", access_mode: "full",
restrict_to_workspace: false, 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 { } finally {
globalThis.fetch = original globalThis.fetch = original
} }
+3 -1
View File
@@ -507,7 +507,9 @@ export class NanobotTui {
modelPreset: this.modelPreset, modelPreset: this.modelPreset,
workspace: options.workspace, workspace: options.workspace,
access: options.access, 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(), beforeOpen: () => this.closeTransientMenus(),
refreshScope: () => this.refreshSessionMetadata(this.client.activeChatId), refreshScope: () => this.refreshSessionMetadata(this.client.activeChatId),
onModel: (preset) => this.sendGatewayCommand(`/model ${preset}`, "side_channel", true), onModel: (preset) => this.sendGatewayCommand(`/model ${preset}`, "side_channel", true),
+7
View File
@@ -49,6 +49,12 @@ export class PickerMenu<T> {
paddingRight: 1, paddingRight: 1,
backgroundColor: RGBA.defaultBackground(), backgroundColor: RGBA.defaultBackground(),
visible: false, visible: false,
onMouseDown: (event) => {
if (event.button !== 0) return
event.preventDefault()
event.stopPropagation()
this.renderer.clearSelection()
},
}) })
} }
@@ -138,6 +144,7 @@ export class PickerMenu<T> {
if (event.button !== 0) return if (event.button !== 0) return
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
this.renderer.clearSelection()
this.selected = index this.selected = index
this.options.onSelect?.(item) this.options.onSelect?.(item)
}, },
+7 -9
View File
@@ -27,7 +27,7 @@ interface RuntimeControlsOptions {
modelPreset: string modelPreset: string
workspace: string workspace: string
access: string access: string
available: () => { ready: boolean; active: boolean } available: () => boolean
beforeOpen: () => void beforeOpen: () => void
refreshScope: () => Promise<void> refreshScope: () => Promise<void>
onModel: (name: string) => void onModel: (name: string) => void
@@ -165,6 +165,7 @@ export class RuntimeControls {
if (event.button !== 0) return if (event.button !== 0) return
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
this.renderer.clearSelection()
open() open()
}, },
}) })
@@ -199,7 +200,7 @@ export class RuntimeControls {
} }
private async openModel(): Promise<void> { private async openModel(): Promise<void> {
if (!this.canOpen("Model")) return if (!this.canOpen()) return
try { try {
await this.load(true) await this.load(true)
} catch (error) { } catch (error) {
@@ -222,7 +223,7 @@ export class RuntimeControls {
} }
private async openAccess(): Promise<void> { private async openAccess(): Promise<void> {
if (!this.canOpen("Access")) return if (!this.canOpen()) return
try { try {
await Promise.all([this.load(true), this.options.refreshScope()]) await Promise.all([this.load(true), this.options.refreshScope()])
} catch (error) { } catch (error) {
@@ -250,12 +251,9 @@ export class RuntimeControls {
this.opened() this.opened()
} }
private canOpen(label: string): boolean { private canOpen(): boolean {
const state = this.options.available() if (this.options.available()) return true
if (state.ready && !state.active) return true this.options.onStatus("Preparing chat…")
this.options.onStatus(state.active
? `${label} can be changed between turns`
: "Preparing chat…")
return false return false
} }