fix(tui): keep context view concise

This commit is contained in:
chengyongru
2026-08-20 15:25:54 +08:00
committed by chengyongru
parent c5ae958bc3
commit 2b491340d8
4 changed files with 53 additions and 39 deletions
+6 -5
View File
@@ -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()
-1
View File
@@ -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,
}
+43
View File
@@ -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")
})
})
+4 -33
View File
@@ -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
}
}