/**
 * @file SessionDetail.tsx
 * @description Displays detailed information about a specific session, including its agents, events, and cost breakdown, with real-time updates and an expandable agent hierarchy view.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */
/* =============================================================================
 * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
 * =============================================================================
 * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
 *
 * ## Design constraints
 * - Local-first: no telemetry leaves the machine unless the user configures webhooks.
 * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
 *   philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
 * - Destructive flows stay behind explicit confirmation modals and server-side gates.
 * - Internationalization: user-visible strings belong in i18n JSON, not literals here.
 *
 * ## Remote data & SSH
 * Remote Data Sources let operators aggregate multiple machines. SSH entries describe
 * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
 * scoped GET via `?sources=`. Health checks and import history surface in Settings.
 *
 * ## Internal dependencies
 * - `../lib/api`
 * - `../lib/eventBus`
 * - `../components/AgentCard`
 * - `../components/SessionOverview`
 * - `../components/conversation/ConversationView`
 * - `../components/StatusBadge`
 * - `../lib/types`
 * - `../components/EventDetail`
 * - `../components/EventFilters`
 * - `../components/EventFiltersInfo`
 * - `../components/Skeleton`
 * - `../lib/event-grouping`
 *
 * ## Public surface
 * - `SessionDetail` — exported API; see TSDoc on the symbol for behavior.
 *
 * ## Testing pointers
 * - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
 * - Server contract changes require `npm run test:server` and OpenAPI sync.
 * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
 *
 * ## Related docs
 * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline.
 * - `docs/API.md` — REST reference.
 * - `.claude/skills/file-headers/` — mandatory `@author` header policy.
 * ============================================================================= */
/* -----------------------------------------------------------------------------
 * EXPORT CATALOG — quick index of symbols defined below (documentation only).
 * -----------------------------------------------------------------------------
 * **SessionDetail**
 *   Part of this module's public contract. Downstream imports should treat
 *   the signature and return type as stable unless release notes say otherwise.
 *   When behavior changes, update the `@file` overview and relevant tests.
 *
 * ----------------------------------------------------------------------------- */

import { useEffect, useState, useCallback, useMemo, useRef, type ReactNode } from "react";
import { Link, useParams, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
  ArrowLeft,
  Bot,
  Clock,
  FolderOpen,
  Cpu,
  RefreshCw,
  DollarSign,
  ChevronDown,
  ChevronRight,
  GitBranch,
  MessageSquare,
  List,
  AlertCircle,
  Play,
  ExternalLink,
  Workflow,
  Hourglass,
} from "lucide-react";
import { api } from "../lib/api";
import { eventBus } from "../lib/eventBus";
import { isRemoteDataRefreshMessage } from "../lib/remoteDataEvents";
import { AgentCard } from "../components/AgentCard";
import { SessionOverview } from "../components/SessionOverview";
import { ConversationView } from "../components/conversation/ConversationView";
import { SessionStatusBadge, AgentStatusBadge, REASON_ICONS } from "../components/StatusBadge";
import { CopyButton } from "../components/event-views/primitives";
import {
  effectiveSessionStatus,
  isSessionAwaitingInput,
  sessionAwaitingReason,
  AWAITING_REASON_CONFIG,
} from "../lib/types";
import { EventDetail } from "../components/EventDetail";
import {
  EventFilters,
  EMPTY_FILTERS,
  isEmptyFilters,
  expandStatusToEventTypes,
} from "../components/EventFilters";
import type { EventFiltersValue } from "../components/EventFilters";
import { EventFiltersInfo } from "../components/EventFiltersInfo";
import { Skeleton } from "../components/Skeleton";
import {
  agentOriginLabel,
  buildEventTitle,
  buildOriginLabel,
  projectFromCwd,
  projectFromEvent,
  statusFromEventType,
} from "../lib/event-grouping";
import type { AgentInfo } from "../lib/event-grouping";
import {
  formatDateTime,
  formatDuration,
  fmtCostFull,
  timeAgo,
  formatModelName,
} from "../lib/format";
import type {
  Session,
  Agent,
  DashboardEvent,
  CostResult,
  TranscriptInfo,
  WorkflowRun,
} from "../lib/types";
import { WorkflowRunsPanel } from "../components/workflows/WorkflowRunsPanel";

type DetailTab = "agents" | "conversation" | "timeline";

const EVENTS_INITIAL_BATCH = 50;
const EVENTS_MORE_BATCH = 500;
// Live-refresh bounds - see ActivityFeed for rationale.
const EVENTS_MAX_REFRESH = 500;
const EVENTS_REFRESH_DEBOUNCE_MS = 500;

export function SessionDetail() {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const { t } = useTranslation("sessions");
  const { t: wfT } = useTranslation("workflows");
  const [session, setSession] = useState<Session | null>(null);
  const [agents, setAgents] = useState<Agent[]>([]);
  const [workflows, setWorkflows] = useState<WorkflowRun[]>([]);
  const [events, setEvents] = useState<DashboardEvent[]>([]);
  const [eventsTotal, setEventsTotal] = useState(0);
  const [eventsLoadingMore, setEventsLoadingMore] = useState(false);
  const [filters, setFilters] = useState<EventFiltersValue>(EMPTY_FILTERS);
  const [cost, setCost] = useState<CostResult | null>(null);
  const [loading, setLoading] = useState(true);
  // True when this session is currently being driven by an in-flight Run
  // handle on /run. Drives the "Open in Run page" banner up top.
  const [isDashboardRun, setIsDashboardRun] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [expandedAgents, setExpandedAgents] = useState<Set<string>>(() => {
    return new Set<string>();
  });
  const [activeTab, setActiveTab] = useState<DetailTab>("agents");
  // Keep tabs mounted once visited so switching between them doesn't unmount/
  // remount their subtrees (which causes a perceptible flash on click).
  const [visitedTabs, setVisitedTabs] = useState<Set<DetailTab>>(() => new Set(["agents"]));
  useEffect(() => {
    setVisitedTabs((prev) => (prev.has(activeTab) ? prev : new Set(prev).add(activeTab)));
  }, [activeTab]);
  const [transcripts, setTranscripts] = useState<TranscriptInfo[]>([]);
  const [pendingTranscriptId, setPendingTranscriptId] = useState<string | null>(null);
  const [transcriptNotFound, setTranscriptNotFound] = useState(false);
  const notFoundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [expandedEvents, setExpandedEvents] = useState<Set<number>>(() => 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;
    });
  }

  // Refs let the websocket handler access latest state without re-subscribing.
  const eventApiParamsRef = useRef<Record<string, unknown> | null>(null);
  const eventsLoadedCountRef = useRef(0);
  const eventsRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  eventsLoadedCountRef.current = events.length;
  const goBack = useCallback(() => {
    const historyState =
      typeof window !== "undefined" ? (window.history.state as { idx?: number } | null) : null;
    if ((historyState?.idx ?? 0) > 0) {
      navigate(-1);
      return;
    }
    navigate("/sessions");
  }, [navigate]);

  // Auto-dismiss not-found warning after 8 seconds
  useEffect(() => {
    if (transcriptNotFound) {
      notFoundTimerRef.current = setTimeout(() => setTranscriptNotFound(false), 8000);
      return () => {
        if (notFoundTimerRef.current) clearTimeout(notFoundTimerRef.current);
      };
    }
  }, [transcriptNotFound]);

  // Probe /api/run to see whether THIS session is currently being driven by
  // an in-flight Run handle. If so, surface a banner so the user can hop
  // back to /run instead of viewing this as a passive transcript.
  useEffect(() => {
    if (!id) return;
    let cancelled = false;
    const probe = () => {
      // Defensive: tests mock the api module without a `run` namespace.
      if (!api.run || typeof api.run.list !== "function") return;
      api.run
        .list()
        .then((r) => {
          if (cancelled) return;
          const live = r.items.some((h) => h.sessionId === id && h.status === "running");
          setIsDashboardRun(live);
        })
        .catch(() => undefined);
    };
    probe();
    const t = setInterval(probe, 10000);
    const unsub = eventBus.subscribe((msg) => {
      if (msg.type === "run_status") probe();
    });
    return () => {
      cancelled = true;
      clearInterval(t);
      unsub();
    };
  }, [id]);

  const load = useCallback(async () => {
    if (!id) return;
    try {
      const [data, costData] = await Promise.all([
        api.sessions.get(id),
        api.pricing.sessionCost(id).catch(() => null),
      ]);
      setSession(data.session);
      setAgents(data.agents);
      setWorkflows(data.workflows || []);
      setCost(costData);
      setError(null);
    } catch (err) {
      setError(err instanceof Error ? err.message : t("detail.failedLoad"));
    } finally {
      setLoading(false);
    }
  }, [id, t]);

  useEffect(() => {
    load();
  }, [load]);

  // Load transcripts list (for Agent → Conversation navigation ID mapping)
  useEffect(() => {
    if (!id) return;
    let cancelled = false;
    api.sessions
      .transcripts(id)
      .then((result) => {
        if (!cancelled) setTranscripts(result.transcripts);
      })
      .catch(() => {});
    return () => {
      cancelled = true;
    };
  }, [id]);

  // Navigate to Conversation tab and select the matching transcript when clicking an agent
  const navigateToAgentConversation = useCallback(
    (agent: Agent) => {
      // Clear any previous not-found warning
      setTranscriptNotFound(false);

      const findTranscriptId = (ts: TranscriptInfo[]): string | null => {
        // 1. Exact match via db_agent_id (most reliable)
        const exactMatch = ts.find((t) => t.db_agent_id === agent.id);
        if (exactMatch) return exactMatch.id;

        // 2. Main agent fallback
        if (agent.type === "main") return "main";

        // 3. Fallback: match by subagent_type or type, then narrow by name
        let candidates = ts.filter((t) => t.type !== "main");
        if (agent.subagent_type) {
          const byType = candidates.filter(
            (t) => t.subagent_type === agent.subagent_type || t.type === agent.subagent_type
          );
          if (byType.length > 0) candidates = byType;
        }
        if (agent.name && candidates.length > 1) {
          const byName = candidates.filter((t) => t.name === agent.name);
          if (byName.length > 0) candidates = byName;
        }
        if (candidates.length === 1) return candidates[0]!.id;

        return null;
      };

      // Always switch to the conversation tab - the user clicked a leaf agent
      // and expects to see its conversation. If no exact transcript match is
      // found, the tab still opens and the not-found banner explains why.
      setActiveTab("conversation");

      const transcriptId = findTranscriptId(transcripts);

      if (transcriptId) {
        setPendingTranscriptId(transcriptId);
      } else if (transcripts.length === 0 && id) {
        // Transcripts not loaded yet - fetch them and retry. The tab is
        // already showing; this just selects the right transcript when ready.
        api.sessions
          .transcripts(id)
          .then((result) => {
            setTranscripts(result.transcripts);
            const freshId = findTranscriptId(result.transcripts);
            if (freshId) {
              setPendingTranscriptId(freshId);
            } else {
              setTranscriptNotFound(true);
            }
          })
          .catch(() => {
            setTranscriptNotFound(true);
          });
      } else {
        // Transcripts are loaded but no specific match for this agent.
        setTranscriptNotFound(true);
      }
    },
    [transcripts, id]
  );

  // Compute compaction labels: "#1", "#2", etc. based on started_at order
  const compactionLabels = useMemo(() => {
    const map = new Map<string, string>();
    const compactions = agents
      .filter((a) => a.subagent_type === "compaction")
      .sort((a, b) => (a.started_at || "").localeCompare(b.started_at || ""));
    compactions.forEach((a, i) => {
      const time = a.started_at
        ? new Date(a.started_at).toLocaleTimeString(undefined, {
            hour: "2-digit",
            minute: "2-digit",
          })
        : "";
      map.set(a.id, `#${i + 1}${time ? ` · ${time}` : ""}`);
    });
    return map;
  }, [agents]);

  // Event list is fetched separately from the session metadata so it can
  // respect the user's filters and use the server-driven pagination. Status
  // presets expand into event_type values and merge with any explicit
  // event_type selection.
  const eventApiParams = useMemo(() => {
    if (!id) return null;
    const statusExpanded = expandStatusToEventTypes(filters.status);
    const eventTypeMerged = Array.from(new Set<string>([...filters.event_type, ...statusExpanded]));
    return {
      session_id: [id],
      event_type: eventTypeMerged.length > 0 ? eventTypeMerged : undefined,
      tool_name: filters.tool_name.length > 0 ? filters.tool_name : undefined,
      agent_id: filters.agent_id.length > 0 ? filters.agent_id : undefined,
      q: filters.q || undefined,
      from: filters.from ? new Date(filters.from).toISOString() : undefined,
      to: filters.to ? new Date(filters.to).toISOString() : undefined,
    };
  }, [id, filters]);

  eventApiParamsRef.current = eventApiParams;

  // Build an AgentInfo map from the already-fetched agents list so rows can
  // render the subagent pill (subagent_type) without an extra fetch.
  const agentInfoById = useMemo(() => {
    const map = new Map<string, AgentInfo>();
    for (const a of agents) {
      map.set(a.id, {
        type: a.type,
        subagent_type: a.subagent_type,
        name: a.name,
        parent_agent_id: a.parent_agent_id,
      });
    }
    return map;
  }, [agents]);

  // Single-entry session-name lookup so EventDetail can surface the session
  // label above the raw id, mirroring the agentInfoById pattern. Falls back
  // to "Session abcdefgh" when session.name is null/empty so the row always
  // shows *something* identifiable - matches the header's display logic.
  const sessionNameById = useMemo(() => {
    const map = new Map<string, string>();
    if (session?.id) {
      const label = session.name?.trim() || `Session ${session.id.slice(0, 8)}`;
      map.set(session.id, label);
    }
    return map;
  }, [session?.id, session?.name]);

  // Precompute project per event so flat-row rendering doesn't re-parse JSON.
  const projectByEventId = useMemo(() => {
    const map = new Map<number, string | null>();
    for (const e of events) map.set(e.id, projectFromEvent(e));
    return map;
  }, [events]);

  const loadEvents = useCallback(async () => {
    if (!eventApiParams) return;
    try {
      const { events: data, total } = await api.events.list({
        ...eventApiParams,
        limit: EVENTS_INITIAL_BATCH,
        offset: 0,
      });
      setEvents(data);
      setEventsTotal(total);
    } catch (err) {
      console.error("Failed to load session events:", err);
    }
  }, [eventApiParams]);

  useEffect(() => {
    loadEvents();
  }, [loadEvents]);

  const loadMoreEvents = useCallback(async () => {
    if (!eventApiParams) return;
    setEventsLoadingMore(true);
    try {
      const { events: data, total } = await api.events.list({
        ...eventApiParams,
        limit: EVENTS_MORE_BATCH,
        offset: events.length,
      });
      setEvents((prev) => [...prev, ...data]);
      setEventsTotal(total);
    } catch (err) {
      console.error("Failed to load more session events:", err);
    } finally {
      setEventsLoadingMore(false);
    }
  }, [eventApiParams, events.length]);
  // Auto-expand agents that have working subagents (at any depth)
  useEffect(() => {
    const parentsWithActiveChildren = new Set<string>();
    for (const a of agents) {
      if (a.parent_agent_id && a.status === "working") {
        parentsWithActiveChildren.add(a.parent_agent_id);
      }
    }
    if (parentsWithActiveChildren.size > 0) {
      const agentMap = new Map(agents.map((a) => [a.id, a]));
      const toExpand = new Set<string>();
      for (const pid of parentsWithActiveChildren) {
        let cur = pid;
        while (cur) {
          toExpand.add(cur);
          const parent = agentMap.get(cur);
          cur = parent?.parent_agent_id ?? "";
        }
      }
      setExpandedAgents((prev) => new Set([...prev, ...toExpand]));
    }
  }, [agents]);

  // Pagination-preserving filtered refresh, mirrors ActivityFeed's behavior.
  const refreshEventsWithPagination = useCallback(async () => {
    const params = eventApiParamsRef.current;
    if (!params) return;
    const target = Math.max(
      eventsLoadedCountRef.current || EVENTS_INITIAL_BATCH,
      EVENTS_INITIAL_BATCH
    );
    const size = Math.min(target, EVENTS_MAX_REFRESH);
    try {
      const { events: data, total } = await api.events.list({
        ...params,
        limit: size,
        offset: 0,
      });
      setEvents(data);
      setEventsTotal(total);
    } catch (err) {
      console.error("Failed to refresh session events:", err);
    }
  }, []);

  useEffect(() => {
    const unsubscribe = eventBus.subscribe((msg) => {
      if (isRemoteDataRefreshMessage(msg)) {
        load();
        if (eventsRefreshTimerRef.current) clearTimeout(eventsRefreshTimerRef.current);
        eventsRefreshTimerRef.current = setTimeout(() => {
          eventsRefreshTimerRef.current = null;
          refreshEventsWithPagination();
        }, EVENTS_REFRESH_DEBOUNCE_MS);
        return;
      }
      if (
        msg.type === "agent_created" ||
        msg.type === "agent_updated" ||
        msg.type === "session_updated"
      ) {
        load();
      }
      if (msg.type === "new_event") {
        // Debounce bursts into one filter-aware refetch that preserves the
        // current "Load more" pagination size.
        if (eventsRefreshTimerRef.current) clearTimeout(eventsRefreshTimerRef.current);
        eventsRefreshTimerRef.current = setTimeout(() => {
          eventsRefreshTimerRef.current = null;
          refreshEventsWithPagination();
        }, EVENTS_REFRESH_DEBOUNCE_MS);
      }
    });
    return () => {
      unsubscribe();
      if (eventsRefreshTimerRef.current) {
        clearTimeout(eventsRefreshTimerRef.current);
        eventsRefreshTimerRef.current = null;
      }
    };
  }, [load, refreshEventsWithPagination]);

  if (loading) {
    return (
      <div className="animate-fade-in space-y-8" aria-busy="true">
        <div className="flex items-start gap-4">
          <Skeleton className="w-8 h-8" rounded="md" />
          <div className="flex-1 space-y-3">
            <div className="flex items-center gap-3">
              <Skeleton className="h-6 w-64" />
              <Skeleton className="h-5 w-16" rounded="full" />
            </div>
            <div className="flex flex-wrap gap-3">
              <Skeleton className="h-5 w-40" />
              <Skeleton className="h-5 w-32" />
              <Skeleton className="h-5 w-24" />
            </div>
          </div>
        </div>
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
          {Array.from({ length: 4 }).map((_, i) => (
            <div key={i} className="card p-5 space-y-2">
              <Skeleton className="h-3 w-20" />
              <Skeleton className="h-7 w-24" />
            </div>
          ))}
        </div>
        {Array.from({ length: 2 }).map((_, i) => (
          <div key={i} className="card p-5 space-y-3">
            <Skeleton className="h-4 w-40" />
            <Skeleton className="h-3 w-full" />
            <Skeleton className="h-3 w-11/12" />
            <Skeleton className="h-3 w-5/6" />
          </div>
        ))}
      </div>
    );
  }

  if (error || !session) {
    return (
      <div className="text-center py-20">
        <p className="text-status-danger mb-2">{error || t("detail.notFound")}</p>
        <button onClick={goBack} className="btn-ghost mt-4">
          <ArrowLeft className="w-4 h-4" /> {t("detail.backToSessions")}
        </button>
      </div>
    );
  }

  return (
    <div className="animate-fade-in space-y-8">
      {/* Header */}
      <div className="flex items-start gap-4">
        <button onClick={goBack} className="btn-ghost mt-1">
          <ArrowLeft className="w-4 h-4" />
        </button>
        <div className="flex-1">
          <div className="flex items-center gap-3 mb-2">
            <h2 className="text-xl font-semibold text-fg-primary">
              {session.name || `${t("defaultName")}${session.id.slice(0, 8)}`}
            </h2>
            <SessionStatusBadge
              status={effectiveSessionStatus(session)}
              reason={sessionAwaitingReason(session)}
            />
          </div>
          <div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-1">
            <span className="inline-flex items-center gap-1 text-xs text-fg-muted font-mono bg-surface-2 px-2 py-1 rounded">
              <span title={session.id}>{session.id.slice(0, 16)}</span>
              <CopyButton text={session.id} />
            </span>
            {session.model && (
              <span className="inline-flex items-center gap-1.5 text-xs text-fg-secondary bg-surface-2 px-2 py-1 rounded">
                <Cpu className="w-3 h-3 text-fg-muted" />
                {formatModelName(session.model)}
              </span>
            )}
            <span className="inline-flex items-center gap-1.5 text-xs text-fg-secondary bg-surface-2 px-2 py-1 rounded">
              <Clock className="w-3 h-3 text-fg-muted" />
              {formatDateTime(session.started_at)}
              {session.ended_at && (
                <span className="text-fg-muted ml-1">
                  ({formatDuration(session.started_at, session.ended_at)})
                </span>
              )}
            </span>
            {cost && cost.total_cost > 0 && (
              <span className="inline-flex items-center gap-1.5 text-xs font-medium text-status-success bg-status-success/10 px-2 py-1 rounded">
                <DollarSign className="w-3 h-3" />
                {fmtCostFull(cost.total_cost).slice(1)}
              </span>
            )}
          </div>
          {session.cwd && (
            <div className="flex items-center gap-1.5 text-xs text-fg-muted mt-2 min-w-0">
              <FolderOpen className="w-3 h-3 flex-shrink-0" />
              <span className="font-mono truncate" title={session.cwd}>
                {session.cwd}
              </span>
              <CopyButton text={session.cwd} />
            </div>
          )}
        </div>
        <button onClick={load} className="btn-ghost">
          <RefreshCw className="w-4 h-4" />
        </button>
      </div>

      {/* Waiting-for-input callout: WHY the session sits in the yellow Waiting
          state (awaiting_reason) and for how long. Reason-specific icon/label
          when the server sent a known reason; a generic hourglass fallback for
          legacy rows that predate the awaiting_reason column. Urgent reasons
          (permission prompt / interruption) render hotter amber than the calm
          idle-between-turns ones. */}
      {isSessionAwaitingInput(session) &&
        (() => {
          const reason = sessionAwaitingReason(session);
          const cfg = reason ? AWAITING_REASON_CONFIG[reason] : null;
          const ReasonIcon = reason ? REASON_ICONS[reason] : Hourglass;
          const urgent = cfg?.urgent ?? false;
          return (
            <div
              className={`flex items-center gap-3 rounded-lg border px-4 py-2.5 ${
                urgent
                  ? "border-status-warning/40 bg-status-warning/[0.08]"
                  : "border-yellow-500/25 bg-yellow-500/[0.05]"
              }`}
            >
              <span
                className={`w-7 h-7 rounded-md inline-flex items-center justify-center flex-shrink-0 border ${
                  urgent
                    ? "bg-status-warning/15 border-status-warning/30"
                    : "bg-yellow-500/10 border-yellow-500/25"
                }`}
              >
                <ReasonIcon
                  className={`w-3.5 h-3.5 ${urgent ? "text-status-warning" : "text-yellow-300"}`}
                />
              </span>
              <div className="flex-1 min-w-0">
                <div
                  className={`text-sm font-medium ${urgent ? "text-status-warning" : "text-yellow-200"}`}
                >
                  {t("detail.waitingBanner.title")}
                  {cfg && (
                    <span className={urgent ? "text-status-warning/90" : "text-yellow-300/80"}>
                      {" · "}
                      {t(cfg.labelKey)}
                    </span>
                  )}
                </div>
                <div
                  className={`text-[11px] ${urgent ? "text-status-warning/70" : "text-yellow-400/60"}`}
                >
                  {cfg ? t(cfg.descKey) : t("detail.waitingBanner.generic")}
                </div>
              </div>
              {session.awaiting_input_since && (
                <span
                  className={`text-[11px] flex-shrink-0 flex items-center gap-1.5 ${
                    urgent ? "text-status-warning/80" : "text-yellow-400/70"
                  }`}
                >
                  <span
                    className={`w-1.5 h-1.5 rounded-full animate-pulse-dot ${
                      urgent ? "bg-status-warning" : "bg-yellow-400"
                    }`}
                    aria-hidden="true"
                  />
                  {timeAgo(session.awaiting_input_since)}
                </span>
              )}
            </div>
          );
        })()}

      {isDashboardRun && (
        <Link
          to={`/run?session=${encodeURIComponent(id || "")}`}
          className="flex items-center gap-3 rounded-lg border border-status-success/30 bg-status-success/[0.06] hover:bg-status-success/[0.12] hover:border-status-success/50 px-4 py-2.5 transition-colors group"
        >
          <span className="w-7 h-7 rounded-md bg-status-success/15 border border-status-success/30 inline-flex items-center justify-center flex-shrink-0">
            <Play className="w-3.5 h-3.5 text-status-success" />
          </span>
          <div className="flex-1 min-w-0">
            <div className="text-sm font-medium text-status-success">
              {t("detail.dashboardRun.title", "This session is being driven from the Run page")}
            </div>
            <div className="text-[11px] text-status-success/70">
              {t(
                "detail.dashboardRun.body",
                "Send follow-ups, watch streaming output, or stop the run from there."
              )}
            </div>
          </div>
          <ExternalLink className="w-4 h-4 text-status-success/70 group-hover:text-status-success flex-shrink-0" />
        </Link>
      )}

      {/* Tab Navigation */}
      <div className="flex items-center gap-1 border-b border-border">
        <button
          onClick={() => {
            setActiveTab("agents");
            setTranscriptNotFound(false);
          }}
          className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
            activeTab === "agents"
              ? "border-violet-500 text-violet-400"
              : "border-transparent text-fg-muted hover:text-fg-secondary"
          }`}
        >
          <Bot className="w-4 h-4" />
          {t("detail.agents")} ({agents.length})
        </button>
        <button
          onClick={() => {
            setActiveTab("conversation");
            setTranscriptNotFound(false);
          }}
          className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
            activeTab === "conversation"
              ? "border-violet-500 text-violet-400"
              : "border-transparent text-fg-muted hover:text-fg-secondary"
          }`}
        >
          <MessageSquare className="w-4 h-4" />
          {t("detail.conversation")}
        </button>
        <button
          onClick={() => {
            setActiveTab("timeline");
            setTranscriptNotFound(false);
          }}
          className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
            activeTab === "timeline"
              ? "border-violet-500 text-violet-400"
              : "border-transparent text-fg-muted hover:text-fg-secondary"
          }`}
        >
          <List className="w-4 h-4" />
          {t("detail.timeline", { shown: events.length, total: eventsTotal })}
        </button>
      </div>

      {/* Tab Content */}
      {transcriptNotFound && (
        <div className="flex items-center gap-2 px-4 py-2.5 mb-3 text-sm text-status-warning bg-status-warning/10 border border-status-warning/20 rounded-lg">
          <AlertCircle className="w-4 h-4 flex-shrink-0" />
          <span>{t("detail.transcriptNotFound")}</span>
          <button
            onClick={() => setTranscriptNotFound(false)}
            className="ml-auto text-status-warning/60 hover:text-status-warning transition-colors"
          >
            <List className="w-3.5 h-3.5" />
          </button>
        </div>
      )}

      {visitedTabs.has("agents") && (
        <div hidden={activeTab !== "agents"}>
          <SessionOverview session={session} agents={agents} />

          {workflows.length > 0 && (
            <div className="mb-4">
              <h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider mb-2 flex items-center gap-1.5">
                <Workflow className="w-3.5 h-3.5 text-violet-400" />
                {wfT("runs.sessionTitle")}
                <span className="text-fg-muted font-mono">· {workflows.length}</span>
              </h3>
              <WorkflowRunsPanel runs={workflows} hideSessionLink />
            </div>
          )}

          {agents.length === 0 ? (
            <p className="text-sm text-fg-muted">{t("detail.noAgents")}</p>
          ) : (
            <>
              <h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider mb-3 flex items-center gap-1.5">
                <Bot className="w-3.5 h-3.5 text-violet-400" />
                {t("detail.agents")}
                <span className="text-fg-muted font-mono">· {agents.length}</span>
              </h3>
              <div className="space-y-2" data-testid="agent-tree">
                {(() => {
                  // Build parent→children map for the full tree (works at any depth)
                  const agentMap = new Map(agents.map((a) => [a.id, a]));
                  const childrenByParent = new Map<string, Agent[]>();
                  const rootAgents: Agent[] = [];
                  for (const a of agents) {
                    if (a.parent_agent_id && agentMap.has(a.parent_agent_id)) {
                      const list = childrenByParent.get(a.parent_agent_id) || [];
                      list.push(a);
                      childrenByParent.set(a.parent_agent_id, list);
                    } else if (!a.parent_agent_id || !agentMap.has(a.parent_agent_id)) {
                      rootAgents.push(a);
                    }
                  }
                  // Sort roots and children by started_at ascending (chronological order)
                  rootAgents.sort((a, b) => (a.started_at || "").localeCompare(b.started_at || ""));
                  for (const key of childrenByParent.keys()) {
                    childrenByParent
                      .get(key)!
                      .sort((a, b) => (a.started_at || "").localeCompare(b.started_at || ""));
                  }

                  // Count all descendants (recursive) for collapsed badge.
                  // `seen` guards against a cyclic parent_agent_id (corrupt data)
                  // that would otherwise recurse forever and crash the page.
                  function countDescendants(id: string, seen = new Set<string>()): number {
                    if (seen.has(id)) return 0;
                    seen.add(id);
                    const kids = childrenByParent.get(id) || [];
                    return kids.reduce((sum, k) => sum + 1 + countDescendants(k.id, seen), 0);
                  }

                  // Recursive agent node renderer. `ancestors` guards against a
                  // cyclic parent_agent_id so a bad link can't stack-overflow.
                  function renderAgentNode(
                    agent: Agent,
                    depth: number,
                    ancestors: Set<string> = new Set()
                  ): ReactNode {
                    if (ancestors.has(agent.id)) return null;
                    const childAncestors = new Set(ancestors).add(agent.id);
                    const children = childrenByParent.get(agent.id) || [];
                    const isExpanded = expandedAgents.has(agent.id);
                    const hasChildren = children.length > 0;
                    const isSubagent = depth > 0;
                    const totalDesc = hasChildren ? countDescendants(agent.id) : 0;
                    const toggleExpanded = () =>
                      setExpandedAgents((prev) => {
                        const next = new Set(prev);
                        if (next.has(agent.id)) next.delete(agent.id);
                        else next.add(agent.id);
                        return next;
                      });

                    return (
                      <div key={agent.id}>
                        <div className="flex items-center gap-1 min-w-0">
                          {hasChildren && (
                            <button
                              onClick={toggleExpanded}
                              className="p-1 text-fg-muted hover:text-fg-secondary transition-colors flex-shrink-0"
                              aria-label={isExpanded ? "Collapse subagents" : "Expand subagents"}
                              aria-expanded={isExpanded}
                            >
                              {isExpanded ? (
                                <ChevronDown className="w-4 h-4" />
                              ) : (
                                <ChevronRight className="w-4 h-4" />
                              )}
                            </button>
                          )}
                          {isSubagent && !hasChildren && <span className="w-6 flex-shrink-0" />}
                          {isSubagent && (
                            <GitBranch className="w-3 h-3 text-violet-400 flex-shrink-0" />
                          )}
                          <div className="flex-1 min-w-0">
                            <AgentCard
                              agent={agent}
                              // The /api/sessions/:id session row has no `cost`
                              // column (cost is loaded separately into `cost`
                              // state), so inject the computed session total here
                              // — otherwise the MAIN agent card, which shows
                              // session.cost, renders no cost on this page.
                              // Subagent cards ignore session.cost (they use
                              // agent.cost), so this only affects the main card.
                              session={
                                session
                                  ? { ...session, cost: cost?.total_cost ?? session.cost }
                                  : undefined
                              }
                              label={compactionLabels.get(agent.id)}
                              // Card click always opens the agent conversation —
                              // same behavior whether or not it has children.
                              // Expand/collapse of the subagent list is handled
                              // solely by the chevron button, so clicking a
                              // parent (incl. the main agent) no longer toggles.
                              onClick={() => navigateToAgentConversation(agent)}
                            />
                          </div>
                        </div>

                        {/* Recursive children (collapsible) */}
                        {hasChildren && isExpanded && (
                          <div className="ml-6 mt-1 space-y-1 border-l-2 border-violet-500/20 pl-3">
                            {children.map((child) =>
                              renderAgentNode(child, depth + 1, childAncestors)
                            )}
                          </div>
                        )}

                        {/* Descendant count badge when collapsed */}
                        {hasChildren && !isExpanded && (
                          <button
                            onClick={() =>
                              setExpandedAgents((prev) => new Set([...prev, agent.id]))
                            }
                            className="ml-7 mt-1 text-[11px] text-violet-400 hover:text-violet-300 transition-colors"
                          >
                            {t("common:subagent_label", { count: totalDesc })}
                          </button>
                        )}
                      </div>
                    );
                  }

                  // Separate true orphans (subagent whose parent_agent_id references a missing agent)
                  const orphans = rootAgents.filter(
                    (a) =>
                      a.type === "subagent" && a.parent_agent_id && !agentMap.has(a.parent_agent_id)
                  );
                  const roots = rootAgents.filter(
                    (a) =>
                      !(
                        a.type === "subagent" &&
                        a.parent_agent_id &&
                        !agentMap.has(a.parent_agent_id)
                      )
                  );

                  return (
                    <>
                      {roots.map((agent) => renderAgentNode(agent, 0))}

                      {/* Orphaned subagents */}
                      {orphans.length > 0 && (
                        <div className="mt-4">
                          <p className="text-[11px] text-fg-muted mb-2 uppercase tracking-wider">
                            {t("detail.unparented")}
                          </p>
                          <div className="space-y-1">
                            {orphans.map((agent) => renderAgentNode(agent, 1))}
                          </div>
                        </div>
                      )}
                    </>
                  );
                })()}
              </div>
            </>
          )}

          {/* Cost Breakdown - shown under Agents tab */}
          {cost && cost.breakdown.length > 0 && cost.total_cost > 0 && (
            <div className="mt-8">
              <h3 className="text-sm font-medium text-fg-secondary mb-4 flex items-center gap-2">
                <DollarSign className="w-4 h-4" />
                {t("detail.costBreakdown")}
              </h3>
              <div className="card overflow-x-auto">
                <table className="w-full min-w-[600px]">
                  <thead>
                    <tr className="border-b border-border text-left">
                      <th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
                        {t("common:cost.model")}
                      </th>
                      <th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
                        {t("common:token.input")}
                      </th>
                      <th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
                        {t("common:token.output")}
                      </th>
                      <th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
                        {t("common:token.cacheRead")}
                      </th>
                      <th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
                        {t("common:token.cacheWrite")}
                      </th>
                      <th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
                        {t("common:cost.cost")}
                      </th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-border">
                    {cost.breakdown.map((row) => (
                      <tr key={row.model} className="hover:bg-surface-4 transition-colors">
                        <td className="px-5 py-2.5 text-sm font-mono text-fg-secondary">
                          {formatModelName(row.model)}
                        </td>
                        <td className="px-5 py-2.5 text-sm text-fg-secondary text-right font-mono">
                          {row.input_tokens.toLocaleString()}
                        </td>
                        <td className="px-5 py-2.5 text-sm text-fg-secondary text-right font-mono">
                          {row.output_tokens.toLocaleString()}
                        </td>
                        <td className="px-5 py-2.5 text-sm text-fg-secondary text-right font-mono">
                          {row.cache_read_tokens.toLocaleString()}
                        </td>
                        <td className="px-5 py-2.5 text-sm text-fg-secondary text-right font-mono">
                          {row.cache_write_tokens.toLocaleString()}
                        </td>
                        <td className="px-5 py-2.5 text-sm text-status-success text-right font-mono font-medium">
                          {fmtCostFull(row.cost, 4)}
                        </td>
                      </tr>
                    ))}
                    <tr className="bg-surface-2">
                      <td className="px-5 py-2.5 text-sm font-medium text-fg-secondary" colSpan={5}>
                        {t("common:total")}
                      </td>
                      <td className="px-5 py-2.5 text-sm text-status-success text-right font-mono font-semibold">
                        {fmtCostFull(cost.total_cost, 4)}
                      </td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </div>
          )}
        </div>
      )}

      {visitedTabs.has("conversation") && (
        <div hidden={activeTab !== "conversation"}>
          <ConversationView sessionId={session.id} initialTranscriptId={pendingTranscriptId} />
        </div>
      )}

      {visitedTabs.has("timeline") && (
        <div hidden={activeTab !== "timeline"}>
          <div className="mb-3">
            <EventFiltersInfo />
          </div>
          <div className="mb-3">
            <EventFilters
              value={filters}
              onChange={setFilters}
              hideSessionFilter
              agentOptions={agents.map((a) => ({ id: a.id, label: a.name || a.id }))}
            />
          </div>
          {events.length === 0 ? (
            <p className="text-sm text-fg-muted">
              {isEmptyFilters(filters) ? t("detail.noEvents") : t("common:eventFilters.noResults")}
            </p>
          ) : (
            <div className="card overflow-hidden">
              <div className="divide-y divide-border max-h-[600px] overflow-y-auto overflow-x-auto">
                {events.map((event, i) => {
                  const key = event.id ?? i;
                  const isOpen = event.id != null && expandedEvents.has(event.id);
                  return (
                    <div key={key}>
                      <button
                        type="button"
                        onClick={() => event.id != null && toggleEvent(event.id)}
                        aria-expanded={isOpen}
                        aria-label={
                          isOpen ? t("common:eventDetail.collapse") : t("common:eventDetail.expand")
                        }
                        className="w-full text-left px-5 py-3 flex items-center gap-4 hover:bg-surface-4 transition-colors min-w-0 cursor-pointer"
                      >
                        <span
                          className={`text-fg-muted text-[10px] w-3 flex-shrink-0 transition-transform ${isOpen ? "rotate-90" : ""}`}
                          aria-hidden="true"
                        >
                          ▶
                        </span>
                        <div className="w-16 text-[11px] text-fg-muted font-mono flex-shrink-0">
                          {timeAgo(event.created_at)}
                        </div>
                        <AgentStatusBadge status={statusFromEventType(event.event_type)} />
                        {(() => {
                          // Session is implicit on this page - project is
                          // still shown so the row identifies the working
                          // directory when you share / search. Events without
                          // their own cwd (e.g. TurnDuration) fall back to the
                          // session's cwd so every row carries the dir prefix.
                          const project =
                            projectByEventId.get(event.id) ?? projectFromCwd(session?.cwd) ?? null;
                          const origin = buildOriginLabel(
                            project,
                            null,
                            agentOriginLabel(event.agent_id, agentInfoById)
                          );
                          return (
                            <span className="text-sm text-fg-secondary flex-1 truncate">
                              {origin && (
                                <span
                                  className="text-fg-muted mr-1"
                                  title={event.agent_id ?? undefined}
                                >
                                  {origin} ·
                                </span>
                              )}
                              {buildEventTitle(event)}
                            </span>
                          );
                        })()}
                        {event.tool_name && (
                          <span className="text-[11px] px-2 py-0.5 bg-surface-2 rounded text-fg-muted font-mono">
                            {event.tool_name}
                          </span>
                        )}
                      </button>
                      {isOpen && (
                        <EventDetail
                          event={event}
                          agentInfoById={agentInfoById}
                          sessionNameById={sessionNameById}
                        />
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          )}
          {events.length < eventsTotal && (
            <div className="flex items-center justify-between mt-3 px-1">
              <span className="text-xs text-fg-muted">
                {t("common:eventFilters.showing", { shown: events.length, total: eventsTotal })}
              </span>
              <button
                type="button"
                onClick={loadMoreEvents}
                disabled={eventsLoadingMore}
                className="px-3 py-1.5 text-xs font-medium rounded-md bg-accent/15 text-accent hover:bg-accent/25 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
              >
                {eventsLoadingMore
                  ? t("common:eventFilters.loading")
                  : t("common:eventFilters.loadMore")}
              </button>
            </div>
          )}
        </div>
      )}
    </div>
  );
}
