feat(ui): show session cost in VS Code context usage readout (#2991)
Share the desktop session-card currency formatter (lib/money.ts) and surface the current session's cost in the extension chat-header context usage tooltip.
This commit is contained in:
@@ -11,6 +11,7 @@ import { computeCacheHitRate } from '@/stores/utils/tokenUtils';
|
|||||||
import { useSessions, useSessionMessageRecords } from '@/sync/sync-context';
|
import { useSessions, useSessionMessageRecords } from '@/sync/sync-context';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||||
|
import { formatMoney } from '@/lib/money';
|
||||||
import {
|
import {
|
||||||
derivePartsLabel,
|
derivePartsLabel,
|
||||||
deriveUserSnippet,
|
deriveUserSnippet,
|
||||||
@@ -236,16 +237,6 @@ const computeContextBreakdown = (
|
|||||||
|
|
||||||
const formatNumber = (value: number): string => value.toLocaleString(getCurrentIntlLocale());
|
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 => {
|
const formatDateTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => {
|
||||||
if (!timestamp || !Number.isFinite(timestamp)) return '-';
|
if (!timestamp || !Number.isFinite(timestamp)) return '-';
|
||||||
return formatDateTimeForPreference(timestamp, timeFormatPreference, {
|
return formatDateTimeForPreference(timestamp, timeFormatPreference, {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { SessionDialogs } from '@/components/session/SessionDialogs';
|
|||||||
import { ChatView } from '@/components/views/ChatView';
|
import { ChatView } from '@/components/views/ChatView';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useViewportStore } from '@/sync/viewport-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 { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||||
import { contextTokensFromBreakdown } from '@/stores/utils/tokenUtils';
|
import { contextTokensFromBreakdown } from '@/stores/utils/tokenUtils';
|
||||||
@@ -666,6 +666,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
|||||||
const providers = useConfigStore((state) => state.providers);
|
const providers = useConfigStore((state) => state.providers);
|
||||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||||
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
||||||
|
const currentSession = useSession(currentSessionId ?? '');
|
||||||
const currentSessionMessages = useSessionMessages(currentSessionId ?? '');
|
const currentSessionMessages = useSessionMessages(currentSessionId ?? '');
|
||||||
const currentSessionMessagesResolved = useSessionMessagesResolved(currentSessionId ?? '');
|
const currentSessionMessagesResolved = useSessionMessagesResolved(currentSessionId ?? '');
|
||||||
const quotaResults = useQuotaStore((state) => state.results);
|
const quotaResults = useQuotaStore((state) => state.results);
|
||||||
@@ -1022,6 +1023,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
|||||||
percentage={stableContextUsage.percentage}
|
percentage={stableContextUsage.percentage}
|
||||||
contextLimit={stableContextUsage.contextLimit}
|
contextLimit={stableContextUsage.contextLimit}
|
||||||
outputLimit={stableContextUsage.outputLimit ?? 0}
|
outputLimit={stableContextUsage.outputLimit ?? 0}
|
||||||
|
cost={(currentSession?.cost ?? 0) > 0 ? currentSession?.cost : null}
|
||||||
className="h-9 shrink-0 pl-1 pr-1 typography-ui-label"
|
className="h-9 shrink-0 pl-1 pr-1 typography-ui-label"
|
||||||
valueClassName="font-semibold leading-none"
|
valueClassName="font-semibold leading-none"
|
||||||
hideIcon
|
hideIcon
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
|||||||
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { formatMoney } from '@/lib/money';
|
||||||
import { clampPercent, resolveUsageTone } from '@/lib/quota';
|
import { clampPercent, resolveUsageTone } from '@/lib/quota';
|
||||||
|
|
||||||
interface ContextUsageDisplayProps {
|
interface ContextUsageDisplayProps {
|
||||||
@@ -12,6 +13,7 @@ interface ContextUsageDisplayProps {
|
|||||||
colorPercentage?: number;
|
colorPercentage?: number;
|
||||||
contextLimit: number;
|
contextLimit: number;
|
||||||
outputLimit?: number;
|
outputLimit?: number;
|
||||||
|
cost?: number | null;
|
||||||
size?: 'default' | 'compact';
|
size?: 'default' | 'compact';
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
hideIcon?: boolean;
|
hideIcon?: boolean;
|
||||||
@@ -29,6 +31,7 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
colorPercentage,
|
colorPercentage,
|
||||||
contextLimit,
|
contextLimit,
|
||||||
outputLimit,
|
outputLimit,
|
||||||
|
cost = null,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
@@ -73,10 +76,13 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
const circularProgressOffset = circularProgressCircumference * (1 - progressPct / 100);
|
const circularProgressOffset = circularProgressCircumference * (1 - progressPct / 100);
|
||||||
|
|
||||||
const safeOutputLimit = typeof outputLimit === 'number' ? Math.max(outputLimit, 0) : 0;
|
const safeOutputLimit = typeof outputLimit === 'number' ? Math.max(outputLimit, 0) : 0;
|
||||||
|
const normalizedCost = cost ?? 0;
|
||||||
|
const hasCost = normalizedCost > 0 && Number.isFinite(normalizedCost);
|
||||||
const tooltipLines = [
|
const tooltipLines = [
|
||||||
t('contextUsage.tooltip.usedTokens', { tokens: formatTokens(totalTokens) }),
|
t('contextUsage.tooltip.usedTokens', { tokens: formatTokens(totalTokens) }),
|
||||||
t('contextUsage.tooltip.contextLimit', { tokens: formatTokens(contextLimit) }),
|
t('contextUsage.tooltip.contextLimit', { tokens: formatTokens(contextLimit) }),
|
||||||
t('contextUsage.tooltip.outputLimit', { tokens: formatTokens(safeOutputLimit) }),
|
t('contextUsage.tooltip.outputLimit', { tokens: formatTokens(safeOutputLimit) }),
|
||||||
|
...(hasCost ? [t('contextUsage.tooltip.cost', { cost: formatMoney(normalizedCost) })] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const isInteractive = !isMobile && typeof onClick === 'function';
|
const isInteractive = !isMobile && typeof onClick === 'function';
|
||||||
@@ -183,6 +189,12 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
<span className="typography-meta text-muted-foreground">{t('contextUsage.mobile.outputLimit')}</span>
|
<span className="typography-meta text-muted-foreground">{t('contextUsage.mobile.outputLimit')}</span>
|
||||||
<span className="typography-meta text-foreground font-medium">{formatTokens(safeOutputLimit)}</span>
|
<span className="typography-meta text-foreground font-medium">{formatTokens(safeOutputLimit)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{hasCost ? (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="typography-meta text-muted-foreground">{t('contextUsage.mobile.cost')}</span>
|
||||||
|
<span className="typography-meta text-foreground font-medium">{formatMoney(normalizedCost)}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="flex justify-between items-center pt-1 border-t border-border/40">
|
<div className="flex justify-between items-center pt-1 border-t border-border/40">
|
||||||
<span className="typography-meta text-muted-foreground">{t('contextUsage.mobile.usage')}</span>
|
<span className="typography-meta text-muted-foreground">{t('contextUsage.mobile.usage')}</span>
|
||||||
<span className={cn('typography-meta font-semibold', getPercentageColor(colorPct))}>
|
<span className={cn('typography-meta font-semibold', getPercentageColor(colorPct))}>
|
||||||
|
|||||||
@@ -1284,10 +1284,12 @@ export const dict = {
|
|||||||
'contextUsage.mobile.usedTokens': 'Verwendete Tokens',
|
'contextUsage.mobile.usedTokens': 'Verwendete Tokens',
|
||||||
'contextUsage.mobile.contextLimit': 'Kontextlimit',
|
'contextUsage.mobile.contextLimit': 'Kontextlimit',
|
||||||
'contextUsage.mobile.outputLimit': 'Ausgabelimit',
|
'contextUsage.mobile.outputLimit': 'Ausgabelimit',
|
||||||
|
'contextUsage.mobile.cost': 'Kosten',
|
||||||
'contextUsage.mobile.usage': 'Nutzung',
|
'contextUsage.mobile.usage': 'Nutzung',
|
||||||
'contextUsage.tooltip.usedTokens': 'Verwendete Tokens: {tokens}',
|
'contextUsage.tooltip.usedTokens': 'Verwendete Tokens: {tokens}',
|
||||||
'contextUsage.tooltip.contextLimit': 'Kontextlimit: {tokens}',
|
'contextUsage.tooltip.contextLimit': 'Kontextlimit: {tokens}',
|
||||||
'contextUsage.tooltip.outputLimit': 'Ausgabelimit: {tokens}',
|
'contextUsage.tooltip.outputLimit': 'Ausgabelimit: {tokens}',
|
||||||
|
'contextUsage.tooltip.cost': 'Kosten: {cost}',
|
||||||
'contextSidebar.session.untitled': 'Unbenannte Sitzung',
|
'contextSidebar.session.untitled': 'Unbenannte Sitzung',
|
||||||
'contextSidebar.empty.openSession': 'Öffnen Sie eine Sitzung, um den Kontext zu prüfen.',
|
'contextSidebar.empty.openSession': 'Öffnen Sie eine Sitzung, um den Kontext zu prüfen.',
|
||||||
'contextSidebar.section.context': 'Kontext',
|
'contextSidebar.section.context': 'Kontext',
|
||||||
|
|||||||
@@ -1437,10 +1437,12 @@ export const dict = {
|
|||||||
'contextUsage.mobile.usedTokens': 'Used tokens',
|
'contextUsage.mobile.usedTokens': 'Used tokens',
|
||||||
'contextUsage.mobile.contextLimit': 'Context limit',
|
'contextUsage.mobile.contextLimit': 'Context limit',
|
||||||
'contextUsage.mobile.outputLimit': 'Output limit',
|
'contextUsage.mobile.outputLimit': 'Output limit',
|
||||||
|
'contextUsage.mobile.cost': 'Cost',
|
||||||
'contextUsage.mobile.usage': 'Usage',
|
'contextUsage.mobile.usage': 'Usage',
|
||||||
'contextUsage.tooltip.usedTokens': 'Used tokens: {tokens}',
|
'contextUsage.tooltip.usedTokens': 'Used tokens: {tokens}',
|
||||||
'contextUsage.tooltip.contextLimit': 'Context limit: {tokens}',
|
'contextUsage.tooltip.contextLimit': 'Context limit: {tokens}',
|
||||||
'contextUsage.tooltip.outputLimit': 'Output limit: {tokens}',
|
'contextUsage.tooltip.outputLimit': 'Output limit: {tokens}',
|
||||||
|
'contextUsage.tooltip.cost': 'Cost: {cost}',
|
||||||
'contextSidebar.session.untitled': 'Untitled Session',
|
'contextSidebar.session.untitled': 'Untitled Session',
|
||||||
'contextSidebar.empty.openSession': 'Open a session to inspect context.',
|
'contextSidebar.empty.openSession': 'Open a session to inspect context.',
|
||||||
'contextSidebar.section.context': 'Context',
|
'contextSidebar.section.context': 'Context',
|
||||||
|
|||||||
@@ -1403,10 +1403,12 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"contextUsage.mobile.usedTokens": "Tokens usados",
|
"contextUsage.mobile.usedTokens": "Tokens usados",
|
||||||
"contextUsage.mobile.contextLimit": "Límite de contexto",
|
"contextUsage.mobile.contextLimit": "Límite de contexto",
|
||||||
"contextUsage.mobile.outputLimit": "Límite de salida",
|
"contextUsage.mobile.outputLimit": "Límite de salida",
|
||||||
|
"contextUsage.mobile.cost": "Costo",
|
||||||
"contextUsage.mobile.usage": "Uso",
|
"contextUsage.mobile.usage": "Uso",
|
||||||
"contextUsage.tooltip.usedTokens": "Tokens usados: {tokens}",
|
"contextUsage.tooltip.usedTokens": "Tokens usados: {tokens}",
|
||||||
"contextUsage.tooltip.contextLimit": "Límite de contexto: {tokens}",
|
"contextUsage.tooltip.contextLimit": "Límite de contexto: {tokens}",
|
||||||
"contextUsage.tooltip.outputLimit": "Límite de salida: {tokens}",
|
"contextUsage.tooltip.outputLimit": "Límite de salida: {tokens}",
|
||||||
|
"contextUsage.tooltip.cost": "Costo: {cost}",
|
||||||
"contextSidebar.session.untitled": "Sesión sin título",
|
"contextSidebar.session.untitled": "Sesión sin título",
|
||||||
"contextSidebar.empty.openSession": "Abrir una sesión para inspeccionar el contexto.",
|
"contextSidebar.empty.openSession": "Abrir una sesión para inspeccionar el contexto.",
|
||||||
"contextSidebar.section.context": "Contexto",
|
"contextSidebar.section.context": "Contexto",
|
||||||
|
|||||||
@@ -1202,10 +1202,12 @@ export const dict = {
|
|||||||
'contextUsage.mobile.usedTokens': 'Jetons utilisés',
|
'contextUsage.mobile.usedTokens': 'Jetons utilisés',
|
||||||
'contextUsage.mobile.contextLimit': 'Limite de contexte',
|
'contextUsage.mobile.contextLimit': 'Limite de contexte',
|
||||||
'contextUsage.mobile.outputLimit': 'Limite de sortie',
|
'contextUsage.mobile.outputLimit': 'Limite de sortie',
|
||||||
|
'contextUsage.mobile.cost': 'Coût',
|
||||||
'contextUsage.mobile.usage': 'Usage',
|
'contextUsage.mobile.usage': 'Usage',
|
||||||
'contextUsage.tooltip.usedTokens': 'Jetons utilisés : {tokens}',
|
'contextUsage.tooltip.usedTokens': 'Jetons utilisés : {tokens}',
|
||||||
'contextUsage.tooltip.contextLimit': 'Limite de contexte : {tokens}',
|
'contextUsage.tooltip.contextLimit': 'Limite de contexte : {tokens}',
|
||||||
'contextUsage.tooltip.outputLimit': 'Limite de sortie : {tokens}',
|
'contextUsage.tooltip.outputLimit': 'Limite de sortie : {tokens}',
|
||||||
|
'contextUsage.tooltip.cost': 'Coût : {cost}',
|
||||||
'contextSidebar.session.untitled': 'Session sans titre',
|
'contextSidebar.session.untitled': 'Session sans titre',
|
||||||
'contextSidebar.empty.openSession': 'Ouvrez une session pour inspecter le contexte.',
|
'contextSidebar.empty.openSession': 'Ouvrez une session pour inspecter le contexte.',
|
||||||
'contextSidebar.section.context': 'Contexte',
|
'contextSidebar.section.context': 'Contexte',
|
||||||
|
|||||||
@@ -1433,10 +1433,12 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'contextUsage.mobile.usedTokens': '使用トークン',
|
'contextUsage.mobile.usedTokens': '使用トークン',
|
||||||
'contextUsage.mobile.contextLimit': 'コンテキスト制限',
|
'contextUsage.mobile.contextLimit': 'コンテキスト制限',
|
||||||
'contextUsage.mobile.outputLimit': '出力制限',
|
'contextUsage.mobile.outputLimit': '出力制限',
|
||||||
|
'contextUsage.mobile.cost': 'コスト',
|
||||||
'contextUsage.mobile.usage': '使用量',
|
'contextUsage.mobile.usage': '使用量',
|
||||||
'contextUsage.tooltip.usedTokens': '使用トークン: {tokens}',
|
'contextUsage.tooltip.usedTokens': '使用トークン: {tokens}',
|
||||||
'contextUsage.tooltip.contextLimit': 'コンテキスト制限: {tokens}',
|
'contextUsage.tooltip.contextLimit': 'コンテキスト制限: {tokens}',
|
||||||
'contextUsage.tooltip.outputLimit': '出力制限: {tokens}',
|
'contextUsage.tooltip.outputLimit': '出力制限: {tokens}',
|
||||||
|
'contextUsage.tooltip.cost': 'コスト: {cost}',
|
||||||
'contextSidebar.session.untitled': '無題のセッション',
|
'contextSidebar.session.untitled': '無題のセッション',
|
||||||
'contextSidebar.empty.openSession': 'セッションを開いてコンテキストを確認します。',
|
'contextSidebar.empty.openSession': 'セッションを開いてコンテキストを確認します。',
|
||||||
'contextSidebar.section.context': 'コンテキスト',
|
'contextSidebar.section.context': 'コンテキスト',
|
||||||
|
|||||||
@@ -1439,10 +1439,12 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'contextUsage.mobile.usedTokens': '사용한 토큰',
|
'contextUsage.mobile.usedTokens': '사용한 토큰',
|
||||||
'contextUsage.mobile.contextLimit': '컨텍스트 한도',
|
'contextUsage.mobile.contextLimit': '컨텍스트 한도',
|
||||||
'contextUsage.mobile.outputLimit': '출력 한도',
|
'contextUsage.mobile.outputLimit': '출력 한도',
|
||||||
|
'contextUsage.mobile.cost': '비용',
|
||||||
'contextUsage.mobile.usage': '사용량',
|
'contextUsage.mobile.usage': '사용량',
|
||||||
'contextUsage.tooltip.usedTokens': '사용됨 토큰: {tokens}',
|
'contextUsage.tooltip.usedTokens': '사용됨 토큰: {tokens}',
|
||||||
'contextUsage.tooltip.contextLimit': '컨텍스트 한도: {tokens}',
|
'contextUsage.tooltip.contextLimit': '컨텍스트 한도: {tokens}',
|
||||||
'contextUsage.tooltip.outputLimit': '출력 한도: {tokens}',
|
'contextUsage.tooltip.outputLimit': '출력 한도: {tokens}',
|
||||||
|
'contextUsage.tooltip.cost': '비용: {cost}',
|
||||||
'contextSidebar.session.untitled': '제목 없는 세션',
|
'contextSidebar.session.untitled': '제목 없는 세션',
|
||||||
'contextSidebar.empty.openSession': '컨텍스트를 볼 세션을 여세요.',
|
'contextSidebar.empty.openSession': '컨텍스트를 볼 세션을 여세요.',
|
||||||
'contextSidebar.section.context': '컨텍스트',
|
'contextSidebar.section.context': '컨텍스트',
|
||||||
|
|||||||
@@ -1639,11 +1639,13 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'contextUsage.aria.label': 'Użycie kontekstu',
|
'contextUsage.aria.label': 'Użycie kontekstu',
|
||||||
'contextUsage.mobile.contextLimit': 'Limit kontekstu',
|
'contextUsage.mobile.contextLimit': 'Limit kontekstu',
|
||||||
'contextUsage.mobile.outputLimit': 'Limit wyjścia',
|
'contextUsage.mobile.outputLimit': 'Limit wyjścia',
|
||||||
|
'contextUsage.mobile.cost': 'Koszt',
|
||||||
'contextUsage.mobile.title': 'Użycie kontekstu',
|
'contextUsage.mobile.title': 'Użycie kontekstu',
|
||||||
'contextUsage.mobile.usage': 'Zużycie',
|
'contextUsage.mobile.usage': 'Zużycie',
|
||||||
'contextUsage.mobile.usedTokens': 'Zużyte tokeny',
|
'contextUsage.mobile.usedTokens': 'Zużyte tokeny',
|
||||||
'contextUsage.tooltip.contextLimit': 'Limit kontekstu: {tokens}',
|
'contextUsage.tooltip.contextLimit': 'Limit kontekstu: {tokens}',
|
||||||
'contextUsage.tooltip.outputLimit': 'Limit wyjścia: {tokens}',
|
'contextUsage.tooltip.outputLimit': 'Limit wyjścia: {tokens}',
|
||||||
|
'contextUsage.tooltip.cost': 'Koszt: {cost}',
|
||||||
'contextUsage.tooltip.usedTokens': 'Zużyte tokeny: {tokens}',
|
'contextUsage.tooltip.usedTokens': 'Zużyte tokeny: {tokens}',
|
||||||
'desktopHostSwitcher.actions.add': 'Dodaj',
|
'desktopHostSwitcher.actions.add': 'Dodaj',
|
||||||
'desktopHostSwitcher.actions.addInstance': 'Dodaj instancję',
|
'desktopHostSwitcher.actions.addInstance': 'Dodaj instancję',
|
||||||
|
|||||||
@@ -1403,10 +1403,12 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"contextUsage.mobile.usedTokens": "Tokens usados",
|
"contextUsage.mobile.usedTokens": "Tokens usados",
|
||||||
"contextUsage.mobile.contextLimit": "Limite de contexto",
|
"contextUsage.mobile.contextLimit": "Limite de contexto",
|
||||||
"contextUsage.mobile.outputLimit": "Limite de saída",
|
"contextUsage.mobile.outputLimit": "Limite de saída",
|
||||||
|
"contextUsage.mobile.cost": "Custo",
|
||||||
"contextUsage.mobile.usage": "Uso",
|
"contextUsage.mobile.usage": "Uso",
|
||||||
"contextUsage.tooltip.usedTokens": "Tokens usados: {tokens}",
|
"contextUsage.tooltip.usedTokens": "Tokens usados: {tokens}",
|
||||||
"contextUsage.tooltip.contextLimit": "Limite de contexto: {tokens}",
|
"contextUsage.tooltip.contextLimit": "Limite de contexto: {tokens}",
|
||||||
"contextUsage.tooltip.outputLimit": "Limite de saída: {tokens}",
|
"contextUsage.tooltip.outputLimit": "Limite de saída: {tokens}",
|
||||||
|
"contextUsage.tooltip.cost": "Custo: {cost}",
|
||||||
"contextSidebar.session.untitled": "Sessão sem título",
|
"contextSidebar.session.untitled": "Sessão sem título",
|
||||||
"contextSidebar.empty.openSession": "Abrir uma sessão para inspecionar o contexto.",
|
"contextSidebar.empty.openSession": "Abrir uma sessão para inspecionar o contexto.",
|
||||||
"contextSidebar.section.context": "Contexto",
|
"contextSidebar.section.context": "Contexto",
|
||||||
|
|||||||
@@ -1403,10 +1403,12 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"contextUsage.mobile.usedTokens": "Використані токени",
|
"contextUsage.mobile.usedTokens": "Використані токени",
|
||||||
"contextUsage.mobile.contextLimit": "Обмеження контексту",
|
"contextUsage.mobile.contextLimit": "Обмеження контексту",
|
||||||
"contextUsage.mobile.outputLimit": "Ліміт виводу",
|
"contextUsage.mobile.outputLimit": "Ліміт виводу",
|
||||||
|
"contextUsage.mobile.cost": "Вартість",
|
||||||
"contextUsage.mobile.usage": "Використання",
|
"contextUsage.mobile.usage": "Використання",
|
||||||
"contextUsage.tooltip.usedTokens": "Використані токени: {tokens}",
|
"contextUsage.tooltip.usedTokens": "Використані токени: {tokens}",
|
||||||
"contextUsage.tooltip.contextLimit": "Обмеження контексту: {tokens}",
|
"contextUsage.tooltip.contextLimit": "Обмеження контексту: {tokens}",
|
||||||
"contextUsage.tooltip.outputLimit": "Ліміт виводу: {tokens}",
|
"contextUsage.tooltip.outputLimit": "Ліміт виводу: {tokens}",
|
||||||
|
"contextUsage.tooltip.cost": "Вартість: {cost}",
|
||||||
"contextSidebar.session.untitled": "Сесія без назви",
|
"contextSidebar.session.untitled": "Сесія без назви",
|
||||||
"contextSidebar.empty.openSession": "Відкрийте сесію, щоб перевірити контекст.",
|
"contextSidebar.empty.openSession": "Відкрийте сесію, щоб перевірити контекст.",
|
||||||
"contextSidebar.section.context": "Контекст",
|
"contextSidebar.section.context": "Контекст",
|
||||||
|
|||||||
@@ -1403,10 +1403,12 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'contextUsage.mobile.usedTokens': '已用 Token',
|
'contextUsage.mobile.usedTokens': '已用 Token',
|
||||||
'contextUsage.mobile.contextLimit': '上下文上限',
|
'contextUsage.mobile.contextLimit': '上下文上限',
|
||||||
'contextUsage.mobile.outputLimit': '输出上限',
|
'contextUsage.mobile.outputLimit': '输出上限',
|
||||||
|
'contextUsage.mobile.cost': '成本',
|
||||||
'contextUsage.mobile.usage': '使用率',
|
'contextUsage.mobile.usage': '使用率',
|
||||||
'contextUsage.tooltip.usedTokens': '已用 Token:{tokens}',
|
'contextUsage.tooltip.usedTokens': '已用 Token:{tokens}',
|
||||||
'contextUsage.tooltip.contextLimit': '上下文上限:{tokens}',
|
'contextUsage.tooltip.contextLimit': '上下文上限:{tokens}',
|
||||||
'contextUsage.tooltip.outputLimit': '输出上限:{tokens}',
|
'contextUsage.tooltip.outputLimit': '输出上限:{tokens}',
|
||||||
|
'contextUsage.tooltip.cost': '成本:{cost}',
|
||||||
'contextSidebar.session.untitled': '未命名会话',
|
'contextSidebar.session.untitled': '未命名会话',
|
||||||
'contextSidebar.empty.openSession': '请先打开会话以查看上下文。',
|
'contextSidebar.empty.openSession': '请先打开会话以查看上下文。',
|
||||||
'contextSidebar.section.context': '上下文',
|
'contextSidebar.section.context': '上下文',
|
||||||
|
|||||||
@@ -1413,10 +1413,12 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'contextUsage.mobile.usedTokens': '已用 Token',
|
'contextUsage.mobile.usedTokens': '已用 Token',
|
||||||
'contextUsage.mobile.contextLimit': '上下文上限',
|
'contextUsage.mobile.contextLimit': '上下文上限',
|
||||||
'contextUsage.mobile.outputLimit': '輸出上限',
|
'contextUsage.mobile.outputLimit': '輸出上限',
|
||||||
|
'contextUsage.mobile.cost': '成本',
|
||||||
'contextUsage.mobile.usage': '使用率',
|
'contextUsage.mobile.usage': '使用率',
|
||||||
'contextUsage.tooltip.usedTokens': '已用 Token:{tokens}',
|
'contextUsage.tooltip.usedTokens': '已用 Token:{tokens}',
|
||||||
'contextUsage.tooltip.contextLimit': '上下文上限:{tokens}',
|
'contextUsage.tooltip.contextLimit': '上下文上限:{tokens}',
|
||||||
'contextUsage.tooltip.outputLimit': '輸出上限:{tokens}',
|
'contextUsage.tooltip.outputLimit': '輸出上限:{tokens}',
|
||||||
|
'contextUsage.tooltip.cost': '成本:{cost}',
|
||||||
'contextSidebar.session.untitled': '未命名會話',
|
'contextSidebar.session.untitled': '未命名會話',
|
||||||
'contextSidebar.empty.openSession': '請先開啟會話以查看上下文。',
|
'contextSidebar.empty.openSession': '請先開啟會話以查看上下文。',
|
||||||
'contextSidebar.section.context': '上下文',
|
'contextSidebar.section.context': '上下文',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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: 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).
|
- 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).
|
- 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.
|
- 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.
|
- 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).
|
- Skills Catalog: the source is now named ClawHub instead of "ClawdHub" (thanks to @makeittech).
|
||||||
|
|||||||
Reference in New Issue
Block a user