Просмотр исходного кода

feat(desktop): Display stored totals for Tokens and Cost in Desktop Session Context (#28887)

OpeOginni 2 месяцев назад
Родитель
Сommit
683aca5dbe

+ 17 - 14
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 = () => (
     <div>
+      <Show when={tokens()}>
+        {(value) => (
+          <div class="flex items-center gap-2">
+            <span class="text-text-invert-strong">{getSessionTokenTotal(value())?.toLocaleString(language.intl())}</span>
+            <span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
+          </div>
+        )}
+      </Show>
       <Show when={context()}>
         {(ctx) => (
-          <>
-            <div class="flex items-center gap-2">
-              <span class="text-text-invert-strong">{ctx().total.toLocaleString(language.intl())}</span>
-              <span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
-            </div>
-            <div class="flex items-center gap-2">
-              <span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
-              <span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
-            </div>
-          </>
+          <div class="flex items-center gap-2">
+            <span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
+            <span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
+          </div>
         )}
       </Show>
       <div class="flex items-center gap-2">

+ 31 - 24
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)
   })
 })

+ 17 - 31
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
+}

+ 9 - 9
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()) },