import type { ProviderInstanceId, ServerProvider, ServerProviderUsageWindow, UsageLimitSourceSnapshots, } from "@t3tools/contracts"; import { collectProviderUsageLimits, formatResetsIn, type LimitPace, limitsNotice, paceOf, remainingPercent, } from "@t3tools/shared/usageLimits"; import type { ReactNode } from "react"; import { usePrimarySettings } from "../../hooks/useSettings"; import { useNowMinute } from "../../hooks/useNowMinute"; import { type ContextWindowSnapshot, formatContextWindowTokens } from "../../lib/contextWindow"; import { cn } from "../../lib/utils"; import { formatUpcomingTimestamp } from "../../timestampFormat"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { formatContextWindowCompactionMessage } from "./ContextWindowMeter.logic"; import { ThreadDetailsSection } from "./ThreadDetailsSection"; const PACE_LABEL: Record = { ahead: "Ahead of pace", on: "On pace", under: "Under pace", }; export interface ThreadDetailsUsageInput { readonly providerInstanceId: ProviderInstanceId | null; readonly providers: readonly ServerProvider[]; readonly sources: UsageLimitSourceSnapshots; readonly contextWindow: ContextWindowSnapshot | null; } type UsageTone = "error" | "warning" | null; /** The selected provider's limit windows. Each row reads as room left. */ export function ThreadDetailsUsage({ providerInstanceId, providers, sources, }: Omit) { const nowMinute = useNowMinute(); const now = Date.parse(`${nowMinute}:00Z`); const report = providerInstanceId !== null ? collectProviderUsageLimits(providerInstanceId, providers, sources, now) : null; // Only this instance's data: the report keeps one instance per login, and any other entry may be // a different login or workspace, so fall back to the instance's own snapshot. const limits = report?.accounts.find((entry) => entry.instanceId === providerInstanceId)?.limits ?? providers.find((provider) => provider.instanceId === providerInstanceId)?.usageLimits; if (!limits) return null; const notice = limitsNotice(limits); return (
{notice ? ( {notice}} /> ) : ( limits.windows.map((window) => ) )}
); } /** The thread's context window. The bar fills as context is used; the tick marks auto-compaction. */ export function ThreadDetailsContext({ usage }: { readonly usage: ContextWindowSnapshot }) { const maxTokens = usage.maxTokens != null && usage.maxTokens > 0 ? usage.maxTokens : null; const used = maxTokens !== null && usage.usedPercentage !== null ? Math.round(Math.max(0, Math.min(100, usage.usedPercentage))) : null; const threshold = usage.autoCompactThreshold ?? null; const tick = maxTokens !== null && threshold !== null && threshold > 0 && threshold < maxTokens ? (threshold / maxTokens) * 100 : null; const usedTokens = formatContextWindowTokens(usage.usedTokens); return (
= 90 ? "error" : used >= 80 ? "warning" : null} tick={tick} muted value={ maxTokens !== null ? `${usedTokens} / ${formatContextWindowTokens(maxTokens)}` : usedTokens } ariaValue={used !== null ? `${used}% used` : `${usedTokens} tokens used`} tooltip={ <> {used !== null ? `Context: ${used}% used` : `Context: ${usedTokens} tokens used`} {maxTokens !== null ? ( {usage.usedTokens.toLocaleString("en-US")} of {maxTokens.toLocaleString("en-US")}{" "} tokens ) : null} {threshold !== null && threshold > 0 ? ( {formatContextWindowCompactionMessage(null, threshold)} ) : null} } />
); } function LimitRow({ window, now, }: { readonly window: ServerProviderUsageWindow; readonly now: number; }) { const timestampFormat = usePrimarySettings((settings) => settings.timestampFormat); const left = remainingPercent(window); const pace = paceOf(window, now); const resetsAt = window.resetsAt ? Date.parse(window.resetsAt) : Number.NaN; return ( {window.label}: {left}% left {window.resetsAt && resetsAt > now ? ( {formatResetsIn(window, now)?.replace("resets in", "Resets in")} ยท{" "} {formatUpcomingTimestamp(window.resetsAt, timestampFormat, now)} ) : null} {pace ? {PACE_LABEL[pace]} : null} } /> ); } /** One label, hairline, and value row; the whole row opens the details tooltip. */ function UsageRow({ label, fill, tone, tick = null, value, ariaValue = value, tooltip, muted = false, }: { readonly label: string; /** Bar fill from 0 to 100. Null hides the bar. */ readonly fill: number | null; readonly tone: UsageTone; /** A marker on the bar, from 0 to 100. */ readonly tick?: number | null; readonly value: string; readonly ariaValue?: string; readonly tooltip: ReactNode; readonly muted?: boolean; }) { return ( } > {label} {fill !== null ? ( {fill > 0 ? ( ) : null} {tick !== null ? ( ) : null} ) : null} {value} {/* Rows sit 8px inside the card; clear its edge instead of overlapping it. */}
{tooltip}
); }