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:
Yun Feng
2026-08-19 00:34:23 +03:00
committed by GitHub
parent 47525c0cf5
commit 36ce73f753
16 changed files with 58 additions and 11 deletions
@@ -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, {
@@ -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<VSCodeHeaderProps> = ({ 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<VSCodeHeaderProps> = ({ 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
@@ -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<ContextUsageDisplayProps> = ({
colorPercentage,
contextLimit,
outputLimit,
cost = null,
size = 'default',
isMobile = false,
hideIcon = false,
@@ -73,10 +76,13 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
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<ContextUsageDisplayProps> = ({
<span className="typography-meta text-muted-foreground">{t('contextUsage.mobile.outputLimit')}</span>
<span className="typography-meta text-foreground font-medium">{formatTokens(safeOutputLimit)}</span>
</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">
<span className="typography-meta text-muted-foreground">{t('contextUsage.mobile.usage')}</span>
<span className={cn('typography-meta font-semibold', getPercentageColor(colorPct))}>