Files
codeg/src/components/layout/status-bar-tokens.tsx
2026-03-13 23:35:51 +08:00

228 lines
7.4 KiB
TypeScript

"use client"
import { useCallback, useSyncExternalStore } from "react"
import { Coins } from "lucide-react"
import { useTranslations } from "next-intl"
import { useSessionStats } from "@/contexts/session-stats-context"
import { useConnectionStore } from "@/contexts/acp-connections-context"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
const ICON_RADIUS = 6
const ICON_CENTER = 8
const ICON_VIEWBOX = 16
const ICON_CIRCUMFERENCE = 2 * Math.PI * ICON_RADIUS
function formatNumber(n: number): string {
return n.toLocaleString()
}
function formatTokenCount(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return String(n)
}
function formatPercent(percent: number | null): string {
if (percent == null) return "--"
return `${percent.toFixed(1)}%`
}
export function StatusBarTokens() {
const t = useTranslations("Folder.statusBar.tokens")
const store = useConnectionStore()
const { sessionStats } = useSessionStats()
const usage = sessionStats?.total_usage
const subscribeActiveKey = useCallback(
(cb: () => void) => store.subscribeActiveKey(cb),
[store]
)
const getActiveKey = useCallback(() => store.getActiveKey(), [store])
const activeKey = useSyncExternalStore(
subscribeActiveKey,
getActiveKey,
getActiveKey
)
const subscribeConn = useCallback(
(cb: () => void) => {
if (!activeKey) return () => {}
return store.subscribeKey(activeKey, cb)
},
[store, activeKey]
)
const getConnSnapshot = useCallback(
() => (activeKey ? store.getConnection(activeKey) : undefined),
[store, activeKey]
)
const activeConn = useSyncExternalStore(
subscribeConn,
getConnSnapshot,
getConnSnapshot
)
const rawLiveUsed = activeConn?.usage?.used ?? null
const rawLiveSize = activeConn?.usage?.size ?? null
// Treat live used=0 as "no data" so we fall back to sessionStats —
// Claude Code sends used=0 for synthetic local commands (/context etc.)
const liveContextUsed =
rawLiveUsed != null && rawLiveUsed > 0 ? rawLiveUsed : null
const liveContextMax =
rawLiveSize != null && rawLiveSize > 0 ? rawLiveSize : null
const contextUsed =
liveContextUsed ?? sessionStats?.context_window_used_tokens ?? null
const contextMax =
liveContextMax ?? sessionStats?.context_window_max_tokens ?? null
const contextPercentRaw =
(liveContextUsed != null && liveContextMax != null && liveContextMax > 0
? (liveContextUsed / liveContextMax) * 100
: sessionStats?.context_window_usage_percent) ??
(contextUsed != null && contextMax != null && contextMax > 0
? (contextUsed / contextMax) * 100
: null)
const contextPercent =
contextPercentRaw == null
? null
: Math.max(0, Math.min(100, contextPercentRaw))
const hasContext = contextPercent != null
const hasUsage = usage != null
const fallbackTotal = hasUsage
? usage.input_tokens +
usage.output_tokens +
usage.cache_creation_input_tokens +
usage.cache_read_input_tokens
: null
const total = sessionStats?.total_tokens ?? fallbackTotal
const dashOffset = ICON_CIRCUMFERENCE * (1 - (contextPercent ?? 0) / 100)
const rows: {
key: "input" | "output" | "cacheRead" | "cacheWrite" | "total"
value: number
}[] = []
if (hasUsage) {
rows.push(
{ key: "input", value: usage.input_tokens },
{ key: "output", value: usage.output_tokens },
{ key: "cacheRead", value: usage.cache_read_input_tokens },
{ key: "cacheWrite", value: usage.cache_creation_input_tokens }
)
}
if (total != null) {
rows.push({ key: "total", value: total })
}
const hasTokenSection = rows.length > 0
if (!hasContext && !hasTokenSection) return null
return (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center gap-1 hover:text-foreground transition-colors">
{hasContext ? (
<>
<svg
aria-label={t("contextWindowUsageAria")}
className="size-3.5"
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
>
<circle
cx={ICON_CENTER}
cy={ICON_CENTER}
r={ICON_RADIUS}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
opacity="0.25"
/>
<circle
cx={ICON_CENTER}
cy={ICON_CENTER}
r={ICON_RADIUS}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeDasharray={`${ICON_CIRCUMFERENCE} ${ICON_CIRCUMFERENCE}`}
strokeDashoffset={dashOffset}
style={{
transformOrigin: "center",
transform: "rotate(-90deg)",
}}
opacity="0.75"
/>
</svg>
<span>{formatPercent(contextPercent)}</span>
</>
) : (
<>
<Coins className="size-3.5" />
<span>{formatTokenCount(total ?? 0)}</span>
</>
)}
</button>
</PopoverTrigger>
<PopoverContent side="top" align="end" className="w-56 gap-2 p-3 text-xs">
{hasContext ? (
<div
className={`space-y-1 ${
hasUsage ? "mb-0.5 border-b border-border pb-0.5" : ""
}`}
>
<div className="flex items-center justify-between gap-2 text-xs font-medium whitespace-nowrap">
<span>{t("contextWindow")}</span>
<span className="tabular-nums shrink-0">
{formatPercent(contextPercent)}
</span>
</div>
<div className="relative h-1.5 overflow-hidden rounded-full bg-muted">
<div
className="absolute inset-y-0 left-0 bg-foreground/70"
style={{ width: `${contextPercent ?? 0}%` }}
/>
</div>
<div className="flex items-center justify-between text-xs leading-none text-muted-foreground">
<span>{t("usedMax")}</span>
<span className="tabular-nums">
{contextUsed == null || contextMax == null
? "--"
: `${formatNumber(contextUsed)} / ${formatNumber(contextMax)}`}
</span>
</div>
</div>
) : null}
{hasTokenSection ? (
<>
<div className="mb-0 mt-0.5 text-xs leading-none font-medium">
{t("tokenUsage")}
</div>
<div className="space-y-0">
{rows.map((row) => (
<div
key={row.key}
className={`flex items-center justify-between py-0.5 text-xs leading-none ${
row.key === "total"
? "mt-0.5 border-t border-border pt-0.5 font-medium"
: "text-muted-foreground"
}`}
>
<span>{t(row.key)}</span>
<span className="tabular-nums">
{formatNumber(row.value)}
</span>
</div>
))}
</div>
</>
) : null}
</PopoverContent>
</Popover>
)
}