mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-01 08:42:20 +03:00
style(tui): simplify runtime header controls
This commit is contained in:
+19
-28
@@ -205,7 +205,7 @@ describe("NanobotTui layout", () => {
|
|||||||
expect(occurrences(frame, "Ask nanobot anything")).toBe(1)
|
expect(occurrences(frame, "Ask nanobot anything")).toBe(1)
|
||||||
expect(occurrences(frame, "Ready")).toBe(0)
|
expect(occurrences(frame, "Ready")).toBe(0)
|
||||||
expect(occurrences(frame, "Getting ready…")).toBe(1)
|
expect(occurrences(frame, "Getting ready…")).toBe(1)
|
||||||
expect(occurrences(frame, "nanobot test/model")).toBe(1)
|
expect(occurrences(frame, "default ▾")).toBe(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.accept({ event: "attached", chat_id: "chat" })
|
app.accept({ event: "attached", chat_id: "chat" })
|
||||||
@@ -985,7 +985,6 @@ describe("NanobotTui layout", () => {
|
|||||||
const ui = app as unknown as {
|
const ui = app as unknown as {
|
||||||
composer: TextareaRenderable
|
composer: TextareaRenderable
|
||||||
sessionMenu: { visible: boolean }
|
sessionMenu: { visible: boolean }
|
||||||
titleText: { plainText: string }
|
|
||||||
runtimeControls: { modelText: { plainText: string } }
|
runtimeControls: { modelText: { plainText: string } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -999,8 +998,7 @@ describe("NanobotTui layout", () => {
|
|||||||
ui.composer.submit()
|
ui.composer.submit()
|
||||||
await waitUntil(() => attached.length === 1)
|
await waitUntil(() => attached.length === 1)
|
||||||
expect(attached).toEqual(["other"])
|
expect(attached).toEqual(["other"])
|
||||||
expect(ui.titleText.plainText).toContain("Release checklist")
|
expect(ui.runtimeControls.modelText.plainText).toBe("Deep Research ▾")
|
||||||
expect(ui.runtimeControls.modelText.plainText).toContain("Deep Research")
|
|
||||||
expect(ui.runtimeControls.modelText.plainText).not.toContain("test/model")
|
expect(ui.runtimeControls.modelText.plainText).not.toContain("test/model")
|
||||||
|
|
||||||
app.accept({ event: "attached", chat_id: "other" })
|
app.accept({ event: "attached", chat_id: "other" })
|
||||||
@@ -1009,8 +1007,7 @@ describe("NanobotTui layout", () => {
|
|||||||
ui.composer.submit()
|
ui.composer.submit()
|
||||||
await waitUntil(() => newChats.length === 1)
|
await waitUntil(() => newChats.length === 1)
|
||||||
expect(newChats).toEqual(["new"])
|
expect(newChats).toEqual(["new"])
|
||||||
expect(ui.titleText.plainText).toContain("New chat")
|
expect(ui.runtimeControls.modelText.plainText).toBe("default ▾")
|
||||||
expect(ui.runtimeControls.modelText.plainText).toContain("test/model")
|
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = original
|
globalThis.fetch = original
|
||||||
}
|
}
|
||||||
@@ -1182,7 +1179,8 @@ describe("NanobotTui layout", () => {
|
|||||||
model_preset: "Codex",
|
model_preset: "Codex",
|
||||||
})
|
})
|
||||||
await setup.flush()
|
await setup.flush()
|
||||||
expect(ui.runtimeControls.modelText.plainText).toContain("Codex openai/gpt-5.6")
|
expect(ui.runtimeControls.modelText.plainText).toBe("Codex ▾")
|
||||||
|
expect(ui.runtimeControls.modelText.plainText).not.toContain("openai/gpt-5.6")
|
||||||
|
|
||||||
app.accept({
|
app.accept({
|
||||||
event: "runtime_model_updated",
|
event: "runtime_model_updated",
|
||||||
@@ -1190,7 +1188,7 @@ describe("NanobotTui layout", () => {
|
|||||||
model_preset: "DeepSeek",
|
model_preset: "DeepSeek",
|
||||||
})
|
})
|
||||||
await setup.flush()
|
await setup.flush()
|
||||||
expect(ui.runtimeControls.modelText.plainText).toContain("Codex openai/gpt-5.6")
|
expect(ui.runtimeControls.modelText.plainText).toBe("Codex ▾")
|
||||||
expect(ui.runtimeControls.modelText.plainText).not.toContain("DeepSeek")
|
expect(ui.runtimeControls.modelText.plainText).not.toContain("DeepSeek")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1212,8 +1210,8 @@ describe("NanobotTui layout", () => {
|
|||||||
})
|
})
|
||||||
await setup.flush()
|
await setup.flush()
|
||||||
|
|
||||||
expect(ui.runtimeControls.modelText.plainText).toContain("deepseek/deepseek-chat")
|
expect(ui.runtimeControls.modelText.plainText).toBe("default ▾")
|
||||||
expect(ui.runtimeControls.modelText.plainText).not.toContain("Codex")
|
expect(ui.runtimeControls.modelText.plainText).not.toContain("deepseek/deepseek-chat")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("refreshes the canonical preset after the model command completes", async () => {
|
test("refreshes the canonical preset after the model command completes", async () => {
|
||||||
@@ -1309,7 +1307,6 @@ describe("NanobotTui layout", () => {
|
|||||||
menuRoot: { getChildren(): unknown[] }
|
menuRoot: { getChildren(): unknown[] }
|
||||||
}
|
}
|
||||||
composer: TextareaRenderable
|
composer: TextareaRenderable
|
||||||
titleText: TextRenderable
|
|
||||||
status: TextRenderable
|
status: TextRenderable
|
||||||
meta: TextRenderable
|
meta: TextRenderable
|
||||||
}
|
}
|
||||||
@@ -1324,7 +1321,6 @@ describe("NanobotTui layout", () => {
|
|||||||
expect(ui.runtimeControls.modelText.selectable).toBe(false)
|
expect(ui.runtimeControls.modelText.selectable).toBe(false)
|
||||||
expect(ui.runtimeControls.accessText.selectable).toBe(false)
|
expect(ui.runtimeControls.accessText.selectable).toBe(false)
|
||||||
expect(ui.runtimeControls.contextText.selectable).toBe(false)
|
expect(ui.runtimeControls.contextText.selectable).toBe(false)
|
||||||
expect(ui.titleText.selectable).toBe(false)
|
|
||||||
expect(ui.status.selectable).toBe(false)
|
expect(ui.status.selectable).toBe(false)
|
||||||
expect(ui.meta.selectable).toBe(false)
|
expect(ui.meta.selectable).toBe(false)
|
||||||
app.accept({ event: "goal_status", chat_id: "chat", status: "running" })
|
app.accept({ event: "goal_status", chat_id: "chat", status: "running" })
|
||||||
@@ -1394,7 +1390,7 @@ describe("NanobotTui layout", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("opens and switches sessions from the clickable title", async () => {
|
test("switches sessions only through the sessions command", async () => {
|
||||||
const original = globalThis.fetch
|
const original = globalThis.fetch
|
||||||
globalThis.fetch = ((input: string | URL | Request) => {
|
globalThis.fetch = ((input: string | URL | Request) => {
|
||||||
const url = String(input)
|
const url = String(input)
|
||||||
@@ -1420,14 +1416,18 @@ describe("NanobotTui layout", () => {
|
|||||||
const ui = app as unknown as {
|
const ui = app as unknown as {
|
||||||
composer: TextareaRenderable
|
composer: TextareaRenderable
|
||||||
sessionMenu: { visible: boolean; root: { getChildren(): unknown[] } }
|
sessionMenu: { visible: boolean; root: { getChildren(): unknown[] } }
|
||||||
titleText: TextRenderable
|
title: { getChildren(): unknown[] }
|
||||||
status: TextRenderable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await waitUntil(() => (app as unknown as { ready: boolean }).ready)
|
await waitUntil(() => (app as unknown as { ready: boolean }).ready)
|
||||||
await setup.renderOnce()
|
await setup.renderOnce()
|
||||||
await setup.mockMouse.click(ui.titleText.x + 2, ui.titleText.y)
|
const titleItems = ui.title.getChildren() as TextRenderable[]
|
||||||
|
expect(titleItems.some((item) => item.id === "nanobot-tui-title-text")).toBe(false)
|
||||||
|
expect(ui.sessionMenu.visible).toBe(false)
|
||||||
|
|
||||||
|
ui.composer.setText("/sessions")
|
||||||
|
ui.composer.submit()
|
||||||
await waitUntil(() => ui.sessionMenu.visible)
|
await waitUntil(() => ui.sessionMenu.visible)
|
||||||
await setup.flush()
|
await setup.flush()
|
||||||
expect(ui.composer.placeholder).toBe("Search sessions")
|
expect(ui.composer.placeholder).toBe("Search sessions")
|
||||||
@@ -1441,15 +1441,6 @@ describe("NanobotTui layout", () => {
|
|||||||
expect(attached).toEqual(["other"])
|
expect(attached).toEqual(["other"])
|
||||||
expect(ui.sessionMenu.visible).toBe(false)
|
expect(ui.sessionMenu.visible).toBe(false)
|
||||||
expect(ui.composer.focused).toBe(true)
|
expect(ui.composer.focused).toBe(true)
|
||||||
expect(ui.titleText.plainText).toContain("Release checklist")
|
|
||||||
|
|
||||||
app.accept({ event: "attached", chat_id: "other" })
|
|
||||||
await setup.mockMouse.click(ui.titleText.x + 2, ui.titleText.y)
|
|
||||||
await waitUntil(() => ui.sessionMenu.visible)
|
|
||||||
ui.composer.blur()
|
|
||||||
await setup.mockMouse.click(ui.status.x, ui.status.y)
|
|
||||||
expect(ui.sessionMenu.visible).toBe(false)
|
|
||||||
expect(ui.composer.focused).toBe(true)
|
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = original
|
globalThis.fetch = original
|
||||||
}
|
}
|
||||||
@@ -1715,7 +1706,7 @@ describe("NanobotTui layout", () => {
|
|||||||
await setup.flush()
|
await setup.flush()
|
||||||
const frame = setup.captureCharFrame()
|
const frame = setup.captureCharFrame()
|
||||||
expect(frame).toContain("Release checklist")
|
expect(frame).toContain("Release checklist")
|
||||||
expect(occurrences(frame, "Current chat")).toBe(1)
|
expect(occurrences(frame, "Current chat")).toBe(0)
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = original
|
globalThis.fetch = original
|
||||||
}
|
}
|
||||||
@@ -1959,7 +1950,7 @@ describe("NanobotTui layout", () => {
|
|||||||
} else if (width >= 28 && height >= 9) {
|
} else if (width >= 28 && height >= 9) {
|
||||||
expect(occurrences(frame, "Enter now · Tab next")).toBe(1)
|
expect(occurrences(frame, "Enter now · Tab next")).toBe(1)
|
||||||
}
|
}
|
||||||
expect(occurrences(frame, "nanobot test/model")).toBe(height >= 14 ? 1 : 0)
|
expect(occurrences(frame, "default ▾")).toBe(height >= 14 ? 1 : 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3122,7 +3113,7 @@ describe("NanobotTui with a Herdr pane title reporter", () => {
|
|||||||
await setup.flush()
|
await setup.flush()
|
||||||
const activeFrame = setup.captureCharFrame()
|
const activeFrame = setup.captureCharFrame()
|
||||||
expect(activeFrame).toContain(">_ nanobot")
|
expect(activeFrame).toContain(">_ nanobot")
|
||||||
expect(activeFrame).toContain("test/model")
|
expect(activeFrame).toContain("default ▾")
|
||||||
expect(occurrences(activeFrame, "› Ship the Herdr integration")).toBe(1)
|
expect(occurrences(activeFrame, "› Ship the Herdr integration")).toBe(1)
|
||||||
expect(occurrences(activeFrame, "app.ts")).toBe(1)
|
expect(occurrences(activeFrame, "app.ts")).toBe(1)
|
||||||
expect(ui.composer.placeholder).toBe("Enter send now · Tab send next")
|
expect(ui.composer.placeholder).toBe("Enter send now · Tab send next")
|
||||||
|
|||||||
@@ -442,7 +442,6 @@ export class NanobotTui {
|
|||||||
private readonly client: ChatClient
|
private readonly client: ChatClient
|
||||||
private readonly shell: BoxRenderable
|
private readonly shell: BoxRenderable
|
||||||
private readonly title: BoxRenderable
|
private readonly title: BoxRenderable
|
||||||
private readonly titleText: TextRenderable
|
|
||||||
private readonly composerFrame: BoxRenderable
|
private readonly composerFrame: BoxRenderable
|
||||||
private readonly composer: TextareaRenderable
|
private readonly composer: TextareaRenderable
|
||||||
private composerSyntax: SyntaxStyle
|
private composerSyntax: SyntaxStyle
|
||||||
@@ -649,28 +648,6 @@ export class NanobotTui {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: RGBA.defaultBackground(),
|
backgroundColor: RGBA.defaultBackground(),
|
||||||
})
|
})
|
||||||
this.titleText = new TextRenderable(renderer, {
|
|
||||||
id: "nanobot-tui-title-text",
|
|
||||||
content: "nanobot",
|
|
||||||
height: 1,
|
|
||||||
flexShrink: 0,
|
|
||||||
truncate: true,
|
|
||||||
fg: this.palette.muted,
|
|
||||||
selectable: false,
|
|
||||||
onMouseOver: () => { this.titleText.fg = this.palette.accent },
|
|
||||||
onMouseOut: () => this.renderTitleColor(),
|
|
||||||
onMouseDown: (event) => {
|
|
||||||
if (event.button !== 0) return
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
this.renderer.clearSelection()
|
|
||||||
if (this.sessionLoading || this.sessionMenu.visible) {
|
|
||||||
this.closeSessions()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
void this.openSessions()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.runtimeControls = new RuntimeControls(
|
this.runtimeControls = new RuntimeControls(
|
||||||
renderer,
|
renderer,
|
||||||
runtimeControlsTheme(this.palette),
|
runtimeControlsTheme(this.palette),
|
||||||
@@ -703,7 +680,6 @@ export class NanobotTui {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
this.title.add(this.titleText)
|
|
||||||
this.title.add(this.runtimeControls.modelText)
|
this.title.add(this.runtimeControls.modelText)
|
||||||
this.title.add(this.runtimeControls.accessText)
|
this.title.add(this.runtimeControls.accessText)
|
||||||
this.title.add(this.runtimeControls.contextText)
|
this.title.add(this.runtimeControls.contextText)
|
||||||
@@ -1877,7 +1853,6 @@ export class NanobotTui {
|
|||||||
this.composer.syntaxStyle = this.composerSyntax
|
this.composer.syntaxStyle = this.composerSyntax
|
||||||
this.syncComposerImageHighlights(this.composer.plainText)
|
this.syncComposerImageHighlights(this.composer.plainText)
|
||||||
void this.renderer.idle().catch(() => {}).finally(() => previousComposerSyntax.destroy())
|
void this.renderer.idle().catch(() => {}).finally(() => previousComposerSyntax.destroy())
|
||||||
this.renderTitleColor()
|
|
||||||
this.status.fg = this.palette.muted
|
this.status.fg = this.palette.muted
|
||||||
this.meta.fg = this.palette.faint
|
this.meta.fg = this.palette.faint
|
||||||
this.updateMeta()
|
this.updateMeta()
|
||||||
@@ -1957,9 +1932,6 @@ export class NanobotTui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateTitle(): void {
|
private updateTitle(): void {
|
||||||
const identity = this.sessionTitle.trim() || "nanobot"
|
|
||||||
this.titleText.maxWidth = Math.max(8, Math.floor(this.renderer.width * 0.38))
|
|
||||||
this.titleText.content = identity
|
|
||||||
const context = this.contextTokens === null
|
const context = this.contextTokens === null
|
||||||
? ""
|
? ""
|
||||||
: ` ~${formatTokenCount(this.contextTokens)}${this.contextWindowTokens
|
: ` ~${formatTokenCount(this.contextTokens)}${this.contextWindowTokens
|
||||||
@@ -1969,12 +1941,6 @@ export class NanobotTui {
|
|||||||
this.runtimeControls.updateContext(context)
|
this.runtimeControls.updateContext(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderTitleColor(): void {
|
|
||||||
this.titleText.fg = this.sessionLoading || this.sessionMenu.visible
|
|
||||||
? this.palette.accent
|
|
||||||
: this.palette.muted
|
|
||||||
}
|
|
||||||
|
|
||||||
private resizeComposer(): void {
|
private resizeComposer(): void {
|
||||||
const verticalPadding = this.renderer.height >= 12 ? 1 : 0
|
const verticalPadding = this.renderer.height >= 12 ? 1 : 0
|
||||||
const maxContentHeight = Math.max(1, Math.min(12, Math.floor(this.renderer.height / 3)))
|
const maxContentHeight = Math.max(1, Math.min(12, Math.floor(this.renderer.height / 3)))
|
||||||
@@ -2385,7 +2351,6 @@ export class NanobotTui {
|
|||||||
this.contextPanel.hide()
|
this.contextPanel.hide()
|
||||||
this.clearComposer()
|
this.clearComposer()
|
||||||
this.sessionLoading = true
|
this.sessionLoading = true
|
||||||
this.renderTitleColor()
|
|
||||||
const loadId = ++this.sessionLoadId
|
const loadId = ++this.sessionLoadId
|
||||||
this.status.content = "Loading sessions…"
|
this.status.content = "Loading sessions…"
|
||||||
try {
|
try {
|
||||||
@@ -2412,7 +2377,6 @@ export class NanobotTui {
|
|||||||
this.defaultModelPreset,
|
this.defaultModelPreset,
|
||||||
)
|
)
|
||||||
this.startSessionRefresh()
|
this.startSessionRefresh()
|
||||||
this.renderTitleColor()
|
|
||||||
this.sessionMenu.update(this.composer.plainText, limit)
|
this.sessionMenu.update(this.composer.plainText, limit)
|
||||||
this.syncComposerPlaceholder()
|
this.syncComposerPlaceholder()
|
||||||
this.updateMeta()
|
this.updateMeta()
|
||||||
@@ -2420,7 +2384,6 @@ export class NanobotTui {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (loadId !== this.sessionLoadId) return
|
if (loadId !== this.sessionLoadId) return
|
||||||
this.sessionLoading = false
|
this.sessionLoading = false
|
||||||
this.renderTitleColor()
|
|
||||||
this.status.content = error instanceof Error ? error.message : String(error)
|
this.status.content = error instanceof Error ? error.message : String(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2579,7 +2542,6 @@ export class NanobotTui {
|
|||||||
this.sessionLoadId += 1
|
this.sessionLoadId += 1
|
||||||
this.sessionLoading = false
|
this.sessionLoading = false
|
||||||
this.hideSessionMenu()
|
this.hideSessionMenu()
|
||||||
this.renderTitleColor()
|
|
||||||
this.clearComposer()
|
this.clearComposer()
|
||||||
this.syncComposerPlaceholder()
|
this.syncComposerPlaceholder()
|
||||||
this.composer.focus()
|
this.composer.focus()
|
||||||
|
|||||||
@@ -309,10 +309,7 @@ export class RuntimeControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private render(): void {
|
private render(): void {
|
||||||
const runtime = this.modelPreset !== "default"
|
this.modelText.content = `${this.modelPreset} ▾`
|
||||||
? [this.modelPreset, this.model].filter(Boolean).join(" ")
|
|
||||||
: this.model
|
|
||||||
this.modelText.content = ` ${runtime} ▾`
|
|
||||||
const access = this.scope.access_mode === "full" ? "full access" : "workspace access"
|
const access = this.scope.access_mode === "full" ? "full access" : "workspace access"
|
||||||
this.accessText.content = ` ${access} ▾`
|
this.accessText.content = ` ${access} ▾`
|
||||||
this.renderColors()
|
this.renderColors()
|
||||||
|
|||||||
Reference in New Issue
Block a user