From 4d59b059eec242a5be1d2883642015f450d30fc7 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Tue, 28 Jul 2026 13:57:19 +0200 Subject: [PATCH] feat(tui): add verbose turn token usage (#39281) --- packages/tui/src/component/devtools-bar.tsx | 11 ++++ packages/tui/src/component/dialog-config.tsx | 4 +- packages/tui/src/config/index.tsx | 4 +- packages/tui/src/routes/session/index.tsx | 55 +++++++++++++++++++- packages/tui/src/routes/session/rows.ts | 2 +- 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx index 45c031069c2..8fa55144e1b 100644 --- a/packages/tui/src/component/devtools-bar.tsx +++ b/packages/tui/src/component/devtools-bar.tsx @@ -77,6 +77,7 @@ export function DevToolsBar() { const runtime = createMemo(() => runtimeStatus(frontendSamples())) const timing = () => config.data.debug?.timing ?? false const turnTokens = () => config.data.debug?.turn_tokens ?? false + const verboseTurnTokens = () => turnTokens() === "verbose" const offEscape = keymap.intercept( "key", @@ -380,6 +381,16 @@ export function DevToolsBar() { > {turnTokens() ? "[x]" : "[ ]"} Turn token usage + + void config.update((draft) => { + draft.debug = { ...draft.debug, turn_tokens: verboseTurnTokens() ? true : "verbose" } + }) + } + hoverBackground + > + {verboseTurnTokens() ? "[x]" : "[ ]"} Turn token usage (verbose) + {(group) => ( diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index 0cf13f88ac8..49d5f93650e 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -254,8 +254,8 @@ export const settings: Setting[] = [ category: "Debug", path: ["debug", "turn_tokens"], default: false, - values: [false, true], - labels: ["off", "on"], + values: [false, true, "verbose"], + labels: ["off", "on", "verbose"], keywords: ["tokens", "usage", "debug"], }, ] diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 424e8dbc9a8..70e2ed3d304 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -159,7 +159,9 @@ export const Info = Schema.Struct({ Schema.Struct({ devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools debug bar" }), timing: Schema.optional(Schema.Boolean).annotate({ description: "Show time-to-first-draw diagnostics" }), - turn_tokens: Schema.optional(Schema.Boolean).annotate({ description: "Show per-turn token usage diagnostics" }), + turn_tokens: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("verbose")])).annotate({ + description: "Show per-turn token usage diagnostics, optionally with tool call inputs", + }), }), ).annotate({ description: "Debugging settings" }), animations: Schema.optional(Schema.Boolean).annotate({ description: "Enable interface animations" }), diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 4b1717b4886..917e3a4e833 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1104,6 +1104,7 @@ function TurnTokenUsage(props: { }) { const config = useConfig() const { themeV2 } = useTheme() + const verbose = () => config.data.debug?.turn_tokens === "verbose" const steps = createMemo(() => { let previousCache = props.previousCache return props.messageIDs.flatMap((messageID) => { @@ -1123,6 +1124,7 @@ function TurnTokenUsage(props: { return [ { finish: message.finish === "tool-calls" ? "tool-call" : (message.finish ?? "unknown"), + tools: verbose() ? message.content.filter((part) => part.type === "tool") : [], newTokens, cached: message.tokens.cache.read, total, @@ -1138,7 +1140,7 @@ function TurnTokenUsage(props: { total: Math.max("Total".length, ...steps().map((item) => item.total.toLocaleString().length)), })) return ( - 0}> + 0}> @@ -1161,7 +1163,7 @@ function TurnTokenUsage(props: { {(item) => ( - + {item.finish.padEnd(columns().step + 2)} {item.newTokens.toLocaleString().padStart(columns().newTokens)} @@ -1171,6 +1173,7 @@ function TurnTokenUsage(props: { {" "} {item.total.toLocaleString().padStart(columns().total)} + ! Likely cache bust: {item.reuseDrop?.toLocaleString()} fewer cached tokens than the previous step @@ -1184,6 +1187,54 @@ function TurnTokenUsage(props: { ) } +function TurnTokenToolCalls(props: { tools: SessionMessageAssistantTool[] }) { + const { themeV2 } = useTheme() + const nameWidth = () => Math.max(0, ...props.tools.map((tool) => tool.name.length)) + 2 + return ( + 0}> + + + {(tool) => ( + + + {tool.name} + + + {turnTokenToolSummary(tool)} + + + )} + + + + ) +} + +function turnTokenToolSummary(tool: SessionMessageAssistantTool) { + const data = tool.state.input + if (typeof data === "string") return data + const primaryKey = ["command", "id", "pattern", "url", "query", "path", "description", "code"].find( + (key) => key in data, + ) + const input = Object.entries(data).filter(([, value]) => + ["string", "number", "boolean"].includes(typeof value), + ) + const primary = input.find(([key]) => key === primaryKey)?.[1] + const details = input.filter(([key]) => key !== primaryKey).map(([key, value]) => `${key}: ${String(value)}`) + return [primary === undefined ? "" : String(primary), ...details].filter(Boolean).join(" ") +} + function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) { const { themeV2 } = useTheme() const shortcut = Keymap.useShortcut("session.background") diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index 3c201840119..1f8e814793c 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -41,7 +41,7 @@ export function createSessionRows(sessionID: Accessor) { const config = useConfig() const [rows, setRows] = createStore([]) const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID - const turnTokens = () => config.data.debug?.turn_tokens === true + const turnTokens = () => Boolean(config.data.debug?.turn_tokens) function reduce() { const messages = data.session.message.list(sessionID())