mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
fix(tui): keep context view concise
This commit is contained in:
+6
-5
@@ -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" })
|
setup = await createRenderer({ width: 96, height: 26, screenMode: "alternate-screen" })
|
||||||
const original = globalThis.fetch
|
const original = globalThis.fetch
|
||||||
globalThis.fetch = ((input: string | URL | Request) => {
|
globalThis.fetch = ((input: string | URL | Request) => {
|
||||||
@@ -1079,15 +1079,16 @@ describe("NanobotTui layout", () => {
|
|||||||
expect(ui.runtimeControls.contextText.plainText).toContain("~2.2k ctx")
|
expect(ui.runtimeControls.contextText.plainText).toContain("~2.2k ctx")
|
||||||
const frame = setup.captureCharFrame()
|
const frame = setup.captureCharFrame()
|
||||||
|
|
||||||
expect(frame).toContain("Agent context")
|
expect(frame).toContain("~2.2k tokens · 10 replay · 16 archived")
|
||||||
expect(frame).toContain("~2.2k session tokens · 10 replay messages · 16 archived · summary active")
|
|
||||||
expect(frame).toContain("The earlier turns agreed on a release plan.")
|
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)
|
setup.resize(40, 10)
|
||||||
await setup.renderOnce()
|
await setup.renderOnce()
|
||||||
const compact = setup.captureCharFrame()
|
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)
|
expect(occurrences(compact, "Ask nanobot anything")).toBe(1)
|
||||||
|
|
||||||
setup.mockInput.pressEscape()
|
setup.mockInput.pressEscape()
|
||||||
|
|||||||
@@ -258,7 +258,6 @@ function runtimeControlsTheme(palette: Palette) {
|
|||||||
function contextPanelTheme(palette: Palette): ContextPanelTheme {
|
function contextPanelTheme(palette: Palette): ContextPanelTheme {
|
||||||
return {
|
return {
|
||||||
text: palette.text,
|
text: palette.text,
|
||||||
muted: palette.muted,
|
|
||||||
border: palette.border,
|
border: palette.border,
|
||||||
accent: palette.accent,
|
accent: palette.accent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
BoxRenderable,
|
BoxRenderable,
|
||||||
TextAttributes,
|
|
||||||
TextRenderable,
|
TextRenderable,
|
||||||
type CliRenderer,
|
type CliRenderer,
|
||||||
} from "@opentui/core"
|
} from "@opentui/core"
|
||||||
@@ -9,7 +8,6 @@ import type { SessionContextSnapshot } from "./protocol"
|
|||||||
|
|
||||||
export interface ContextPanelTheme {
|
export interface ContextPanelTheme {
|
||||||
text: string
|
text: string
|
||||||
muted: string
|
|
||||||
border: string
|
border: string
|
||||||
accent: 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. */
|
/** Read-only explanation of the session-owned context replayed to the agent. */
|
||||||
export class ContextPanel {
|
export class ContextPanel {
|
||||||
readonly root: BoxRenderable
|
readonly root: BoxRenderable
|
||||||
private readonly title: TextRenderable
|
|
||||||
private readonly stats: TextRenderable
|
private readonly stats: TextRenderable
|
||||||
private readonly summary: TextRenderable
|
private readonly summary: TextRenderable
|
||||||
private readonly note: TextRenderable
|
|
||||||
|
|
||||||
constructor(renderer: CliRenderer, theme: ContextPanelTheme) {
|
constructor(renderer: CliRenderer, theme: ContextPanelTheme) {
|
||||||
this.root = new BoxRenderable(renderer, {
|
this.root = new BoxRenderable(renderer, {
|
||||||
id: "nanobot-tui-context-panel",
|
id: "nanobot-tui-context-panel",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxHeight: 12,
|
maxHeight: 9,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
border: true,
|
border: true,
|
||||||
@@ -42,14 +38,6 @@ export class ContextPanel {
|
|||||||
paddingRight: 1,
|
paddingRight: 1,
|
||||||
visible: false,
|
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, {
|
this.stats = new TextRenderable(renderer, {
|
||||||
id: "nanobot-tui-context-stats",
|
id: "nanobot-tui-context-stats",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -65,17 +53,8 @@ export class ContextPanel {
|
|||||||
fg: theme.text,
|
fg: theme.text,
|
||||||
wrapMode: "word",
|
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.stats)
|
||||||
this.root.add(this.summary)
|
this.root.add(this.summary)
|
||||||
this.root.add(this.note)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get visible(): boolean {
|
get visible(): boolean {
|
||||||
@@ -83,13 +62,8 @@ export class ContextPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show(context: SessionContextSnapshot): void {
|
show(context: SessionContextSnapshot): void {
|
||||||
const archived = context.archivedMessages > 0
|
this.stats.content = `~${formatTokenCount(context.estimatedSessionTokens)} tokens · ${context.replayMessages} replay · ${context.archivedMessages} archived`
|
||||||
? `${context.archivedMessages} archived · summary ${context.archivedSummary ? "active" : "unavailable"}`
|
this.summary.content = context.archivedSummary ?? ""
|
||||||
: "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.root.visible = true
|
this.root.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,15 +76,12 @@ export class ContextPanel {
|
|||||||
const medium = terminalHeight < 20
|
const medium = terminalHeight < 20
|
||||||
this.summary.visible = !compact
|
this.summary.visible = !compact
|
||||||
this.summary.maxHeight = medium ? 2 : 6
|
this.summary.maxHeight = medium ? 2 : 6
|
||||||
this.note.visible = !medium
|
this.root.maxHeight = compact ? 3 : medium ? 5 : 9
|
||||||
this.root.maxHeight = compact ? 4 : medium ? 6 : 12
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTheme(theme: ContextPanelTheme): void {
|
setTheme(theme: ContextPanelTheme): void {
|
||||||
this.root.borderColor = theme.border
|
this.root.borderColor = theme.border
|
||||||
this.title.fg = theme.text
|
|
||||||
this.stats.fg = theme.accent
|
this.stats.fg = theme.accent
|
||||||
this.summary.fg = theme.text
|
this.summary.fg = theme.text
|
||||||
this.note.fg = theme.muted
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user