import { useMemo } from 'react'; import { Clock, Cpu, Hash, Layers, RefreshCw, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import type { Message } from '@/api/types'; interface TurnEntry { message: Message; turnNumber: number; elapsed: string; toolCallCount: number; } interface Props { messages: Message[]; chatId: string; onClose: () => void; onScrollToMessage: (messageId: string) => void; } function formatElapsed(startedAt: string | null, finishedAt: string | null): string { if (!startedAt || !finishedAt) return '—'; const start = new Date(startedAt).getTime(); const end = new Date(finishedAt).getTime(); if (Number.isNaN(start) || Number.isNaN(end)) return '—'; const ms = end - start; if (ms < 0) return '—'; if (ms < 1000) return `${ms}ms`; if (ms < 60_000) return `${Math.round(ms / 1000)}s`; const mins = Math.floor(ms / 60_000); const secs = Math.round((ms % 60_000) / 1000); return `${mins}m ${secs}s`; } /** * SessionTimeline — vertical timeline of assistant turns in a chat. * * Renders a side-panel overlay with each turn's model, tokens, duration, * and tool-call count. Clicking a turn scrolls the main chat to that * message. The latest turn shows a "Scroll to latest" restore button. */ export function SessionTimeline({ messages, onClose, onScrollToMessage }: Props) { const turns = useMemo(() => { const assistantMsgs = messages.filter( (m) => m.role === 'assistant' && m.status === 'complete', ); return assistantMsgs.map((message, i) => ({ message, turnNumber: i + 1, elapsed: formatElapsed(message.started_at, message.finished_at), toolCallCount: message.tool_calls?.length ?? 0, })); }, [messages]); const latestTurnId = turns.length > 0 ? turns[turns.length - 1]!.message.id : null; return (
{/* Header */}

Session Timeline

{/* Timeline entries */}
{turns.length === 0 ? (
No assistant turns yet.
) : (
{turns.map((turn, i) => { const isLatest = turn.message.id === latestTurnId; return (
{/* Vertical connector line */} {i < turns.length - 1 && (
)} {/* Timeline dot button */} {/* Content card */}
onScrollToMessage(turn.message.id)} > {/* Turn number + latest badge */}
Turn {turn.turnNumber} {isLatest && ( Latest )}
{/* Model name */}
{turn.message.model ?? 'Unknown model'}
{/* Token count with breakdown */} {turn.message.tokens_used != null && (
{turn.message.tokens_used.toLocaleString()} total {turn.message.cache_tokens != null && turn.message.cache_tokens > 0 && ( ({turn.message.cache_tokens.toLocaleString()} cache) )} {turn.message.reasoning_tokens != null && turn.message.reasoning_tokens > 0 && ( ({turn.message.reasoning_tokens.toLocaleString()} reasoning) )}
)} {/* Duration + tool calls */}
{turn.elapsed} {turn.toolCallCount > 0 && ( {turn.toolCallCount} tool call{turn.toolCallCount !== 1 ? 's' : ''} )}
{/* Restore button for latest turn */} {isLatest && ( )}
); })}
)}
); }