/** * @file EventTimeline.tsx * @description A session's events as a flat, expandable list — one row per * event (age, status badge, origin · title, tool), opening into EventDetail. * Used by the session analysis page's Timeline tab and the read-only snapshot * page. * @author Nguyễn Ngọc Trí Vĩ */ import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { AgentStatusBadge } from "./StatusBadge"; import { EventDetail } from "./EventDetail"; import { agentOriginLabel, buildEventTitle, buildOriginLabel, projectFromCwd, projectFromEvent, statusFromEventType, } from "../lib/event-grouping"; import type { AgentInfo } from "../lib/event-grouping"; import { timeAgo } from "../lib/format"; import type { DashboardEvent } from "../lib/types"; export function EventTimeline({ events, agentInfoById, sessionNameById, fallbackCwd, }: { events: DashboardEvent[]; agentInfoById: Map; sessionNameById: Map; /** The session's cwd, for events that carry none of their own. */ fallbackCwd?: string | null; }) { const { t } = useTranslation("sessions"); const [expandedEvents, setExpandedEvents] = useState>(() => new Set()); function toggleEvent(id: number) { setExpandedEvents((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } // Precompute project per event so flat-row rendering doesn't re-parse JSON. const projectByEventId = useMemo(() => { const map = new Map(); for (const e of events) map.set(e.id, projectFromEvent(e)); return map; }, [events]); return (
{events.map((event, i) => { const key = event.id ?? i; const isOpen = event.id != null && expandedEvents.has(event.id); return ( // Off-screen rows skip layout/paint (a long timeline is not virtualized).
{isOpen && ( )}
); })}
); }