From 6d6d58d329d1c8509a1888a6689fcb4fbb3816a0 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Mon, 31 Aug 2026 16:53:08 +0800 Subject: [PATCH] style(tui): simplify runtime header controls --- tui/src/app.test.ts | 47 +++++++++++++++---------------------- tui/src/app.ts | 38 ------------------------------ tui/src/runtime-controls.ts | 5 +--- 3 files changed, 20 insertions(+), 70 deletions(-) diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index a2d6ae8c5..5d7a115ee 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -205,7 +205,7 @@ describe("NanobotTui layout", () => { expect(occurrences(frame, "Ask nanobot anything")).toBe(1) expect(occurrences(frame, "Ready")).toBe(0) 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" }) @@ -985,7 +985,6 @@ describe("NanobotTui layout", () => { const ui = app as unknown as { composer: TextareaRenderable sessionMenu: { visible: boolean } - titleText: { plainText: string } runtimeControls: { modelText: { plainText: string } } } @@ -999,8 +998,7 @@ describe("NanobotTui layout", () => { ui.composer.submit() await waitUntil(() => attached.length === 1) expect(attached).toEqual(["other"]) - expect(ui.titleText.plainText).toContain("Release checklist") - expect(ui.runtimeControls.modelText.plainText).toContain("Deep Research") + expect(ui.runtimeControls.modelText.plainText).toBe("Deep Research ▾") expect(ui.runtimeControls.modelText.plainText).not.toContain("test/model") app.accept({ event: "attached", chat_id: "other" }) @@ -1009,8 +1007,7 @@ describe("NanobotTui layout", () => { ui.composer.submit() await waitUntil(() => newChats.length === 1) expect(newChats).toEqual(["new"]) - expect(ui.titleText.plainText).toContain("New chat") - expect(ui.runtimeControls.modelText.plainText).toContain("test/model") + expect(ui.runtimeControls.modelText.plainText).toBe("default ▾") } finally { globalThis.fetch = original } @@ -1182,7 +1179,8 @@ describe("NanobotTui layout", () => { model_preset: "Codex", }) 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({ event: "runtime_model_updated", @@ -1190,7 +1188,7 @@ describe("NanobotTui layout", () => { model_preset: "DeepSeek", }) 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") }) @@ -1212,8 +1210,8 @@ describe("NanobotTui layout", () => { }) await setup.flush() - expect(ui.runtimeControls.modelText.plainText).toContain("deepseek/deepseek-chat") - expect(ui.runtimeControls.modelText.plainText).not.toContain("Codex") + expect(ui.runtimeControls.modelText.plainText).toBe("default ▾") + expect(ui.runtimeControls.modelText.plainText).not.toContain("deepseek/deepseek-chat") }) test("refreshes the canonical preset after the model command completes", async () => { @@ -1309,7 +1307,6 @@ describe("NanobotTui layout", () => { menuRoot: { getChildren(): unknown[] } } composer: TextareaRenderable - titleText: TextRenderable status: TextRenderable meta: TextRenderable } @@ -1324,7 +1321,6 @@ describe("NanobotTui layout", () => { expect(ui.runtimeControls.modelText.selectable).toBe(false) expect(ui.runtimeControls.accessText.selectable).toBe(false) expect(ui.runtimeControls.contextText.selectable).toBe(false) - expect(ui.titleText.selectable).toBe(false) expect(ui.status.selectable).toBe(false) expect(ui.meta.selectable).toBe(false) 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 globalThis.fetch = ((input: string | URL | Request) => { const url = String(input) @@ -1420,14 +1416,18 @@ describe("NanobotTui layout", () => { const ui = app as unknown as { composer: TextareaRenderable sessionMenu: { visible: boolean; root: { getChildren(): unknown[] } } - titleText: TextRenderable - status: TextRenderable + title: { getChildren(): unknown[] } } try { await waitUntil(() => (app as unknown as { ready: boolean }).ready) 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 setup.flush() expect(ui.composer.placeholder).toBe("Search sessions") @@ -1441,15 +1441,6 @@ describe("NanobotTui layout", () => { expect(attached).toEqual(["other"]) expect(ui.sessionMenu.visible).toBe(false) 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 { globalThis.fetch = original } @@ -1715,7 +1706,7 @@ describe("NanobotTui layout", () => { await setup.flush() const frame = setup.captureCharFrame() expect(frame).toContain("Release checklist") - expect(occurrences(frame, "Current chat")).toBe(1) + expect(occurrences(frame, "Current chat")).toBe(0) } finally { globalThis.fetch = original } @@ -1959,7 +1950,7 @@ describe("NanobotTui layout", () => { } else if (width >= 28 && height >= 9) { 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() const activeFrame = setup.captureCharFrame() 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, "app.ts")).toBe(1) expect(ui.composer.placeholder).toBe("Enter send now · Tab send next") diff --git a/tui/src/app.ts b/tui/src/app.ts index a76eb79e9..53856a157 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -442,7 +442,6 @@ export class NanobotTui { private readonly client: ChatClient private readonly shell: BoxRenderable private readonly title: BoxRenderable - private readonly titleText: TextRenderable private readonly composerFrame: BoxRenderable private readonly composer: TextareaRenderable private composerSyntax: SyntaxStyle @@ -649,28 +648,6 @@ export class NanobotTui { alignItems: "center", 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( renderer, 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.accessText) this.title.add(this.runtimeControls.contextText) @@ -1877,7 +1853,6 @@ export class NanobotTui { this.composer.syntaxStyle = this.composerSyntax this.syncComposerImageHighlights(this.composer.plainText) void this.renderer.idle().catch(() => {}).finally(() => previousComposerSyntax.destroy()) - this.renderTitleColor() this.status.fg = this.palette.muted this.meta.fg = this.palette.faint this.updateMeta() @@ -1957,9 +1932,6 @@ export class NanobotTui { } 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 ? "" : ` ~${formatTokenCount(this.contextTokens)}${this.contextWindowTokens @@ -1969,12 +1941,6 @@ export class NanobotTui { this.runtimeControls.updateContext(context) } - private renderTitleColor(): void { - this.titleText.fg = this.sessionLoading || this.sessionMenu.visible - ? this.palette.accent - : this.palette.muted - } - private resizeComposer(): void { const verticalPadding = this.renderer.height >= 12 ? 1 : 0 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.clearComposer() this.sessionLoading = true - this.renderTitleColor() const loadId = ++this.sessionLoadId this.status.content = "Loading sessions…" try { @@ -2412,7 +2377,6 @@ export class NanobotTui { this.defaultModelPreset, ) this.startSessionRefresh() - this.renderTitleColor() this.sessionMenu.update(this.composer.plainText, limit) this.syncComposerPlaceholder() this.updateMeta() @@ -2420,7 +2384,6 @@ export class NanobotTui { } catch (error) { if (loadId !== this.sessionLoadId) return this.sessionLoading = false - this.renderTitleColor() this.status.content = error instanceof Error ? error.message : String(error) } } @@ -2579,7 +2542,6 @@ export class NanobotTui { this.sessionLoadId += 1 this.sessionLoading = false this.hideSessionMenu() - this.renderTitleColor() this.clearComposer() this.syncComposerPlaceholder() this.composer.focus() diff --git a/tui/src/runtime-controls.ts b/tui/src/runtime-controls.ts index 756354d12..9f055f29a 100644 --- a/tui/src/runtime-controls.ts +++ b/tui/src/runtime-controls.ts @@ -309,10 +309,7 @@ export class RuntimeControls { } private render(): void { - const runtime = this.modelPreset !== "default" - ? [this.modelPreset, this.model].filter(Boolean).join(" ") - : this.model - this.modelText.content = ` ${runtime} ▾` + this.modelText.content = `${this.modelPreset} ▾` const access = this.scope.access_mode === "full" ? "full access" : "workspace access" this.accessText.content = ` ${access} ▾` this.renderColors()