diff --git a/packages/ui/src/components/layout/ContextSidebarTab.tsx b/packages/ui/src/components/layout/ContextSidebarTab.tsx index 3f41e331..d6f5d549 100644 --- a/packages/ui/src/components/layout/ContextSidebarTab.tsx +++ b/packages/ui/src/components/layout/ContextSidebarTab.tsx @@ -11,6 +11,7 @@ import { computeCacheHitRate } from '@/stores/utils/tokenUtils'; import { useSessions, useSessionMessageRecords } from '@/sync/sync-context'; import { copyTextToClipboard } from '@/lib/clipboard'; import { getCurrentIntlLocale, useI18n } from '@/lib/i18n'; +import { formatMoney } from '@/lib/money'; import { derivePartsLabel, deriveUserSnippet, @@ -236,16 +237,6 @@ const computeContextBreakdown = ( const formatNumber = (value: number): string => value.toLocaleString(getCurrentIntlLocale()); -const formatMoney = (value: number): string => { - if (!Number.isFinite(value) || value <= 0) return new Intl.NumberFormat(getCurrentIntlLocale(), { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(0); - return new Intl.NumberFormat(getCurrentIntlLocale(), { - style: 'currency', - currency: 'USD', - minimumFractionDigits: value < 0.01 ? 4 : 2, - maximumFractionDigits: value < 0.01 ? 4 : 2, - }).format(value); -}; - const formatDateTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => { if (!timestamp || !Number.isFinite(timestamp)) return '-'; return formatDateTimeForPreference(timestamp, timeFormatPreference, { diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index 0b592fe0..5ccf8a46 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -5,7 +5,7 @@ import { SessionDialogs } from '@/components/session/SessionDialogs'; import { ChatView } from '@/components/views/ChatView'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useViewportStore } from '@/sync/viewport-store'; -import { useSessions, useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context'; +import { useSessions, useDirectorySync, useSession, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context'; import { useConfigStore } from '@/stores/useConfigStore'; import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; import { contextTokensFromBreakdown } from '@/stores/utils/tokenUtils'; @@ -666,6 +666,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on const providers = useConfigStore((state) => state.providers); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const activeProjectId = useProjectsStore((state) => state.activeProjectId); + const currentSession = useSession(currentSessionId ?? ''); const currentSessionMessages = useSessionMessages(currentSessionId ?? ''); const currentSessionMessagesResolved = useSessionMessagesResolved(currentSessionId ?? ''); const quotaResults = useQuotaStore((state) => state.results); @@ -1022,6 +1023,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on percentage={stableContextUsage.percentage} contextLimit={stableContextUsage.contextLimit} outputLimit={stableContextUsage.outputLimit ?? 0} + cost={(currentSession?.cost ?? 0) > 0 ? currentSession?.cost : null} className="h-9 shrink-0 pl-1 pr-1 typography-ui-label" valueClassName="font-semibold leading-none" hideIcon diff --git a/packages/ui/src/components/ui/ContextUsageDisplay.tsx b/packages/ui/src/components/ui/ContextUsageDisplay.tsx index cb0061b4..5f126919 100644 --- a/packages/ui/src/components/ui/ContextUsageDisplay.tsx +++ b/packages/ui/src/components/ui/ContextUsageDisplay.tsx @@ -4,6 +4,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; +import { formatMoney } from '@/lib/money'; import { clampPercent, resolveUsageTone } from '@/lib/quota'; interface ContextUsageDisplayProps { @@ -12,6 +13,7 @@ interface ContextUsageDisplayProps { colorPercentage?: number; contextLimit: number; outputLimit?: number; + cost?: number | null; size?: 'default' | 'compact'; isMobile?: boolean; hideIcon?: boolean; @@ -29,6 +31,7 @@ export const ContextUsageDisplay: React.FC = ({ colorPercentage, contextLimit, outputLimit, + cost = null, size = 'default', isMobile = false, hideIcon = false, @@ -73,10 +76,13 @@ export const ContextUsageDisplay: React.FC = ({ const circularProgressOffset = circularProgressCircumference * (1 - progressPct / 100); const safeOutputLimit = typeof outputLimit === 'number' ? Math.max(outputLimit, 0) : 0; + const normalizedCost = cost ?? 0; + const hasCost = normalizedCost > 0 && Number.isFinite(normalizedCost); const tooltipLines = [ t('contextUsage.tooltip.usedTokens', { tokens: formatTokens(totalTokens) }), t('contextUsage.tooltip.contextLimit', { tokens: formatTokens(contextLimit) }), t('contextUsage.tooltip.outputLimit', { tokens: formatTokens(safeOutputLimit) }), + ...(hasCost ? [t('contextUsage.tooltip.cost', { cost: formatMoney(normalizedCost) })] : []), ]; const isInteractive = !isMobile && typeof onClick === 'function'; @@ -183,6 +189,12 @@ export const ContextUsageDisplay: React.FC = ({ {t('contextUsage.mobile.outputLimit')} {formatTokens(safeOutputLimit)} + {hasCost ? ( +
+ {t('contextUsage.mobile.cost')} + {formatMoney(normalizedCost)} +
+ ) : null}
{t('contextUsage.mobile.usage')} diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 125994af..c0c19339 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -1284,10 +1284,12 @@ export const dict = { 'contextUsage.mobile.usedTokens': 'Verwendete Tokens', 'contextUsage.mobile.contextLimit': 'Kontextlimit', 'contextUsage.mobile.outputLimit': 'Ausgabelimit', + 'contextUsage.mobile.cost': 'Kosten', 'contextUsage.mobile.usage': 'Nutzung', 'contextUsage.tooltip.usedTokens': 'Verwendete Tokens: {tokens}', 'contextUsage.tooltip.contextLimit': 'Kontextlimit: {tokens}', 'contextUsage.tooltip.outputLimit': 'Ausgabelimit: {tokens}', + 'contextUsage.tooltip.cost': 'Kosten: {cost}', 'contextSidebar.session.untitled': 'Unbenannte Sitzung', 'contextSidebar.empty.openSession': 'Öffnen Sie eine Sitzung, um den Kontext zu prüfen.', 'contextSidebar.section.context': 'Kontext', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 41d8bfab..a5443615 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -1437,10 +1437,12 @@ export const dict = { 'contextUsage.mobile.usedTokens': 'Used tokens', 'contextUsage.mobile.contextLimit': 'Context limit', 'contextUsage.mobile.outputLimit': 'Output limit', + 'contextUsage.mobile.cost': 'Cost', 'contextUsage.mobile.usage': 'Usage', 'contextUsage.tooltip.usedTokens': 'Used tokens: {tokens}', 'contextUsage.tooltip.contextLimit': 'Context limit: {tokens}', 'contextUsage.tooltip.outputLimit': 'Output limit: {tokens}', + 'contextUsage.tooltip.cost': 'Cost: {cost}', 'contextSidebar.session.untitled': 'Untitled Session', 'contextSidebar.empty.openSession': 'Open a session to inspect context.', 'contextSidebar.section.context': 'Context', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 3c7aebb1..9bda2677 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1403,10 +1403,12 @@ export const dict: Record = { "contextUsage.mobile.usedTokens": "Tokens usados", "contextUsage.mobile.contextLimit": "Límite de contexto", "contextUsage.mobile.outputLimit": "Límite de salida", + "contextUsage.mobile.cost": "Costo", "contextUsage.mobile.usage": "Uso", "contextUsage.tooltip.usedTokens": "Tokens usados: {tokens}", "contextUsage.tooltip.contextLimit": "Límite de contexto: {tokens}", "contextUsage.tooltip.outputLimit": "Límite de salida: {tokens}", + "contextUsage.tooltip.cost": "Costo: {cost}", "contextSidebar.session.untitled": "Sesión sin título", "contextSidebar.empty.openSession": "Abrir una sesión para inspeccionar el contexto.", "contextSidebar.section.context": "Contexto", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index e2c0f1bd..d481a420 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -1202,10 +1202,12 @@ export const dict = { 'contextUsage.mobile.usedTokens': 'Jetons utilisés', 'contextUsage.mobile.contextLimit': 'Limite de contexte', 'contextUsage.mobile.outputLimit': 'Limite de sortie', + 'contextUsage.mobile.cost': 'Coût', 'contextUsage.mobile.usage': 'Usage', 'contextUsage.tooltip.usedTokens': 'Jetons utilisés : {tokens}', 'contextUsage.tooltip.contextLimit': 'Limite de contexte : {tokens}', 'contextUsage.tooltip.outputLimit': 'Limite de sortie : {tokens}', + 'contextUsage.tooltip.cost': 'Coût : {cost}', 'contextSidebar.session.untitled': 'Session sans titre', 'contextSidebar.empty.openSession': 'Ouvrez une session pour inspecter le contexte.', 'contextSidebar.section.context': 'Contexte', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index f231df35..afc6aae3 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -1433,10 +1433,12 @@ export const dict: Record = { 'contextUsage.mobile.usedTokens': '使用トークン', 'contextUsage.mobile.contextLimit': 'コンテキスト制限', 'contextUsage.mobile.outputLimit': '出力制限', + 'contextUsage.mobile.cost': 'コスト', 'contextUsage.mobile.usage': '使用量', 'contextUsage.tooltip.usedTokens': '使用トークン: {tokens}', 'contextUsage.tooltip.contextLimit': 'コンテキスト制限: {tokens}', 'contextUsage.tooltip.outputLimit': '出力制限: {tokens}', + 'contextUsage.tooltip.cost': 'コスト: {cost}', 'contextSidebar.session.untitled': '無題のセッション', 'contextSidebar.empty.openSession': 'セッションを開いてコンテキストを確認します。', 'contextSidebar.section.context': 'コンテキスト', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 4d924744..eb632cdb 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -1439,10 +1439,12 @@ export const dict: Record = { 'contextUsage.mobile.usedTokens': '사용한 토큰', 'contextUsage.mobile.contextLimit': '컨텍스트 한도', 'contextUsage.mobile.outputLimit': '출력 한도', + 'contextUsage.mobile.cost': '비용', 'contextUsage.mobile.usage': '사용량', 'contextUsage.tooltip.usedTokens': '사용됨 토큰: {tokens}', 'contextUsage.tooltip.contextLimit': '컨텍스트 한도: {tokens}', 'contextUsage.tooltip.outputLimit': '출력 한도: {tokens}', + 'contextUsage.tooltip.cost': '비용: {cost}', 'contextSidebar.session.untitled': '제목 없는 세션', 'contextSidebar.empty.openSession': '컨텍스트를 볼 세션을 여세요.', 'contextSidebar.section.context': '컨텍스트', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 91123a37..db34f661 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1639,11 +1639,13 @@ export const dict: Record = { 'contextUsage.aria.label': 'Użycie kontekstu', 'contextUsage.mobile.contextLimit': 'Limit kontekstu', 'contextUsage.mobile.outputLimit': 'Limit wyjścia', + 'contextUsage.mobile.cost': 'Koszt', 'contextUsage.mobile.title': 'Użycie kontekstu', 'contextUsage.mobile.usage': 'Zużycie', 'contextUsage.mobile.usedTokens': 'Zużyte tokeny', 'contextUsage.tooltip.contextLimit': 'Limit kontekstu: {tokens}', 'contextUsage.tooltip.outputLimit': 'Limit wyjścia: {tokens}', + 'contextUsage.tooltip.cost': 'Koszt: {cost}', 'contextUsage.tooltip.usedTokens': 'Zużyte tokeny: {tokens}', 'desktopHostSwitcher.actions.add': 'Dodaj', 'desktopHostSwitcher.actions.addInstance': 'Dodaj instancję', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 925ef6eb..d97e3a40 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1403,10 +1403,12 @@ export const dict: Record = { "contextUsage.mobile.usedTokens": "Tokens usados", "contextUsage.mobile.contextLimit": "Limite de contexto", "contextUsage.mobile.outputLimit": "Limite de saída", + "contextUsage.mobile.cost": "Custo", "contextUsage.mobile.usage": "Uso", "contextUsage.tooltip.usedTokens": "Tokens usados: {tokens}", "contextUsage.tooltip.contextLimit": "Limite de contexto: {tokens}", "contextUsage.tooltip.outputLimit": "Limite de saída: {tokens}", + "contextUsage.tooltip.cost": "Custo: {cost}", "contextSidebar.session.untitled": "Sessão sem título", "contextSidebar.empty.openSession": "Abrir uma sessão para inspecionar o contexto.", "contextSidebar.section.context": "Contexto", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index d27adfb2..f585de04 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1403,10 +1403,12 @@ export const dict: Record = { "contextUsage.mobile.usedTokens": "Використані токени", "contextUsage.mobile.contextLimit": "Обмеження контексту", "contextUsage.mobile.outputLimit": "Ліміт виводу", + "contextUsage.mobile.cost": "Вартість", "contextUsage.mobile.usage": "Використання", "contextUsage.tooltip.usedTokens": "Використані токени: {tokens}", "contextUsage.tooltip.contextLimit": "Обмеження контексту: {tokens}", "contextUsage.tooltip.outputLimit": "Ліміт виводу: {tokens}", + "contextUsage.tooltip.cost": "Вартість: {cost}", "contextSidebar.session.untitled": "Сесія без назви", "contextSidebar.empty.openSession": "Відкрийте сесію, щоб перевірити контекст.", "contextSidebar.section.context": "Контекст", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index df562496..77e7bf55 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1403,10 +1403,12 @@ export const dict: Record = { 'contextUsage.mobile.usedTokens': '已用 Token', 'contextUsage.mobile.contextLimit': '上下文上限', 'contextUsage.mobile.outputLimit': '输出上限', + 'contextUsage.mobile.cost': '成本', 'contextUsage.mobile.usage': '使用率', 'contextUsage.tooltip.usedTokens': '已用 Token:{tokens}', 'contextUsage.tooltip.contextLimit': '上下文上限:{tokens}', 'contextUsage.tooltip.outputLimit': '输出上限:{tokens}', + 'contextUsage.tooltip.cost': '成本:{cost}', 'contextSidebar.session.untitled': '未命名会话', 'contextSidebar.empty.openSession': '请先打开会话以查看上下文。', 'contextSidebar.section.context': '上下文', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index d908d97b..349d4769 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -1413,10 +1413,12 @@ export const dict: Record = { 'contextUsage.mobile.usedTokens': '已用 Token', 'contextUsage.mobile.contextLimit': '上下文上限', 'contextUsage.mobile.outputLimit': '輸出上限', + 'contextUsage.mobile.cost': '成本', 'contextUsage.mobile.usage': '使用率', 'contextUsage.tooltip.usedTokens': '已用 Token:{tokens}', 'contextUsage.tooltip.contextLimit': '上下文上限:{tokens}', 'contextUsage.tooltip.outputLimit': '輸出上限:{tokens}', + 'contextUsage.tooltip.cost': '成本:{cost}', 'contextSidebar.session.untitled': '未命名會話', 'contextSidebar.empty.openSession': '請先開啟會話以查看上下文。', 'contextSidebar.section.context': '上下文', diff --git a/packages/ui/src/lib/money.ts b/packages/ui/src/lib/money.ts new file mode 100644 index 00000000..9f14dd2d --- /dev/null +++ b/packages/ui/src/lib/money.ts @@ -0,0 +1,19 @@ +import { getCurrentIntlLocale } from './i18n'; + + +export const formatMoney = (value: number): string => { + if (!Number.isFinite(value) || value <= 0) { + return new Intl.NumberFormat(getCurrentIntlLocale(), { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(0); + } + return new Intl.NumberFormat(getCurrentIntlLocale(), { + style: 'currency', + currency: 'USD', + minimumFractionDigits: value < 0.01 ? 4 : 2, + maximumFractionDigits: value < 0.01 ? 4 : 2, + }).format(value); +}; diff --git a/packages/vscode/CHANGELOG.md b/packages/vscode/CHANGELOG.md index 17fd41a0..2de53b67 100644 --- a/packages/vscode/CHANGELOG.md +++ b/packages/vscode/CHANGELOG.md @@ -7,6 +7,7 @@ - Chat: new chats no longer start against a deleted last worktree directory; they fall back to the active project instead of saving the first message and never starting. - Chat: typing with Chinese, Japanese, or Korean input methods no longer interrupts composition or jumps the cursor to the end of the composer (thanks to @makeittech). - The context usage readout no longer climbs over 100% after turns with many tool calls and no longer jumps when reopening an older session; it now shows what the window actually holds (thanks to @pocharlies). +- Usage: the context usage readout in the chat header now also shows the session's cost in its tooltip. - Attachments: extracted Office and OpenDocument content is now capped and presented more compactly, preventing large documents and their images from overwhelming the message context. - Projects: project names now match the folder name exactly, so `.ssh` and `opencode-claude` are no longer shown as `.Ssh` and `Opencode Claude`; names you renamed yourself are kept. - Skills Catalog: the source is now named ClawHub instead of "ClawdHub" (thanks to @makeittech).