From 70edb69654cf18f5cd89de63f797e3429c12998d Mon Sep 17 00:00:00 2001 From: JudicaelPoumay Date: Sat, 14 Feb 2026 13:26:49 +0100 Subject: [PATCH 01/16] feat(util,app): add sub-agent cost breakdown with recursive session walker Adds a costs() utility that recursively walks sub-agent sessions to compute own vs total cost. Updates UI/TUI to display the breakdown. Fixes #11027 --- .../src/components/session-context-usage.tsx | 7 +- .../session/session-context-metrics.test.ts | 45 ++++++- .../session/session-context-metrics.ts | 31 ++++- .../session/session-context-tab.tsx | 6 +- packages/app/src/i18n/ar.ts | 3 + packages/app/src/i18n/br.ts | 3 + packages/app/src/i18n/bs.ts | 3 + packages/app/src/i18n/da.ts | 6 + packages/app/src/i18n/de.ts | 3 + packages/app/src/i18n/en.ts | 3 + packages/app/src/i18n/es.ts | 3 + packages/app/src/i18n/fr.ts | 3 + packages/app/src/i18n/ja.ts | 3 + packages/app/src/i18n/ko.ts | 3 + packages/app/src/i18n/no.ts | 3 + packages/app/src/i18n/pl.ts | 3 + packages/app/src/i18n/ru.ts | 6 + packages/app/src/i18n/th.ts | 3 + packages/app/src/i18n/zh.ts | 3 + .../src/cli/cmd/tui/routes/session/header.tsx | 16 +-- .../src/cli/cmd/tui/routes/session/index.tsx | 15 +++ .../cli/cmd/tui/routes/session/sidebar.tsx | 8 +- packages/ui/src/components/message-part.tsx | 49 ++++++++ packages/ui/src/i18n/en.ts | 2 + packages/ui/src/i18n/no.ts | 2 + packages/util/src/cost.test.ts | 111 ++++++++++++++++++ packages/util/src/cost.ts | 52 ++++++++ 27 files changed, 375 insertions(+), 20 deletions(-) create mode 100644 packages/util/src/cost.test.ts create mode 100644 packages/util/src/cost.ts diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index c6e60d3edebd..ec3a5ba44b9b 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -44,10 +44,13 @@ export function SessionContextUsage(props: SessionContextUsageProps) { }), ) - const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all)) + const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all, sync.data)) const context = createMemo(() => metrics().context) const cost = createMemo(() => { - return usd().format(metrics().totalCost) + const agent = usd().format(metrics().ownCost) + const total = usd().format(metrics().totalCost) + const value = `(current agent ${agent}; total ${total})` + return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value }) const openContext = () => { 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..b980700795dc 100644 --- a/packages/app/src/components/session/session-context-metrics.test.ts +++ b/packages/app/src/components/session/session-context-metrics.test.ts @@ -8,9 +8,11 @@ const assistant = ( cost: number, providerID = "openai", modelID = "gpt-4.1", + sessionID = "s1", ) => { return { id, + sessionID, role: "assistant", providerID, modelID, @@ -28,9 +30,10 @@ const assistant = ( } as unknown as Message } -const user = (id: string) => { +const user = (id: string, sessionID = "s1") => { return { id, + sessionID, role: "user", cost: 0, time: { created: 1 }, @@ -98,4 +101,44 @@ describe("getSessionContextMetrics", () => { expect(metrics.totalCost).toBe(0) expect(metrics.context).toBeUndefined() }) + + test("computes costs with store including sub-agents", () => { + const messages = [ + assistant("a1", { input: 10, output: 10, reasoning: 0, read: 0, write: 0 }, 0.5, "openai", "gpt-4.1", "s1"), + ] + const store = { + message: { + s1: [{ id: "a1", role: "assistant", cost: 0.5 }], + s2: [{ id: "a2", role: "assistant", cost: 1.0 }], + }, + part: { + a1: [{ type: "tool", tool: "task", state: { metadata: { sessionId: "s2" } } }], + }, + } + + const metrics = getSessionContextMetrics(messages, [], store) + + expect(metrics.totalCost).toBe(1.5) + expect(metrics.ownCost).toBe(0.5) + expect(metrics.missing).toEqual([]) + }) + + test("identifies missing child sessions in store", () => { + const messages = [ + assistant("a1", { input: 10, output: 10, reasoning: 0, read: 0, write: 0 }, 0.5, "openai", "gpt-4.1", "s1"), + ] + const store = { + message: { + s1: [{ id: "a1", role: "assistant", cost: 0.5 }], + }, + part: { + a1: [{ type: "tool", tool: "task", state: { metadata: { sessionId: "s2" } } }], + }, + } + + const metrics = getSessionContextMetrics(messages, [], store) + + expect(metrics.totalCost).toBe(0.5) + expect(metrics.missing).toEqual(["s2"]) + }) }) diff --git a/packages/app/src/components/session/session-context-metrics.ts b/packages/app/src/components/session/session-context-metrics.ts index 0789b05f1730..ae78e00e60e8 100644 --- a/packages/app/src/components/session/session-context-metrics.ts +++ b/packages/app/src/components/session/session-context-metrics.ts @@ -1,3 +1,4 @@ +import { costs } from "@opencode-ai/util/cost" import type { AssistantMessage, Message } from "@opencode-ai/sdk/v2/client" type Provider = { @@ -31,6 +32,8 @@ type Context = { type Metrics = { totalCost: number + ownCost: number + missing: string[] context: Context | undefined } @@ -47,10 +50,22 @@ 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[] = [], + store?: { message: Record; part: Record }, +): Metrics => { + const id = messages[0]?.sessionID + const result = store && id ? costs(id, store as any) : undefined + + const totalCost = result + ? result.total + : messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0) + const ownCost = result ? result.own : totalCost + const missing = result ? result.missing : [] + const message = lastAssistantWithTokens(messages) - if (!message) return { totalCost, context: undefined } + if (!message) return { totalCost, ownCost, missing, context: undefined } const provider = providers.find((item) => item.id === message.providerID) const model = provider?.models[message.modelID] @@ -59,6 +74,8 @@ const build = (messages: Message[] = [], providers: Provider[] = []): Metrics => return { totalCost, + ownCost, + missing, context: { message, provider, @@ -77,6 +94,10 @@ const build = (messages: Message[] = [], providers: Provider[] = []): Metrics => } } -export function getSessionContextMetrics(messages: Message[] = [], providers: Provider[] = []) { - return build(messages, providers) +export function getSessionContextMetrics( + messages: Message[] = [], + providers: Provider[] = [], + store?: { message: Record; part: Record }, +) { + return build(messages, providers, store) } diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 81220b3adb2f..d76ac6af0e96 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -104,12 +104,13 @@ export function SessionContextTab(props: SessionContextTabProps) { }), ) - const metrics = createMemo(() => getSessionContextMetrics(props.messages(), sync.data.provider.all)) + const metrics = createMemo(() => getSessionContextMetrics(props.messages(), sync.data.provider.all, sync.data)) const ctx = createMemo(() => metrics().context) const formatter = createMemo(() => createSessionContextFormatter(language.locale())) const cost = createMemo(() => { - return usd().format(metrics().totalCost) + const value = usd().format(metrics().totalCost) + return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value }) const counts = createMemo(() => { @@ -185,6 +186,7 @@ export function SessionContextTab(props: SessionContextTabProps) { }, { label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) }, { label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) }, + { label: "context.stats.ownCost", value: () => formatter().currency(metrics().ownCost) }, { label: "context.stats.totalCost", value: cost }, { label: "context.stats.sessionCreated", value: () => formatter().time(props.info()?.time.created) }, { label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) }, diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index e3792a3c3cc3..5d8ead4b2dff 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -328,7 +328,10 @@ export const dict = { "context.stats.cacheTokens": "رموز التخزين المؤقت (قراءة/كتابة)", "context.stats.userMessages": "رسائل المستخدم", "context.stats.assistantMessages": "رسائل المساعد", + "context.stats.ownCost": "تكلفة الجلسة", "context.stats.totalCost": "التكلفة الإجمالية", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (تم التحميل)", "context.stats.sessionCreated": "تم إنشاء الجلسة", "context.stats.lastActivity": "آخر نشاط", "context.usage.tokens": "رموز", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 07d6ce467aeb..d67f16a248a5 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -329,7 +329,10 @@ export const dict = { "context.stats.cacheTokens": "Tokens de Cache (leitura/escrita)", "context.stats.userMessages": "Mensagens de Usuário", "context.stats.assistantMessages": "Mensagens do Assistente", + "context.stats.ownCost": "Custo da sessão", "context.stats.totalCost": "Custo Total", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (carregado)", "context.stats.sessionCreated": "Sessão Criada", "context.stats.lastActivity": "Última Atividade", "context.usage.tokens": "Tokens", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 7d10da6ed875..6db3ea5e81c3 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -361,7 +361,10 @@ export const dict = { "context.stats.cacheTokens": "Cache tokeni (čitanje/pisanje)", "context.stats.userMessages": "Korisničke poruke", "context.stats.assistantMessages": "Poruke asistenta", + "context.stats.ownCost": "Trošak sesije", "context.stats.totalCost": "Ukupni trošak", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (učitano)", "context.stats.sessionCreated": "Sesija kreirana", "context.stats.lastActivity": "Posljednja aktivnost", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index ac5c4d494b12..da6cc1e727d1 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -359,7 +359,13 @@ export const dict = { "context.stats.cacheTokens": "Cache Tokens (læs/skriv)", "context.stats.userMessages": "Brugerbeskeder", "context.stats.assistantMessages": "Assistentbeskeder", + "context.stats.ownCost": "Sessionsomkostninger", + "context.stats.ownCost": "Sessionsomkostninger", "context.stats.totalCost": "Samlede omkostninger", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (indlæst)", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (indlæst)", "context.stats.sessionCreated": "Session oprettet", "context.stats.lastActivity": "Seneste aktivitet", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 99a950631074..c40f12058706 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -336,7 +336,10 @@ export const dict = { "context.stats.cacheTokens": "Cache-Token (lesen/schreiben)", "context.stats.userMessages": "Benutzernachrichten", "context.stats.assistantMessages": "Assistentennachrichten", + "context.stats.ownCost": "Sitzungskosten", "context.stats.totalCost": "Gesamtkosten", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (geladen)", "context.stats.sessionCreated": "Sitzung erstellt", "context.stats.lastActivity": "Letzte Aktivität", "context.usage.tokens": "Token", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 99513edaa173..d1bcba6f09e9 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -360,7 +360,10 @@ export const dict = { "context.stats.cacheTokens": "Cache Tokens (read/write)", "context.stats.userMessages": "User Messages", "context.stats.assistantMessages": "Assistant Messages", + "context.stats.ownCost": "Session Cost", "context.stats.totalCost": "Total Cost", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (loaded)", "context.stats.sessionCreated": "Session Created", "context.stats.lastActivity": "Last Activity", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 7a6c4974e091..b48fabc95ba8 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -361,7 +361,10 @@ export const dict = { "context.stats.cacheTokens": "Tokens de Caché (lectura/escritura)", "context.stats.userMessages": "Mensajes de Usuario", "context.stats.assistantMessages": "Mensajes de Asistente", + "context.stats.ownCost": "Costo de la sesión", "context.stats.totalCost": "Costo Total", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (cargado)", "context.stats.sessionCreated": "Sesión Creada", "context.stats.lastActivity": "Última Actividad", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index fc3bf2667943..2649e96214ad 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -330,7 +330,10 @@ export const dict = { "context.stats.cacheTokens": "Jetons de cache (lecture/écriture)", "context.stats.userMessages": "Messages utilisateur", "context.stats.assistantMessages": "Messages assistant", + "context.stats.ownCost": "Coût de la session", "context.stats.totalCost": "Coût total", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (chargé)", "context.stats.sessionCreated": "Session créée", "context.stats.lastActivity": "Dernière activité", "context.usage.tokens": "Jetons", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index b597db02a586..23ac62d6401c 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -328,7 +328,10 @@ export const dict = { "context.stats.cacheTokens": "キャッシュトークン (読込/書込)", "context.stats.userMessages": "ユーザーメッセージ", "context.stats.assistantMessages": "アシスタントメッセージ", + "context.stats.ownCost": "セッションコスト", "context.stats.totalCost": "総コスト", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (ロード済み)", "context.stats.sessionCreated": "セッション作成日時", "context.stats.lastActivity": "最終アクティビティ", "context.usage.tokens": "トークン", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 525bd035651f..93020ba07bb4 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -331,7 +331,10 @@ export const dict = { "context.stats.cacheTokens": "캐시 토큰 (읽기/쓰기)", "context.stats.userMessages": "사용자 메시지", "context.stats.assistantMessages": "어시스턴트 메시지", + "context.stats.ownCost": "세션 비용", "context.stats.totalCost": "총 비용", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (로드됨)", "context.stats.sessionCreated": "세션 생성됨", "context.stats.lastActivity": "최근 활동", "context.usage.tokens": "토큰", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 98e79e1896af..ebebd7da6ca1 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -362,7 +362,10 @@ export const dict = { "context.stats.cacheTokens": "Cache-tokens (les/skriv)", "context.stats.userMessages": "Brukermeldinger", "context.stats.assistantMessages": "Assistentmeldinger", + "context.stats.ownCost": "Sesjonskostnad", "context.stats.totalCost": "Total kostnad", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (lastet)", "context.stats.sessionCreated": "Sesjon opprettet", "context.stats.lastActivity": "Siste aktivitet", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 983c9c14ac1b..439d64658247 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -329,7 +329,10 @@ export const dict = { "context.stats.cacheTokens": "Tokeny pamięci podręcznej (odczyt/zapis)", "context.stats.userMessages": "Wiadomości użytkownika", "context.stats.assistantMessages": "Wiadomości asystenta", + "context.stats.ownCost": "Koszt sesji", "context.stats.totalCost": "Całkowity koszt", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (załadowano)", "context.stats.sessionCreated": "Utworzono sesję", "context.stats.lastActivity": "Ostatnia aktywność", "context.usage.tokens": "Tokeny", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index f2c87fe0f1ed..c84e219b8949 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -361,7 +361,13 @@ export const dict = { "context.stats.cacheTokens": "Токены кэша (чтение/запись)", "context.stats.userMessages": "Сообщения пользователя", "context.stats.assistantMessages": "Сообщения ассистента", + "context.stats.ownCost": "Стоимость сессии", + "context.stats.ownCost": "Стоимость сессии", "context.stats.totalCost": "Общая стоимость", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (загружено)", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (загружено)", "context.stats.sessionCreated": "Сессия создана", "context.stats.lastActivity": "Последняя активность", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 689e82118968..f68609c818c7 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -359,7 +359,10 @@ export const dict = { "context.stats.cacheTokens": "โทเค็นแคช (อ่าน/เขียน)", "context.stats.userMessages": "ข้อความผู้ใช้", "context.stats.assistantMessages": "ข้อความผู้ช่วย", + "context.stats.ownCost": "ค่าใช้จ่ายเซสชัน", "context.stats.totalCost": "ต้นทุนทั้งหมด", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (โหลดแล้ว)", "context.stats.sessionCreated": "สร้างเซสชันเมื่อ", "context.stats.lastActivity": "กิจกรรมล่าสุด", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 1b40013b60de..047f791f0fef 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -374,7 +374,10 @@ export const dict = { "context.stats.cacheTokens": "缓存 token(读/写)", "context.stats.userMessages": "用户消息", "context.stats.assistantMessages": "助手消息", + "context.stats.ownCost": "会话成本", "context.stats.totalCost": "总成本", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (已加载)", "context.stats.sessionCreated": "创建时间", "context.stats.lastActivity": "最后活动", "context.usage.tokens": "Token", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 0c5ea9a85723..394bb7bcb1df 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -1,6 +1,7 @@ import { type Accessor, createMemo, createSignal, Match, Show, Switch } from "solid-js" import { useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" +import { costs } from "@opencode-ai/util/cost" import { pipe, sumBy } from "remeda" import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" @@ -23,7 +24,7 @@ const ContextInfo = (props: { context: Accessor; cost: Acces return ( - {props.context()} ({props.cost()}) + {props.context()} {props.cost()} ) @@ -36,14 +37,15 @@ export function Header() { const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const cost = createMemo(() => { - const total = pipe( - messages(), - sumBy((x) => (x.role === "assistant" ? x.cost : 0)), - ) - return new Intl.NumberFormat("en-US", { + const result = costs(route.sessionID, sync.data) + const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", - }).format(total) + }) + const agent = formatter.format(result.own) + const total = formatter.format(result.total) + const formatted = `(current agent ${agent}; total ${total})` + return result.missing.length > 0 ? `${formatted} (loaded)` : formatted }) const context = createMemo(() => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index b843bda1c9db..0c7d5a62e86f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -15,6 +15,7 @@ import { Dynamic } from "solid-js/web" import path from "path" import { useRoute, useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" +import { costs } from "@opencode-ai/util/cost" import { SplitBorder } from "@tui/component/border" import { Spinner } from "@tui/component/spinner" import { selectedForeground, useTheme } from "@tui/context/theme" @@ -1879,6 +1880,20 @@ function Task(props: ToolProps) { {props.input.description} ({tools().length} toolcalls) + + {(sessionId) => { + const result = costs(sessionId(), sync.data) + const total = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(result.total) + return ( + + ({total}){result.missing.length > 0 ? " (loaded)" : ""} + + ) + }} + {(item) => { const title = item().state.status === "completed" ? (item().state as any).title : "" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 4ffe91558ed7..540a212f729e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -1,4 +1,5 @@ import { useSync } from "@tui/context/sync" +import { costs } from "@opencode-ai/util/cost" import { createMemo, For, Show, Switch, Match } from "solid-js" import { createStore } from "solid-js/store" import { useTheme } from "../../context/theme" @@ -41,11 +42,12 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ) const cost = createMemo(() => { - const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) - return new Intl.NumberFormat("en-US", { + const result = costs(props.sessionID, sync.data) + const formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", - }).format(total) + }).format(result.total) + return result.missing.length > 0 ? `${formatted} (loaded)` : formatted }) const context = createMemo(() => { diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 3f61b3186d30..ae71d92df3c7 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -45,6 +45,7 @@ import { ImagePreview } from "./image-preview" import { findLast } from "@opencode-ai/util/array" import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path" import { checksum } from "@opencode-ai/util/encode" +import { costs } from "@opencode-ai/util/cost" import { Tooltip } from "./tooltip" import { IconButton } from "./icon-button" import { createAutoScroll } from "../hooks" @@ -921,6 +922,44 @@ ToolRegistry.register({ }, 50) } + const stats = createMemo(() => { + const sid = childSessionId() + if (!sid) return undefined + const messages = data.store.message[sid] + if (!messages || messages.length === 0) return undefined + + const last = findLast( + messages, + (m) => + m.role === "assistant" && + (m as AssistantMessage).tokens.input + + (m as AssistantMessage).tokens.output + + (m as AssistantMessage).tokens.reasoning > + 0, + ) as AssistantMessage | undefined + if (!last) return undefined + + const tokens = + last.tokens.input + + last.tokens.output + + last.tokens.reasoning + + last.tokens.cache.read + + last.tokens.cache.write + const limit = (data.store as any).provider?.all + ? (() => { + const providers = (data.store as any).provider.all as { + id: string + models: Record + }[] + const p = providers.find((item) => item.id === last.providerID) + return p?.models[last.modelID]?.limit.context + })() + : undefined + const usage = limit ? Math.round((tokens / limit) * 100) : undefined + const result = costs(sid, data.store) + return { tokens, usage, own: result.own, total: result.total, missing: result.missing } + }) + const trigger = () => (
@@ -941,6 +980,16 @@ ToolRegistry.register({ + + {(s) => ( + + {s().tokens.toLocaleString() + " tokens"} + {s().usage !== undefined ? ` (${s().usage}%)` : ""} + {` ($${s().total.toFixed(2)})`} + {s().missing.length > 0 ? i18n.t("ui.messagePart.loaded") : ""} + + )} +
) diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 9b6ab0bd6d9e..89b79b7f8ff2 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -42,6 +42,8 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "Considering next steps", "ui.messagePart.diagnostic.error": "Error", + "ui.messagePart.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "ui.messagePart.loaded": " (loaded)", "ui.messagePart.title.edit": "Edit", "ui.messagePart.title.write": "Write", "ui.messagePart.option.typeOwnAnswer": "Type your own answer", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index 7982b3ac75ed..2fdf848ccabb 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -45,6 +45,8 @@ export const dict: Record = { "ui.sessionTurn.status.consideringNextSteps": "Vurderer neste trinn", "ui.messagePart.diagnostic.error": "Feil", + "ui.messagePart.costBreakdown": "(agent {{agentCost}}; totalt {{totalCost}})", + "ui.messagePart.loaded": " (lastet)", "ui.messagePart.title.edit": "Rediger", "ui.messagePart.title.write": "Skriv", "ui.messagePart.option.typeOwnAnswer": "Skriv ditt eget svar", diff --git a/packages/util/src/cost.test.ts b/packages/util/src/cost.test.ts new file mode 100644 index 000000000000..79b426864512 --- /dev/null +++ b/packages/util/src/cost.test.ts @@ -0,0 +1,111 @@ +import { expect, test } from "bun:test" +import { costs } from "./cost" + +test("no sub-agents", () => { + const store = { + message: { + s1: [ + { id: "m1", role: "user", cost: 0 }, + { id: "m2", role: "assistant", cost: 0.5 }, + ], + }, + part: {}, + } + const result = costs("s1", store as any) + expect(result.own).toBe(0.5) + expect(result.total).toBe(0.5) + expect(result.missing).toEqual([]) +}) + +test("one loaded sub-agent", () => { + const store = { + message: { + s1: [{ id: "m1", role: "assistant", cost: 0.5 }], + s2: [{ id: "m2", role: "assistant", cost: 1.0 }], + }, + part: { + m1: [{ type: "tool", tool: "task", state: { metadata: { sessionId: "s2" } } }], + }, + } + const result = costs("s1", store as any) + expect(result.own).toBe(0.5) + expect(result.total).toBe(1.5) + expect(result.missing).toEqual([]) +}) + +test("nested sub-agents", () => { + const store = { + message: { + s1: [{ id: "m1", role: "assistant", cost: 0.5 }], + s2: [{ id: "m2", role: "assistant", cost: 1.0 }], + s3: [{ id: "m3", role: "assistant", cost: 2.0 }], + }, + part: { + m1: [{ type: "tool", tool: "task", state: { metadata: { sessionId: "s2" } } }], + m2: [{ type: "tool", tool: "task", state: { metadata: { sessionId: "s3" } } }], + }, + } + const result = costs("s1", store as any) + expect(result.own).toBe(0.5) + expect(result.total).toBe(3.5) + expect(result.missing).toEqual([]) +}) + +test("cycle detection", () => { + const store = { + message: { + s1: [{ id: "m1", role: "assistant", cost: 0.5 }], + s2: [{ id: "m2", role: "assistant", cost: 1.0 }], + }, + part: { + m1: [{ type: "tool", tool: "task", state: { metadata: { sessionId: "s2" } } }], + m2: [{ type: "tool", tool: "task", state: { metadata: { sessionId: "s1" } } }], + }, + } + const result = costs("s1", store as any) + expect(result.own).toBe(0.5) + expect(result.total).toBe(1.5) + expect(result.missing).toEqual([]) +}) + +test("missing child session", () => { + const store = { + message: { + s1: [{ id: "m1", role: "assistant", cost: 0.5 }], + }, + part: { + m1: [{ type: "tool", tool: "task", state: { metadata: { sessionId: "s2" } } }], + }, + } + const result = costs("s1", store as any) + expect(result.own).toBe(0.5) + expect(result.total).toBe(0.5) + expect(result.missing).toEqual(["s2"]) +}) + +test("empty or undefined messages", () => { + const store = { + message: {}, + part: {}, + } + const result = costs("s1", store as any) + expect(result.own).toBe(0) + expect(result.total).toBe(0) + expect(result.missing).toEqual(["s1"]) +}) + +test("only assistant messages counted", () => { + const store = { + message: { + s1: [ + { id: "m1", role: "user", cost: 100 }, + { id: "m2", role: "assistant", cost: 0.5 }, + { id: "m3", role: "system", cost: 100 }, + ], + }, + part: {}, + } + const result = costs("s1", store as any) + expect(result.own).toBe(0.5) + expect(result.total).toBe(0.5) +}) diff --git a/packages/util/src/cost.ts b/packages/util/src/cost.ts new file mode 100644 index 000000000000..dfb40de6dc72 --- /dev/null +++ b/packages/util/src/cost.ts @@ -0,0 +1,52 @@ +export interface CostMessage { + id: string + role: string + cost?: number +} + +export interface CostPart { + type: string + tool?: string + state?: Record +} + +export interface CostStore { + message: Record + part: Record +} + +export function costs(id: string, store: CostStore) { + const missing: string[] = [] + const visited = new Set() + + const walk = (sid: string): number => { + if (visited.has(sid)) return 0 + visited.add(sid) + + const messages = store.message[sid] + if (!messages) { + missing.push(sid) + return 0 + } + + return messages + .filter((msg) => msg.role === "assistant") + .reduce((sum, msg) => { + const parts = store.part[msg.id] ?? [] + const subCosts = parts + .filter((part) => part.type === "tool" && part.tool === "task") + .map((part) => (part.state?.metadata as Record | undefined)?.sessionId) + .filter((child): child is string => typeof child === "string") + .reduce((acc, child) => acc + walk(child), 0) + return sum + (msg.cost ?? 0) + subCosts + }, 0) + } + + const own = (store.message[id] ?? []) + .filter((msg) => msg.role === "assistant") + .reduce((sum, msg) => sum + (msg.cost ?? 0), 0) + + const total = walk(id) + + return { own, total, missing } +} From 7b8be973e2bf744fc5e3a9c56d2492d082cbd7ab Mon Sep 17 00:00:00 2001 From: JudicaelPoumay Date: Sat, 14 Feb 2026 14:04:33 +0100 Subject: [PATCH 02/16] fix: resolve i18n duplicates, enterprise type errors, and session race conditions --- .../session/session-context-format.ts | 9 ++ packages/app/src/custom-elements.d.ts | 2 +- packages/app/src/i18n/da.ts | 3 - packages/app/src/i18n/ru.ts | 3 - packages/enterprise/src/app.tsx | 3 +- packages/enterprise/src/custom-elements.d.ts | 2 +- packages/opencode/src/session/index.ts | 82 +++++++++++-------- packages/opencode/src/session/summary.ts | 3 +- packages/ui/src/i18n/zh.ts | 2 + 9 files changed, 66 insertions(+), 43 deletions(-) mode change 120000 => 100644 packages/app/src/custom-elements.d.ts mode change 120000 => 100644 packages/enterprise/src/custom-elements.d.ts diff --git a/packages/app/src/components/session/session-context-format.ts b/packages/app/src/components/session/session-context-format.ts index e7c536d58411..58a14bcd0f0f 100644 --- a/packages/app/src/components/session/session-context-format.ts +++ b/packages/app/src/components/session/session-context-format.ts @@ -1,6 +1,10 @@ import { DateTime } from "luxon" export function createSessionContextFormatter(locale: string) { + const usd = new Intl.NumberFormat(locale, { + style: "currency", + currency: "USD", + }) return { number(value: number | null | undefined) { if (value === undefined) return "—" @@ -12,6 +16,11 @@ export function createSessionContextFormatter(locale: string) { if (value === null) return "—" return value.toLocaleString(locale) + "%" }, + currency(value: number | null | undefined) { + if (value === undefined) return "—" + if (value === null) return "—" + return usd.format(value) + }, time(value: number | undefined) { if (!value) return "—" return DateTime.fromMillis(value).setLocale(locale).toLocaleString(DateTime.DATETIME_MED) diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts deleted file mode 120000 index e4ea0d6cebda..000000000000 --- a/packages/app/src/custom-elements.d.ts +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts new file mode 100644 index 000000000000..86cb284693e6 --- /dev/null +++ b/packages/app/src/custom-elements.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index da6cc1e727d1..303fabfbcbc6 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -360,12 +360,9 @@ export const dict = { "context.stats.userMessages": "Brugerbeskeder", "context.stats.assistantMessages": "Assistentbeskeder", "context.stats.ownCost": "Sessionsomkostninger", - "context.stats.ownCost": "Sessionsomkostninger", "context.stats.totalCost": "Samlede omkostninger", "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", "context.stats.loaded": " (indlæst)", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (indlæst)", "context.stats.sessionCreated": "Session oprettet", "context.stats.lastActivity": "Seneste aktivitet", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index c84e219b8949..fed59cc662a5 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -362,12 +362,9 @@ export const dict = { "context.stats.userMessages": "Сообщения пользователя", "context.stats.assistantMessages": "Сообщения ассистента", "context.stats.ownCost": "Стоимость сессии", - "context.stats.ownCost": "Стоимость сессии", "context.stats.totalCost": "Общая стоимость", "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", "context.stats.loaded": " (загружено)", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (загружено)", "context.stats.sessionCreated": "Сессия создана", "context.stats.lastActivity": "Последняя активность", diff --git a/packages/enterprise/src/app.tsx b/packages/enterprise/src/app.tsx index 6f9cdcafb893..72bf0f7965bd 100644 --- a/packages/enterprise/src/app.tsx +++ b/packages/enterprise/src/app.tsx @@ -57,7 +57,8 @@ function detectLocale() { function UiI18nBridge(props: ParentProps) { const locale = createMemo(() => detectLocale()) const t = (key: keyof typeof uiEn, params?: UiI18nParams) => { - const value = locale() === "zh" ? (uiZh[key] ?? uiEn[key]) : uiEn[key] + const zhDict = uiZh as Partial> + const value = locale() === "zh" ? (zhDict[key] ?? uiEn[key]) : uiEn[key] const text = value ?? String(key) return resolveTemplate(text, params) } diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts deleted file mode 120000 index e4ea0d6cebda..000000000000 --- a/packages/enterprise/src/custom-elements.d.ts +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts new file mode 100644 index 000000000000..86cb284693e6 --- /dev/null +++ b/packages/enterprise/src/custom-elements.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 38007a0a7f0d..351bd7a12db5 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -563,22 +563,30 @@ export namespace Session { export const updateMessage = fn(MessageV2.Info, async (msg) => { const time_created = msg.role === "user" ? msg.time.created : msg.time.created const { id, sessionID, ...data } = msg - Database.use((db) => { - db.insert(MessageTable) - .values({ - id, - session_id: sessionID, - time_created, - data, - }) - .onConflictDoUpdate({ target: MessageTable.id, set: { data } }) - .run() - Database.effect(() => - Bus.publish(MessageV2.Event.Updated, { - info: msg, - }), - ) - }) + try { + Database.use((db) => { + db.insert(MessageTable) + .values({ + id, + session_id: sessionID, + time_created, + data, + }) + .onConflictDoUpdate({ target: MessageTable.id, set: { data } }) + .run() + Database.effect(() => + Bus.publish(MessageV2.Event.Updated, { + info: msg, + }), + ) + }) + } catch (e) { + if (e instanceof Error && e.message.includes("FOREIGN KEY constraint failed")) { + log.warn("ignoring message update for missing session", { sessionID, messageID: id }) + return msg + } + throw e + } return msg }) @@ -628,23 +636,31 @@ export namespace Session { export const updatePart = fn(UpdatePartInput, async (part) => { const { id, messageID, sessionID, ...data } = part const time = Date.now() - Database.use((db) => { - db.insert(PartTable) - .values({ - id, - message_id: messageID, - session_id: sessionID, - time_created: time, - data, - }) - .onConflictDoUpdate({ target: PartTable.id, set: { data } }) - .run() - Database.effect(() => - Bus.publish(MessageV2.Event.PartUpdated, { - part, - }), - ) - }) + try { + Database.use((db) => { + db.insert(PartTable) + .values({ + id, + message_id: messageID, + session_id: sessionID, + time_created: time, + data, + }) + .onConflictDoUpdate({ target: PartTable.id, set: { data } }) + .run() + Database.effect(() => + Bus.publish(MessageV2.Event.PartUpdated, { + part, + }), + ) + }) + } catch (e) { + if (e instanceof Error && e.message.includes("FOREIGN KEY constraint failed")) { + log.warn("ignoring part update for missing message/session", { sessionID, messageID, partID: id }) + return part + } + throw e + } return part }) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index c3e14ddd691f..f09f2b32aa26 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -109,7 +109,8 @@ export namespace SessionSummary { const messages = input.messages.filter( (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID), ) - const msgWithParts = messages.find((m) => m.info.id === input.messageID)! + const msgWithParts = messages.find((m) => m.info.id === input.messageID) + if (!msgWithParts) return const userMsg = msgWithParts.info as MessageV2.User const diffs = await computeDiff({ messages }) userMsg.summary = { diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index dcb8062a3365..7c3bc12e593f 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -46,6 +46,8 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步", "ui.messagePart.diagnostic.error": "错误", + "ui.messagePart.costBreakdown": "(智能体 {{agentCost}}; 总计 {{totalCost}})", + "ui.messagePart.loaded": " (已加载)", "ui.messagePart.title.edit": "编辑", "ui.messagePart.title.write": "写入", "ui.messagePart.option.typeOwnAnswer": "输入自己的答案", From 40fb64a6a552ff4bf87046578b28fd41537c3e79 Mon Sep 17 00:00:00 2001 From: JudicaelPoumay Date: Sat, 14 Feb 2026 18:17:30 +0100 Subject: [PATCH 03/16] feat(ui): add reactive session syncing and polish cost display --- .../src/components/session-context-usage.tsx | 20 +++++++++++-- .../session/session-context-tab.tsx | 8 +++++ .../src/cli/cmd/tui/routes/session/index.tsx | 29 +++++++++++++++---- packages/ui/src/components/message-part.tsx | 8 +++++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index ec3a5ba44b9b..25b9eaa1a5d4 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -1,4 +1,4 @@ -import { Match, Show, Switch, createMemo } from "solid-js" +import { Match, Show, Switch, createMemo, createEffect } from "solid-js" import { Tooltip } from "@opencode-ai/ui/tooltip" import { ProgressCircle } from "@opencode-ai/ui/progress-circle" import { Button } from "@opencode-ai/ui/button" @@ -45,6 +45,14 @@ export function SessionContextUsage(props: SessionContextUsageProps) { ) const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all, sync.data)) + createEffect(() => { + const missing = metrics().missing + if (missing.length > 0) { + for (const id of missing) { + sync.session.sync(id) + } + } + }) const context = createMemo(() => metrics().context) const cost = createMemo(() => { const agent = usd().format(metrics().ownCost) @@ -95,16 +103,22 @@ export function SessionContextUsage(props: SessionContextUsageProps) { - {circle()} + +
+ {circle()} + {cost()} +
+
diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index d76ac6af0e96..70f2c4337a08 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -105,6 +105,14 @@ export function SessionContextTab(props: SessionContextTabProps) { ) const metrics = createMemo(() => getSessionContextMetrics(props.messages(), sync.data.provider.all, sync.data)) + createEffect(() => { + const missing = metrics().missing + if (missing.length > 0) { + for (const id of missing) { + sync.session.sync(id) + } + } + }) const ctx = createMemo(() => metrics().context) const formatter = createMemo(() => createSessionContextFormatter(language.locale())) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 6997d70e052b..5eb2ff37ac0c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -192,6 +192,15 @@ export function Session() { }) }) + createEffect(() => { + const result = costs(route.sessionID, sync.data) + if (result.missing.length > 0) { + for (const id of result.missing) { + sync.session.sync(id) + } + } + }) + const toast = useToast() const sdk = useSDK() @@ -1882,14 +1891,22 @@ function Task(props: ToolProps) { {(sessionId) => { - const result = costs(sessionId(), sync.data) - const total = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(result.total) + const result = createMemo(() => costs(sessionId(), sync.data)) + const own = createMemo(() => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(result().own), + ) + const total = createMemo(() => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(result().total), + ) return ( - ({total}){result.missing.length > 0 ? " (loaded)" : ""} + ({total}) ) }} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index ae71d92df3c7..8f511e3e9c9d 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -959,6 +959,14 @@ ToolRegistry.register({ const result = costs(sid, data.store) return { tokens, usage, own: result.own, total: result.total, missing: result.missing } }) + createEffect(() => { + const missing = stats()?.missing + if (missing && missing.length > 0) { + for (const id of missing) { + data.syncSession?.(id) + } + } + }) const trigger = () => (
From 6d55c87a71818ac558aeccccf56ee71865240709 Mon Sep 17 00:00:00 2001 From: JudicaelPoumay Date: Sat, 14 Feb 2026 18:32:28 +0100 Subject: [PATCH 04/16] =?UTF-8?q?fix:=20address=20reviewer=20feedback=20?= =?UTF-8?q?=E2=80=94=20localize=20cost=20breakdown,=20refine=20types,=20us?= =?UTF-8?q?e=20findLast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session-context-usage.tsx | 7 +++-- .../session/session-context-metrics.ts | 28 ++++++------------- .../src/cli/cmd/tui/routes/session/header.tsx | 5 ++-- .../cli/cmd/tui/routes/session/sidebar.tsx | 5 ++-- packages/opencode/src/util/locale.ts | 4 +++ 5 files changed, 21 insertions(+), 28 deletions(-) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 25b9eaa1a5d4..90626f3c3b75 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -55,9 +55,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) { }) const context = createMemo(() => metrics().context) const cost = createMemo(() => { - const agent = usd().format(metrics().ownCost) - const total = usd().format(metrics().totalCost) - const value = `(current agent ${agent}; total ${total})` + const value = language.t("context.stats.costBreakdown", { + agentCost: usd().format(metrics().ownCost), + totalCost: usd().format(metrics().totalCost), + }) return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value }) diff --git a/packages/app/src/components/session/session-context-metrics.ts b/packages/app/src/components/session/session-context-metrics.ts index ae78e00e60e8..cd21a74f585a 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 { costs } from "@opencode-ai/util/cost" +import { costs, type CostStore } from "@opencode-ai/util/cost" import type { AssistantMessage, Message } from "@opencode-ai/sdk/v2/client" type Provider = { @@ -41,22 +41,14 @@ 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 lastAssistantWithTokens = (messages: Message[]) => + messages.findLast( + (msg): msg is AssistantMessage => msg.role === "assistant" && tokenTotal(msg as AssistantMessage) > 0, + ) -const build = ( - messages: Message[] = [], - providers: Provider[] = [], - store?: { message: Record; part: Record }, -): Metrics => { +const build = (messages: Message[] = [], providers: Provider[] = [], store?: CostStore): Metrics => { const id = messages[0]?.sessionID - const result = store && id ? costs(id, store as any) : undefined + const result = store && id ? costs(id, store) : undefined const totalCost = result ? result.total @@ -94,10 +86,6 @@ const build = ( } } -export function getSessionContextMetrics( - messages: Message[] = [], - providers: Provider[] = [], - store?: { message: Record; part: Record }, -) { +export function getSessionContextMetrics(messages: Message[] = [], providers: Provider[] = [], store?: CostStore) { return build(messages, providers, store) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 394bb7bcb1df..9fde969e52dc 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -9,6 +9,7 @@ import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2" import { useCommandDialog } from "@tui/component/dialog-command" import { useKeybind } from "../../context/keybind" import { useTerminalDimensions } from "@opentui/solid" +import { Locale } from "@/util/locale" const Title = (props: { session: Accessor }) => { const { theme } = useTheme() @@ -42,9 +43,7 @@ export function Header() { style: "currency", currency: "USD", }) - const agent = formatter.format(result.own) - const total = formatter.format(result.total) - const formatted = `(current agent ${agent}; total ${total})` + const formatted = Locale.costBreakdown(formatter.format(result.own), formatter.format(result.total)) return result.missing.length > 0 ? `${formatted} (loaded)` : formatted }) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 540a212f729e..b6f667efa944 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -43,10 +43,11 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { const cost = createMemo(() => { const result = costs(props.sessionID, sync.data) - const formatted = new Intl.NumberFormat("en-US", { + const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", - }).format(result.total) + }) + const formatted = Locale.costBreakdown(formatter.format(result.own), formatter.format(result.total)) return result.missing.length > 0 ? `${formatted} (loaded)` : formatted }) diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index 653da09a0b7d..0bf703b67ada 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -78,4 +78,8 @@ export namespace Locale { const template = count === 1 ? singular : plural return template.replace("{}", count.toString()) } + + export function costBreakdown(agentCost: string, totalCost: string): string { + return `(agent ${agentCost}; total ${totalCost})` + } } From bc2633051a8a16336309e3b9e4ef47b355036dee Mon Sep 17 00:00:00 2001 From: JudicaelPoumay Date: Sun, 15 Feb 2026 11:24:22 +0100 Subject: [PATCH 05/16] refactor: remove cost indicators from UI and i18n --- CHANGELOG.md | 17 +++++++++++++ CONTRIBUTING.md | 10 ++++++++ .../src/components/session-context-usage.tsx | 25 +------------------ .../session/session-context-tab.tsx | 3 +-- packages/app/src/i18n/ar.ts | 5 ---- packages/app/src/i18n/br.ts | 5 ---- packages/app/src/i18n/bs.ts | 5 ---- packages/app/src/i18n/da.ts | 5 ---- packages/app/src/i18n/de.ts | 5 ---- packages/app/src/i18n/en.ts | 5 ---- packages/app/src/i18n/es.ts | 5 ---- packages/app/src/i18n/fr.ts | 5 ---- packages/app/src/i18n/ja.ts | 5 ---- packages/app/src/i18n/ko.ts | 5 ---- packages/app/src/i18n/no.ts | 5 ---- packages/app/src/i18n/pl.ts | 5 ---- packages/app/src/i18n/ru.ts | 5 ---- packages/app/src/i18n/th.ts | 5 ---- packages/app/src/i18n/zh.ts | 5 ---- packages/app/src/i18n/zht.ts | 2 -- packages/opencode/script/build.ts | 4 ++- .../src/cli/cmd/tui/routes/session/header.tsx | 19 +++----------- .../cli/cmd/tui/routes/session/sidebar.tsx | 16 ------------ packages/opencode/src/util/locale.ts | 4 --- packages/ui/src/components/message-part.tsx | 1 - packages/ui/src/i18n/en.ts | 2 -- packages/ui/src/i18n/no.ts | 2 -- packages/ui/src/i18n/zh.ts | 2 -- 28 files changed, 36 insertions(+), 146 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000000..2b397cc30731 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Removed + +- Removed cost display from the session header and sidebar in the TUI. +- Removed `ownCost` and `totalCost` from the session context tab. +- Removed cost tooltips and display from session context usage. +- Removed cost display from message parts in the UI. +- Cleaned up unused i18n keys related to costs (`context.stats.ownCost`, `context.stats.totalCost`, `context.stats.costBreakdown`, `context.usage.cost`, `ui.messagePart.costBreakdown`). +- Removed `costBreakdown` utility function. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4bec009ef467..3d1df0e4a95d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,6 +64,16 @@ Then run it with: Replace `` with your platform (e.g., `darwin-arm64`, `linux-x64`). +### Updating the local CLI + +After making code changes, you can update your local `opencode` command by rebuilding the standalone executable: + +```bash +bun run ./packages/opencode/script/build.ts --single +``` + +If you have added the `dist/bin` directory to your PATH, the `opencode` command will automatically pick up the new build. On Windows, if the build fails because `opencode.exe` is in use, try closing any running opencode processes or renaming the existing `opencode.exe` to `opencode.exe.old` before rebuilding. + - Core pieces: - `packages/opencode`: OpenCode core business logic & server. - `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 90626f3c3b75..a6508b8db45b 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -36,14 +36,6 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const view = createMemo(() => layout.view(sessionKey)) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) - const usd = createMemo( - () => - new Intl.NumberFormat(language.locale(), { - style: "currency", - currency: "USD", - }), - ) - const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all, sync.data)) createEffect(() => { const missing = metrics().missing @@ -54,13 +46,6 @@ export function SessionContextUsage(props: SessionContextUsageProps) { } }) const context = createMemo(() => metrics().context) - const cost = createMemo(() => { - const value = language.t("context.stats.costBreakdown", { - agentCost: usd().format(metrics().ownCost), - totalCost: usd().format(metrics().totalCost), - }) - return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value - }) const openContext = () => { if (!params.id) return @@ -93,10 +78,6 @@ export function SessionContextUsage(props: SessionContextUsageProps) { )} -
- {cost()} - {language.t("context.usage.cost")} -
) @@ -105,10 +86,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { -
- {circle()} - {cost()} -
+
{circle()}
diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 70f2c4337a08..a17fd98f6872 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -117,8 +117,7 @@ export function SessionContextTab(props: SessionContextTabProps) { const formatter = createMemo(() => createSessionContextFormatter(language.locale())) const cost = createMemo(() => { - const value = usd().format(metrics().totalCost) - return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value + return usd().format(metrics().totalCost) }) const counts = createMemo(() => { diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 5d8ead4b2dff..968a4ec1a10c 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -328,15 +328,10 @@ export const dict = { "context.stats.cacheTokens": "رموز التخزين المؤقت (قراءة/كتابة)", "context.stats.userMessages": "رسائل المستخدم", "context.stats.assistantMessages": "رسائل المساعد", - "context.stats.ownCost": "تكلفة الجلسة", - "context.stats.totalCost": "التكلفة الإجمالية", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (تم التحميل)", "context.stats.sessionCreated": "تم إنشاء الجلسة", "context.stats.lastActivity": "آخر نشاط", "context.usage.tokens": "رموز", "context.usage.usage": "استخدام", - "context.usage.cost": "تكلفة", "context.usage.clickToView": "انقر لعرض السياق", "context.usage.view": "عرض استخدام السياق", "language.en": "English", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index d67f16a248a5..1bd3e266025c 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -329,15 +329,10 @@ export const dict = { "context.stats.cacheTokens": "Tokens de Cache (leitura/escrita)", "context.stats.userMessages": "Mensagens de Usuário", "context.stats.assistantMessages": "Mensagens do Assistente", - "context.stats.ownCost": "Custo da sessão", - "context.stats.totalCost": "Custo Total", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (carregado)", "context.stats.sessionCreated": "Sessão Criada", "context.stats.lastActivity": "Última Atividade", "context.usage.tokens": "Tokens", "context.usage.usage": "Uso", - "context.usage.cost": "Custo", "context.usage.clickToView": "Clique para ver o contexto", "context.usage.view": "Ver uso do contexto", "language.en": "English", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 6db3ea5e81c3..0b43b1fcd7a2 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -361,16 +361,11 @@ export const dict = { "context.stats.cacheTokens": "Cache tokeni (čitanje/pisanje)", "context.stats.userMessages": "Korisničke poruke", "context.stats.assistantMessages": "Poruke asistenta", - "context.stats.ownCost": "Trošak sesije", - "context.stats.totalCost": "Ukupni trošak", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (učitano)", "context.stats.sessionCreated": "Sesija kreirana", "context.stats.lastActivity": "Posljednja aktivnost", "context.usage.tokens": "Tokeni", "context.usage.usage": "Korištenje", - "context.usage.cost": "Trošak", "context.usage.clickToView": "Klikni da vidiš kontekst", "context.usage.view": "Prikaži korištenje konteksta", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 303fabfbcbc6..25b65770d2c1 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -359,16 +359,11 @@ export const dict = { "context.stats.cacheTokens": "Cache Tokens (læs/skriv)", "context.stats.userMessages": "Brugerbeskeder", "context.stats.assistantMessages": "Assistentbeskeder", - "context.stats.ownCost": "Sessionsomkostninger", - "context.stats.totalCost": "Samlede omkostninger", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (indlæst)", "context.stats.sessionCreated": "Session oprettet", "context.stats.lastActivity": "Seneste aktivitet", "context.usage.tokens": "Tokens", "context.usage.usage": "Forbrug", - "context.usage.cost": "Omkostning", "context.usage.clickToView": "Klik for at se kontekst", "context.usage.view": "Se kontekstforbrug", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index c40f12058706..81e157de3bac 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -336,15 +336,10 @@ export const dict = { "context.stats.cacheTokens": "Cache-Token (lesen/schreiben)", "context.stats.userMessages": "Benutzernachrichten", "context.stats.assistantMessages": "Assistentennachrichten", - "context.stats.ownCost": "Sitzungskosten", - "context.stats.totalCost": "Gesamtkosten", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (geladen)", "context.stats.sessionCreated": "Sitzung erstellt", "context.stats.lastActivity": "Letzte Aktivität", "context.usage.tokens": "Token", "context.usage.usage": "Nutzung", - "context.usage.cost": "Kosten", "context.usage.clickToView": "Klicken, um Kontext anzuzeigen", "context.usage.view": "Kontextnutzung anzeigen", "language.en": "English", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index d1bcba6f09e9..b74f98471f25 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -360,16 +360,11 @@ export const dict = { "context.stats.cacheTokens": "Cache Tokens (read/write)", "context.stats.userMessages": "User Messages", "context.stats.assistantMessages": "Assistant Messages", - "context.stats.ownCost": "Session Cost", - "context.stats.totalCost": "Total Cost", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (loaded)", "context.stats.sessionCreated": "Session Created", "context.stats.lastActivity": "Last Activity", "context.usage.tokens": "Tokens", "context.usage.usage": "Usage", - "context.usage.cost": "Cost", "context.usage.clickToView": "Click to view context", "context.usage.view": "View context usage", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index b48fabc95ba8..eb6aef9fa697 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -361,16 +361,11 @@ export const dict = { "context.stats.cacheTokens": "Tokens de Caché (lectura/escritura)", "context.stats.userMessages": "Mensajes de Usuario", "context.stats.assistantMessages": "Mensajes de Asistente", - "context.stats.ownCost": "Costo de la sesión", - "context.stats.totalCost": "Costo Total", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (cargado)", "context.stats.sessionCreated": "Sesión Creada", "context.stats.lastActivity": "Última Actividad", "context.usage.tokens": "Tokens", "context.usage.usage": "Uso", - "context.usage.cost": "Costo", "context.usage.clickToView": "Haz clic para ver contexto", "context.usage.view": "Ver uso del contexto", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 2649e96214ad..e6e0366886ed 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -330,15 +330,10 @@ export const dict = { "context.stats.cacheTokens": "Jetons de cache (lecture/écriture)", "context.stats.userMessages": "Messages utilisateur", "context.stats.assistantMessages": "Messages assistant", - "context.stats.ownCost": "Coût de la session", - "context.stats.totalCost": "Coût total", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (chargé)", "context.stats.sessionCreated": "Session créée", "context.stats.lastActivity": "Dernière activité", "context.usage.tokens": "Jetons", "context.usage.usage": "Utilisation", - "context.usage.cost": "Coût", "context.usage.clickToView": "Cliquez pour voir le contexte", "context.usage.view": "Voir l'utilisation du contexte", "language.en": "English", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 23ac62d6401c..92b91b203c74 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -328,15 +328,10 @@ export const dict = { "context.stats.cacheTokens": "キャッシュトークン (読込/書込)", "context.stats.userMessages": "ユーザーメッセージ", "context.stats.assistantMessages": "アシスタントメッセージ", - "context.stats.ownCost": "セッションコスト", - "context.stats.totalCost": "総コスト", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (ロード済み)", "context.stats.sessionCreated": "セッション作成日時", "context.stats.lastActivity": "最終アクティビティ", "context.usage.tokens": "トークン", "context.usage.usage": "使用量", - "context.usage.cost": "コスト", "context.usage.clickToView": "クリックしてコンテキストを表示", "context.usage.view": "コンテキスト使用量を表示", "language.en": "English", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 93020ba07bb4..98c58764ded8 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -331,15 +331,10 @@ export const dict = { "context.stats.cacheTokens": "캐시 토큰 (읽기/쓰기)", "context.stats.userMessages": "사용자 메시지", "context.stats.assistantMessages": "어시스턴트 메시지", - "context.stats.ownCost": "세션 비용", - "context.stats.totalCost": "총 비용", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (로드됨)", "context.stats.sessionCreated": "세션 생성됨", "context.stats.lastActivity": "최근 활동", "context.usage.tokens": "토큰", "context.usage.usage": "사용량", - "context.usage.cost": "비용", "context.usage.clickToView": "컨텍스트를 보려면 클릭", "context.usage.view": "컨텍스트 사용량 보기", "language.en": "English", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index ebebd7da6ca1..be402015b2e5 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -362,16 +362,11 @@ export const dict = { "context.stats.cacheTokens": "Cache-tokens (les/skriv)", "context.stats.userMessages": "Brukermeldinger", "context.stats.assistantMessages": "Assistentmeldinger", - "context.stats.ownCost": "Sesjonskostnad", - "context.stats.totalCost": "Total kostnad", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (lastet)", "context.stats.sessionCreated": "Sesjon opprettet", "context.stats.lastActivity": "Siste aktivitet", "context.usage.tokens": "Tokens", "context.usage.usage": "Forbruk", - "context.usage.cost": "Kostnad", "context.usage.clickToView": "Klikk for å se kontekst", "context.usage.view": "Se kontekstforbruk", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 439d64658247..57978f89552b 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -329,15 +329,10 @@ export const dict = { "context.stats.cacheTokens": "Tokeny pamięci podręcznej (odczyt/zapis)", "context.stats.userMessages": "Wiadomości użytkownika", "context.stats.assistantMessages": "Wiadomości asystenta", - "context.stats.ownCost": "Koszt sesji", - "context.stats.totalCost": "Całkowity koszt", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (załadowano)", "context.stats.sessionCreated": "Utworzono sesję", "context.stats.lastActivity": "Ostatnia aktywność", "context.usage.tokens": "Tokeny", "context.usage.usage": "Użycie", - "context.usage.cost": "Koszt", "context.usage.clickToView": "Kliknij, aby zobaczyć kontekst", "context.usage.view": "Pokaż użycie kontekstu", "language.en": "English", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index fed59cc662a5..4e5db231f4cf 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -361,16 +361,11 @@ export const dict = { "context.stats.cacheTokens": "Токены кэша (чтение/запись)", "context.stats.userMessages": "Сообщения пользователя", "context.stats.assistantMessages": "Сообщения ассистента", - "context.stats.ownCost": "Стоимость сессии", - "context.stats.totalCost": "Общая стоимость", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (загружено)", "context.stats.sessionCreated": "Сессия создана", "context.stats.lastActivity": "Последняя активность", "context.usage.tokens": "Токены", "context.usage.usage": "Использование", - "context.usage.cost": "Стоимость", "context.usage.clickToView": "Нажмите для просмотра контекста", "context.usage.view": "Показать использование контекста", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index f68609c818c7..aac8d624ad42 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -359,16 +359,11 @@ export const dict = { "context.stats.cacheTokens": "โทเค็นแคช (อ่าน/เขียน)", "context.stats.userMessages": "ข้อความผู้ใช้", "context.stats.assistantMessages": "ข้อความผู้ช่วย", - "context.stats.ownCost": "ค่าใช้จ่ายเซสชัน", - "context.stats.totalCost": "ต้นทุนทั้งหมด", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (โหลดแล้ว)", "context.stats.sessionCreated": "สร้างเซสชันเมื่อ", "context.stats.lastActivity": "กิจกรรมล่าสุด", "context.usage.tokens": "โทเค็น", "context.usage.usage": "การใช้งาน", - "context.usage.cost": "ต้นทุน", "context.usage.clickToView": "คลิกเพื่อดูบริบท", "context.usage.view": "ดูการใช้บริบท", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 047f791f0fef..68c219fbf667 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -374,15 +374,10 @@ export const dict = { "context.stats.cacheTokens": "缓存 token(读/写)", "context.stats.userMessages": "用户消息", "context.stats.assistantMessages": "助手消息", - "context.stats.ownCost": "会话成本", - "context.stats.totalCost": "总成本", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (已加载)", "context.stats.sessionCreated": "创建时间", "context.stats.lastActivity": "最后活动", "context.usage.tokens": "Token", "context.usage.usage": "使用率", - "context.usage.cost": "成本", "context.usage.clickToView": "点击查看上下文", "context.usage.view": "查看上下文用量", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 34aec01b9cb1..c818b0a14c2e 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -358,13 +358,11 @@ export const dict = { "context.stats.cacheTokens": "快取 token(讀/寫)", "context.stats.userMessages": "使用者訊息", "context.stats.assistantMessages": "助手訊息", - "context.stats.totalCost": "總成本", "context.stats.sessionCreated": "建立時間", "context.stats.lastActivity": "最後活動", "context.usage.tokens": "Token", "context.usage.usage": "使用量", - "context.usage.cost": "成本", "context.usage.clickToView": "點擊查看上下文", "context.usage.view": "檢視上下文用量", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index ddb4769912d7..5074e196eaa2 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -135,7 +135,9 @@ const targets = singleFlag }) : allTargets -await $`rm -rf dist` +try { + await $`rm -rf dist` +} catch (e) {} const binaries: Record = {} if (!skipInstall) { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 9fde969e52dc..74a0e0442526 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -1,7 +1,6 @@ import { type Accessor, createMemo, createSignal, Match, Show, Switch } from "solid-js" import { useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" -import { costs } from "@opencode-ai/util/cost" import { pipe, sumBy } from "remeda" import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" @@ -20,12 +19,12 @@ const Title = (props: { session: Accessor }) => { ) } -const ContextInfo = (props: { context: Accessor; cost: Accessor }) => { +const ContextInfo = (props: { context: Accessor }) => { const { theme } = useTheme() return ( - {props.context()} {props.cost()} + {props.context()} ) @@ -37,16 +36,6 @@ export function Header() { const session = createMemo(() => sync.session.get(route.sessionID)!) const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) - const cost = createMemo(() => { - const result = costs(route.sessionID, sync.data) - const formatter = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }) - const formatted = Locale.costBreakdown(formatter.format(result.own), formatter.format(result.total)) - return result.missing.length > 0 ? `${formatted} (loaded)` : formatted - }) - const context = createMemo(() => { const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage if (!last) return @@ -87,7 +76,7 @@ export function Header() { Subagent session - + - <ContextInfo context={context} cost={cost} /> + <ContextInfo context={context} /> </box> </Match> </Switch> diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index b6f667efa944..cd62ecbf502c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -1,14 +1,9 @@ import { useSync } from "@tui/context/sync" -import { costs } from "@opencode-ai/util/cost" import { createMemo, For, Show, Switch, Match } from "solid-js" import { createStore } from "solid-js/store" import { useTheme } from "../../context/theme" -import { Locale } from "@/util/locale" -import path from "path" import type { AssistantMessage } from "@opencode-ai/sdk/v2" -import { Global } from "@/global" import { Installation } from "@/installation" -import { useKeybind } from "../../context/keybind" import { useDirectory } from "../../context/directory" import { useKV } from "../../context/kv" import { TodoItem } from "../../component/todo-item" @@ -41,16 +36,6 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ).length, ) - const cost = createMemo(() => { - const result = costs(props.sessionID, sync.data) - const formatter = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }) - const formatted = Locale.costBreakdown(formatter.format(result.own), formatter.format(result.total)) - return result.missing.length > 0 ? `${formatted} (loaded)` : formatted - }) - const context = createMemo(() => { const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage if (!last) return @@ -99,7 +84,6 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { </text> <text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text> <text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text> - <text fg={theme.textMuted}>{cost()} spent</text> </box> <Show when={mcpEntries().length > 0}> <box> diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index 0bf703b67ada..653da09a0b7d 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -78,8 +78,4 @@ export namespace Locale { const template = count === 1 ? singular : plural return template.replace("{}", count.toString()) } - - export function costBreakdown(agentCost: string, totalCost: string): string { - return `(agent ${agentCost}; total ${totalCost})` - } } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 8f511e3e9c9d..7465e2f77c37 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -994,7 +994,6 @@ ToolRegistry.register({ {s().tokens.toLocaleString() + " tokens"} {s().usage !== undefined ? ` (${s().usage}%)` : ""} {` ($${s().total.toFixed(2)})`} - {s().missing.length > 0 ? i18n.t("ui.messagePart.loaded") : ""} </span> )} </Show> diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 89b79b7f8ff2..9b6ab0bd6d9e 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -42,8 +42,6 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "Considering next steps", "ui.messagePart.diagnostic.error": "Error", - "ui.messagePart.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "ui.messagePart.loaded": " (loaded)", "ui.messagePart.title.edit": "Edit", "ui.messagePart.title.write": "Write", "ui.messagePart.option.typeOwnAnswer": "Type your own answer", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index 2fdf848ccabb..7982b3ac75ed 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -45,8 +45,6 @@ export const dict: Record<Keys, string> = { "ui.sessionTurn.status.consideringNextSteps": "Vurderer neste trinn", "ui.messagePart.diagnostic.error": "Feil", - "ui.messagePart.costBreakdown": "(agent {{agentCost}}; totalt {{totalCost}})", - "ui.messagePart.loaded": " (lastet)", "ui.messagePart.title.edit": "Rediger", "ui.messagePart.title.write": "Skriv", "ui.messagePart.option.typeOwnAnswer": "Skriv ditt eget svar", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 7c3bc12e593f..dcb8062a3365 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -46,8 +46,6 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步", "ui.messagePart.diagnostic.error": "错误", - "ui.messagePart.costBreakdown": "(智能体 {{agentCost}}; 总计 {{totalCost}})", - "ui.messagePart.loaded": " (已加载)", "ui.messagePart.title.edit": "编辑", "ui.messagePart.title.write": "写入", "ui.messagePart.option.typeOwnAnswer": "输入自己的答案", From 1d23c0d1c5a65b5e5a37b4be56f1305730ba4a8b Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 11:38:15 +0100 Subject: [PATCH 06/16] Revert "refactor: remove cost indicators from UI and i18n" This reverts commit bc2633051a8a16336309e3b9e4ef47b355036dee. --- CHANGELOG.md | 17 ------------- CONTRIBUTING.md | 10 -------- .../src/components/session-context-usage.tsx | 25 ++++++++++++++++++- .../session/session-context-tab.tsx | 3 ++- packages/app/src/i18n/ar.ts | 5 ++++ packages/app/src/i18n/br.ts | 5 ++++ packages/app/src/i18n/bs.ts | 5 ++++ packages/app/src/i18n/da.ts | 5 ++++ packages/app/src/i18n/de.ts | 5 ++++ packages/app/src/i18n/en.ts | 5 ++++ packages/app/src/i18n/es.ts | 5 ++++ packages/app/src/i18n/fr.ts | 5 ++++ packages/app/src/i18n/ja.ts | 5 ++++ packages/app/src/i18n/ko.ts | 5 ++++ packages/app/src/i18n/no.ts | 5 ++++ packages/app/src/i18n/pl.ts | 5 ++++ packages/app/src/i18n/ru.ts | 5 ++++ packages/app/src/i18n/th.ts | 5 ++++ packages/app/src/i18n/zh.ts | 5 ++++ packages/app/src/i18n/zht.ts | 2 ++ packages/opencode/script/build.ts | 4 +-- .../src/cli/cmd/tui/routes/session/header.tsx | 19 +++++++++++--- .../cli/cmd/tui/routes/session/sidebar.tsx | 16 ++++++++++++ packages/opencode/src/util/locale.ts | 4 +++ packages/ui/src/components/message-part.tsx | 1 + packages/ui/src/i18n/en.ts | 2 ++ packages/ui/src/i18n/no.ts | 2 ++ packages/ui/src/i18n/zh.ts | 2 ++ 28 files changed, 146 insertions(+), 36 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 2b397cc30731..000000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,17 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Removed - -- Removed cost display from the session header and sidebar in the TUI. -- Removed `ownCost` and `totalCost` from the session context tab. -- Removed cost tooltips and display from session context usage. -- Removed cost display from message parts in the UI. -- Cleaned up unused i18n keys related to costs (`context.stats.ownCost`, `context.stats.totalCost`, `context.stats.costBreakdown`, `context.usage.cost`, `ui.messagePart.costBreakdown`). -- Removed `costBreakdown` utility function. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d1df0e4a95d..4bec009ef467 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,16 +64,6 @@ Then run it with: Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`). -### Updating the local CLI - -After making code changes, you can update your local `opencode` command by rebuilding the standalone executable: - -```bash -bun run ./packages/opencode/script/build.ts --single -``` - -If you have added the `dist/bin` directory to your PATH, the `opencode` command will automatically pick up the new build. On Windows, if the build fails because `opencode.exe` is in use, try closing any running opencode processes or renaming the existing `opencode.exe` to `opencode.exe.old` before rebuilding. - - Core pieces: - `packages/opencode`: OpenCode core business logic & server. - `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index a6508b8db45b..90626f3c3b75 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -36,6 +36,14 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const view = createMemo(() => layout.view(sessionKey)) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) + const usd = createMemo( + () => + new Intl.NumberFormat(language.locale(), { + style: "currency", + currency: "USD", + }), + ) + const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all, sync.data)) createEffect(() => { const missing = metrics().missing @@ -46,6 +54,13 @@ export function SessionContextUsage(props: SessionContextUsageProps) { } }) const context = createMemo(() => metrics().context) + const cost = createMemo(() => { + const value = language.t("context.stats.costBreakdown", { + agentCost: usd().format(metrics().ownCost), + totalCost: usd().format(metrics().totalCost), + }) + return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value + }) const openContext = () => { if (!params.id) return @@ -78,6 +93,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) { </> )} </Show> + <div class="flex items-center gap-2"> + <span class="text-text-invert-strong">{cost()}</span> + <span class="text-text-invert-base">{language.t("context.usage.cost")}</span> + </div> </div> ) @@ -86,7 +105,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) { <Tooltip value={tooltipValue()} placement="top"> <Switch> <Match when={variant() === "indicator"}> - <div class="flex items-center gap-1.5">{circle()}</div> + <div class="flex items-center gap-1.5"> + {circle()} + <span class="text-12-regular text-text-weak">{cost()}</span> + </div> </Match> <Match when={true}> <Button @@ -97,6 +119,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { aria-label={language.t("context.usage.view")} > {circle()} + <span class="text-12-regular text-text-weak">{cost()}</span> </Button> </Match> </Switch> diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index a17fd98f6872..70f2c4337a08 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -117,7 +117,8 @@ export function SessionContextTab(props: SessionContextTabProps) { const formatter = createMemo(() => createSessionContextFormatter(language.locale())) const cost = createMemo(() => { - return usd().format(metrics().totalCost) + const value = usd().format(metrics().totalCost) + return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value }) const counts = createMemo(() => { diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 968a4ec1a10c..5d8ead4b2dff 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -328,10 +328,15 @@ export const dict = { "context.stats.cacheTokens": "رموز التخزين المؤقت (قراءة/كتابة)", "context.stats.userMessages": "رسائل المستخدم", "context.stats.assistantMessages": "رسائل المساعد", + "context.stats.ownCost": "تكلفة الجلسة", + "context.stats.totalCost": "التكلفة الإجمالية", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (تم التحميل)", "context.stats.sessionCreated": "تم إنشاء الجلسة", "context.stats.lastActivity": "آخر نشاط", "context.usage.tokens": "رموز", "context.usage.usage": "استخدام", + "context.usage.cost": "تكلفة", "context.usage.clickToView": "انقر لعرض السياق", "context.usage.view": "عرض استخدام السياق", "language.en": "English", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 1bd3e266025c..d67f16a248a5 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -329,10 +329,15 @@ export const dict = { "context.stats.cacheTokens": "Tokens de Cache (leitura/escrita)", "context.stats.userMessages": "Mensagens de Usuário", "context.stats.assistantMessages": "Mensagens do Assistente", + "context.stats.ownCost": "Custo da sessão", + "context.stats.totalCost": "Custo Total", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (carregado)", "context.stats.sessionCreated": "Sessão Criada", "context.stats.lastActivity": "Última Atividade", "context.usage.tokens": "Tokens", "context.usage.usage": "Uso", + "context.usage.cost": "Custo", "context.usage.clickToView": "Clique para ver o contexto", "context.usage.view": "Ver uso do contexto", "language.en": "English", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 0b43b1fcd7a2..6db3ea5e81c3 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -361,11 +361,16 @@ export const dict = { "context.stats.cacheTokens": "Cache tokeni (čitanje/pisanje)", "context.stats.userMessages": "Korisničke poruke", "context.stats.assistantMessages": "Poruke asistenta", + "context.stats.ownCost": "Trošak sesije", + "context.stats.totalCost": "Ukupni trošak", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (učitano)", "context.stats.sessionCreated": "Sesija kreirana", "context.stats.lastActivity": "Posljednja aktivnost", "context.usage.tokens": "Tokeni", "context.usage.usage": "Korištenje", + "context.usage.cost": "Trošak", "context.usage.clickToView": "Klikni da vidiš kontekst", "context.usage.view": "Prikaži korištenje konteksta", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 25b65770d2c1..303fabfbcbc6 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -359,11 +359,16 @@ export const dict = { "context.stats.cacheTokens": "Cache Tokens (læs/skriv)", "context.stats.userMessages": "Brugerbeskeder", "context.stats.assistantMessages": "Assistentbeskeder", + "context.stats.ownCost": "Sessionsomkostninger", + "context.stats.totalCost": "Samlede omkostninger", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (indlæst)", "context.stats.sessionCreated": "Session oprettet", "context.stats.lastActivity": "Seneste aktivitet", "context.usage.tokens": "Tokens", "context.usage.usage": "Forbrug", + "context.usage.cost": "Omkostning", "context.usage.clickToView": "Klik for at se kontekst", "context.usage.view": "Se kontekstforbrug", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 81e157de3bac..c40f12058706 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -336,10 +336,15 @@ export const dict = { "context.stats.cacheTokens": "Cache-Token (lesen/schreiben)", "context.stats.userMessages": "Benutzernachrichten", "context.stats.assistantMessages": "Assistentennachrichten", + "context.stats.ownCost": "Sitzungskosten", + "context.stats.totalCost": "Gesamtkosten", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (geladen)", "context.stats.sessionCreated": "Sitzung erstellt", "context.stats.lastActivity": "Letzte Aktivität", "context.usage.tokens": "Token", "context.usage.usage": "Nutzung", + "context.usage.cost": "Kosten", "context.usage.clickToView": "Klicken, um Kontext anzuzeigen", "context.usage.view": "Kontextnutzung anzeigen", "language.en": "English", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index b74f98471f25..d1bcba6f09e9 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -360,11 +360,16 @@ export const dict = { "context.stats.cacheTokens": "Cache Tokens (read/write)", "context.stats.userMessages": "User Messages", "context.stats.assistantMessages": "Assistant Messages", + "context.stats.ownCost": "Session Cost", + "context.stats.totalCost": "Total Cost", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (loaded)", "context.stats.sessionCreated": "Session Created", "context.stats.lastActivity": "Last Activity", "context.usage.tokens": "Tokens", "context.usage.usage": "Usage", + "context.usage.cost": "Cost", "context.usage.clickToView": "Click to view context", "context.usage.view": "View context usage", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index eb6aef9fa697..b48fabc95ba8 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -361,11 +361,16 @@ export const dict = { "context.stats.cacheTokens": "Tokens de Caché (lectura/escritura)", "context.stats.userMessages": "Mensajes de Usuario", "context.stats.assistantMessages": "Mensajes de Asistente", + "context.stats.ownCost": "Costo de la sesión", + "context.stats.totalCost": "Costo Total", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (cargado)", "context.stats.sessionCreated": "Sesión Creada", "context.stats.lastActivity": "Última Actividad", "context.usage.tokens": "Tokens", "context.usage.usage": "Uso", + "context.usage.cost": "Costo", "context.usage.clickToView": "Haz clic para ver contexto", "context.usage.view": "Ver uso del contexto", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index e6e0366886ed..2649e96214ad 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -330,10 +330,15 @@ export const dict = { "context.stats.cacheTokens": "Jetons de cache (lecture/écriture)", "context.stats.userMessages": "Messages utilisateur", "context.stats.assistantMessages": "Messages assistant", + "context.stats.ownCost": "Coût de la session", + "context.stats.totalCost": "Coût total", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (chargé)", "context.stats.sessionCreated": "Session créée", "context.stats.lastActivity": "Dernière activité", "context.usage.tokens": "Jetons", "context.usage.usage": "Utilisation", + "context.usage.cost": "Coût", "context.usage.clickToView": "Cliquez pour voir le contexte", "context.usage.view": "Voir l'utilisation du contexte", "language.en": "English", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 92b91b203c74..23ac62d6401c 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -328,10 +328,15 @@ export const dict = { "context.stats.cacheTokens": "キャッシュトークン (読込/書込)", "context.stats.userMessages": "ユーザーメッセージ", "context.stats.assistantMessages": "アシスタントメッセージ", + "context.stats.ownCost": "セッションコスト", + "context.stats.totalCost": "総コスト", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (ロード済み)", "context.stats.sessionCreated": "セッション作成日時", "context.stats.lastActivity": "最終アクティビティ", "context.usage.tokens": "トークン", "context.usage.usage": "使用量", + "context.usage.cost": "コスト", "context.usage.clickToView": "クリックしてコンテキストを表示", "context.usage.view": "コンテキスト使用量を表示", "language.en": "English", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 98c58764ded8..93020ba07bb4 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -331,10 +331,15 @@ export const dict = { "context.stats.cacheTokens": "캐시 토큰 (읽기/쓰기)", "context.stats.userMessages": "사용자 메시지", "context.stats.assistantMessages": "어시스턴트 메시지", + "context.stats.ownCost": "세션 비용", + "context.stats.totalCost": "총 비용", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (로드됨)", "context.stats.sessionCreated": "세션 생성됨", "context.stats.lastActivity": "최근 활동", "context.usage.tokens": "토큰", "context.usage.usage": "사용량", + "context.usage.cost": "비용", "context.usage.clickToView": "컨텍스트를 보려면 클릭", "context.usage.view": "컨텍스트 사용량 보기", "language.en": "English", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index be402015b2e5..ebebd7da6ca1 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -362,11 +362,16 @@ export const dict = { "context.stats.cacheTokens": "Cache-tokens (les/skriv)", "context.stats.userMessages": "Brukermeldinger", "context.stats.assistantMessages": "Assistentmeldinger", + "context.stats.ownCost": "Sesjonskostnad", + "context.stats.totalCost": "Total kostnad", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (lastet)", "context.stats.sessionCreated": "Sesjon opprettet", "context.stats.lastActivity": "Siste aktivitet", "context.usage.tokens": "Tokens", "context.usage.usage": "Forbruk", + "context.usage.cost": "Kostnad", "context.usage.clickToView": "Klikk for å se kontekst", "context.usage.view": "Se kontekstforbruk", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 57978f89552b..439d64658247 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -329,10 +329,15 @@ export const dict = { "context.stats.cacheTokens": "Tokeny pamięci podręcznej (odczyt/zapis)", "context.stats.userMessages": "Wiadomości użytkownika", "context.stats.assistantMessages": "Wiadomości asystenta", + "context.stats.ownCost": "Koszt sesji", + "context.stats.totalCost": "Całkowity koszt", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (załadowano)", "context.stats.sessionCreated": "Utworzono sesję", "context.stats.lastActivity": "Ostatnia aktywność", "context.usage.tokens": "Tokeny", "context.usage.usage": "Użycie", + "context.usage.cost": "Koszt", "context.usage.clickToView": "Kliknij, aby zobaczyć kontekst", "context.usage.view": "Pokaż użycie kontekstu", "language.en": "English", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 4e5db231f4cf..fed59cc662a5 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -361,11 +361,16 @@ export const dict = { "context.stats.cacheTokens": "Токены кэша (чтение/запись)", "context.stats.userMessages": "Сообщения пользователя", "context.stats.assistantMessages": "Сообщения ассистента", + "context.stats.ownCost": "Стоимость сессии", + "context.stats.totalCost": "Общая стоимость", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (загружено)", "context.stats.sessionCreated": "Сессия создана", "context.stats.lastActivity": "Последняя активность", "context.usage.tokens": "Токены", "context.usage.usage": "Использование", + "context.usage.cost": "Стоимость", "context.usage.clickToView": "Нажмите для просмотра контекста", "context.usage.view": "Показать использование контекста", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index aac8d624ad42..f68609c818c7 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -359,11 +359,16 @@ export const dict = { "context.stats.cacheTokens": "โทเค็นแคช (อ่าน/เขียน)", "context.stats.userMessages": "ข้อความผู้ใช้", "context.stats.assistantMessages": "ข้อความผู้ช่วย", + "context.stats.ownCost": "ค่าใช้จ่ายเซสชัน", + "context.stats.totalCost": "ต้นทุนทั้งหมด", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (โหลดแล้ว)", "context.stats.sessionCreated": "สร้างเซสชันเมื่อ", "context.stats.lastActivity": "กิจกรรมล่าสุด", "context.usage.tokens": "โทเค็น", "context.usage.usage": "การใช้งาน", + "context.usage.cost": "ต้นทุน", "context.usage.clickToView": "คลิกเพื่อดูบริบท", "context.usage.view": "ดูการใช้บริบท", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 68c219fbf667..047f791f0fef 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -374,10 +374,15 @@ export const dict = { "context.stats.cacheTokens": "缓存 token(读/写)", "context.stats.userMessages": "用户消息", "context.stats.assistantMessages": "助手消息", + "context.stats.ownCost": "会话成本", + "context.stats.totalCost": "总成本", + "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "context.stats.loaded": " (已加载)", "context.stats.sessionCreated": "创建时间", "context.stats.lastActivity": "最后活动", "context.usage.tokens": "Token", "context.usage.usage": "使用率", + "context.usage.cost": "成本", "context.usage.clickToView": "点击查看上下文", "context.usage.view": "查看上下文用量", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index c818b0a14c2e..34aec01b9cb1 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -358,11 +358,13 @@ export const dict = { "context.stats.cacheTokens": "快取 token(讀/寫)", "context.stats.userMessages": "使用者訊息", "context.stats.assistantMessages": "助手訊息", + "context.stats.totalCost": "總成本", "context.stats.sessionCreated": "建立時間", "context.stats.lastActivity": "最後活動", "context.usage.tokens": "Token", "context.usage.usage": "使用量", + "context.usage.cost": "成本", "context.usage.clickToView": "點擊查看上下文", "context.usage.view": "檢視上下文用量", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 5074e196eaa2..ddb4769912d7 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -135,9 +135,7 @@ const targets = singleFlag }) : allTargets -try { - await $`rm -rf dist` -} catch (e) {} +await $`rm -rf dist` const binaries: Record<string, string> = {} if (!skipInstall) { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 74a0e0442526..9fde969e52dc 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -1,6 +1,7 @@ import { type Accessor, createMemo, createSignal, Match, Show, Switch } from "solid-js" import { useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" +import { costs } from "@opencode-ai/util/cost" import { pipe, sumBy } from "remeda" import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" @@ -19,12 +20,12 @@ const Title = (props: { session: Accessor<Session> }) => { ) } -const ContextInfo = (props: { context: Accessor<string | undefined> }) => { +const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Accessor<string> }) => { const { theme } = useTheme() return ( <Show when={props.context()}> <text fg={theme.textMuted} wrapMode="none" flexShrink={0}> - {props.context()} + {props.context()} {props.cost()} </text> </Show> ) @@ -36,6 +37,16 @@ export function Header() { const session = createMemo(() => sync.session.get(route.sessionID)!) const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) + const cost = createMemo(() => { + const result = costs(route.sessionID, sync.data) + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }) + const formatted = Locale.costBreakdown(formatter.format(result.own), formatter.format(result.total)) + return result.missing.length > 0 ? `${formatted} (loaded)` : formatted + }) + const context = createMemo(() => { const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage if (!last) return @@ -76,7 +87,7 @@ export function Header() { <text fg={theme.text}> <b>Subagent session</b> </text> - <ContextInfo context={context} /> + <ContextInfo context={context} cost={cost} /> </box> <box flexDirection="row" gap={2}> <box @@ -115,7 +126,7 @@ export function Header() { <Match when={true}> <box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={1}> <Title session={session} /> - <ContextInfo context={context} /> + <ContextInfo context={context} cost={cost} /> </box> </Match> </Switch> diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index cd62ecbf502c..b6f667efa944 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -1,9 +1,14 @@ import { useSync } from "@tui/context/sync" +import { costs } from "@opencode-ai/util/cost" import { createMemo, For, Show, Switch, Match } from "solid-js" import { createStore } from "solid-js/store" import { useTheme } from "../../context/theme" +import { Locale } from "@/util/locale" +import path from "path" import type { AssistantMessage } from "@opencode-ai/sdk/v2" +import { Global } from "@/global" import { Installation } from "@/installation" +import { useKeybind } from "../../context/keybind" import { useDirectory } from "../../context/directory" import { useKV } from "../../context/kv" import { TodoItem } from "../../component/todo-item" @@ -36,6 +41,16 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ).length, ) + const cost = createMemo(() => { + const result = costs(props.sessionID, sync.data) + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }) + const formatted = Locale.costBreakdown(formatter.format(result.own), formatter.format(result.total)) + return result.missing.length > 0 ? `${formatted} (loaded)` : formatted + }) + const context = createMemo(() => { const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage if (!last) return @@ -84,6 +99,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { </text> <text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text> <text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text> + <text fg={theme.textMuted}>{cost()} spent</text> </box> <Show when={mcpEntries().length > 0}> <box> diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index 653da09a0b7d..0bf703b67ada 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -78,4 +78,8 @@ export namespace Locale { const template = count === 1 ? singular : plural return template.replace("{}", count.toString()) } + + export function costBreakdown(agentCost: string, totalCost: string): string { + return `(agent ${agentCost}; total ${totalCost})` + } } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 7465e2f77c37..8f511e3e9c9d 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -994,6 +994,7 @@ ToolRegistry.register({ {s().tokens.toLocaleString() + " tokens"} {s().usage !== undefined ? ` (${s().usage}%)` : ""} {` ($${s().total.toFixed(2)})`} + {s().missing.length > 0 ? i18n.t("ui.messagePart.loaded") : ""} </span> )} </Show> diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 9b6ab0bd6d9e..89b79b7f8ff2 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -42,6 +42,8 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "Considering next steps", "ui.messagePart.diagnostic.error": "Error", + "ui.messagePart.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", + "ui.messagePart.loaded": " (loaded)", "ui.messagePart.title.edit": "Edit", "ui.messagePart.title.write": "Write", "ui.messagePart.option.typeOwnAnswer": "Type your own answer", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index 7982b3ac75ed..2fdf848ccabb 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -45,6 +45,8 @@ export const dict: Record<Keys, string> = { "ui.sessionTurn.status.consideringNextSteps": "Vurderer neste trinn", "ui.messagePart.diagnostic.error": "Feil", + "ui.messagePart.costBreakdown": "(agent {{agentCost}}; totalt {{totalCost}})", + "ui.messagePart.loaded": " (lastet)", "ui.messagePart.title.edit": "Rediger", "ui.messagePart.title.write": "Skriv", "ui.messagePart.option.typeOwnAnswer": "Skriv ditt eget svar", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index dcb8062a3365..7c3bc12e593f 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -46,6 +46,8 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步", "ui.messagePart.diagnostic.error": "错误", + "ui.messagePart.costBreakdown": "(智能体 {{agentCost}}; 总计 {{totalCost}})", + "ui.messagePart.loaded": " (已加载)", "ui.messagePart.title.edit": "编辑", "ui.messagePart.title.write": "写入", "ui.messagePart.option.typeOwnAnswer": "输入自己的答案", From 2b048c92c2b6537e6999e68eff5c5457a07c1dac Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 12:02:18 +0100 Subject: [PATCH 07/16] refactor: simplify cost display by removing agent cost and loaded status indicators --- .../app/src/components/session-context-usage.tsx | 6 +----- .../src/components/session/session-context-tab.tsx | 3 +-- packages/app/src/i18n/ar.ts | 3 --- packages/app/src/i18n/br.ts | 4 +--- packages/app/src/i18n/bs.ts | 4 ---- packages/app/src/i18n/da.ts | 3 --- packages/app/src/i18n/de.ts | 3 --- packages/app/src/i18n/en.ts | 3 --- packages/app/src/i18n/es.ts | 3 --- packages/app/src/i18n/fr.ts | 3 --- packages/app/src/i18n/ja.ts | 3 --- packages/app/src/i18n/ko.ts | 3 --- packages/app/src/i18n/no.ts | 3 --- packages/app/src/i18n/pl.ts | 3 --- packages/app/src/i18n/ru.ts | 3 --- packages/app/src/i18n/th.ts | 3 --- packages/app/src/i18n/zh.ts | 3 --- .../src/cli/cmd/tui/routes/session/header.tsx | 14 +++++++------- .../src/cli/cmd/tui/routes/session/sidebar.tsx | 8 +++----- packages/opencode/src/util/locale.ts | 4 ---- packages/ui/src/components/message-part.tsx | 1 - packages/ui/src/i18n/en.ts | 2 -- packages/ui/src/i18n/no.ts | 2 -- packages/ui/src/i18n/zh.ts | 2 -- 24 files changed, 13 insertions(+), 76 deletions(-) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 90626f3c3b75..7d84d9798f2b 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -55,11 +55,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { }) const context = createMemo(() => metrics().context) const cost = createMemo(() => { - const value = language.t("context.stats.costBreakdown", { - agentCost: usd().format(metrics().ownCost), - totalCost: usd().format(metrics().totalCost), - }) - return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value + return usd().format(metrics().totalCost) }) const openContext = () => { diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 70f2c4337a08..340152ccddcb 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -118,7 +118,7 @@ export function SessionContextTab(props: SessionContextTabProps) { const cost = createMemo(() => { const value = usd().format(metrics().totalCost) - return metrics().missing.length > 0 ? `${value}${language.t("context.stats.loaded")}` : value + return value }) const counts = createMemo(() => { @@ -194,7 +194,6 @@ export function SessionContextTab(props: SessionContextTabProps) { }, { label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) }, { label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) }, - { label: "context.stats.ownCost", value: () => formatter().currency(metrics().ownCost) }, { label: "context.stats.totalCost", value: cost }, { label: "context.stats.sessionCreated", value: () => formatter().time(props.info()?.time.created) }, { label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) }, diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 5d8ead4b2dff..e3792a3c3cc3 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -328,10 +328,7 @@ export const dict = { "context.stats.cacheTokens": "رموز التخزين المؤقت (قراءة/كتابة)", "context.stats.userMessages": "رسائل المستخدم", "context.stats.assistantMessages": "رسائل المساعد", - "context.stats.ownCost": "تكلفة الجلسة", "context.stats.totalCost": "التكلفة الإجمالية", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (تم التحميل)", "context.stats.sessionCreated": "تم إنشاء الجلسة", "context.stats.lastActivity": "آخر نشاط", "context.usage.tokens": "رموز", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index d67f16a248a5..dc2f725c4b2b 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -329,10 +329,7 @@ export const dict = { "context.stats.cacheTokens": "Tokens de Cache (leitura/escrita)", "context.stats.userMessages": "Mensagens de Usuário", "context.stats.assistantMessages": "Mensagens do Assistente", - "context.stats.ownCost": "Custo da sessão", "context.stats.totalCost": "Custo Total", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (carregado)", "context.stats.sessionCreated": "Sessão Criada", "context.stats.lastActivity": "Última Atividade", "context.usage.tokens": "Tokens", @@ -340,6 +337,7 @@ export const dict = { "context.usage.cost": "Custo", "context.usage.clickToView": "Clique para ver o contexto", "context.usage.view": "Ver uso do contexto", + "language.en": "English", "language.zh": "简体中文", "language.zht": "繁體中文", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 6db3ea5e81c3..919428469850 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -361,13 +361,9 @@ export const dict = { "context.stats.cacheTokens": "Cache tokeni (čitanje/pisanje)", "context.stats.userMessages": "Korisničke poruke", "context.stats.assistantMessages": "Poruke asistenta", - "context.stats.ownCost": "Trošak sesije", "context.stats.totalCost": "Ukupni trošak", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (učitano)", "context.stats.sessionCreated": "Sesija kreirana", "context.stats.lastActivity": "Posljednja aktivnost", - "context.usage.tokens": "Tokeni", "context.usage.usage": "Korištenje", "context.usage.cost": "Trošak", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 303fabfbcbc6..ac5c4d494b12 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -359,10 +359,7 @@ export const dict = { "context.stats.cacheTokens": "Cache Tokens (læs/skriv)", "context.stats.userMessages": "Brugerbeskeder", "context.stats.assistantMessages": "Assistentbeskeder", - "context.stats.ownCost": "Sessionsomkostninger", "context.stats.totalCost": "Samlede omkostninger", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (indlæst)", "context.stats.sessionCreated": "Session oprettet", "context.stats.lastActivity": "Seneste aktivitet", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index c40f12058706..99a950631074 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -336,10 +336,7 @@ export const dict = { "context.stats.cacheTokens": "Cache-Token (lesen/schreiben)", "context.stats.userMessages": "Benutzernachrichten", "context.stats.assistantMessages": "Assistentennachrichten", - "context.stats.ownCost": "Sitzungskosten", "context.stats.totalCost": "Gesamtkosten", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (geladen)", "context.stats.sessionCreated": "Sitzung erstellt", "context.stats.lastActivity": "Letzte Aktivität", "context.usage.tokens": "Token", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index d1bcba6f09e9..99513edaa173 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -360,10 +360,7 @@ export const dict = { "context.stats.cacheTokens": "Cache Tokens (read/write)", "context.stats.userMessages": "User Messages", "context.stats.assistantMessages": "Assistant Messages", - "context.stats.ownCost": "Session Cost", "context.stats.totalCost": "Total Cost", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (loaded)", "context.stats.sessionCreated": "Session Created", "context.stats.lastActivity": "Last Activity", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index b48fabc95ba8..7a6c4974e091 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -361,10 +361,7 @@ export const dict = { "context.stats.cacheTokens": "Tokens de Caché (lectura/escritura)", "context.stats.userMessages": "Mensajes de Usuario", "context.stats.assistantMessages": "Mensajes de Asistente", - "context.stats.ownCost": "Costo de la sesión", "context.stats.totalCost": "Costo Total", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (cargado)", "context.stats.sessionCreated": "Sesión Creada", "context.stats.lastActivity": "Última Actividad", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 2649e96214ad..fc3bf2667943 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -330,10 +330,7 @@ export const dict = { "context.stats.cacheTokens": "Jetons de cache (lecture/écriture)", "context.stats.userMessages": "Messages utilisateur", "context.stats.assistantMessages": "Messages assistant", - "context.stats.ownCost": "Coût de la session", "context.stats.totalCost": "Coût total", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (chargé)", "context.stats.sessionCreated": "Session créée", "context.stats.lastActivity": "Dernière activité", "context.usage.tokens": "Jetons", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 23ac62d6401c..b597db02a586 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -328,10 +328,7 @@ export const dict = { "context.stats.cacheTokens": "キャッシュトークン (読込/書込)", "context.stats.userMessages": "ユーザーメッセージ", "context.stats.assistantMessages": "アシスタントメッセージ", - "context.stats.ownCost": "セッションコスト", "context.stats.totalCost": "総コスト", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (ロード済み)", "context.stats.sessionCreated": "セッション作成日時", "context.stats.lastActivity": "最終アクティビティ", "context.usage.tokens": "トークン", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 93020ba07bb4..525bd035651f 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -331,10 +331,7 @@ export const dict = { "context.stats.cacheTokens": "캐시 토큰 (읽기/쓰기)", "context.stats.userMessages": "사용자 메시지", "context.stats.assistantMessages": "어시스턴트 메시지", - "context.stats.ownCost": "세션 비용", "context.stats.totalCost": "총 비용", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (로드됨)", "context.stats.sessionCreated": "세션 생성됨", "context.stats.lastActivity": "최근 활동", "context.usage.tokens": "토큰", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index ebebd7da6ca1..98e79e1896af 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -362,10 +362,7 @@ export const dict = { "context.stats.cacheTokens": "Cache-tokens (les/skriv)", "context.stats.userMessages": "Brukermeldinger", "context.stats.assistantMessages": "Assistentmeldinger", - "context.stats.ownCost": "Sesjonskostnad", "context.stats.totalCost": "Total kostnad", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (lastet)", "context.stats.sessionCreated": "Sesjon opprettet", "context.stats.lastActivity": "Siste aktivitet", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 439d64658247..983c9c14ac1b 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -329,10 +329,7 @@ export const dict = { "context.stats.cacheTokens": "Tokeny pamięci podręcznej (odczyt/zapis)", "context.stats.userMessages": "Wiadomości użytkownika", "context.stats.assistantMessages": "Wiadomości asystenta", - "context.stats.ownCost": "Koszt sesji", "context.stats.totalCost": "Całkowity koszt", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (załadowano)", "context.stats.sessionCreated": "Utworzono sesję", "context.stats.lastActivity": "Ostatnia aktywność", "context.usage.tokens": "Tokeny", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index fed59cc662a5..f2c87fe0f1ed 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -361,10 +361,7 @@ export const dict = { "context.stats.cacheTokens": "Токены кэша (чтение/запись)", "context.stats.userMessages": "Сообщения пользователя", "context.stats.assistantMessages": "Сообщения ассистента", - "context.stats.ownCost": "Стоимость сессии", "context.stats.totalCost": "Общая стоимость", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (загружено)", "context.stats.sessionCreated": "Сессия создана", "context.stats.lastActivity": "Последняя активность", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index f68609c818c7..689e82118968 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -359,10 +359,7 @@ export const dict = { "context.stats.cacheTokens": "โทเค็นแคช (อ่าน/เขียน)", "context.stats.userMessages": "ข้อความผู้ใช้", "context.stats.assistantMessages": "ข้อความผู้ช่วย", - "context.stats.ownCost": "ค่าใช้จ่ายเซสชัน", "context.stats.totalCost": "ต้นทุนทั้งหมด", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (โหลดแล้ว)", "context.stats.sessionCreated": "สร้างเซสชันเมื่อ", "context.stats.lastActivity": "กิจกรรมล่าสุด", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 047f791f0fef..1b40013b60de 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -374,10 +374,7 @@ export const dict = { "context.stats.cacheTokens": "缓存 token(读/写)", "context.stats.userMessages": "用户消息", "context.stats.assistantMessages": "助手消息", - "context.stats.ownCost": "会话成本", "context.stats.totalCost": "总成本", - "context.stats.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "context.stats.loaded": " (已加载)", "context.stats.sessionCreated": "创建时间", "context.stats.lastActivity": "最后活动", "context.usage.tokens": "Token", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 9fde969e52dc..09523d2200cb 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -9,7 +9,6 @@ import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2" import { useCommandDialog } from "@tui/component/dialog-command" import { useKeybind } from "../../context/keybind" import { useTerminalDimensions } from "@opentui/solid" -import { Locale } from "@/util/locale" const Title = (props: { session: Accessor<Session> }) => { const { theme } = useTheme() @@ -25,7 +24,7 @@ const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Acces return ( <Show when={props.context()}> <text fg={theme.textMuted} wrapMode="none" flexShrink={0}> - {props.context()} {props.cost()} + {props.context()} ({props.cost()}) </text> </Show> ) @@ -38,13 +37,14 @@ export function Header() { const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const cost = createMemo(() => { - const result = costs(route.sessionID, sync.data) - const formatter = new Intl.NumberFormat("en-US", { + const total = pipe( + messages(), + sumBy((x) => (x.role === "assistant" ? x.cost : 0)), + ) + return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", - }) - const formatted = Locale.costBreakdown(formatter.format(result.own), formatter.format(result.total)) - return result.missing.length > 0 ? `${formatted} (loaded)` : formatted + }).format(total) }) const context = createMemo(() => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index b6f667efa944..c59656e375be 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -42,13 +42,11 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ) const cost = createMemo(() => { - const result = costs(props.sessionID, sync.data) - const formatter = new Intl.NumberFormat("en-US", { + const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) + return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", - }) - const formatted = Locale.costBreakdown(formatter.format(result.own), formatter.format(result.total)) - return result.missing.length > 0 ? `${formatted} (loaded)` : formatted + }).format(total) }) const context = createMemo(() => { diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index 0bf703b67ada..653da09a0b7d 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -78,8 +78,4 @@ export namespace Locale { const template = count === 1 ? singular : plural return template.replace("{}", count.toString()) } - - export function costBreakdown(agentCost: string, totalCost: string): string { - return `(agent ${agentCost}; total ${totalCost})` - } } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 8f511e3e9c9d..7465e2f77c37 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -994,7 +994,6 @@ ToolRegistry.register({ {s().tokens.toLocaleString() + " tokens"} {s().usage !== undefined ? ` (${s().usage}%)` : ""} {` ($${s().total.toFixed(2)})`} - {s().missing.length > 0 ? i18n.t("ui.messagePart.loaded") : ""} </span> )} </Show> diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 89b79b7f8ff2..9b6ab0bd6d9e 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -42,8 +42,6 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "Considering next steps", "ui.messagePart.diagnostic.error": "Error", - "ui.messagePart.costBreakdown": "(agent {{agentCost}}; total {{totalCost}})", - "ui.messagePart.loaded": " (loaded)", "ui.messagePart.title.edit": "Edit", "ui.messagePart.title.write": "Write", "ui.messagePart.option.typeOwnAnswer": "Type your own answer", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index 2fdf848ccabb..7982b3ac75ed 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -45,8 +45,6 @@ export const dict: Record<Keys, string> = { "ui.sessionTurn.status.consideringNextSteps": "Vurderer neste trinn", "ui.messagePart.diagnostic.error": "Feil", - "ui.messagePart.costBreakdown": "(agent {{agentCost}}; totalt {{totalCost}})", - "ui.messagePart.loaded": " (lastet)", "ui.messagePart.title.edit": "Rediger", "ui.messagePart.title.write": "Skriv", "ui.messagePart.option.typeOwnAnswer": "Skriv ditt eget svar", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 7c3bc12e593f..dcb8062a3365 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -46,8 +46,6 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步", "ui.messagePart.diagnostic.error": "错误", - "ui.messagePart.costBreakdown": "(智能体 {{agentCost}}; 总计 {{totalCost}})", - "ui.messagePart.loaded": " (已加载)", "ui.messagePart.title.edit": "编辑", "ui.messagePart.title.write": "写入", "ui.messagePart.option.typeOwnAnswer": "输入自己的答案", From 65b34e5024bade6d2ade9e67f58ba9ee2b1ef4dd Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 12:26:25 +0100 Subject: [PATCH 08/16] clean up --- packages/app/src/custom-elements.d.ts | 2 +- packages/app/src/i18n/br.ts | 1 - packages/app/src/i18n/bs.ts | 1 + packages/enterprise/src/app.tsx | 3 +-- packages/enterprise/src/custom-elements.d.ts | 2 +- packages/opencode/src/cli/cmd/tui/routes/session/header.tsx | 6 ++---- .../opencode/src/cli/cmd/tui/routes/session/sidebar.tsx | 3 ++- 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts index 86cb284693e6..e4ea0d6cebda 100644 --- a/packages/app/src/custom-elements.d.ts +++ b/packages/app/src/custom-elements.d.ts @@ -1 +1 @@ -/// <reference path="../../ui/src/custom-elements.d.ts" /> +../../ui/src/custom-elements.d.ts \ No newline at end of file diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index dc2f725c4b2b..07d6ce467aeb 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -337,7 +337,6 @@ export const dict = { "context.usage.cost": "Custo", "context.usage.clickToView": "Clique para ver o contexto", "context.usage.view": "Ver uso do contexto", - "language.en": "English", "language.zh": "简体中文", "language.zht": "繁體中文", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 919428469850..7d10da6ed875 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -364,6 +364,7 @@ export const dict = { "context.stats.totalCost": "Ukupni trošak", "context.stats.sessionCreated": "Sesija kreirana", "context.stats.lastActivity": "Posljednja aktivnost", + "context.usage.tokens": "Tokeni", "context.usage.usage": "Korištenje", "context.usage.cost": "Trošak", diff --git a/packages/enterprise/src/app.tsx b/packages/enterprise/src/app.tsx index 72bf0f7965bd..6f9cdcafb893 100644 --- a/packages/enterprise/src/app.tsx +++ b/packages/enterprise/src/app.tsx @@ -57,8 +57,7 @@ function detectLocale() { function UiI18nBridge(props: ParentProps) { const locale = createMemo(() => detectLocale()) const t = (key: keyof typeof uiEn, params?: UiI18nParams) => { - const zhDict = uiZh as Partial<Record<keyof typeof uiEn, string>> - const value = locale() === "zh" ? (zhDict[key] ?? uiEn[key]) : uiEn[key] + const value = locale() === "zh" ? (uiZh[key] ?? uiEn[key]) : uiEn[key] const text = value ?? String(key) return resolveTemplate(text, params) } diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts index 86cb284693e6..e4ea0d6cebda 100644 --- a/packages/enterprise/src/custom-elements.d.ts +++ b/packages/enterprise/src/custom-elements.d.ts @@ -1 +1 @@ -/// <reference path="../../ui/src/custom-elements.d.ts" /> +../../ui/src/custom-elements.d.ts \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 09523d2200cb..3125c8088d56 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -37,10 +37,8 @@ export function Header() { const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const cost = createMemo(() => { - const total = pipe( - messages(), - sumBy((x) => (x.role === "assistant" ? x.cost : 0)), - ) + const result = costs(route.sessionID, sync.data) + const total = result.total return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index c59656e375be..c224543ace74 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -42,7 +42,8 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ) const cost = createMemo(() => { - const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) + const result = costs(route.sessionID, sync.data) + const total = result.total return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", From bb8c73697398504f3ecb861c2dd6f22432028532 Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 12:28:20 +0100 Subject: [PATCH 09/16] fix(types): update custom-elements.d.ts reference --- packages/app/src/custom-elements.d.ts | 2 +- packages/enterprise/src/custom-elements.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts index e4ea0d6cebda..86cb284693e6 100644 --- a/packages/app/src/custom-elements.d.ts +++ b/packages/app/src/custom-elements.d.ts @@ -1 +1 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file +/// <reference path="../../ui/src/custom-elements.d.ts" /> diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts index e4ea0d6cebda..86cb284693e6 100644 --- a/packages/enterprise/src/custom-elements.d.ts +++ b/packages/enterprise/src/custom-elements.d.ts @@ -1 +1 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file +/// <reference path="../../ui/src/custom-elements.d.ts" /> From 4e8e78f8dcda00a0970b09e6d21ef31454a67ffa Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 12:28:41 +0100 Subject: [PATCH 10/16] fix(tui): use correct props variable in sidebar --- packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index c224543ace74..fd42e6ee35b3 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -42,7 +42,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ) const cost = createMemo(() => { - const result = costs(route.sessionID, sync.data) + const result = costs(props.sessionID, sync.data) const total = result.total return new Intl.NumberFormat("en-US", { style: "currency", From bff8a10a0ed5b79eb210876276ff281e1a7e7b2b Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 12:55:53 +0100 Subject: [PATCH 11/16] chore: restore custom-elements.d.ts as symlinks to match dev --- packages/app/src/custom-elements.d.ts | 2 +- packages/enterprise/src/custom-elements.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 120000 packages/app/src/custom-elements.d.ts mode change 100644 => 120000 packages/enterprise/src/custom-elements.d.ts diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts deleted file mode 100644 index 86cb284693e6..000000000000 --- a/packages/app/src/custom-elements.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference path="../../ui/src/custom-elements.d.ts" /> diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts new file mode 120000 index 000000000000..e4ea0d6cebda --- /dev/null +++ b/packages/app/src/custom-elements.d.ts @@ -0,0 +1 @@ +../../ui/src/custom-elements.d.ts \ No newline at end of file diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts deleted file mode 100644 index 86cb284693e6..000000000000 --- a/packages/enterprise/src/custom-elements.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference path="../../ui/src/custom-elements.d.ts" /> diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts new file mode 120000 index 000000000000..e4ea0d6cebda --- /dev/null +++ b/packages/enterprise/src/custom-elements.d.ts @@ -0,0 +1 @@ +../../ui/src/custom-elements.d.ts \ No newline at end of file From 27ce8001b56c14fbf571c1d28b16abd94aa6f599 Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 13:16:47 +0100 Subject: [PATCH 12/16] refactor: prune unnecessary changes from cost breakdown PR --- .../app/src/components/session/session-context-format.ts | 9 --------- packages/opencode/src/session/summary.ts | 3 +-- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/app/src/components/session/session-context-format.ts b/packages/app/src/components/session/session-context-format.ts index 58a14bcd0f0f..e7c536d58411 100644 --- a/packages/app/src/components/session/session-context-format.ts +++ b/packages/app/src/components/session/session-context-format.ts @@ -1,10 +1,6 @@ import { DateTime } from "luxon" export function createSessionContextFormatter(locale: string) { - const usd = new Intl.NumberFormat(locale, { - style: "currency", - currency: "USD", - }) return { number(value: number | null | undefined) { if (value === undefined) return "—" @@ -16,11 +12,6 @@ export function createSessionContextFormatter(locale: string) { if (value === null) return "—" return value.toLocaleString(locale) + "%" }, - currency(value: number | null | undefined) { - if (value === undefined) return "—" - if (value === null) return "—" - return usd.format(value) - }, time(value: number | undefined) { if (!value) return "—" return DateTime.fromMillis(value).setLocale(locale).toLocaleString(DateTime.DATETIME_MED) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index f09f2b32aa26..c3e14ddd691f 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -109,8 +109,7 @@ export namespace SessionSummary { const messages = input.messages.filter( (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID), ) - const msgWithParts = messages.find((m) => m.info.id === input.messageID) - if (!msgWithParts) return + const msgWithParts = messages.find((m) => m.info.id === input.messageID)! const userMsg = msgWithParts.info as MessageV2.User const diffs = await computeDiff({ messages }) userMsg.summary = { From a2c39abb277d87a7c02e097d59a4b15112485509 Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 13:56:07 +0100 Subject: [PATCH 13/16] refactor: prune noisy changes and inline redundant variables in cost breakdown --- .../session/session-context-metrics.ts | 36 +++++++++++-------- .../session/session-context-tab.tsx | 3 +- .../src/cli/cmd/tui/routes/session/header.tsx | 5 ++- .../src/cli/cmd/tui/routes/session/index.tsx | 12 +------ .../cli/cmd/tui/routes/session/sidebar.tsx | 3 +- packages/ui/src/components/message-part.tsx | 2 +- 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/packages/app/src/components/session/session-context-metrics.ts b/packages/app/src/components/session/session-context-metrics.ts index cd21a74f585a..2846e5db6040 100644 --- a/packages/app/src/components/session/session-context-metrics.ts +++ b/packages/app/src/components/session/session-context-metrics.ts @@ -41,23 +41,29 @@ 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[]) => - messages.findLast( - (msg): msg is AssistantMessage => msg.role === "assistant" && tokenTotal(msg as AssistantMessage) > 0, - ) +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 as AssistantMessage) <= 0) continue + return msg as AssistantMessage + } +} const build = (messages: Message[] = [], providers: Provider[] = [], store?: CostStore): Metrics => { const id = messages[0]?.sessionID const result = store && id ? costs(id, store) : undefined - const totalCost = result - ? result.total - : messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0) - const ownCost = result ? result.own : totalCost - const missing = result ? result.missing : [] - const message = lastAssistantWithTokens(messages) - if (!message) return { totalCost, ownCost, missing, context: undefined } + if (!message) + return { + totalCost: result + ? result.total + : messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0), + ownCost: result ? result.own : 0, + missing: result ? result.missing : [], + context: undefined, + } const provider = providers.find((item) => item.id === message.providerID) const model = provider?.models[message.modelID] @@ -65,9 +71,11 @@ const build = (messages: Message[] = [], providers: Provider[] = [], store?: Cos const total = tokenTotal(message) return { - totalCost, - ownCost, - missing, + totalCost: result + ? result.total + : messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0), + ownCost: result ? result.own : 0, + missing: result ? result.missing : [], context: { message, provider, diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 340152ccddcb..99e77415368b 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -117,8 +117,7 @@ export function SessionContextTab(props: SessionContextTabProps) { const formatter = createMemo(() => createSessionContextFormatter(language.locale())) const cost = createMemo(() => { - const value = usd().format(metrics().totalCost) - return value + return usd().format(metrics().totalCost) }) const counts = createMemo(() => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 3125c8088d56..e605c5b98673 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -37,12 +37,11 @@ export function Header() { const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const cost = createMemo(() => { - const result = costs(route.sessionID, sync.data) - const total = result.total + const result = costs(route.sessionID, sync.data) return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", - }).format(total) + }).format(result.total) }) const context = createMemo(() => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 5eb2ff37ac0c..06b9d40d4151 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1892,23 +1892,13 @@ function Task(props: ToolProps<typeof TaskTool>) { <Show when={props.metadata.sessionId}> {(sessionId) => { const result = createMemo(() => costs(sessionId(), sync.data)) - const own = createMemo(() => - new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(result().own), - ) const total = createMemo(() => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(result().total), ) - return ( - <text style={{ fg: theme.textMuted }}> - ({total}) - </text> - ) + return <text style={{ fg: theme.textMuted }}>({total})</text> }} </Show> <Show when={current()}> diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index fd42e6ee35b3..c005bfcf310c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -43,11 +43,10 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { const cost = createMemo(() => { const result = costs(props.sessionID, sync.data) - const total = result.total return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", - }).format(total) + }).format(result.total) }) const context = createMemo(() => { diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 7465e2f77c37..f4862b48accb 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -957,7 +957,7 @@ ToolRegistry.register({ : undefined const usage = limit ? Math.round((tokens / limit) * 100) : undefined const result = costs(sid, data.store) - return { tokens, usage, own: result.own, total: result.total, missing: result.missing } + return { tokens, usage, total: result.total, missing: result.missing } }) createEffect(() => { const missing = stats()?.missing From 1a9101e12010d69dda62a591fcc420d98d0600e1 Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 14:13:43 +0100 Subject: [PATCH 14/16] chore: trigger CI rerun From b074e1295c72f5755aa7ab45be5c49e241113195 Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 14:14:40 +0100 Subject: [PATCH 15/16] fix(enterprise): fix type error in custom-elements.d.ts to unblock CI --- packages/enterprise/src/custom-elements.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts index e4ea0d6cebda..86cb284693e6 120000 --- a/packages/enterprise/src/custom-elements.d.ts +++ b/packages/enterprise/src/custom-elements.d.ts @@ -1 +1 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file +/// <reference path="../../ui/src/custom-elements.d.ts" /> From 33bcb81d1ce5f3f2c9c94e92d51d9bfef376b58d Mon Sep 17 00:00:00 2001 From: JudicaelPoumay <judicael.poumay@gmail.com> Date: Sun, 15 Feb 2026 14:14:53 +0100 Subject: [PATCH 16/16] fix(app): fix type error in custom-elements.d.ts to unblock CI --- packages/app/src/custom-elements.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts index e4ea0d6cebda..86cb284693e6 120000 --- a/packages/app/src/custom-elements.d.ts +++ b/packages/app/src/custom-elements.d.ts @@ -1 +1 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file +/// <reference path="../../ui/src/custom-elements.d.ts" />