From 2b491340d8082ca7b603ecef20fbf25af4b77f41 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Thu, 20 Aug 2026 15:04:02 +0800 Subject: [PATCH] fix(tui): keep context view concise --- tui/src/app.test.ts | 11 +++++---- tui/src/app.ts | 1 - tui/src/context-panel.test.ts | 43 +++++++++++++++++++++++++++++++++++ tui/src/context-panel.ts | 37 ++++-------------------------- 4 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 tui/src/context-panel.test.ts diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index 522f92ca7..cb5aa175b 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -1042,7 +1042,7 @@ describe("NanobotTui layout", () => { } }) - test("explains the session-owned agent context without exposing private reasoning", async () => { + test("shows compact session context without exposing private reasoning", async () => { setup = await createRenderer({ width: 96, height: 26, screenMode: "alternate-screen" }) const original = globalThis.fetch globalThis.fetch = ((input: string | URL | Request) => { @@ -1079,15 +1079,16 @@ describe("NanobotTui layout", () => { expect(ui.runtimeControls.contextText.plainText).toContain("~2.2k ctx") const frame = setup.captureCharFrame() - expect(frame).toContain("Agent context") - expect(frame).toContain("~2.2k session tokens · 10 replay messages · 16 archived · summary active") + expect(frame).toContain("~2.2k tokens · 10 replay · 16 archived") expect(frame).toContain("The earlier turns agreed on a release plan.") - expect(frame).toContain("memory, instructions, and skills are added separately") + expect(frame).not.toContain("Agent context") + expect(frame).not.toContain("summary active") + expect(frame).not.toContain("memory, instructions, and skills are added separately") setup.resize(40, 10) await setup.renderOnce() const compact = setup.captureCharFrame() - expect(occurrences(compact, "Agent context")).toBe(1) + expect(occurrences(compact, "Agent context")).toBe(0) expect(occurrences(compact, "Ask nanobot anything")).toBe(1) setup.mockInput.pressEscape() diff --git a/tui/src/app.ts b/tui/src/app.ts index de133171d..678654829 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -258,7 +258,6 @@ function runtimeControlsTheme(palette: Palette) { function contextPanelTheme(palette: Palette): ContextPanelTheme { return { text: palette.text, - muted: palette.muted, border: palette.border, accent: palette.accent, } diff --git a/tui/src/context-panel.test.ts b/tui/src/context-panel.test.ts new file mode 100644 index 000000000..15c48d15f --- /dev/null +++ b/tui/src/context-panel.test.ts @@ -0,0 +1,43 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testing" + +import { ContextPanel } from "./context-panel" +import type { SessionContextSnapshot } from "./protocol" + +const context: SessionContextSnapshot = { + totalMessages: 8, + archivedMessages: 0, + replayMessages: 8, + estimatedReplayTokens: 950, + estimatedSummaryTokens: 0, + estimatedSessionTokens: 950, + archivedSummary: null, + archivedSummaryAt: null, + lastUsage: null, +} + +describe("ContextPanel", () => { + let setup: TestRendererSetup | undefined + + afterEach(() => { + if (setup && !setup.renderer.isDestroyed) setup.renderer.destroy() + setup = undefined + }) + + test("omits explanatory copy when no compacted summary exists", async () => { + setup = await createTestRenderer({ width: 80, height: 18, screenMode: "alternate-screen" }) + const panel = new ContextPanel(setup.renderer, { + text: "#FFFFFF", + border: "#555555", + accent: "#EF8E30", + }) + setup.renderer.root.add(panel.root) + panel.show(context) + await setup.renderOnce() + + const frame = setup.captureCharFrame() + expect(frame).toContain("~950 tokens · 8 replay · 0 archived") + expect(frame).not.toContain("Agent context") + expect(frame).not.toContain("no compacted summary") + }) +}) diff --git a/tui/src/context-panel.ts b/tui/src/context-panel.ts index 5a2be11f6..393791c26 100644 --- a/tui/src/context-panel.ts +++ b/tui/src/context-panel.ts @@ -1,6 +1,5 @@ import { BoxRenderable, - TextAttributes, TextRenderable, type CliRenderer, } from "@opentui/core" @@ -9,7 +8,6 @@ import type { SessionContextSnapshot } from "./protocol" export interface ContextPanelTheme { text: string - muted: string border: string accent: string } @@ -23,16 +21,14 @@ export function formatTokenCount(value: number): string { /** Read-only explanation of the session-owned context replayed to the agent. */ export class ContextPanel { readonly root: BoxRenderable - private readonly title: TextRenderable private readonly stats: TextRenderable private readonly summary: TextRenderable - private readonly note: TextRenderable constructor(renderer: CliRenderer, theme: ContextPanelTheme) { this.root = new BoxRenderable(renderer, { id: "nanobot-tui-context-panel", width: "100%", - maxHeight: 12, + maxHeight: 9, flexShrink: 0, flexDirection: "column", border: true, @@ -42,14 +38,6 @@ export class ContextPanel { paddingRight: 1, visible: false, }) - this.title = new TextRenderable(renderer, { - id: "nanobot-tui-context-title", - content: "Agent context", - width: "100%", - height: 1, - fg: theme.text, - attributes: TextAttributes.BOLD, - }) this.stats = new TextRenderable(renderer, { id: "nanobot-tui-context-stats", content: "", @@ -65,17 +53,8 @@ export class ContextPanel { fg: theme.text, wrapMode: "word", }) - this.note = new TextRenderable(renderer, { - id: "nanobot-tui-context-note", - content: "Session view only · memory, instructions, and skills are added separately · Esc close", - width: "100%", - fg: theme.muted, - wrapMode: "word", - }) - this.root.add(this.title) this.root.add(this.stats) this.root.add(this.summary) - this.root.add(this.note) } get visible(): boolean { @@ -83,13 +62,8 @@ export class ContextPanel { } show(context: SessionContextSnapshot): void { - const archived = context.archivedMessages > 0 - ? `${context.archivedMessages} archived · summary ${context.archivedSummary ? "active" : "unavailable"}` - : "No archived messages" - this.stats.content = `~${formatTokenCount(context.estimatedSessionTokens)} session tokens · ${context.replayMessages} replay messages · ${archived}` - this.summary.content = context.archivedSummary - ? `Summary\n${context.archivedSummary}` - : "The agent is currently replaying raw session messages; no compacted summary exists yet." + this.stats.content = `~${formatTokenCount(context.estimatedSessionTokens)} tokens · ${context.replayMessages} replay · ${context.archivedMessages} archived` + this.summary.content = context.archivedSummary ?? "" this.root.visible = true } @@ -102,15 +76,12 @@ export class ContextPanel { const medium = terminalHeight < 20 this.summary.visible = !compact this.summary.maxHeight = medium ? 2 : 6 - this.note.visible = !medium - this.root.maxHeight = compact ? 4 : medium ? 6 : 12 + this.root.maxHeight = compact ? 3 : medium ? 5 : 9 } setTheme(theme: ContextPanelTheme): void { this.root.borderColor = theme.border - this.title.fg = theme.text this.stats.fg = theme.accent this.summary.fg = theme.text - this.note.fg = theme.muted } }