diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx
index 95a76d6e848b..3b26a0b3b556 100644
--- a/packages/app/src/components/session-context-usage.tsx
+++ b/packages/app/src/components/session-context-usage.tsx
@@ -9,7 +9,7 @@ import { useSync } from "@/context/sync"
import { useLanguage } from "@/context/language"
import { useProviders } from "@/hooks/use-providers"
import { useSDK } from "@/context/sdk"
-import { getSessionContextMetrics } from "@/components/session/session-context-metrics"
+import { getSessionContext, getSessionTokenTotal } from "@/components/session/session-context-metrics"
import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionTabs } from "@/pages/session/helpers"
@@ -45,6 +45,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
})
const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
+ const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
const usd = createMemo(
() =>
@@ -54,10 +55,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
}),
)
- const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
- const context = createMemo(() => metrics().context)
+ const context = createMemo(() => getSessionContext(messages(), [...providers.all().values()]))
+ const tokens = createMemo(() => info()?.tokens)
const cost = createMemo(() => {
- return usd().format(metrics().totalCost)
+ return usd().format(info()?.cost ?? 0)
})
const openContext = () => {
@@ -82,18 +83,20 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const tooltipValue = () => (
+
+ {(value) => (
+
+ {getSessionTokenTotal(value())?.toLocaleString(language.intl())}
+ {language.t("context.usage.tokens")}
+
+ )}
+
{(ctx) => (
- <>
-
- {ctx().total.toLocaleString(language.intl())}
- {language.t("context.usage.tokens")}
-
-
- {ctx().usage ?? 0}%
- {language.t("context.usage.usage")}
-
- >
+
+ {ctx().usage ?? 0}%
+ {language.t("context.usage.usage")}
+
)}
diff --git a/packages/app/src/components/session/session-context-metrics.test.ts b/packages/app/src/components/session/session-context-metrics.test.ts
index 0e109a71bdac..5b11bf143393 100644
--- a/packages/app/src/components/session/session-context-metrics.test.ts
+++ b/packages/app/src/components/session/session-context-metrics.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { Message } from "@opencode-ai/sdk/v2/client"
-import { getSessionContextMetrics } from "./session-context-metrics"
+import { getSessionContext, getSessionTokenTotal } from "./session-context-metrics"
const assistant = (
id: string,
@@ -37,8 +37,8 @@ const user = (id: string) => {
} as unknown as Message
}
-describe("getSessionContextMetrics", () => {
- test("computes totals and usage from latest assistant with tokens", () => {
+describe("getSessionContext", () => {
+ test("computes usage from latest assistant with tokens", () => {
const messages = [
user("u1"),
assistant("a1", { input: 0, output: 0, reasoning: 0, read: 0, write: 0 }, 0.5),
@@ -57,45 +57,52 @@ describe("getSessionContextMetrics", () => {
},
]
- const metrics = getSessionContextMetrics(messages, providers)
+ const ctx = getSessionContext(messages, providers)
- expect(metrics.totalCost).toBe(1.75)
- expect(metrics.context?.message.id).toBe("a2")
- expect(metrics.context?.total).toBe(500)
- expect(metrics.context?.usage).toBe(50)
- expect(metrics.context?.providerLabel).toBe("OpenAI")
- expect(metrics.context?.modelLabel).toBe("GPT-4.1")
+ expect(ctx?.message.id).toBe("a2")
+ expect(ctx?.usage).toBe(50)
+ expect(ctx?.providerLabel).toBe("OpenAI")
+ expect(ctx?.modelLabel).toBe("GPT-4.1")
})
test("preserves fallback labels and null usage when model metadata is missing", () => {
const messages = [assistant("a1", { input: 40, output: 10, reasoning: 0, read: 0, write: 0 }, 0.1, "p-1", "m-1")]
const providers = [{ id: "p-1", models: {} }]
- const metrics = getSessionContextMetrics(messages, providers)
+ const ctx = getSessionContext(messages, providers)
- expect(metrics.context?.providerLabel).toBe("p-1")
- expect(metrics.context?.modelLabel).toBe("m-1")
- expect(metrics.context?.limit).toBeUndefined()
- expect(metrics.context?.usage).toBeNull()
+ expect(ctx?.providerLabel).toBe("p-1")
+ expect(ctx?.modelLabel).toBe("m-1")
+ expect(ctx?.limit).toBeUndefined()
+ expect(ctx?.usage).toBeNull()
})
test("recomputes when message array is mutated in place", () => {
const messages = [assistant("a1", { input: 10, output: 10, reasoning: 10, read: 10, write: 10 }, 0.25)]
const providers = [{ id: "openai", models: {} }]
- const one = getSessionContextMetrics(messages, providers)
+ const one = getSessionContext(messages, providers)
messages.push(assistant("a2", { input: 100, output: 20, reasoning: 0, read: 0, write: 0 }, 0.75))
- const two = getSessionContextMetrics(messages, providers)
+ const two = getSessionContext(messages, providers)
- expect(one.context?.message.id).toBe("a1")
- expect(two.context?.message.id).toBe("a2")
- expect(two.totalCost).toBe(1)
+ expect(one?.message.id).toBe("a1")
+ expect(two?.message.id).toBe("a2")
})
- test("returns empty metrics when inputs are undefined", () => {
- const metrics = getSessionContextMetrics(undefined, undefined)
+ test("returns undefined when inputs are undefined", () => {
+ const ctx = getSessionContext(undefined, undefined)
- expect(metrics.totalCost).toBe(0)
- expect(metrics.context).toBeUndefined()
+ expect(ctx).toBeUndefined()
+ })
+
+ test("computes stored session token totals", () => {
+ expect(
+ getSessionTokenTotal({
+ input: 10,
+ output: 20,
+ reasoning: 30,
+ cache: { read: 40, write: 50 },
+ }),
+ ).toBe(150)
})
})
diff --git a/packages/app/src/components/session/session-context-metrics.ts b/packages/app/src/components/session/session-context-metrics.ts
index 0789b05f1730..84cc984abfab 100644
--- a/packages/app/src/components/session/session-context-metrics.ts
+++ b/packages/app/src/components/session/session-context-metrics.ts
@@ -1,4 +1,4 @@
-import type { AssistantMessage, Message } from "@opencode-ai/sdk/v2/client"
+import type { AssistantMessage, Message, Session } from "@opencode-ai/sdk/v2/client"
type Provider = {
id: string
@@ -21,19 +21,9 @@ type Context = {
modelLabel: string
limit: number | undefined
input: number
- output: number
- reasoning: number
- cacheRead: number
- cacheWrite: number
- total: number
usage: number | null
}
-type Metrics = {
- totalCost: number
- context: Context | undefined
-}
-
const tokenTotal = (msg: AssistantMessage) => {
return msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write
}
@@ -47,10 +37,9 @@ const lastAssistantWithTokens = (messages: Message[]) => {
}
}
-const build = (messages: Message[] = [], providers: Provider[] = []): Metrics => {
- const totalCost = messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0)
+const build = (messages: Message[] = [], providers: Provider[] = []): Context | undefined => {
const message = lastAssistantWithTokens(messages)
- if (!message) return { totalCost, context: undefined }
+ if (!message) return undefined
const provider = providers.find((item) => item.id === message.providerID)
const model = provider?.models[message.modelID]
@@ -58,25 +47,22 @@ const build = (messages: Message[] = [], providers: Provider[] = []): Metrics =>
const total = tokenTotal(message)
return {
- totalCost,
- context: {
- message,
- provider,
- model,
- providerLabel: provider?.name ?? message.providerID,
- modelLabel: model?.name ?? message.modelID,
- limit,
- input: message.tokens.input,
- output: message.tokens.output,
- reasoning: message.tokens.reasoning,
- cacheRead: message.tokens.cache.read,
- cacheWrite: message.tokens.cache.write,
- total,
- usage: limit ? Math.round((total / limit) * 100) : null,
- },
+ message,
+ provider,
+ model,
+ providerLabel: provider?.name ?? message.providerID,
+ modelLabel: model?.name ?? message.modelID,
+ limit,
+ input: message.tokens.input,
+ usage: limit ? Math.round((total / limit) * 100) : null,
}
}
-export function getSessionContextMetrics(messages: Message[] = [], providers: Provider[] = []) {
+export function getSessionContext(messages: Message[] = [], providers: Provider[] = []) {
return build(messages, providers)
}
+
+export function getSessionTokenTotal(tokens: Session["tokens"] | undefined) {
+ if (!tokens) return undefined
+ return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
+}
diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx
index e5fa48a914a2..8757c693678b 100644
--- a/packages/app/src/components/session/session-context-tab.tsx
+++ b/packages/app/src/components/session/session-context-tab.tsx
@@ -15,7 +15,7 @@ import { useLanguage } from "@/context/language"
import { useProviders } from "@/hooks/use-providers"
import { useSDK } from "@/context/sdk"
import { useSessionLayout } from "@/pages/session/session-layout"
-import { getSessionContextMetrics } from "./session-context-metrics"
+import { getSessionContext, getSessionTokenTotal } from "./session-context-metrics"
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
import { createSessionContextFormatter } from "./session-context-format"
@@ -134,12 +134,12 @@ export function SessionContextTab() {
}),
)
- const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
- const ctx = createMemo(() => metrics().context)
+ const ctx = createMemo(() => getSessionContext(messages(), [...providers.all().values()]))
+ const tokens = createMemo(() => info()?.tokens)
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
const cost = createMemo(() => {
- return usd().format(metrics().totalCost)
+ return usd().format(info()?.cost ?? 0)
})
const counts = createMemo(() => {
@@ -204,14 +204,14 @@ export function SessionContextTab() {
{ label: "context.stats.provider", value: providerLabel },
{ label: "context.stats.model", value: modelLabel },
{ label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) },
- { label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) },
+ { label: "context.stats.totalTokens", value: () => formatter().number(getSessionTokenTotal(tokens())) },
{ label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) },
- { label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) },
- { label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.output) },
- { label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.reasoning) },
+ { label: "context.stats.inputTokens", value: () => formatter().number(tokens()?.input) },
+ { label: "context.stats.outputTokens", value: () => formatter().number(tokens()?.output) },
+ { label: "context.stats.reasoningTokens", value: () => formatter().number(tokens()?.reasoning) },
{
label: "context.stats.cacheTokens",
- value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`,
+ value: () => `${formatter().number(tokens()?.cache.read)} / ${formatter().number(tokens()?.cache.write)}`,
},
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) },
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) },