"use client"; // MissionOutputReader — the mission's reading surface. // // Agent phases produce 40–55kB markdown briefs per turn. Before this, // the only way to see them was a 300px-tall
 nested inside a 260px
// run box inside the page scroller, showing the first 6,000 chars with
// no way to reach the rest. This replaces that with a document reader:
//
//   left rail   every document in the mission, grouped by phase
//   right pane  the selected document IN FULL, rendered as markdown
//   outline     that document's headings, click to jump
//
// Exactly ONE scroll container per column — no nesting. The rail and the
// document scroll independently; the page body never scrolls.

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Copy, Download, FileText, Loader2 } from "lucide-react";

import {
  getMissionDocument,
  listMissionDocuments,
  type MissionDocument,
  type MissionPhase,
  type PhaseKind,
} from "@/lib/api/missions";
import { MarkdownBlock, outlineOf } from "./MarkdownBlock";

const mono =
  "ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";

const PHASE_LABEL: Record = {
  research: "Research",
  coding: "Coding",
  benchmark: "Benchmark",
  security_scan: "Security scan",
};

/** `code_archeologist` → `Code Archeologist`. */
function humanRole(role: string): string {
  return role
    .split(/[_\s]+/)
    .filter(Boolean)
    .map((w) => w[0].toUpperCase() + w.slice(1))
    .join(" ");
}

function sizeLabel(chars: number): string {
  return chars >= 1000 ? `${Math.round(chars / 1000)}k` : `${chars}`;
}

type DocKey = string;
const keyOf = (d: Pick): DocKey =>
  `${d.run_id}:${d.index}`;

export function MissionOutputReader({
  missionId,
  phases,
  visible,
}: {
  missionId: string;
  phases: MissionPhase[];
  visible: boolean;
}) {
  const [docs, setDocs] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [selected, setSelected] = useState(null);
  const [body, setBody] = useState("");
  const [bodyLoading, setBodyLoading] = useState(false);
  const [copied, setCopied] = useState(false);
  const docScroll = useRef(null);

  const loadList = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await listMissionDocuments(missionId);
      setDocs(res.documents);
      // Default to the newest document so the pane is never empty.
      setSelected((prev) => {
        if (prev && res.documents.some((d) => keyOf(d) === prev)) return prev;
        const last = res.documents[res.documents.length - 1];
        return last ? keyOf(last) : null;
      });
    } catch (e) {
      setError(e instanceof Error ? e.message : "failed to load documents");
    } finally {
      setLoading(false);
    }
  }, [missionId]);

  useEffect(() => {
    if (!visible) return;
    void loadList();
  }, [visible, loadList]);

  const selectedDoc = useMemo(
    () => docs.find((d) => keyOf(d) === selected) ?? null,
    [docs, selected],
  );

  // Fetch the selected document's full text.
  useEffect(() => {
    if (!visible || !selectedDoc) {
      setBody("");
      return;
    }
    let alive = true;
    setBodyLoading(true);
    getMissionDocument(missionId, selectedDoc.run_id, selectedDoc.index)
      .then((d) => {
        if (!alive) return;
        setBody(d.body);
        setError(null);
        // A new document starts at the top, not wherever the last one sat.
        docScroll.current?.scrollTo({ top: 0 });
      })
      .catch((e) => {
        if (!alive) return;
        setError(e instanceof Error ? e.message : "failed to load document");
        setBody("");
      })
      .finally(() => {
        if (alive) setBodyLoading(false);
      });
    return () => {
      alive = false;
    };
  }, [missionId, selectedDoc, visible]);

  const outline = useMemo(() => (body ? outlineOf(body) : []), [body]);

  // Group documents under their phase, in phase order. Documents whose
  // phase is unknown (ad-hoc runs) collect under a trailing bucket.
  const groups = useMemo(() => {
    const byPhase = new Map();
    for (const d of docs) {
      const k = d.phase_id ?? "__unphased";
      const list = byPhase.get(k);
      if (list) list.push(d);
      else byPhase.set(k, [d]);
    }
    const ordered = [...phases]
      .sort((a, b) => a.order_idx - b.order_idx)
      .filter((p) => byPhase.has(p.id))
      .map((p) => ({
        id: p.id,
        label: `${PHASE_LABEL[p.kind] ?? p.kind}`,
        docs: byPhase.get(p.id) ?? [],
      }));
    const loose = byPhase.get("__unphased");
    if (loose?.length) {
      ordered.push({
        id: "__unphased",
        label: "Other runs",
        docs: loose,
      });
    }
    return ordered;
  }, [docs, phases]);

  const jumpTo = useCallback((id: string) => {
    const el = document.getElementById(id);
    if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
  }, []);

  const copyBody = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(body);
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1500);
    } catch {
      // Clipboard can be blocked; the download button is the fallback.
    }
  }, [body]);

  const downloadBody = useCallback(() => {
    if (!selectedDoc) return;
    const blob = new Blob([body], { type: "text/markdown" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `${selectedDoc.role}-${selectedDoc.index + 1}.md`;
    a.click();
    URL.revokeObjectURL(url);
  }, [body, selectedDoc]);

  if (loading && docs.length === 0) {
    return (
      
Loading documents…
); } if (!loading && docs.length === 0) { return (
No agent output yet. Documents appear here as each phase's agents finish their turns. {error && ( {error} )}
); } return (
{/* ── rail: every document, grouped by phase ── */}
{groups.map((g) => (
{g.label} · {g.docs.length}
{g.docs.map((d) => { const k = keyOf(d); const active = k === selected; return ( ); })}
))}
{/* ── document: the only place long-form content is read ── */}
{error && (
{error}
)} {selectedDoc && (
{selectedDoc.title}
{humanRole(selectedDoc.role)} · {selectedDoc.node_id} ·{" "} {selectedDoc.chars.toLocaleString()} chars
)} {bodyLoading ? (
Loading document…
) : body ? ( ) : null}
{/* ── outline: headings of the open document ── */}
Outline
{outline.length === 0 ? ( No headings ) : ( outline.map((h) => ( )) )}
); } const readerBtn: React.CSSProperties = { flex: "none", display: "inline-flex", alignItems: "center", gap: 5, padding: "4px 9px", borderRadius: 7, border: "1px solid rgba(255,255,255,.10)", background: "transparent", color: "#a0a0a8", fontFamily: mono, fontSize: 10, cursor: "pointer", };