Context
Context window usage ring with a hover card of token and cost breakdown.
import { Context, ContextCacheUsage, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextTrigger,} from "@/components/ai/context";
const usage = { inputTokens: 98200, outputTokens: 18400, totalTokens: 124000, inputTokenDetails: { noCacheTokens: 96900, cacheReadTokens: 1300, cacheWriteTokens: 0, }, outputTokenDetails: { textTokens: 12300, reasoningTokens: 6100 },};
export default function Demo() { return ( <div className="flex items-start gap-6"> <Context maxTokens={200000} usedTokens={124000} usage={usage} modelId="anthropic/claude-sonnet-4" > <ContextTrigger /> <ContextContent> <ContextContentHeader /> <ContextContentBody> <ContextInputUsage /> <ContextOutputUsage /> <ContextReasoningUsage /> <ContextCacheUsage /> </ContextContentBody> <ContextContentFooter /> </ContextContent> </Context> <Context maxTokens={200000} usedTokens={182000}> <ContextTrigger /> <ContextContent> <ContextContentHeader /> </ContextContent> </Context> </div> );}<script setup lang="ts">import { Context, ContextCacheUsage, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextTrigger,} from "@/components/ai/context";
const usage = { inputTokens: 98200, outputTokens: 18400, totalTokens: 124000, inputTokenDetails: { noCacheTokens: 96900, cacheReadTokens: 1300, cacheWriteTokens: 0, }, outputTokenDetails: { textTokens: 12300, reasoningTokens: 6100 },};</script>
<template> <div class="flex items-start gap-6"> <Context :max-tokens="200000" :used-tokens="124000" :usage="usage" model-id="anthropic/claude-sonnet-4"> <ContextTrigger /> <ContextContent> <ContextContentHeader /> <ContextContentBody> <ContextInputUsage /> <ContextOutputUsage /> <ContextReasoningUsage /> <ContextCacheUsage /> </ContextContentBody> <ContextContentFooter /> </ContextContent> </Context> <Context :max-tokens="200000" :used-tokens="182000"> <ContextTrigger /> <ContextContent> <ContextContentHeader /> </ContextContent> </Context> </div></template><script lang="ts"> import { Context, ContextCacheUsage, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextTrigger, } from "$lib/components/ai/context";
const usage = { inputTokens: 98200, outputTokens: 18400, totalTokens: 124000, inputTokenDetails: { noCacheTokens: 96900, cacheReadTokens: 1300, cacheWriteTokens: 0, }, outputTokenDetails: { textTokens: 12300, reasoningTokens: 6100 }, };</script>
<div class="flex items-start gap-6"> <Context maxTokens={200000} usedTokens={124000} {usage} modelId="anthropic/claude-sonnet-4"> <ContextTrigger /> <ContextContent> <ContextContentHeader /> <ContextContentBody> <ContextInputUsage /> <ContextOutputUsage /> <ContextReasoningUsage /> <ContextCacheUsage /> </ContextContentBody> <ContextContentFooter /> </ContextContent> </Context> <Context maxTokens={200000} usedTokens={182000}> <ContextTrigger /> <ContextContent> <ContextContentHeader /> </ContextContent> </Context></div>Built on hover-card and progress; costs come from tokenlens.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-contextpnpm dlx shadcn@latest add @edmi-ui/ai-contextyarn dlx shadcn@latest add @edmi-ui/ai-contextbunx shadcn@latest add @edmi-ui/ai-contextPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install ai tokenlensTerminal window pnpm add ai tokenlensTerminal window yarn add ai tokenlensTerminal window bun add ai tokenlensAdd the Edmi components this one builds on:
button,hover-card,progress.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-contextpnpm dlx shadcn-vue@latest add @edmi-ui/ai-contextyarn dlx shadcn-vue@latest add @edmi-ui/ai-contextbunx shadcn-vue@latest add @edmi-ui/ai-contextPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install ai tokenlens reka-uiTerminal window pnpm add ai tokenlens reka-uiTerminal window yarn add ai tokenlens reka-uiTerminal window bun add ai tokenlens reka-uiAdd the Edmi components this one builds on:
button,hover-card,progress.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-context.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-context.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-context.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-context.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,hover-card,progress.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Context, ContextCacheUsage, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextTrigger,} from "@/components/ai/context";
<div className="flex items-start gap-6"> <Context maxTokens={200000} usedTokens={124000} modelId="anthropic/claude-sonnet-4" > <ContextTrigger /> <ContextContent> <ContextContentHeader /> <ContextContentBody> <ContextInputUsage /> <ContextOutputUsage /> <ContextReasoningUsage /> <ContextCacheUsage /> </ContextContentBody> <ContextContentFooter /> </ContextContent> </Context> <Context maxTokens={200000} usedTokens={182000}> <ContextTrigger /> <ContextContent> <ContextContentHeader /> </ContextContent> </Context></div><script setup lang="ts">import { Context, ContextCacheUsage, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextTrigger,} from "@/components/ai/context";</script>
<template> <div class="flex items-start gap-6"> <Context :max-tokens="200000" :used-tokens="124000" model-id="anthropic/claude-sonnet-4"> <ContextTrigger /> <ContextContent> <ContextContentHeader /> <ContextContentBody> <ContextInputUsage /> <ContextOutputUsage /> <ContextReasoningUsage /> <ContextCacheUsage /> </ContextContentBody> <ContextContentFooter /> </ContextContent> </Context> <Context :max-tokens="200000" :used-tokens="182000"> <ContextTrigger /> <ContextContent> <ContextContentHeader /> </ContextContent> </Context> </div></template><script lang="ts"> import { Context, ContextCacheUsage, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextTrigger, } from "$lib/components/ai/context";</script>
<div class="flex items-start gap-6"> <Context maxTokens={200000} usedTokens={124000} modelId="anthropic/claude-sonnet-4"> <ContextTrigger /> <ContextContent> <ContextContentHeader /> <ContextContentBody> <ContextInputUsage /> <ContextOutputUsage /> <ContextReasoningUsage /> <ContextCacheUsage /> </ContextContentBody> <ContextContentFooter /> </ContextContent> </Context> <Context maxTokens={200000} usedTokens={182000}> <ContextTrigger /> <ContextContent> <ContextContentHeader /> </ContextContent> </Context></div>Component source
Section titled “Component source”"use client";
import type { LanguageModelUsage } from "ai";import { cn } from "cn";import type { ComponentProps } from "react";import { createContext, useContext, useMemo } from "react";import { getUsage } from "tokenlens";import { Button } from "@/registry/edmi/ui/button";import { HoverCard, HoverCardContent, HoverCardTrigger,} from "@/registry/edmi/ui/hover-card";import { Progress } from "@/registry/edmi/ui/progress";
const PERCENT_MAX = 100;const ICON_RADIUS = 10;const ICON_VIEWBOX = 24;const ICON_CENTER = 12;const ICON_STROKE_WIDTH = 2;
type ModelId = string;
interface ContextSchema { usedTokens: number; maxTokens: number; usage?: LanguageModelUsage; modelId?: ModelId;}
const ContextContext = createContext<ContextSchema | null>(null);
const useContextValue = () => { const context = useContext(ContextContext);
if (!context) { throw new Error("Context components must be used within Context"); }
return context;};
export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
export const Context = ({ usedTokens, maxTokens, usage, modelId, ...props}: ContextProps) => { const contextValue = useMemo( () => ({ maxTokens, modelId, usage, usedTokens }), [maxTokens, modelId, usage, usedTokens], );
return ( <ContextContext.Provider value={contextValue}> <HoverCard {...props} /> </ContextContext.Provider> );};
const ContextIcon = () => { const { usedTokens, maxTokens } = useContextValue(); const circumference = 2 * Math.PI * ICON_RADIUS; const usedPercent = Math.min(usedTokens / maxTokens, 1); const dashOffset = circumference * (1 - usedPercent);
return ( <svg aria-label="Model context usage" height="22" role="img" viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`} width="22" > <title>Model context usage</title> <circle cx={ICON_CENTER} cy={ICON_CENTER} fill="none" r={ICON_RADIUS} className="stroke-border" strokeWidth={ICON_STROKE_WIDTH} /> <circle cx={ICON_CENTER} cy={ICON_CENTER} fill="none" r={ICON_RADIUS} className="stroke-brand" strokeDasharray={`${circumference} ${circumference}`} strokeDashoffset={dashOffset} strokeLinecap="round" strokeWidth={ICON_STROKE_WIDTH} style={{ transform: "rotate(-90deg)", transformOrigin: "center" }} /> </svg> );};
export type ContextTriggerProps = ComponentProps<typeof Button>;
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => { const { usedTokens, maxTokens } = useContextValue(); const usedPercent = usedTokens / maxTokens; const renderedPercent = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1, style: "percent", }).format(usedPercent);
return ( <HoverCardTrigger delay={0} closeDelay={0} render={ children ? ( (children as React.ReactElement<Record<string, unknown>>) ) : ( <Button type="button" variant="ghost" {...props} /> ) } > {children ? undefined : ( <> <ContextIcon /> <span className={cn( "font-mono text-xs font-medium", usedPercent >= 0.9 ? "text-warning-text" : "text-foreground", )} > {renderedPercent} </span> </> )} </HoverCardTrigger> );};
export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
export const ContextContent = ({ className, ...props}: ContextContentProps) => ( <HoverCardContent className={cn( "min-w-60 divide-y divide-border overflow-hidden p-0", className, )} {...props} />);
export type ContextContentHeaderProps = ComponentProps<"div">;
export const ContextContentHeader = ({ children, className, ...props}: ContextContentHeaderProps) => { const { usedTokens, maxTokens } = useContextValue(); const usedPercent = usedTokens / maxTokens; const displayPct = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1, style: "percent", }).format(usedPercent); const used = new Intl.NumberFormat("en-US", { notation: "compact", }).format(usedTokens); const total = new Intl.NumberFormat("en-US", { notation: "compact", }).format(maxTokens);
return ( <div className={cn("w-full space-y-2 p-3", className)} {...props}> {children ?? ( <> <div className="flex items-center justify-between gap-3 text-xs"> <p className="font-semibold">{displayPct}</p> <p className="font-mono text-muted-foreground"> {used} / {total} </p> </div> <div className="space-y-2"> <Progress variant="brand" value={usedPercent * PERCENT_MAX} /> </div> </> )} </div> );};
export type ContextContentBodyProps = ComponentProps<"div">;
export const ContextContentBody = ({ children, className, ...props}: ContextContentBodyProps) => ( <div className={cn("w-full space-y-1.5 p-3", className)} {...props}> {children} </div>);
export type ContextContentFooterProps = ComponentProps<"div">;
export const ContextContentFooter = ({ children, className, ...props}: ContextContentFooterProps) => { const { modelId, usage } = useContextValue(); const costUSD = modelId ? getUsage({ modelId, usage: { input: usage?.inputTokens ?? 0, output: usage?.outputTokens ?? 0, }, }).costUSD?.totalUSD : undefined; const totalCost = new Intl.NumberFormat("en-US", { currency: "USD", style: "currency", }).format(costUSD ?? 0);
return ( <div className={cn( "flex w-full items-center justify-between gap-3 bg-muted p-3 text-xs", className, )} {...props} > {children ?? ( <> <span className="text-muted-foreground">Total cost</span> <span className="font-mono font-semibold">{totalCost}</span> </> )} </div> );};
const TokensWithCost = ({ tokens, costText,}: { tokens?: number; costText?: string;}) => ( <span className="font-mono"> {tokens === undefined ? "—" : new Intl.NumberFormat("en-US", { notation: "compact", }).format(tokens)} {costText ? ( <span className="ml-2 text-muted-foreground">• {costText}</span> ) : null} </span>);
export type ContextInputUsageProps = ComponentProps<"div">;
export const ContextInputUsage = ({ className, children, ...props}: ContextInputUsageProps) => { const { usage, modelId } = useContextValue(); const inputTokens = usage?.inputTokens ?? 0;
if (children) { return children; }
if (!inputTokens) { return null; }
const inputCost = modelId ? getUsage({ modelId, usage: { input: inputTokens, output: 0 }, }).costUSD?.totalUSD : undefined; const inputCostText = new Intl.NumberFormat("en-US", { currency: "USD", style: "currency", }).format(inputCost ?? 0);
return ( <div className={cn("flex items-center justify-between text-xs", className)} {...props} > <span className="text-muted-foreground">Input</span> <TokensWithCost costText={inputCostText} tokens={inputTokens} /> </div> );};
export type ContextOutputUsageProps = ComponentProps<"div">;
export const ContextOutputUsage = ({ className, children, ...props}: ContextOutputUsageProps) => { const { usage, modelId } = useContextValue(); const outputTokens = usage?.outputTokens ?? 0;
if (children) { return children; }
if (!outputTokens) { return null; }
const outputCost = modelId ? getUsage({ modelId, usage: { input: 0, output: outputTokens }, }).costUSD?.totalUSD : undefined; const outputCostText = new Intl.NumberFormat("en-US", { currency: "USD", style: "currency", }).format(outputCost ?? 0);
return ( <div className={cn("flex items-center justify-between text-xs", className)} {...props} > <span className="text-muted-foreground">Output</span> <TokensWithCost costText={outputCostText} tokens={outputTokens} /> </div> );};
export type ContextReasoningUsageProps = ComponentProps<"div">;
export const ContextReasoningUsage = ({ className, children, ...props}: ContextReasoningUsageProps) => { const { usage, modelId } = useContextValue(); const reasoningTokens = usage?.outputTokenDetails?.reasoningTokens ?? 0;
if (children) { return children; }
if (!reasoningTokens) { return null; }
const reasoningCost = modelId ? getUsage({ modelId, usage: { reasoningTokens }, }).costUSD?.totalUSD : undefined; const reasoningCostText = new Intl.NumberFormat("en-US", { currency: "USD", style: "currency", }).format(reasoningCost ?? 0);
return ( <div className={cn("flex items-center justify-between text-xs", className)} {...props} > <span className="text-muted-foreground">Reasoning</span> <TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} /> </div> );};
export type ContextCacheUsageProps = ComponentProps<"div">;
export const ContextCacheUsage = ({ className, children, ...props}: ContextCacheUsageProps) => { const { usage, modelId } = useContextValue(); const cacheTokens = usage?.inputTokenDetails?.cacheReadTokens ?? 0;
if (children) { return children; }
if (!cacheTokens) { return null; }
const cacheCost = modelId ? getUsage({ modelId, usage: { cacheReads: cacheTokens, input: 0, output: 0 }, }).costUSD?.totalUSD : undefined; const cacheCostText = new Intl.NumberFormat("en-US", { currency: "USD", style: "currency", }).format(cacheCost ?? 0);
return ( <div className={cn("flex items-center justify-between text-xs", className)} {...props} > <span className="text-muted-foreground">Cache</span> <TokensWithCost costText={cacheCostText} tokens={cacheTokens} /> </div> );};<script setup lang="ts">import type { LanguageModelUsage } from "ai"import type { HoverCardRootProps } from "reka-ui"import type { ModelId } from "./context"import { computed, provide } from "vue"import { HoverCard } from "@/registry/edmi/ui/hover-card"import { ContextKey } from "./context"
interface ContextProps extends /* @vue-ignore */ HoverCardRootProps { usedTokens: number maxTokens: number usage?: LanguageModelUsage modelId?: ModelId}
const props = defineProps<ContextProps>()
provide(ContextKey, { usedTokens: computed(() => props.usedTokens), maxTokens: computed(() => props.maxTokens), usage: computed(() => props.usage), modelId: computed(() => props.modelId),})</script>
<template> <!-- HoverCard props (open, delays) fall through as attrs and win over the 0 defaults. --> <HoverCard :open-delay="0" :close-delay="0"> <slot /> </HoverCard></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { getUsage } from "tokenlens"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { formatUsd, useContextValue } from "./context"import TokensWithCost from "./TokensWithCost.vue"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { usage, modelId } = useContextValue()
const tokens = computed(() => usage.value?.inputTokenDetails?.cacheReadTokens ?? 0)
const costText = computed(() => { if (!tokens.value) return undefined const cost = modelId.value ? getUsage({ modelId: modelId.value, usage: { cacheReads: tokens.value, input: 0, output: 0 }, }).costUSD?.totalUSD : undefined return formatUsd(cost ?? 0)})</script>
<template> <slot v-if="$slots.default" /> <div v-else-if="tokens > 0" :class="cn('flex items-center justify-between text-xs', props.class)" > <span class="text-muted-foreground">Cache</span> <TokensWithCost :cost-text="costText" :tokens="tokens" /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { HoverCardContent } from "@/registry/edmi/ui/hover-card"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <HoverCardContent :class="cn('min-w-60 divide-y divide-border overflow-hidden p-0', props.class)"> <slot /> </HoverCardContent></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div :class="cn('w-full space-y-1.5 p-3', props.class)"> <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { getUsage } from "tokenlens"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { formatUsd, useContextValue } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { modelId, usage } = useContextValue()
const totalCost = computed(() => { const costUSD = modelId.value ? getUsage({ modelId: modelId.value, usage: { input: usage.value?.inputTokens ?? 0, output: usage.value?.outputTokens ?? 0, }, }).costUSD?.totalUSD : undefined return formatUsd(costUSD ?? 0)})</script>
<template> <div :class="cn('flex w-full items-center justify-between gap-3 bg-muted p-3 text-xs', props.class)"> <slot v-if="$slots.default" /> <template v-else> <span class="text-muted-foreground">Total cost</span> <span class="font-mono font-semibold">{{ totalCost }}</span> </template> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Progress } from "@/registry/edmi/ui/progress"import { formatCompact, formatPercent, useContextValue } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const PERCENT_MAX = 100const { usedTokens, maxTokens } = useContextValue()
const usedPercent = computed(() => (maxTokens.value === 0 ? 0 : usedTokens.value / maxTokens.value))const displayPct = computed(() => formatPercent(usedPercent.value))const used = computed(() => formatCompact(usedTokens.value))const total = computed(() => formatCompact(maxTokens.value))</script>
<template> <div :class="cn('w-full space-y-2 p-3', props.class)"> <slot v-if="$slots.default" /> <template v-else> <div class="flex items-center justify-between gap-3 text-xs"> <p class="font-semibold"> {{ displayPct }} </p> <p class="font-mono text-muted-foreground"> {{ used }} / {{ total }} </p> </div> <div class="space-y-2"> <Progress variant="brand" :model-value="usedPercent * PERCENT_MAX" /> </div> </template> </div></template><script setup lang="ts">import { computed } from "vue"import { useContextValue } from "./context"
const ICON_RADIUS = 10const ICON_VIEWBOX = 24const ICON_CENTER = 12const ICON_STROKE_WIDTH = 2
const { usedTokens, maxTokens } = useContextValue()
const circumference = 2 * Math.PI * ICON_RADIUSconst dashOffset = computed(() => { const usedPercent = maxTokens.value === 0 ? 0 : Math.min(usedTokens.value / maxTokens.value, 1) return circumference * (1 - usedPercent)})</script>
<template> <svg aria-label="Model context usage" height="22" role="img" :viewBox="`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`" width="22" > <title>Model context usage</title> <circle :cx="ICON_CENTER" :cy="ICON_CENTER" fill="none" :r="ICON_RADIUS" class="stroke-border" :stroke-width="ICON_STROKE_WIDTH" /> <circle :cx="ICON_CENTER" :cy="ICON_CENTER" fill="none" :r="ICON_RADIUS" class="stroke-brand" :stroke-dasharray="`${circumference} ${circumference}`" :stroke-dashoffset="dashOffset" stroke-linecap="round" :stroke-width="ICON_STROKE_WIDTH" style="transform: rotate(-90deg); transform-origin: center" /> </svg></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { getUsage } from "tokenlens"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { formatUsd, useContextValue } from "./context"import TokensWithCost from "./TokensWithCost.vue"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { usage, modelId } = useContextValue()
const tokens = computed(() => usage.value?.inputTokens ?? 0)
const costText = computed(() => { if (!tokens.value) return undefined const cost = modelId.value ? getUsage({ modelId: modelId.value, usage: { input: tokens.value, output: 0 }, }).costUSD?.totalUSD : undefined return formatUsd(cost ?? 0)})</script>
<template> <slot v-if="$slots.default" /> <div v-else-if="tokens > 0" :class="cn('flex items-center justify-between text-xs', props.class)" > <span class="text-muted-foreground">Input</span> <TokensWithCost :cost-text="costText" :tokens="tokens" /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { getUsage } from "tokenlens"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { formatUsd, useContextValue } from "./context"import TokensWithCost from "./TokensWithCost.vue"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { usage, modelId } = useContextValue()
const tokens = computed(() => usage.value?.outputTokens ?? 0)
const costText = computed(() => { if (!tokens.value) return undefined const cost = modelId.value ? getUsage({ modelId: modelId.value, usage: { input: 0, output: tokens.value }, }).costUSD?.totalUSD : undefined return formatUsd(cost ?? 0)})</script>
<template> <slot v-if="$slots.default" /> <div v-else-if="tokens > 0" :class="cn('flex items-center justify-between text-xs', props.class)" > <span class="text-muted-foreground">Output</span> <TokensWithCost :cost-text="costText" :tokens="tokens" /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { getUsage } from "tokenlens"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { formatUsd, useContextValue } from "./context"import TokensWithCost from "./TokensWithCost.vue"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { usage, modelId } = useContextValue()
const tokens = computed(() => usage.value?.outputTokenDetails?.reasoningTokens ?? 0)
const costText = computed(() => { if (!tokens.value) return undefined const cost = modelId.value ? getUsage({ modelId: modelId.value, usage: { reasoningTokens: tokens.value }, }).costUSD?.totalUSD : undefined return formatUsd(cost ?? 0)})</script>
<template> <slot v-if="$slots.default" /> <div v-else-if="tokens > 0" :class="cn('flex items-center justify-between text-xs', props.class)" > <span class="text-muted-foreground">Reasoning</span> <TokensWithCost :cost-text="costText" :tokens="tokens" /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import { HoverCardTrigger } from "@/registry/edmi/ui/hover-card"import { formatPercent, useContextValue } from "./context"import ContextIcon from "./ContextIcon.vue"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { usedTokens, maxTokens } = useContextValue()
const usedPercent = computed(() => (maxTokens.value === 0 ? 0 : usedTokens.value / maxTokens.value))const renderedPercent = computed(() => formatPercent(usedPercent.value))</script>
<template> <HoverCardTrigger as-child> <slot v-if="$slots.default" /> <Button v-else type="button" variant="ghost" :class="props.class"> <ContextIcon /> <span :class="cn( 'font-mono text-xs font-medium', usedPercent >= 0.9 ? 'text-warning-text' : 'text-foreground', )" > {{ renderedPercent }} </span> </Button> </HoverCardTrigger></template><script setup lang="ts">import { computed } from "vue"import { formatCompact } from "./context"
const props = defineProps<{ tokens?: number costText?: string}>()
const formattedTokens = computed(() => (props.tokens === undefined ? "—" : formatCompact(props.tokens)))</script>
<template> <span class="font-mono"> {{ formattedTokens }} <span v-if="costText" class="ml-2 text-muted-foreground">• {{ costText }}</span> </span></template>import type { LanguageModelUsage } from "ai";import type { ComputedRef, InjectionKey } from "vue";import { inject } from "vue";
export type ModelId = string;
export interface ContextContextValue { usedTokens: ComputedRef<number>; maxTokens: ComputedRef<number>; usage: ComputedRef<LanguageModelUsage | undefined>; modelId: ComputedRef<ModelId | undefined>;}
export const ContextKey: InjectionKey<ContextContextValue> = Symbol("ContextContext");
export function useContextValue(): ContextContextValue { const context = inject<ContextContextValue>(ContextKey); if (!context) { throw new Error("Context components must be used within Context"); } return context;}
export const formatPercent = (value: number) => new Intl.NumberFormat("en-US", { maximumFractionDigits: 1, style: "percent", }).format(value);
export const formatCompact = (value: number) => new Intl.NumberFormat("en-US", { notation: "compact" }).format(value);
export const formatUsd = (value: number) => new Intl.NumberFormat("en-US", { currency: "USD", style: "currency" }).format( value, );export { default as Context } from "./Context.vue";export { default as ContextCacheUsage } from "./ContextCacheUsage.vue";export { default as ContextContent } from "./ContextContent.vue";export { default as ContextContentBody } from "./ContextContentBody.vue";export { default as ContextContentFooter } from "./ContextContentFooter.vue";export { default as ContextContentHeader } from "./ContextContentHeader.vue";export { default as ContextIcon } from "./ContextIcon.vue";export { default as ContextInputUsage } from "./ContextInputUsage.vue";export { default as ContextOutputUsage } from "./ContextOutputUsage.vue";export { default as ContextReasoningUsage } from "./ContextReasoningUsage.vue";export { default as ContextTrigger } from "./ContextTrigger.vue";export * from "./context";export { default as TokensWithCost } from "./TokensWithCost.vue";<script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import { getUsage } from "tokenlens"; import TokensWithCost from "./tokens-with-cost.svelte"; import { formatUsd, useContextValue } from "./use-context.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
const context = useContextValue(); const tokens = $derived(context.usage?.inputTokenDetails?.cacheReadTokens ?? 0); const costText = $derived.by(() => { if (!tokens) return undefined; const cost = context.modelId ? getUsage({ modelId: context.modelId, usage: { cacheReads: tokens, input: 0, output: 0 }, }).costUSD?.totalUSD : undefined; return formatUsd(cost ?? 0); });</script>
{#if children} {@render children()}{:else if tokens > 0} <div class={cn("flex items-center justify-between text-xs", className)} {...restProps}> <span class="text-muted-foreground">Cache</span> <TokensWithCost {costText} {tokens} /> </div>{/if}<script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();</script>
<div class={cn("w-full space-y-1.5 p-3", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import { getUsage } from "tokenlens"; import { formatUsd, useContextValue } from "./use-context.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
const context = useContextValue(); const totalCost = $derived.by(() => { const costUSD = context.modelId ? getUsage({ modelId: context.modelId, usage: { input: context.usage?.inputTokens ?? 0, output: context.usage?.outputTokens ?? 0, }, }).costUSD?.totalUSD : undefined; return formatUsd(costUSD ?? 0); });</script>
<div class={cn("flex w-full items-center justify-between gap-3 bg-muted p-3 text-xs", className)} {...restProps}> {#if children} {@render children()} {:else} <span class="text-muted-foreground">Total cost</span> <span class="font-mono font-semibold">{totalCost}</span> {/if}</div><script lang="ts"> import { Progress } from "$lib/registry/ui/progress/index.js"; import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import { formatCompact, formatPercent, useContextValue } from "./use-context.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
const PERCENT_MAX = 100; const context = useContextValue(); const usedPercent = $derived( context.maxTokens === 0 ? 0 : context.usedTokens / context.maxTokens );</script>
<div class={cn("w-full space-y-2 p-3", className)} {...restProps}> {#if children} {@render children()} {:else} <div class="flex items-center justify-between gap-3 text-xs"> <p class="font-semibold">{formatPercent(usedPercent)}</p> <p class="font-mono text-muted-foreground"> {formatCompact(context.usedTokens)} / {formatCompact(context.maxTokens)} </p> </div> <div class="space-y-2"> <Progress variant="brand" value={usedPercent * PERCENT_MAX} /> </div> {/if}</div><script lang="ts"> import { HoverCardContent } from "$lib/registry/ui/hover-card/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, children, ...restProps }: ComponentProps<typeof HoverCardContent> = $props();</script>
<HoverCardContent bind:ref class={cn("min-w-60 divide-y divide-border overflow-hidden p-0", className)} {...restProps}> {@render children?.()}</HoverCardContent><script lang="ts"> import { useContextValue } from "./use-context.svelte.js";
const ICON_RADIUS = 10; const ICON_VIEWBOX = 24; const ICON_CENTER = 12; const ICON_STROKE_WIDTH = 2;
const context = useContextValue(); const circumference = 2 * Math.PI * ICON_RADIUS; const dashOffset = $derived( circumference * (1 - (context.maxTokens === 0 ? 0 : Math.min(context.usedTokens / context.maxTokens, 1))) );</script>
<svg aria-label="Model context usage" height="22" role="img" viewBox="0 0 {ICON_VIEWBOX} {ICON_VIEWBOX}" width="22"> <title>Model context usage</title> <circle cx={ICON_CENTER} cy={ICON_CENTER} fill="none" r={ICON_RADIUS} class="stroke-border" stroke-width={ICON_STROKE_WIDTH} /> <circle cx={ICON_CENTER} cy={ICON_CENTER} fill="none" r={ICON_RADIUS} class="stroke-brand" stroke-dasharray="{circumference} {circumference}" stroke-dashoffset={dashOffset} stroke-linecap="round" stroke-width={ICON_STROKE_WIDTH} style="transform: rotate(-90deg); transform-origin: center" /></svg><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import { getUsage } from "tokenlens"; import TokensWithCost from "./tokens-with-cost.svelte"; import { formatUsd, useContextValue } from "./use-context.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
const context = useContextValue(); const tokens = $derived(context.usage?.inputTokens ?? 0); const costText = $derived.by(() => { if (!tokens) return undefined; const cost = context.modelId ? getUsage({ modelId: context.modelId, usage: { input: tokens, output: 0 }, }).costUSD?.totalUSD : undefined; return formatUsd(cost ?? 0); });</script>
{#if children} {@render children()}{:else if tokens > 0} <div class={cn("flex items-center justify-between text-xs", className)} {...restProps}> <span class="text-muted-foreground">Input</span> <TokensWithCost {costText} {tokens} /> </div>{/if}<script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import { getUsage } from "tokenlens"; import TokensWithCost from "./tokens-with-cost.svelte"; import { formatUsd, useContextValue } from "./use-context.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
const context = useContextValue(); const tokens = $derived(context.usage?.outputTokens ?? 0); const costText = $derived.by(() => { if (!tokens) return undefined; const cost = context.modelId ? getUsage({ modelId: context.modelId, usage: { input: 0, output: tokens }, }).costUSD?.totalUSD : undefined; return formatUsd(cost ?? 0); });</script>
{#if children} {@render children()}{:else if tokens > 0} <div class={cn("flex items-center justify-between text-xs", className)} {...restProps}> <span class="text-muted-foreground">Output</span> <TokensWithCost {costText} {tokens} /> </div>{/if}<script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import { getUsage } from "tokenlens"; import TokensWithCost from "./tokens-with-cost.svelte"; import { formatUsd, useContextValue } from "./use-context.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
const context = useContextValue(); const tokens = $derived(context.usage?.outputTokenDetails?.reasoningTokens ?? 0); const costText = $derived.by(() => { if (!tokens) return undefined; const cost = context.modelId ? getUsage({ modelId: context.modelId, usage: { reasoningTokens: tokens }, }).costUSD?.totalUSD : undefined; return formatUsd(cost ?? 0); });</script>
{#if children} {@render children()}{:else if tokens > 0} <div class={cn("flex items-center justify-between text-xs", className)} {...restProps}> <span class="text-muted-foreground">Reasoning</span> <TokensWithCost {costText} {tokens} /> </div>{/if}<script lang="ts"> import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js"; import { HoverCardTrigger } from "$lib/registry/ui/hover-card/index.js"; import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import ContextIcon from "./context-icon.svelte"; import { formatPercent, useContextValue } from "./use-context.svelte.js";
let { class: className, child: customChild, children, ...restProps }: Omit<ButtonProps, "href" | "child"> & { /** Bring your own trigger element (bits-ui `child` snippet). */ child?: Snippet<[{ props: Record<string, unknown> }]>; } = $props();
const context = useContextValue(); const usedPercent = $derived( context.maxTokens === 0 ? 0 : context.usedTokens / context.maxTokens );</script>
<HoverCardTrigger> {#snippet child({ props })} {#if customChild} {@render customChild({ props })} {:else} <Button type="button" variant="ghost" class={className} {...props} {...restProps}> {#if children} {@render children()} {:else} <ContextIcon /> <span class={cn( "font-mono text-xs font-medium", usedPercent >= 0.9 ? "text-warning-text" : "text-foreground" )} > {formatPercent(usedPercent)} </span> {/if} </Button> {/if} {/snippet}</HoverCardTrigger><script lang="ts"> import { HoverCard } from "$lib/registry/ui/hover-card/index.js"; import type { LanguageModelUsage } from "ai"; import type { ComponentProps } from "svelte"; import { type ModelId, setContextValue } from "./use-context.svelte.js";
let { usedTokens, maxTokens, usage, modelId, open = $bindable(false), openDelay = 0, closeDelay = 0, children, ...restProps }: ComponentProps<typeof HoverCard> & { usedTokens: number; maxTokens: number; usage?: LanguageModelUsage; modelId?: ModelId; } = $props();
setContextValue({ get usedTokens() { return usedTokens; }, get maxTokens() { return maxTokens; }, get usage() { return usage; }, get modelId() { return modelId; }, });</script>
<HoverCard bind:open {openDelay} {closeDelay} {...restProps}> {@render children?.()}</HoverCard>import Root from "./context.svelte";import CacheUsage from "./context-cache-usage.svelte";import Content from "./context-content.svelte";import ContentBody from "./context-content-body.svelte";import ContentFooter from "./context-content-footer.svelte";import ContentHeader from "./context-content-header.svelte";import Icon from "./context-icon.svelte";import InputUsage from "./context-input-usage.svelte";import OutputUsage from "./context-output-usage.svelte";import ReasoningUsage from "./context-reasoning-usage.svelte";import Trigger from "./context-trigger.svelte";import TokensWithCost from "./tokens-with-cost.svelte";
export * from "./use-context.svelte.js";export { CacheUsage, CacheUsage as ContextCacheUsage, Content, Content as ContextContent, ContentBody, ContentBody as ContextContentBody, ContentFooter, ContentFooter as ContextContentFooter, ContentHeader, ContentHeader as ContextContentHeader, Icon, Icon as ContextIcon, InputUsage, InputUsage as ContextInputUsage, OutputUsage, OutputUsage as ContextOutputUsage, ReasoningUsage, ReasoningUsage as ContextReasoningUsage, Root, // Root as Context, TokensWithCost, Trigger, Trigger as ContextTrigger,};<script lang="ts"> import { formatCompact } from "./use-context.svelte.js";
let { tokens, costText }: { tokens?: number; costText?: string } = $props();</script>
<span class="font-mono"> {tokens === undefined ? "—" : formatCompact(tokens)} {#if costText} <span class="ml-2 text-muted-foreground">• {costText}</span> {/if}</span>import type { LanguageModelUsage } from "ai";import { getContext, setContext } from "svelte";
export type ModelId = string;
/** Getter object so consumers stay reactive when the props change. */export interface ContextValue { readonly usedTokens: number; readonly maxTokens: number; readonly usage: LanguageModelUsage | undefined; readonly modelId: ModelId | undefined;}
const KEY = Symbol("ai-context");
export function setContextValue(value: ContextValue) { return setContext(KEY, value);}
export function useContextValue(): ContextValue { const context = getContext<ContextValue | undefined>(KEY); if (!context) { throw new Error("Context components must be used within Context"); } return context;}
export const formatPercent = (value: number) => new Intl.NumberFormat("en-US", { maximumFractionDigits: 1, style: "percent", }).format(value);
export const formatCompact = (value: number) => new Intl.NumberFormat("en-US", { notation: "compact" }).format(value);
export const formatUsd = (value: number) => new Intl.NumberFormat("en-US", { currency: "USD", style: "currency" }).format( value, );| Part | Prop | Type | Default |
|---|---|---|---|
| Context | maxTokens, usedTokens, usage, modelId |
number, number, LanguageModelUsage, string |
|
| ContextTrigger, ContextContent, ContextContentHeader/Body/Footer | |||
| ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextCacheUsage |
usage is the AI SDK v7 LanguageModelUsage (outputTokenDetails.reasoningTokens, inputTokenDetails.cacheReadTokens).
Vue and Svelte
Section titled “Vue and Svelte”ContextTriggerrenders a ghost button with the ring and percentage; pass your own trigger withrender(React), the default slot (Vue) or achildsnippet (Svelte).
✦ Edmi additions
Section titled “✦ Edmi additions”- The trigger is a small
--brandring plus the percentage; at 90% and above the percentage turns--warning-text. - The progress bar is
Progress variant="brand"; the footer sits on--muted.