import React, { useRef, useEffect, useMemo } from "react"; import type { ParsedMessage } from "../lib/types"; import { MessageBubble } from "./MessageBubble"; import { RedactedDivider } from "./RedactedDivider"; interface Props { messages: ParsedMessage[]; allMessages: ParsedMessage[]; redactedUuids: Set; loading: boolean; searchQuery: string; selectedForRedaction: Set; onToggleRedactionSelection: (uuid: string) => void; autoRedactEnabled: boolean; focusedIndex?: number; } function MessageSkeleton({ delay = 0 }: { delay?: number }) { return (
); } export function SessionViewer({ messages, allMessages, redactedUuids, loading, searchQuery, selectedForRedaction, onToggleRedactionSelection, autoRedactEnabled, focusedIndex = -1, }: Props) { const containerRef = useRef(null); useEffect(() => { if (focusedIndex >= 0 && containerRef.current) { const el = containerRef.current.querySelector( `[data-msg-index="${focusedIndex}"]` ); if (el) { el.scrollIntoView({ behavior: "smooth", block: "nearest" }); } } }, [focusedIndex]); // Auto-scroll to hash anchor on load useEffect(() => { const hash = window.location.hash; if (hash && hash.startsWith("#msg-") && messages.length > 0) { requestAnimationFrame(() => { const el = document.getElementById(hash.slice(1)); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); el.classList.add("search-match-focused", "rounded-xl"); setTimeout(() => el.classList.remove("search-match-focused"), 3000); } }); } }, [messages.length]); // Build display list with redacted dividers and time gaps. // Must be called before any early returns to satisfy Rules of Hooks. const displayItems = useMemo(() => { if (messages.length === 0) return []; const visibleUuids = new Set(messages.map((m) => m.uuid)); const items: Array< | { type: "message"; message: ParsedMessage; messageIndex: number } | { type: "redacted_divider"; key: string } | { type: "time_gap"; key: string; duration: string } > = []; let prevWasRedactedGap = false; let prevTimestamp: string | undefined; let messageIndex = 0; for (const msg of allMessages) { if (redactedUuids.has(msg.uuid)) { prevWasRedactedGap = true; continue; } if (!visibleUuids.has(msg.uuid)) { continue; } if (prevWasRedactedGap) { items.push({ type: "redacted_divider", key: `divider-${msg.uuid}`, }); prevWasRedactedGap = false; } // Insert time gap indicator if > 5 minutes between visible messages if (prevTimestamp && msg.timestamp) { const currTime = new Date(msg.timestamp).getTime(); const prevTime = new Date(prevTimestamp).getTime(); const gap = currTime - prevTime; if (!isNaN(gap) && gap > 5 * 60 * 1000) { items.push({ type: "time_gap", key: `gap-${msg.uuid}`, duration: formatDuration(gap), }); } } if (msg.timestamp) prevTimestamp = msg.timestamp; items.push({ type: "message", message: msg, messageIndex }); messageIndex++; } return items; }, [messages, allMessages, redactedUuids]); if (loading) { return (
{[...Array(4)].map((_, i) => ( ))}
); } if (messages.length === 0 && allMessages.length === 0) { return (

Select a session

Choose a session from the sidebar to view its messages

); } if (messages.length === 0) { return (

No matching messages

Try adjusting your filters or search query

); } return (
{messages.length} message{messages.length !== 1 ? "s" : ""}
{displayItems.map((item, idx) => { if (item.type === "redacted_divider") { return ; } if (item.type === "time_gap") { return (
{item.duration} later
); } const msg = item.message; const lq = searchQuery ? searchQuery.toLowerCase() : ""; const isMatch = searchQuery && (msg.content.toLowerCase().includes(lq) || (msg.toolInput && msg.toolInput.toLowerCase().includes(lq))); const isDimmed = searchQuery && !isMatch; const isFocused = item.messageIndex === focusedIndex; return (
onToggleRedactionSelection(msg.uuid) } autoRedactEnabled={autoRedactEnabled} />
); })}
); } function formatDuration(ms: number): string { const minutes = Math.floor(ms / 60000); if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); const remainMinutes = minutes % 60; if (remainMinutes === 0) return `${hours}h`; return `${hours}h ${remainMinutes}m`; }