Skip to content

Commit 683aca5

Browse files
authored
feat(desktop): Display stored totals for Tokens and Cost in Desktop Session Context (anomalyco#28887)
1 parent 92025e9 commit 683aca5

4 files changed

Lines changed: 74 additions & 78 deletions

File tree

packages/app/src/components/session-context-usage.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useSync } from "@/context/sync"
99
import { useLanguage } from "@/context/language"
1010
import { useProviders } from "@/hooks/use-providers"
1111
import { useSDK } from "@/context/sdk"
12-
import { getSessionContextMetrics } from "@/components/session/session-context-metrics"
12+
import { getSessionContext, getSessionTokenTotal } from "@/components/session/session-context-metrics"
1313
import { useSessionLayout } from "@/pages/session/session-layout"
1414
import { createSessionTabs } from "@/pages/session/helpers"
1515

@@ -45,6 +45,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
4545
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
4646
})
4747
const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
48+
const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
4849

4950
const usd = createMemo(
5051
() =>
@@ -54,10 +55,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
5455
}),
5556
)
5657

57-
const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
58-
const context = createMemo(() => metrics().context)
58+
const context = createMemo(() => getSessionContext(messages(), [...providers.all().values()]))
59+
const tokens = createMemo(() => info()?.tokens)
5960
const cost = createMemo(() => {
60-
return usd().format(metrics().totalCost)
61+
return usd().format(info()?.cost ?? 0)
6162
})
6263

6364
const openContext = () => {
@@ -82,18 +83,20 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
8283

8384
const tooltipValue = () => (
8485
<div>
86+
<Show when={tokens()}>
87+
{(value) => (
88+
<div class="flex items-center gap-2">
89+
<span class="text-text-invert-strong">{getSessionTokenTotal(value())?.toLocaleString(language.intl())}</span>
90+
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
91+
</div>
92+
)}
93+
</Show>
8594
<Show when={context()}>
8695
{(ctx) => (
87-
<>
88-
<div class="flex items-center gap-2">
89-
<span class="text-text-invert-strong">{ctx().total.toLocaleString(language.intl())}</span>
90-
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
91-
</div>
92-
<div class="flex items-center gap-2">
93-
<span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
94-
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
95-
</div>
96-
</>
96+
<div class="flex items-center gap-2">
97+
<span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
98+
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
99+
</div>
97100
)}
98101
</Show>
99102
<div class="flex items-center gap-2">

packages/app/src/components/session/session-context-metrics.test.ts

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test"
22
import type { Message } from "@opencode-ai/sdk/v2/client"
3-
import { getSessionContextMetrics } from "./session-context-metrics"
3+
import { getSessionContext, getSessionTokenTotal } from "./session-context-metrics"
44

55
const assistant = (
66
id: string,
@@ -37,8 +37,8 @@ const user = (id: string) => {
3737
} as unknown as Message
3838
}
3939

40-
describe("getSessionContextMetrics", () => {
41-
test("computes totals and usage from latest assistant with tokens", () => {
40+
describe("getSessionContext", () => {
41+
test("computes usage from latest assistant with tokens", () => {
4242
const messages = [
4343
user("u1"),
4444
assistant("a1", { input: 0, output: 0, reasoning: 0, read: 0, write: 0 }, 0.5),
@@ -57,45 +57,52 @@ describe("getSessionContextMetrics", () => {
5757
},
5858
]
5959

60-
const metrics = getSessionContextMetrics(messages, providers)
60+
const ctx = getSessionContext(messages, providers)
6161

62-
expect(metrics.totalCost).toBe(1.75)
63-
expect(metrics.context?.message.id).toBe("a2")
64-
expect(metrics.context?.total).toBe(500)
65-
expect(metrics.context?.usage).toBe(50)
66-
expect(metrics.context?.providerLabel).toBe("OpenAI")
67-
expect(metrics.context?.modelLabel).toBe("GPT-4.1")
62+
expect(ctx?.message.id).toBe("a2")
63+
expect(ctx?.usage).toBe(50)
64+
expect(ctx?.providerLabel).toBe("OpenAI")
65+
expect(ctx?.modelLabel).toBe("GPT-4.1")
6866
})
6967

7068
test("preserves fallback labels and null usage when model metadata is missing", () => {
7169
const messages = [assistant("a1", { input: 40, output: 10, reasoning: 0, read: 0, write: 0 }, 0.1, "p-1", "m-1")]
7270
const providers = [{ id: "p-1", models: {} }]
7371

74-
const metrics = getSessionContextMetrics(messages, providers)
72+
const ctx = getSessionContext(messages, providers)
7573

76-
expect(metrics.context?.providerLabel).toBe("p-1")
77-
expect(metrics.context?.modelLabel).toBe("m-1")
78-
expect(metrics.context?.limit).toBeUndefined()
79-
expect(metrics.context?.usage).toBeNull()
74+
expect(ctx?.providerLabel).toBe("p-1")
75+
expect(ctx?.modelLabel).toBe("m-1")
76+
expect(ctx?.limit).toBeUndefined()
77+
expect(ctx?.usage).toBeNull()
8078
})
8179

8280
test("recomputes when message array is mutated in place", () => {
8381
const messages = [assistant("a1", { input: 10, output: 10, reasoning: 10, read: 10, write: 10 }, 0.25)]
8482
const providers = [{ id: "openai", models: {} }]
8583

86-
const one = getSessionContextMetrics(messages, providers)
84+
const one = getSessionContext(messages, providers)
8785
messages.push(assistant("a2", { input: 100, output: 20, reasoning: 0, read: 0, write: 0 }, 0.75))
88-
const two = getSessionContextMetrics(messages, providers)
86+
const two = getSessionContext(messages, providers)
8987

90-
expect(one.context?.message.id).toBe("a1")
91-
expect(two.context?.message.id).toBe("a2")
92-
expect(two.totalCost).toBe(1)
88+
expect(one?.message.id).toBe("a1")
89+
expect(two?.message.id).toBe("a2")
9390
})
9491

95-
test("returns empty metrics when inputs are undefined", () => {
96-
const metrics = getSessionContextMetrics(undefined, undefined)
92+
test("returns undefined when inputs are undefined", () => {
93+
const ctx = getSessionContext(undefined, undefined)
9794

98-
expect(metrics.totalCost).toBe(0)
99-
expect(metrics.context).toBeUndefined()
95+
expect(ctx).toBeUndefined()
96+
})
97+
98+
test("computes stored session token totals", () => {
99+
expect(
100+
getSessionTokenTotal({
101+
input: 10,
102+
output: 20,
103+
reasoning: 30,
104+
cache: { read: 40, write: 50 },
105+
}),
106+
).toBe(150)
100107
})
101108
})
Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AssistantMessage, Message } from "@opencode-ai/sdk/v2/client"
1+
import type { AssistantMessage, Message, Session } from "@opencode-ai/sdk/v2/client"
22

33
type Provider = {
44
id: string
@@ -21,19 +21,9 @@ type Context = {
2121
modelLabel: string
2222
limit: number | undefined
2323
input: number
24-
output: number
25-
reasoning: number
26-
cacheRead: number
27-
cacheWrite: number
28-
total: number
2924
usage: number | null
3025
}
3126

32-
type Metrics = {
33-
totalCost: number
34-
context: Context | undefined
35-
}
36-
3727
const tokenTotal = (msg: AssistantMessage) => {
3828
return msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write
3929
}
@@ -47,36 +37,32 @@ const lastAssistantWithTokens = (messages: Message[]) => {
4737
}
4838
}
4939

50-
const build = (messages: Message[] = [], providers: Provider[] = []): Metrics => {
51-
const totalCost = messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0)
40+
const build = (messages: Message[] = [], providers: Provider[] = []): Context | undefined => {
5241
const message = lastAssistantWithTokens(messages)
53-
if (!message) return { totalCost, context: undefined }
42+
if (!message) return undefined
5443

5544
const provider = providers.find((item) => item.id === message.providerID)
5645
const model = provider?.models[message.modelID]
5746
const limit = model?.limit.context
5847
const total = tokenTotal(message)
5948

6049
return {
61-
totalCost,
62-
context: {
63-
message,
64-
provider,
65-
model,
66-
providerLabel: provider?.name ?? message.providerID,
67-
modelLabel: model?.name ?? message.modelID,
68-
limit,
69-
input: message.tokens.input,
70-
output: message.tokens.output,
71-
reasoning: message.tokens.reasoning,
72-
cacheRead: message.tokens.cache.read,
73-
cacheWrite: message.tokens.cache.write,
74-
total,
75-
usage: limit ? Math.round((total / limit) * 100) : null,
76-
},
50+
message,
51+
provider,
52+
model,
53+
providerLabel: provider?.name ?? message.providerID,
54+
modelLabel: model?.name ?? message.modelID,
55+
limit,
56+
input: message.tokens.input,
57+
usage: limit ? Math.round((total / limit) * 100) : null,
7758
}
7859
}
7960

80-
export function getSessionContextMetrics(messages: Message[] = [], providers: Provider[] = []) {
61+
export function getSessionContext(messages: Message[] = [], providers: Provider[] = []) {
8162
return build(messages, providers)
8263
}
64+
65+
export function getSessionTokenTotal(tokens: Session["tokens"] | undefined) {
66+
if (!tokens) return undefined
67+
return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
68+
}

packages/app/src/components/session/session-context-tab.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useLanguage } from "@/context/language"
1515
import { useProviders } from "@/hooks/use-providers"
1616
import { useSDK } from "@/context/sdk"
1717
import { useSessionLayout } from "@/pages/session/session-layout"
18-
import { getSessionContextMetrics } from "./session-context-metrics"
18+
import { getSessionContext, getSessionTokenTotal } from "./session-context-metrics"
1919
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
2020
import { createSessionContextFormatter } from "./session-context-format"
2121

@@ -134,12 +134,12 @@ export function SessionContextTab() {
134134
}),
135135
)
136136

137-
const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
138-
const ctx = createMemo(() => metrics().context)
137+
const ctx = createMemo(() => getSessionContext(messages(), [...providers.all().values()]))
138+
const tokens = createMemo(() => info()?.tokens)
139139
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
140140

141141
const cost = createMemo(() => {
142-
return usd().format(metrics().totalCost)
142+
return usd().format(info()?.cost ?? 0)
143143
})
144144

145145
const counts = createMemo(() => {
@@ -204,14 +204,14 @@ export function SessionContextTab() {
204204
{ label: "context.stats.provider", value: providerLabel },
205205
{ label: "context.stats.model", value: modelLabel },
206206
{ label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) },
207-
{ label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) },
207+
{ label: "context.stats.totalTokens", value: () => formatter().number(getSessionTokenTotal(tokens())) },
208208
{ label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) },
209-
{ label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) },
210-
{ label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.output) },
211-
{ label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.reasoning) },
209+
{ label: "context.stats.inputTokens", value: () => formatter().number(tokens()?.input) },
210+
{ label: "context.stats.outputTokens", value: () => formatter().number(tokens()?.output) },
211+
{ label: "context.stats.reasoningTokens", value: () => formatter().number(tokens()?.reasoning) },
212212
{
213213
label: "context.stats.cacheTokens",
214-
value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`,
214+
value: () => `${formatter().number(tokens()?.cache.read)} / ${formatter().number(tokens()?.cache.write)}`,
215215
},
216216
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) },
217217
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) },

0 commit comments

Comments
 (0)