Files
kandev/apps/web/hooks/use-session-context-window.ts
T
João Salavisa dea2a07cfa feat: add context window usage display to task session (#66)
* feat: add context window usage display to task session

- Add TokenUsageDisplay component showing context window usage as a horizontal progress bar
- Add useSessionContextWindow hook with proper Zustand reactivity (subscribe to primitives)
- Add ContextWindowState to store with setContextWindow action
- Backend: propagate context_window data through orchestrator, watcher, and lifecycle events
- WebSocket handler extracts context_window from session.state_changed metadata
- Display positioned on left side of action bar, updates in real-time

* refactor: remove agent stream event WebSocket broadcasting

Agent stream events (message_chunk, tool_call, etc.) are now handled
internally by the lifecycle manager rather than being broadcast to
the frontend. Messages are accumulated and flushed as comments at
step boundaries.
2026-01-21 02:47:20 +00:00

75 lines
2.8 KiB
TypeScript

import { useEffect, useMemo } from 'react';
import { useAppStore } from '@/components/state-provider';
import { getWebSocketClient } from '@/lib/ws/connection';
import type { ContextWindowEntry } from '@/lib/state/store';
export function useSessionContextWindow(sessionId: string | null): ContextWindowEntry | undefined {
// Subscribe to individual primitive values to ensure reactivity
const size = useAppStore((state) =>
sessionId ? state.contextWindow.bySessionId[sessionId]?.size : undefined
);
const used = useAppStore((state) =>
sessionId ? state.contextWindow.bySessionId[sessionId]?.used : undefined
);
const remaining = useAppStore((state) =>
sessionId ? state.contextWindow.bySessionId[sessionId]?.remaining : undefined
);
const efficiency = useAppStore((state) =>
sessionId ? state.contextWindow.bySessionId[sessionId]?.efficiency : undefined
);
const timestamp = useAppStore((state) =>
sessionId ? state.contextWindow.bySessionId[sessionId]?.timestamp : undefined
);
// Memoize the combined object
const contextWindow = useMemo(() => {
if (size === undefined) return undefined;
return { size, used: used ?? 0, remaining: remaining ?? 0, efficiency: efficiency ?? 0, timestamp };
}, [size, used, remaining, efficiency, timestamp]);
const session = useAppStore((state) =>
sessionId ? state.taskSessions.items[sessionId] : undefined
);
const setContextWindow = useAppStore((state) => state.setContextWindow);
// Populate context window from session metadata if not already in store
useEffect(() => {
if (!sessionId || contextWindow) return;
// Try to extract context_window from session metadata
const metadata = session?.metadata;
if (!metadata || typeof metadata !== 'object') return;
const storedContextWindow = (metadata as Record<string, unknown>).context_window;
if (!storedContextWindow || typeof storedContextWindow !== 'object') return;
// Map stored context window to ContextWindowEntry
const cw = storedContextWindow as Record<string, unknown>;
const entry: ContextWindowEntry = {
size: (cw.size as number) ?? 0,
used: (cw.used as number) ?? 0,
remaining: (cw.remaining as number) ?? 0,
efficiency: (cw.efficiency as number) ?? 0,
timestamp: (cw.timestamp as string) ?? undefined,
};
setContextWindow(sessionId, entry);
}, [sessionId, contextWindow, session?.metadata, setContextWindow]);
// Subscribe to session updates via WebSocket
useEffect(() => {
if (!sessionId) return;
const client = getWebSocketClient();
if (client) {
const unsubscribe = client.subscribeSession(sessionId);
return () => {
unsubscribe();
// Don't clear context window on cleanup - keep it cached
};
}
}, [sessionId]);
return contextWindow;
}