From 4a0c53df082ba0f1f7e32e0bb3c08c3e50625820 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 10 May 2026 00:04:55 -0400 Subject: [PATCH] Use session usage totals in TUI --- .../cli/cmd/tui/component/prompt/index.tsx | 3 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 39 ++++++++++++++++++- .../tui/feature-plugins/sidebar/context.tsx | 3 +- .../opencode/src/cli/cmd/tui/plugin/api.tsx | 3 ++ .../tui/routes/session/subagent-footer.tsx | 2 +- packages/opencode/test/fixture/tui-plugin.ts | 1 + packages/plugin/src/tui.ts | 2 + 7 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index d3bfdfbac3..d4affa5d9c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -337,6 +337,7 @@ export function Prompt(props: PromptProps) { const usage = createMemo(() => { if (!props.sessionID) return + const session = sync.session.get(props.sessionID) const msg = sync.data.message[props.sessionID] ?? [] const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0) if (!last) return @@ -347,7 +348,7 @@ export function Prompt(props: PromptProps) { const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID] const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined - const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0) + const cost = session?.cost ?? 0 return { context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens), cost: cost > 0 ? money.format(cost) : undefined, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 0d4cb2e6e2..f924b83ae4 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -131,6 +131,34 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) } + function partUsage(part: Part) { + if (part.type !== "step-finish") return + return { cost: part.cost, tokens: part.tokens } + } + + function applySessionUsage(sessionID: string, part: Part, sign = 1) { + const usage = partUsage(part) + if (!usage) return + const result = Binary.search(store.session, sessionID, (s) => s.id) + if (!result.found) return + setStore( + "session", + result.index, + produce((draft) => { + draft.cost = (draft.cost ?? 0) + usage.cost * sign + draft.tokens = { + input: (draft.tokens?.input ?? 0) + usage.tokens.input * sign, + output: (draft.tokens?.output ?? 0) + usage.tokens.output * sign, + reasoning: (draft.tokens?.reasoning ?? 0) + usage.tokens.reasoning * sign, + cache: { + read: (draft.tokens?.cache.read ?? 0) + usage.tokens.cache.read * sign, + write: (draft.tokens?.cache.write ?? 0) + usage.tokens.cache.write * sign, + }, + } + }), + ) + } + event.subscribe((event) => { switch (event.type) { case "server.instance.disposed": @@ -294,6 +322,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const messages = store.message[event.properties.sessionID] const result = Binary.search(messages, event.properties.messageID, (m) => m.id) if (result.found) { + for (const part of store.part[event.properties.messageID] ?? []) { + applySessionUsage(event.properties.sessionID, part, -1) + } setStore( "message", event.properties.sessionID, @@ -307,14 +338,18 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "message.part.updated": { const parts = store.part[event.properties.part.messageID] if (!parts) { + applySessionUsage(event.properties.part.sessionID, event.properties.part) setStore("part", event.properties.part.messageID, [event.properties.part]) break } const result = Binary.search(parts, event.properties.part.id, (p) => p.id) if (result.found) { + applySessionUsage(event.properties.part.sessionID, parts[result.index], -1) + applySessionUsage(event.properties.part.sessionID, event.properties.part) setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part)) break } + applySessionUsage(event.properties.part.sessionID, event.properties.part) setStore( "part", event.properties.part.messageID, @@ -346,7 +381,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "message.part.removed": { const parts = store.part[event.properties.messageID] const result = Binary.search(parts, event.properties.partID, (p) => p.id) - if (result.found) + if (result.found) { + applySessionUsage(event.properties.sessionID, parts[result.index], -1) setStore( "part", event.properties.messageID, @@ -354,6 +390,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ draft.splice(result.index, 1) }), ) + } break } diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx index b3cf2beb44..405e8c1458 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx @@ -13,7 +13,8 @@ const money = new Intl.NumberFormat("en-US", { function View(props: { api: TuiPluginApi; session_id: string }) { const theme = () => props.api.theme.current const msg = createMemo(() => props.api.state.session.messages(props.session_id)) - const cost = createMemo(() => msg().reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0)) + const session = createMemo(() => props.api.state.session.get(props.session_id)) + const cost = createMemo(() => session()?.cost ?? 0) const state = createMemo(() => { const last = msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0) diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index 54059f4a2d..8958a92853 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -147,6 +147,9 @@ function stateApi(sync: ReturnType): TuiPluginApi["state"] { count() { return sync.data.session.length }, + get(sessionID) { + return sync.session.get(sessionID) + }, diff(sessionID) { return (sync.data.session_diff[sessionID] ?? []).flatMap((item) => item.file === undefined ? [] : [{ ...item, file: item.file }], diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx index 2a6813ffbe..f4a458b63d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx @@ -42,7 +42,7 @@ export function SubagentFooter() { const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID] const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined - const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0) + const cost = session()?.cost ?? 0 const money = new Intl.NumberFormat("en-US", { style: "currency", diff --git a/packages/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index 62a3ae6e6b..3d894bd0ae 100644 --- a/packages/opencode/test/fixture/tui-plugin.ts +++ b/packages/opencode/test/fixture/tui-plugin.ts @@ -292,6 +292,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { }, session: { count: opts.state?.session?.count ?? (() => 0), + get: opts.state?.session?.get ?? (() => undefined), diff: opts.state?.session?.diff ?? (() => []), todo: opts.state?.session?.todo ?? (() => []), messages: opts.state?.session?.messages ?? (() => []), diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 851b0476e5..d4c2261b28 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -11,6 +11,7 @@ import type { Provider, PermissionRequest, QuestionRequest, + Session, SessionStatus, TextPart, Config as SdkConfig, @@ -310,6 +311,7 @@ export type TuiState = { readonly vcs: { branch?: string } | undefined session: { count: () => number + get: (sessionID: string) => Session | undefined diff: (sessionID: string) => ReadonlyArray todo: (sessionID: string) => ReadonlyArray messages: (sessionID: string) => ReadonlyArray