summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components/session
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-06 09:37:49 -0600
committerGitHub <[email protected]>2026-02-06 09:37:49 -0600
commita4bc883595df9ea0f752079519081bc602408553 (patch)
tree583f21642f431899abe1dfb1f6bd9b2c01dc0206 /packages/app/src/components/session
parentc07077f96c0019b2e18e0e8e1e0383deda08b3e6 (diff)
downloadopencode-a4bc883595df9ea0f752079519081bc602408553.tar.gz
opencode-a4bc883595df9ea0f752079519081bc602408553.zip
chore: refactoring and tests (#12468)
Diffstat (limited to 'packages/app/src/components/session')
-rw-r--r--packages/app/src/components/session/session-context-metrics.test.ts93
-rw-r--r--packages/app/src/components/session/session-context-metrics.ts94
-rw-r--r--packages/app/src/components/session/session-context-tab.tsx47
3 files changed, 194 insertions, 40 deletions
diff --git a/packages/app/src/components/session/session-context-metrics.test.ts b/packages/app/src/components/session/session-context-metrics.test.ts
new file mode 100644
index 000000000..68903a455
--- /dev/null
+++ b/packages/app/src/components/session/session-context-metrics.test.ts
@@ -0,0 +1,93 @@
+import { describe, expect, test } from "bun:test"
+import type { Message } from "@opencode-ai/sdk/v2/client"
+import { getSessionContextMetrics } from "./session-context-metrics"
+
+const assistant = (
+ id: string,
+ tokens: { input: number; output: number; reasoning: number; read: number; write: number },
+ cost: number,
+ providerID = "openai",
+ modelID = "gpt-4.1",
+) => {
+ return {
+ id,
+ role: "assistant",
+ providerID,
+ modelID,
+ cost,
+ tokens: {
+ input: tokens.input,
+ output: tokens.output,
+ reasoning: tokens.reasoning,
+ cache: {
+ read: tokens.read,
+ write: tokens.write,
+ },
+ },
+ time: { created: 1 },
+ } as unknown as Message
+}
+
+const user = (id: string) => {
+ return {
+ id,
+ role: "user",
+ cost: 0,
+ time: { created: 1 },
+ } as unknown as Message
+}
+
+describe("getSessionContextMetrics", () => {
+ test("computes totals and usage from latest assistant with tokens", () => {
+ const messages = [
+ user("u1"),
+ assistant("a1", { input: 0, output: 0, reasoning: 0, read: 0, write: 0 }, 0.5),
+ assistant("a2", { input: 300, output: 100, reasoning: 50, read: 25, write: 25 }, 1.25),
+ ]
+ const providers = [
+ {
+ id: "openai",
+ name: "OpenAI",
+ models: {
+ "gpt-4.1": {
+ name: "GPT-4.1",
+ limit: { context: 1000 },
+ },
+ },
+ },
+ ]
+
+ const metrics = getSessionContextMetrics(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")
+ })
+
+ 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)
+
+ expect(metrics.context?.providerLabel).toBe("p-1")
+ expect(metrics.context?.modelLabel).toBe("m-1")
+ expect(metrics.context?.limit).toBeUndefined()
+ expect(metrics.context?.usage).toBeNull()
+ })
+
+ test("memoizes by message and provider array identity", () => {
+ 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 two = getSessionContextMetrics(messages, providers)
+ const three = getSessionContextMetrics([...messages], providers)
+
+ expect(two).toBe(one)
+ expect(three).not.toBe(one)
+ })
+})
diff --git a/packages/app/src/components/session/session-context-metrics.ts b/packages/app/src/components/session/session-context-metrics.ts
new file mode 100644
index 000000000..2b6edbd95
--- /dev/null
+++ b/packages/app/src/components/session/session-context-metrics.ts
@@ -0,0 +1,94 @@
+import type { AssistantMessage, Message } from "@opencode-ai/sdk/v2/client"
+
+type Provider = {
+ id: string
+ name?: string
+ models: Record<string, Model | undefined>
+}
+
+type Model = {
+ name?: string
+ limit: {
+ context: number
+ }
+}
+
+type Context = {
+ message: AssistantMessage
+ provider?: Provider
+ model?: Model
+ providerLabel: string
+ 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 cache = new WeakMap<Message[], WeakMap<Provider[], Metrics>>()
+
+const tokenTotal = (msg: AssistantMessage) => {
+ return msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write
+}
+
+const lastAssistantWithTokens = (messages: Message[]) => {
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const msg = messages[i]
+ if (msg.role !== "assistant") continue
+ if (tokenTotal(msg) <= 0) continue
+ return msg
+ }
+}
+
+const build = (messages: Message[], providers: Provider[]): Metrics => {
+ const totalCost = messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0)
+ const message = lastAssistantWithTokens(messages)
+ if (!message) return { totalCost, context: undefined }
+
+ const provider = providers.find((item) => item.id === message.providerID)
+ const model = provider?.models[message.modelID]
+ const limit = model?.limit.context
+ 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,
+ },
+ }
+}
+
+export function getSessionContextMetrics(messages: Message[], providers: Provider[]) {
+ const byProvider = cache.get(messages)
+ if (byProvider) {
+ const hit = byProvider.get(providers)
+ if (hit) return hit
+ }
+
+ const value = build(messages, providers)
+ const next = byProvider ?? new WeakMap<Provider[], Metrics>()
+ next.set(providers, value)
+ if (!byProvider) cache.set(messages, next)
+ return value
+}
diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx
index 37733caff..8aae44863 100644
--- a/packages/app/src/components/session/session-context-tab.tsx
+++ b/packages/app/src/components/session/session-context-tab.tsx
@@ -11,8 +11,9 @@ import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { Code } from "@opencode-ai/ui/code"
import { Markdown } from "@opencode-ai/ui/markdown"
-import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
+import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useLanguage } from "@/context/language"
+import { getSessionContextMetrics } from "./session-context-metrics"
interface SessionContextTabProps {
messages: () => Message[]
@@ -34,44 +35,11 @@ export function SessionContextTab(props: SessionContextTabProps) {
}),
)
- const ctx = createMemo(() => {
- const last = findLast(props.messages(), (x) => {
- if (x.role !== "assistant") return false
- const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
- return total > 0
- }) as AssistantMessage
- if (!last) return
-
- const provider = sync.data.provider.all.find((x) => x.id === last.providerID)
- const model = provider?.models[last.modelID]
- const limit = model?.limit.context
-
- const input = last.tokens.input
- const output = last.tokens.output
- const reasoning = last.tokens.reasoning
- const cacheRead = last.tokens.cache.read
- const cacheWrite = last.tokens.cache.write
- const total = input + output + reasoning + cacheRead + cacheWrite
- const usage = limit ? Math.round((total / limit) * 100) : null
-
- return {
- message: last,
- provider,
- model,
- limit,
- input,
- output,
- reasoning,
- cacheRead,
- cacheWrite,
- total,
- usage,
- }
- })
+ const metrics = createMemo(() => getSessionContextMetrics(props.messages(), sync.data.provider.all))
+ const ctx = createMemo(() => metrics().context)
const cost = createMemo(() => {
- const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
- return usd().format(total)
+ return usd().format(metrics().totalCost)
})
const counts = createMemo(() => {
@@ -114,14 +82,13 @@ export function SessionContextTab(props: SessionContextTabProps) {
const providerLabel = createMemo(() => {
const c = ctx()
if (!c) return "—"
- return c.provider?.name ?? c.message.providerID
+ return c.providerLabel
})
const modelLabel = createMemo(() => {
const c = ctx()
if (!c) return "—"
- if (c.model?.name) return c.model.name
- return c.message.modelID
+ return c.modelLabel
})
const breakdown = createMemo(