diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx index fa8b91594..63c2e2264 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx @@ -30,6 +30,7 @@ import { cn } from "@/lib/utils"; // Custom components import MessagesTab from "@/components/session/MessagesTab"; import { SessionStartingEvents } from "@/components/session/SessionStartingEvents"; +import { ContextUsage } from "@/components/session/ContextUsage"; import { FileTree, type FileTreeNode } from "@/components/file-tree"; import { Button } from "@/components/ui/button"; @@ -1495,6 +1496,13 @@ export default function ProjectSessionDetailPage({ /> + {/* Context usage indicator */} + + {/* Kebab menu (both mobile and desktop) */} = 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${Math.round(n / 1_000)}K` + return n.toString() +} + +type ContextUsageProps = { + state: AGUIClientState + /** Model name to determine context limit (e.g., "claude-opus-4-5") */ + model?: string + className?: string +} + +/** Context usage: token count + sparkline */ +export function ContextUsage({ state, model, className = '' }: ContextUsageProps) { + const { used, perTurn } = state.contextUsage + const limit = getContextLimit(model) + const recent = perTurn.slice(-24) + const max = Math.max(...recent, 1) + + return ( +
+ + {formatTokens(used)}/{formatTokens(limit)} + + + {recent.length > 0 && ( +
+ {recent.map((tokens, i) => ( +
+ ))} +
+ )} +
+ ) +} diff --git a/components/frontend/src/hooks/agui/event-handlers.ts b/components/frontend/src/hooks/agui/event-handlers.ts index 2829411cf..9345550d2 100644 --- a/components/frontend/src/hooks/agui/event-handlers.ts +++ b/components/frontend/src/hooks/agui/event-handlers.ts @@ -25,12 +25,12 @@ import type { PlatformMessage, PlatformToolCall, PlatformRawEvent, + PlatformRunFinishedEvent, AGUIMetaEvent, PlatformActivitySnapshotEvent, PlatformActivityDeltaEvent, WireToolCallStartEvent, RunStartedEvent, - RunFinishedEvent, RunErrorEvent, TextMessageStartEvent, TextMessageContentEvent, @@ -212,7 +212,7 @@ function handleRunStarted( function handleRunFinished( state: AGUIClientState, - event: RunFinishedEvent, + event: PlatformRunFinishedEvent, callbacks: EventHandlerCallbacks, ): AGUIClientState { state.status = 'completed' @@ -271,6 +271,17 @@ function handleRunFinished( } state.currentThinking = null + // Track context usage from result.usage + if (event.result?.usage) { + const { input_tokens = 0, output_tokens = 0 } = event.result.usage + const turnTokens = input_tokens + output_tokens + + state.contextUsage = { + used: state.contextUsage.used + turnTokens, + perTurn: [...state.contextUsage.perTurn, turnTokens], + } + } + return state } diff --git a/components/frontend/src/hooks/agui/types.ts b/components/frontend/src/hooks/agui/types.ts index 3622ead09..a1caba861 100644 --- a/components/frontend/src/hooks/agui/types.ts +++ b/components/frontend/src/hooks/agui/types.ts @@ -47,4 +47,8 @@ export const initialState: AGUIClientState = { messageFeedback: new Map(), // Track feedback for messages currentReasoning: null, currentThinking: null, + contextUsage: { + used: 0, + perTurn: [], + }, } diff --git a/components/frontend/src/types/agui.ts b/components/frontend/src/types/agui.ts index 1c6046843..c21e30bef 100644 --- a/components/frontend/src/types/agui.ts +++ b/components/frontend/src/types/agui.ts @@ -113,6 +113,25 @@ export type PlatformRawEvent = { source?: string } +/** Token usage from RUN_FINISHED result */ +export type ResultUsage = { + input_tokens?: number + output_tokens?: number + cache_read_input_tokens?: number + cache_creation_input_tokens?: number +} + +/** Platform extension of RunFinishedEvent with result metadata */ +export type PlatformRunFinishedEvent = RunFinishedEvent & { + result?: { + usage?: ResultUsage + num_turns?: number + duration_ms?: number + total_cost_usd?: number + is_error?: boolean + } +} + // ── Platform Activity types ── // The core ActivitySnapshotEvent/ActivityDeltaEvent are per-message, not // array-based. The platform uses an array-based model for UI rendering. @@ -212,6 +231,11 @@ export type AGUIClientState = { content: string timestamp?: string } | null + // Context window usage tracking + contextUsage: { + used: number + perTurn: number[] + } } // ── Type Guards ── @@ -260,3 +284,30 @@ export function isMessagesSnapshotEvent(event: { type: string }): event is Messa export function isActivitySnapshotEvent(event: { type: string }): event is PlatformActivitySnapshotEvent { return event.type === EventType.ACTIVITY_SNAPSHOT } + +// ── Context Window Utilities ── + +/** Context window limits by model family (tokens) */ +const CONTEXT_LIMITS: Record = { + // 1M context models + 'claude-opus-4': 1_000_000, + // 200K context models (default) + 'claude-sonnet': 200_000, + 'claude-haiku': 200_000, +} + +const DEFAULT_CONTEXT_LIMIT = 200_000 + +/** Get context window limit for a model */ +export function getContextLimit(model: string | undefined): number { + if (!model) return DEFAULT_CONTEXT_LIMIT + + // Check for exact prefix match + for (const [prefix, limit] of Object.entries(CONTEXT_LIMITS)) { + if (model.startsWith(prefix)) { + return limit + } + } + + return DEFAULT_CONTEXT_LIMIT +}