From 14c279fcb42e6050f64213637bb07ec17c1b552a Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Sat, 4 Apr 2026 00:48:48 -0400 Subject: [PATCH] tui: show token usage breakdown in sidebar for transparency Adds input, output and cached token counts to session sidebar so users can monitor their consumption while working. --- .../cli/cmd/tui/routes/session/sidebar.tsx | 33 ++++++++++ .../src/cli/cmd/tui/routes/session/usage.ts | 26 ++++++++ packages/opencode/test/cli/tui/usage.test.ts | 61 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/tui/routes/session/usage.ts create mode 100644 packages/opencode/test/cli/tui/usage.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 45824208bfe..38d7558dc7f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -11,6 +11,9 @@ import { useKeybind } from "../../context/keybind" import { useDirectory } from "../../context/directory" import { useKV } from "../../context/kv" import { TodoItem } from "../../component/todo-item" +// kilocode_change start +import { formatCount, getUsage } from "./usage" +// kilocode_change end export function Sidebar(props: { sessionID: string; overlay?: boolean }) { const sync = useSync() @@ -60,6 +63,17 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { } }) + // kilocode_change start + const usage = createMemo(() => { + const total = getUsage(messages()) + return { + input: formatCount(total.input), + output: formatCount(total.output), + cached: formatCount(total.cached), + } + }) + // kilocode_change end + const directory = useDirectory() const kv = useKV() @@ -109,6 +123,25 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { {context()?.percentage ?? 0}% used {cost()} spent + {/* kilocode_change start */} + + + Token Usage + + + Input + {usage().input} + + + Output + {usage().output} + + + Cached + {usage().cached} + + + {/* kilocode_change end */} 0}> { + if (item.role !== "assistant") return sum + return { + input: sum.input + item.tokens.input, + output: sum.output + item.tokens.output, + cached: sum.cached + item.tokens.cache.read, + } + }, + { + input: 0, + output: 0, + cached: 0, + }, + ) +} + +export function formatCount(input: number) { + return fmt.format(input) +} diff --git a/packages/opencode/test/cli/tui/usage.test.ts b/packages/opencode/test/cli/tui/usage.test.ts new file mode 100644 index 00000000000..b5f7c74d19e --- /dev/null +++ b/packages/opencode/test/cli/tui/usage.test.ts @@ -0,0 +1,61 @@ +// kilocode_change - new file +import { describe, expect, test } from "bun:test" +import type { AssistantMessage, Message, UserMessage } from "@kilocode/sdk/v2" +import { formatCount, getUsage } from "../../../src/cli/cmd/tui/routes/session/usage" + +function assistant(id: string, input: number, output: number, read: number): AssistantMessage { + return { + id, + sessionID: "ses_1", + role: "assistant", + time: { created: 1 }, + parentID: "msg_parent", + modelID: "claude-sonnet", + providerID: "anthropic", + mode: "code", + agent: "code", + path: { cwd: "/tmp", root: "/tmp" }, + cost: 0, + tokens: { + input, + output, + reasoning: 99, + cache: { + read, + write: 7, + }, + }, + } +} + +function user(): UserMessage { + return { + id: "msg_user", + sessionID: "ses_1", + role: "user", + time: { created: 0 }, + agent: "code", + model: { + providerID: "anthropic", + modelID: "claude-sonnet", + }, + } +} + +describe("session usage", () => { + test("sums input, output, and cache read across assistant messages only", () => { + const msg: Message[] = [user(), assistant("a", 1200, 45, 300), assistant("b", 800, 55, 700)] + + expect(getUsage(msg)).toEqual({ + input: 2000, + output: 100, + cached: 1000, + }) + }) + + test("formats full counts with thousands separators", () => { + expect(formatCount(0)).toBe("0") + expect(formatCount(12345)).toBe("12,345") + expect(formatCount(9876543)).toBe("9,876,543") + }) +})