Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -1495,6 +1496,13 @@ export default function ProjectSessionDetailPage({
/>
</div>

{/* Context usage indicator */}
<ContextUsage
state={aguiState}
model={session.spec?.llmSettings?.model}
className="hidden md:flex"
/>

{/* Kebab menu (both mobile and desktop) */}
<SessionHeader
session={session}
Expand Down
49 changes: 49 additions & 0 deletions components/frontend/src/components/session/ContextUsage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Context Usage Indicator
*
* Displays context window utilization in the session UI.
*/

import type { AGUIClientState } from '@/types/agui'
import { getContextLimit } from '@/types/agui'

function formatTokens(n: number): string {
if (n >= 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 (
<div className={`flex items-center gap-3 ${className}`}>
<span className="text-muted-foreground font-mono text-sm">
{formatTokens(used)}/{formatTokens(limit)}
</span>

{recent.length > 0 && (
<div className="flex items-end gap-[2px] h-5">
{recent.map((tokens, i) => (
<div
key={i}
className="w-[3px] bg-green-500 rounded-sm"
style={{ height: `${Math.max(8, (tokens / max) * 100)}%` }}
/>
))}
</div>
)}
</div>
)
}
15 changes: 13 additions & 2 deletions components/frontend/src/hooks/agui/event-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import type {
PlatformMessage,
PlatformToolCall,
PlatformRawEvent,
PlatformRunFinishedEvent,
AGUIMetaEvent,
PlatformActivitySnapshotEvent,
PlatformActivityDeltaEvent,
WireToolCallStartEvent,
RunStartedEvent,
RunFinishedEvent,
RunErrorEvent,
TextMessageStartEvent,
TextMessageContentEvent,
Expand Down Expand Up @@ -212,7 +212,7 @@ function handleRunStarted(

function handleRunFinished(
state: AGUIClientState,
event: RunFinishedEvent,
event: PlatformRunFinishedEvent,
callbacks: EventHandlerCallbacks,
): AGUIClientState {
state.status = 'completed'
Expand Down Expand Up @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions components/frontend/src/hooks/agui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ export const initialState: AGUIClientState = {
messageFeedback: new Map(), // Track feedback for messages
currentReasoning: null,
currentThinking: null,
contextUsage: {
used: 0,
perTurn: [],
},
}
51 changes: 51 additions & 0 deletions components/frontend/src/types/agui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -212,6 +231,11 @@ export type AGUIClientState = {
content: string
timestamp?: string
} | null
// Context window usage tracking
contextUsage: {
used: number
perTurn: number[]
}
}

// ── Type Guards ──
Expand Down Expand Up @@ -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<string, number> = {
// 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
}
Loading