Nicholai 7f5efb84e2 feat(agent): migrate to Anthropic Agents SDK
Replace Vercel AI SDK with Anthropic Claude Agent SDK.
Add standalone agent server (packages/agent-server/)
with MCP tools, JWT auth, and SSE streaming. Introduce
bridge API routes (src/app/api/compass/) and custom
SSE hooks (use-agent, use-compass-chat) replacing
useChat. Remove provider.ts, tools.ts, system-prompt.ts,
github-tools.ts, usage.ts, and old agent route.
2026-02-16 18:37:26 -07:00

393 lines
10 KiB
TypeScript
Executable File

"use client"
import type { LanguageModelUsage } from "./types"
import { type ComponentProps, createContext, useContext } from "react"
import { getUsage } from "tokenlens"
import { Button } from "@/components/ui/button"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
import { Progress } from "@/components/ui/progress"
import { cn } from "@/lib/utils"
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) => (
<ContextContext.Provider
value={{
usedTokens,
maxTokens,
usage,
modelId,
}}
>
<HoverCard closeDelay={0} openDelay={0} {...props} />
</ContextContext.Provider>
)
const ContextIcon = () => {
const { usedTokens, maxTokens } = useContextValue()
const circumference = 2 * Math.PI * ICON_RADIUS
const usedPercent = usedTokens / maxTokens
const dashOffset = circumference * (1 - usedPercent)
return (
<svg
aria-label="Model context usage"
height="20"
role="img"
style={{ color: "currentcolor" }}
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
width="20"
>
<circle
cx={ICON_CENTER}
cy={ICON_CENTER}
fill="none"
opacity="0.25"
r={ICON_RADIUS}
stroke="currentColor"
strokeWidth={ICON_STROKE_WIDTH}
/>
<circle
cx={ICON_CENTER}
cy={ICON_CENTER}
fill="none"
opacity="0.7"
r={ICON_RADIUS}
stroke="currentColor"
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={dashOffset}
strokeLinecap="round"
strokeWidth={ICON_STROKE_WIDTH}
style={{ transformOrigin: "center", transform: "rotate(-90deg)" }}
/>
</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", {
style: "percent",
maximumFractionDigits: 1,
}).format(usedPercent)
return (
<HoverCardTrigger asChild>
{children ?? (
<Button type="button" variant="ghost" {...props}>
<span className="font-medium text-muted-foreground">{renderedPercent}</span>
<ContextIcon />
</Button>
)}
</HoverCardTrigger>
)
}
export type ContextContentProps = ComponentProps<typeof HoverCardContent>
export const ContextContent = ({ className, ...props }: ContextContentProps) => (
<HoverCardContent className={cn("min-w-60 divide-y 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", {
style: "percent",
maximumFractionDigits: 1,
}).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>{displayPct}</p>
<p className="font-mono text-muted-foreground">
{used} / {total}
</p>
</div>
<div className="space-y-2">
<Progress className="bg-muted" value={usedPercent * PERCENT_MAX} />
</div>
</>
)}
</div>
)
}
export type ContextContentBodyProps = ComponentProps<"div">
export const ContextContentBody = ({ children, className, ...props }: ContextContentBodyProps) => (
<div className={cn("w-full 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", {
style: "currency",
currency: "USD",
}).format(costUSD ?? 0)
return (
<div
className={cn(
"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs",
className,
)}
{...props}
>
{children ?? (
<>
<span className="text-muted-foreground">Total cost</span>
<span>{totalCost}</span>
</>
)}
</div>
)
}
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", {
style: "currency",
currency: "USD",
}).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", {
style: "currency",
currency: "USD",
}).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?.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", {
style: "currency",
currency: "USD",
}).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?.cachedInputTokens ?? 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", {
style: "currency",
currency: "USD",
}).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>
)
}
const TokensWithCost = ({ tokens, costText }: { tokens?: number; costText?: string }) => (
<span>
{tokens === undefined
? "—"
: new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(tokens)}
{costText ? <span className="ml-2 text-muted-foreground"> {costText}</span> : null}
</span>
)
/** Demo component for preview */
export default function ContextDemo() {
return (
<div className="flex items-center justify-center p-8">
<Context
maxTokens={128_000}
modelId="openai:gpt-5"
usage={{
inputTokens: 32_000,
outputTokens: 8000,
totalTokens: 40_000,
cachedInputTokens: 0,
reasoningTokens: 0,
inputTokenDetails: {
noCacheTokens: undefined,
cacheReadTokens: undefined,
cacheWriteTokens: undefined,
},
outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
}}
usedTokens={40_000}
>
<ContextTrigger />
<ContextContent>
<ContextContentHeader />
<ContextContentBody>
<ContextInputUsage />
<ContextOutputUsage />
<ContextReasoningUsage />
<ContextCacheUsage />
</ContextContentBody>
<ContextContentFooter />
</ContextContent>
</Context>
</div>
)
}