'use client'; import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { ChevronDown, ChevronUp, ChevronsUpDown, ChevronsDownUp, Copy, Trash2 } from 'lucide-react'; import { requestSnapshotStore } from '@/lib/llm/request-snapshot'; import { buildProviderView } from '@/lib/llm/provider-view'; import type { Message } from '@/lib/llm/core/types'; import { toast } from 'sonner'; const subscribe = (listener: () => void) => requestSnapshotStore.subscribe(listener); const getSnapshot = () => requestSnapshotStore.getSnapshot(); const getEnabled = () => requestSnapshotStore.isEnabled(); /** Versioned expand/collapse-all override — rows stay individually toggleable after. */ export type ForceState = { open: boolean; v: number } | null; function messagePreview(msg: Message): string { if (msg.tool_calls?.length) { const names = msg.tool_calls.map(tc => tc.function?.name || '?').join(', '); return `${msg.tool_calls.length} tool call(s): ${names}`; } if (typeof msg.content === 'string' && msg.content.trim()) { return msg.content.length > 100 ? msg.content.slice(0, 100) + '…' : msg.content; } if (Array.isArray(msg.content) && msg.content.length > 0) { return `[${msg.content.length} content block(s)]`; } if (msg.reasoning_details?.length) return '[reasoning only]'; return '[empty]'; } const ROLE_COLORS: Record = { system: 'text-orange-500', user: 'text-primary', assistant: 'text-green-500', tool: 'text-blue-500', }; function MessageRow({ msg, index, force }: { msg: Message; index: number; force?: ForceState }) { const [isOpen, setIsOpen] = useState(false); useEffect(() => { if (force) setIsOpen(force.open); }, [force]); return (
{isOpen ? : } {index} {msg.role} {msg.reasoning_details?.length ? ( R ) : null} {messagePreview(msg)}
{JSON.stringify(msg, null, 2)}
); } export function MessagesTab() { const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); const enabled = useSyncExternalStore(subscribe, getEnabled, getEnabled); const [providerView, setProviderView] = useState(false); const [force, setForce] = useState(null); // Recomputes only when a new request is captured or the view toggles — // streaming deltas never reach this component. const messages = useMemo(() => { if (!snapshot) return null; return providerView ? buildProviderView(snapshot.messages, snapshot.model) : snapshot.messages; }, [snapshot, providerView]); const handleCopy = async () => { if (!messages) return; try { await navigator.clipboard.writeText(JSON.stringify(messages, null, 2)); toast.success('Message history copied'); } catch { toast.error('Copy failed'); } }; return ( <>
{messages && (
)}
{snapshot && (
{snapshot.provider} · {snapshot.model} · {snapshot.messages.length} messages · {new Date(snapshot.timestamp).toLocaleTimeString()}
)}
{!messages ? (
{enabled ? 'Waiting for the next LLM request — run a generation to capture its message history.' : 'Enable "Capture requests", then run a generation to inspect the exact message history sent to the provider. Local generations only (Server Mode requests are not forwarded).'}
) : ( messages.map((msg, i) => ) )}
); }