feat: surface Copilot usage and quota data across the UI

Add three layers of usage visibility:

- ChatPane footer: premium request count, AIU consumed, and quota
  remaining below the existing context-window bar
- Settings / CopilotStatusCard: on-demand account quota section with
  progress bars, overage indicators, and reset dates fetched via
  the new get-quota sidecar command
- Activity Panel: per-agent token/cost/duration totals on each agent
  row and a Session Usage summary section between agents and timeline

Backend (sidecar):
- New get-quota command using SDK account.getQuota RPC
- New assistant-usage turn-scoped event from SDK assistant.usage
- QuotaSnapshotMapper for both typed and untyped SDK quota payloads
- DTOs: GetQuotaCommandDto, QuotaSnapshotDto, AccountQuotaResultEventDto,
  AssistantUsageEventDto

Frontend:
- Shared types: AssistantUsageEvent, QuotaSnapshot, GetQuotaCommand
- IPC bridge: getQuota channel, assistant-usage event dispatch
- State: SessionRequestUsageMap accumulator with per-agent breakdown
- 8 new tests for accumulator logic and formatting helpers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-29 00:46:09 +01:00
co-authored by Copilot
parent 48efbf36f9
commit 92832c6116
26 changed files with 978 additions and 18 deletions
+18
View File
@@ -13,12 +13,15 @@ import { resolveChatToolingSettings } from '@renderer/lib/chatTooling';
import {
applySessionEventActivity,
applySessionUsageEvent,
applyAssistantUsageEvent,
applyTurnEventLog,
pruneSessionActivities,
pruneSessionUsage,
pruneSessionRequestUsage,
pruneTurnEventLogs,
type SessionActivityMap,
type SessionUsageMap,
type SessionRequestUsageMap,
type TurnEventLogMap,
} from '@renderer/lib/sessionActivity';
import { applySubagentEvent, pruneSubagentMap, type ActiveSubagentMap } from '@renderer/lib/subagentTracker';
@@ -101,6 +104,7 @@ export default function App() {
const { capabilities: sidecarCapabilities, isRefreshing: isRefreshingCapabilities, refresh: refreshCapabilities } = useSidecarCapabilities(api);
const [sessionActivities, setSessionActivities] = useState<SessionActivityMap>({});
const [sessionUsage, setSessionUsage] = useState<SessionUsageMap>({});
const [sessionRequestUsage, setSessionRequestUsage] = useState<SessionRequestUsageMap>({});
const [turnEventLogs, setTurnEventLogs] = useState<TurnEventLogMap>({});
const [activeSubagents, setActiveSubagents] = useState<ActiveSubagentMap>({});
@@ -140,6 +144,12 @@ export default function App() {
ws.sessions.map((session) => session.id),
),
);
setSessionRequestUsage((current) =>
pruneSessionRequestUsage(
current,
ws.sessions.map((session) => session.id),
),
);
setTurnEventLogs((current) =>
pruneTurnEventLogs(
current,
@@ -158,6 +168,7 @@ export default function App() {
setWorkspace((current) => applySessionEventWorkspace(current, event));
setSessionActivities((current) => applySessionEventActivity(current, event));
setSessionUsage((current) => applySessionUsageEvent(current, event));
setSessionRequestUsage((current) => applyAssistantUsageEvent(current, event));
setTurnEventLogs((current) => applyTurnEventLog(current, event));
setActiveSubagents((current) => applySubagentEvent(current, event));
});
@@ -214,6 +225,10 @@ export default function App() {
() => (selectedSession ? sessionUsage[selectedSession.id] : undefined),
[selectedSession, sessionUsage],
);
const requestUsageForSession = useMemo(
() => (selectedSession ? sessionRequestUsage[selectedSession.id] : undefined),
[selectedSession, sessionRequestUsage],
);
const subagentsForSession = useMemo(
() => (selectedSession ? activeSubagents[selectedSession.id] : undefined),
[selectedSession, activeSubagents],
@@ -401,6 +416,7 @@ export default function App() {
runtimeTools={sidecarCapabilities?.runtimeTools}
session={selectedSession}
sessionUsage={usageForSession}
sessionRequestUsage={requestUsageForSession}
activeSubagents={subagentsForSession}
terminalOpen={terminalOpen}
terminalRunning={terminalRunning}
@@ -413,6 +429,7 @@ export default function App() {
onJumpToMessage={jumpToMessage}
pattern={patternForSession}
session={selectedSession}
sessionRequestUsage={requestUsageForSession}
turnEvents={turnEventsForSession}
/>
);
@@ -478,6 +495,7 @@ export default function App() {
onResolveUserDiscoveredTooling={(serverIds, resolution) => {
void api.resolveWorkspaceDiscoveredTooling({ serverIds, resolution });
}}
onGetQuota={() => api.getQuota()}
/>
) : null;
+87 -10
View File
@@ -1,13 +1,18 @@
import { useMemo, type ReactNode } from 'react';
import { Activity, ArrowRight, CheckCircle2, Clock, Cog, ShieldAlert, Sparkles, Users, Zap } from 'lucide-react';
import { Activity, ArrowRight, BarChart3, CheckCircle2, Clock, Cog, ShieldAlert, Sparkles, Users, Zap } from 'lucide-react';
import {
buildAgentActivityRows,
formatAgentActivityLabel,
formatDuration,
formatNanoAiu,
formatTokenCount,
isAgentActivityActive,
isAgentActivityCompleted,
type AgentActivityRow,
type AgentUsageAccumulator,
type SessionActivityState,
type SessionRequestUsageState,
type TurnEventLog,
} from '@renderer/lib/sessionActivity';
import { RunTimeline } from '@renderer/components/RunTimeline';
@@ -70,11 +75,13 @@ function AgentRow({
agent,
accent,
isLast,
agentUsage,
}: {
row: AgentActivityRow;
agent?: PatternAgentDefinition;
accent: (typeof modeAccent)[OrchestrationMode];
isLast: boolean;
agentUsage?: AgentUsageAccumulator;
}) {
const isActive = isAgentActivityActive(row.activity);
const isCompleted = isAgentActivityCompleted(row.activity);
@@ -141,6 +148,27 @@ function AgentRow({
{formatAgentActivityLabel(row.activity)}
</span>
</div>
{/* Per-agent usage summary */}
{agentUsage && agentUsage.requestCount > 0 && (
<div className="mt-0.5 flex items-center gap-1 text-[10px] text-zinc-600">
<span className="tabular-nums">{formatTokenCount(agentUsage.inputTokens)} in</span>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">{formatTokenCount(agentUsage.outputTokens)} out</span>
{agentUsage.cost > 0 && (
<>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">{agentUsage.cost.toFixed(2)} cost</span>
</>
)}
{agentUsage.durationMs > 0 && (
<>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">{formatDuration(agentUsage.durationMs)}</span>
</>
)}
</div>
)}
</div>
</div>
);
@@ -182,6 +210,7 @@ interface ActivityPanelProps {
onJumpToMessage?: (messageId: string) => void;
pattern: PatternDefinition;
session: SessionRecord;
sessionRequestUsage?: SessionRequestUsageState;
turnEvents?: TurnEventLog;
}
@@ -190,6 +219,7 @@ export function ActivityPanel({
onJumpToMessage,
pattern,
session,
sessionRequestUsage,
turnEvents,
}: ActivityPanelProps) {
const activityRows = useMemo(
@@ -241,21 +271,68 @@ export function ActivityPanel({
{activityRows.length > 0 ? (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3">
{activityRows.map((row, index) => (
<AgentRow
accent={accent}
agent={pattern.agents[index]}
isLast={index === activityRows.length - 1}
key={row.key}
row={row}
/>
))}
{activityRows.map((row, index) => {
const agentKey = row.activity?.agentId ?? row.key;
const agentUsage = sessionRequestUsage?.perAgent[agentKey]
?? sessionRequestUsage?.perAgent[row.agentName];
return (
<AgentRow
accent={accent}
agent={pattern.agents[index]}
agentUsage={agentUsage}
isLast={index === activityRows.length - 1}
key={row.key}
row={row}
/>
);
})}
</div>
) : (
<p className="py-4 text-center text-[11px] text-zinc-600">No agents configured</p>
)}
</div>
{/* ── Session usage section ──────────────────────────── */}
{sessionRequestUsage && sessionRequestUsage.requestCount > 0 && (
<div className="mb-4">
<SectionHeader>
<BarChart3 className="size-3" />
<span>Session Usage</span>
</SectionHeader>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3 py-2.5">
<div className="flex flex-wrap items-center gap-1.5 text-[11px] text-zinc-400">
<span className="font-medium tabular-nums">
{sessionRequestUsage.requestCount} premium request{sessionRequestUsage.requestCount === 1 ? '' : 's'}
</span>
{sessionRequestUsage.totalNanoAiu > 0 && (
<>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">{formatNanoAiu(sessionRequestUsage.totalNanoAiu)} AIU</span>
</>
)}
</div>
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[10px] text-zinc-500">
<span className="tabular-nums">{formatTokenCount(sessionRequestUsage.totalInputTokens)} in</span>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">{formatTokenCount(sessionRequestUsage.totalOutputTokens)} out</span>
{sessionRequestUsage.totalCost > 0 && (
<>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">{sessionRequestUsage.totalCost.toFixed(2)} cost</span>
</>
)}
{sessionRequestUsage.totalDurationMs > 0 && (
<>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">{formatDuration(sessionRequestUsage.totalDurationMs)} total</span>
</>
)}
</div>
</div>
</div>
)}
{/* ── Run timeline section ─────────────────────────── */}
<div className="mb-4">
<SectionHeader>
+25
View File
@@ -17,6 +17,8 @@ import type { InteractionMode, MessageMode } from '@shared/contracts/sidecar';
import type { ChatMessageAttachment } from '@shared/domain/attachment';
import { getAttachmentDisplayName, isImageAttachment } from '@shared/domain/attachment';
import type { SessionUsageState } from '@renderer/lib/sessionActivity';
import type { SessionRequestUsageState, AgentUsageAccumulator } from '@renderer/lib/sessionActivity';
import { formatNanoAiu } from '@renderer/lib/sessionActivity';
import type { ActiveSubagent } from '@renderer/lib/subagentTracker';
import {
findModel,
@@ -46,6 +48,7 @@ interface ChatPaneProps {
mcpProbingServerIds?: string[];
runtimeTools?: ReadonlyArray<RuntimeToolDefinition>;
sessionUsage?: SessionUsageState;
sessionRequestUsage?: SessionRequestUsageState;
activeSubagents?: ReadonlyArray<ActiveSubagent>;
terminalOpen?: boolean;
terminalRunning?: boolean;
@@ -75,6 +78,7 @@ export function ChatPane({
mcpProbingServerIds,
runtimeTools,
sessionUsage,
sessionRequestUsage,
activeSubagents,
terminalOpen,
terminalRunning,
@@ -777,6 +781,27 @@ export function ChatPane({
{Math.round((sessionUsage.currentTokens / sessionUsage.tokenLimit) * 100)}% context
</span>
</div>
{sessionRequestUsage && sessionRequestUsage.requestCount > 0 && (
<div className="mt-0.5 flex items-center gap-1 text-[10px] text-zinc-600">
<span className="tabular-nums">
{sessionRequestUsage.requestCount} premium request{sessionRequestUsage.requestCount === 1 ? '' : 's'}
</span>
{sessionRequestUsage.totalNanoAiu > 0 && (
<>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">{formatNanoAiu(sessionRequestUsage.totalNanoAiu)} AIU</span>
</>
)}
{sessionRequestUsage.latestQuotaSnapshots?.premium_interactions && (
<>
<span className="text-zinc-700">·</span>
<span className="tabular-nums">
{Math.round(sessionRequestUsage.latestQuotaSnapshots.premium_interactions.remainingPercentage)}% quota
</span>
</>
)}
</div>
)}
</div>
)}
</div>
+150 -1
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import {
CheckCircle2,
XCircle,
@@ -12,12 +12,15 @@ import {
ArrowUpCircle,
User,
Building2,
BarChart3,
Loader2,
} from 'lucide-react';
import type {
SidecarConnectionDiagnostics,
SidecarConnectionStatus,
SidecarCopilotCliVersionStatus,
QuotaSnapshot,
} from '@shared/contracts/sidecar';
interface CopilotStatusCardProps {
@@ -25,6 +28,7 @@ interface CopilotStatusCardProps {
modelCount: number;
isRefreshing: boolean;
onRefresh: () => void;
onGetQuota?: () => Promise<Record<string, QuotaSnapshot>>;
}
interface StatusConfig {
@@ -125,6 +129,137 @@ function VersionBadge({ status, installedVersion }: { status: SidecarCopilotCliV
}
}
const quotaTypeLabels: Record<string, string> = {
premium_interactions: 'Premium Requests',
chat: 'Chat',
completions: 'Completions',
};
function formatQuotaTypeLabel(key: string): string {
return quotaTypeLabels[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
function formatResetDate(iso: string): string {
try {
const date = new Date(iso);
const now = new Date();
const diffMs = date.getTime() - now.getTime();
const diffDays = Math.ceil(diffMs / 86_400_000);
if (diffDays <= 0) return 'Today';
if (diffDays === 1) return 'Tomorrow';
if (diffDays <= 30) return `In ${diffDays} day${diffDays === 1 ? '' : 's'}`;
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
} catch {
return iso;
}
}
function QuotaSection({
onGetQuota,
}: {
onGetQuota: () => Promise<Record<string, QuotaSnapshot>>;
}) {
const [quotaData, setQuotaData] = useState<Record<string, QuotaSnapshot>>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(undefined);
void onGetQuota()
.then((data) => { if (!cancelled) setQuotaData(data); })
.catch((err: unknown) => {
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
})
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [onGetQuota]);
if (loading) {
return (
<div className="flex items-center gap-2 py-2">
<Loader2 className="size-3.5 animate-spin text-zinc-500" />
<span className="text-[12px] text-zinc-500">Loading quota</span>
</div>
);
}
if (error) {
return (
<div className="flex items-center gap-2 py-2">
<XCircle className="size-3.5 text-red-400" />
<span className="text-[12px] text-zinc-500">Could not load quota</span>
</div>
);
}
if (!quotaData || Object.keys(quotaData).length === 0) {
return (
<div className="py-2">
<span className="text-[12px] text-zinc-600">No quota data available</span>
</div>
);
}
return (
<div className="space-y-3">
{Object.entries(quotaData).map(([key, snapshot]) => {
const usedPct = snapshot.entitlementRequests > 0
? (snapshot.usedRequests / snapshot.entitlementRequests) * 100
: 0;
const barColor = usedPct > 90
? 'bg-red-500'
: usedPct > 70
? 'bg-amber-500'
: 'bg-indigo-500/60';
return (
<div key={key} className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-[12px] font-medium text-zinc-300">
{formatQuotaTypeLabel(key)}
</span>
<span className="text-[11px] tabular-nums text-zinc-500">
{Math.round(snapshot.remainingPercentage)}% remaining
</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-zinc-800">
<div
className={`h-full rounded-full transition-all ${barColor}`}
style={{ width: `${Math.min(100, usedPct)}%` }}
/>
</div>
<div className="flex items-center gap-1.5 text-[10px] text-zinc-500">
<span className="tabular-nums">
{Math.round(snapshot.usedRequests)} of {Math.round(snapshot.entitlementRequests)} used
</span>
{snapshot.overage > 0 && (
<>
<span className="text-zinc-700">·</span>
<span className="tabular-nums text-amber-400">
{Math.round(snapshot.overage)} overage
</span>
</>
)}
{snapshot.resetDate && (
<>
<span className="text-zinc-700">·</span>
<span>Resets {formatResetDate(snapshot.resetDate)}</span>
</>
)}
</div>
</div>
);
})}
</div>
);
}
function AccountSection({ connection }: { connection: SidecarConnectionDiagnostics }) {
const { account } = connection;
if (!account) return null;
@@ -182,6 +317,7 @@ export function CopilotStatusCard({
modelCount,
isRefreshing,
onRefresh,
onGetQuota,
}: CopilotStatusCardProps) {
const [showDetails, setShowDetails] = useState(false);
@@ -242,6 +378,19 @@ export function CopilotStatusCard({
<AccountSection connection={connection} />
)}
{/* Usage & Quota (when healthy and callback provided) */}
{isHealthy && onGetQuota && (
<div className="space-y-2">
<div className="flex items-center gap-1.5 text-[11px] font-medium text-zinc-400">
<BarChart3 className="size-3" />
<span>Usage &amp; Quota</span>
</div>
<div className="rounded-lg border border-zinc-800/60 bg-zinc-900/40 px-3 py-2.5">
<QuotaSection onGetQuota={onGetQuota} />
</div>
</div>
)}
{/* Action hint for non-ready states */}
{!isHealthy && config.actionLabel && (
<div className="flex items-start gap-2 rounded-lg border border-zinc-800 bg-zinc-900/60 px-3 py-2.5">
+7 -1
View File
@@ -5,7 +5,7 @@ import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard';
import { PatternEditor } from '@renderer/components/PatternEditor';
import { LspProfileEditor } from '@renderer/components/settings/LspProfileEditor';
import { McpServerEditor } from '@renderer/components/settings/McpServerEditor';
import type { SidecarCapabilities } from '@shared/contracts/sidecar';
import type { SidecarCapabilities, QuotaSnapshot } from '@shared/contracts/sidecar';
import type { DiscoveredMcpServer, DiscoveredToolingState } from '@shared/domain/discoveredTooling';
import { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
import type { ModelDefinition } from '@shared/domain/models';
@@ -42,6 +42,7 @@ interface SettingsPanelProps {
onOpenAppDataFolder: () => void;
onResetLocalWorkspace: () => Promise<void>;
onResolveUserDiscoveredTooling?: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
onGetQuota?: () => Promise<Record<string, QuotaSnapshot>>;
}
type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting';
@@ -119,6 +120,7 @@ export function SettingsPanel({
onOpenAppDataFolder,
onResetLocalWorkspace,
onResolveUserDiscoveredTooling,
onGetQuota,
}: SettingsPanelProps) {
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
@@ -261,6 +263,7 @@ export function SettingsPanel({
isRefreshing={isRefreshingCapabilities}
modelCount={sidecarCapabilities?.models.length ?? 0}
onRefresh={onRefreshCapabilities}
onGetQuota={onGetQuota}
/>
)}
{activeSection === 'patterns' && (
@@ -365,11 +368,13 @@ function ConnectionSection({
modelCount,
isRefreshing,
onRefresh,
onGetQuota,
}: {
connection?: SidecarCapabilities['connection'];
modelCount: number;
isRefreshing: boolean;
onRefresh: () => void;
onGetQuota?: () => Promise<Record<string, QuotaSnapshot>>;
}) {
return (
<div>
@@ -384,6 +389,7 @@ function ConnectionSection({
connection={connection}
isRefreshing={isRefreshing}
modelCount={modelCount}
onGetQuota={onGetQuota}
onRefresh={onRefresh}
/>
</div>
+117
View File
@@ -1,5 +1,6 @@
import type { PatternDefinition } from '@shared/domain/pattern';
import type { SessionEventRecord } from '@shared/domain/event';
import type { QuotaSnapshot } from '@shared/contracts/sidecar';
export interface AgentActivityState {
agentId: string;
@@ -335,3 +336,119 @@ export function pruneTurnEventLogs(
return changed || Object.keys(next).length !== Object.keys(current).length ? next : current;
}
/* ── Assistant usage accumulator ────────────────────────────── */
export interface AgentUsageAccumulator {
inputTokens: number;
outputTokens: number;
cost: number;
durationMs: number;
requestCount: number;
}
export interface SessionRequestUsageState {
totalInputTokens: number;
totalOutputTokens: number;
totalCost: number;
totalDurationMs: number;
totalNanoAiu: number;
requestCount: number;
perAgent: Record<string, AgentUsageAccumulator>;
latestQuotaSnapshots?: Record<string, QuotaSnapshot>;
}
export type SessionRequestUsageMap = Record<string, SessionRequestUsageState | undefined>;
function createEmptyUsageAccumulator(): AgentUsageAccumulator {
return { inputTokens: 0, outputTokens: 0, cost: 0, durationMs: 0, requestCount: 0 };
}
export function applyAssistantUsageEvent(
current: SessionRequestUsageMap,
event: SessionEventRecord,
): SessionRequestUsageMap {
if (event.kind !== 'assistant-usage') {
return current;
}
const prev = current[event.sessionId] ?? {
totalInputTokens: 0,
totalOutputTokens: 0,
totalCost: 0,
totalDurationMs: 0,
totalNanoAiu: 0,
requestCount: 0,
perAgent: {},
};
const inputTokens = event.usageInputTokens ?? 0;
const outputTokens = event.usageOutputTokens ?? 0;
const cost = event.usageCost ?? 0;
const durationMs = event.usageDuration ?? 0;
const nanoAiu = event.usageTotalNanoAiu ?? 0;
const next: SessionRequestUsageState = {
totalInputTokens: prev.totalInputTokens + inputTokens,
totalOutputTokens: prev.totalOutputTokens + outputTokens,
totalCost: prev.totalCost + cost,
totalDurationMs: prev.totalDurationMs + durationMs,
totalNanoAiu: nanoAiu > 0 ? nanoAiu : prev.totalNanoAiu,
requestCount: prev.requestCount + 1,
perAgent: { ...prev.perAgent },
latestQuotaSnapshots: event.usageQuotaSnapshots ?? prev.latestQuotaSnapshots,
};
const agentKey = event.agentId?.trim() || event.agentName?.trim();
if (agentKey) {
const agentPrev = next.perAgent[agentKey] ?? createEmptyUsageAccumulator();
next.perAgent[agentKey] = {
inputTokens: agentPrev.inputTokens + inputTokens,
outputTokens: agentPrev.outputTokens + outputTokens,
cost: agentPrev.cost + cost,
durationMs: agentPrev.durationMs + durationMs,
requestCount: agentPrev.requestCount + 1,
};
}
return { ...current, [event.sessionId]: next };
}
export function pruneSessionRequestUsage(
current: SessionRequestUsageMap,
sessionIds: Iterable<string>,
): SessionRequestUsageMap {
const allowed = new Set(sessionIds);
const next: SessionRequestUsageMap = {};
let changed = false;
for (const [sessionId, usage] of Object.entries(current)) {
if (!allowed.has(sessionId)) {
changed = true;
continue;
}
next[sessionId] = usage;
}
return changed || Object.keys(next).length !== Object.keys(current).length ? next : current;
}
/* ── Formatting helpers ─────────────────────────────────────── */
export function formatTokenCount(tokens: number): string {
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
return String(tokens);
}
export function formatNanoAiu(nanoAiu: number): string {
const aiu = nanoAiu / 1e9;
if (aiu >= 100) return aiu.toFixed(0);
if (aiu >= 10) return aiu.toFixed(1);
return aiu.toFixed(2);
}
export function formatDuration(ms: number): string {
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
return `${(ms / 1_000).toFixed(1)}s`;
}