import { useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router"; import { Masthead } from "../components/Masthead"; import { useDirective, useDirectives } from "../hooks/useDirectives"; import { useAuth } from "../contexts/AuthContext"; import { DocumentEditor } from "../components/directives/DocumentEditor"; import type { DirectiveSummary, DirectiveStatus } from "../lib/api"; // Status dot color, matching the existing tabular UI's badge palette so the // document mode feels like a sibling of the existing list, not a foreign UI. const STATUS_DOT: Record = { draft: "bg-[#556677]", active: "bg-green-400", idle: "bg-yellow-400", paused: "bg-orange-400", archived: "bg-[#3a4a6a]", }; // ============================================================================= // Sidebar grouping — group directives by lifecycle stage so the file tree // reads like a folder per status. We collapse the noisy ones (Archived) by // default and keep Active / Idle expanded. // ============================================================================= type SidebarGroup = "active" | "idle" | "archived"; const GROUP_LABEL: Record = { active: "active", idle: "idle", archived: "archived", }; function bucketOf(status: DirectiveStatus): SidebarGroup { if (status === "active" || status === "paused") return "active"; if (status === "archived") return "archived"; // draft + idle land in the idle bucket (i.e. "not currently running"). return "idle"; } // ============================================================================= // Sidebar icons (inline SVG, no new deps) // ============================================================================= function FolderIcon({ open = false }: { open?: boolean }) { return ( {open ? ( ) : ( )} ); } function FileIcon() { return ( ); } function Caret({ open }: { open: boolean }) { return ( ); } // ============================================================================= // Sidebar // ============================================================================= interface SidebarProps { directives: DirectiveSummary[]; loading: boolean; selectedId: string | null; onSelect: (id: string) => void; } function DocumentSidebar({ directives, loading, selectedId, onSelect }: SidebarProps) { const groups: Record = useMemo(() => { const out: Record = { active: [], idle: [], archived: [], }; for (const d of directives) { out[bucketOf(d.status)].push(d); } // Sort each group alphabetically so it feels like a stable file tree. (Object.keys(out) as SidebarGroup[]).forEach((k) => { out[k].sort((a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: "base" }), ); }); return out; }, [directives]); // Default-collapsed state per folder. Archived is collapsed by default // (it's history); the other two are open so users see their work. const [openGroups, setOpenGroups] = useState>({ active: true, idle: true, archived: false, }); const toggleGroup = (g: SidebarGroup) => setOpenGroups((prev) => ({ ...prev, [g]: !prev[g] })); return (
{/* Sidebar header */}
Documents {directives.length}
{/* Top-level "directives/" folder */}
directives/
{/* Body */}
{loading && directives.length === 0 ? (
Loading...
) : directives.length === 0 ? (
No directives yet
) : ( (Object.keys(groups) as SidebarGroup[]).map((group) => { const list = groups[group]; if (list.length === 0) return null; const open = openGroups[group]; return (
{/* Group header (sub-folder) */} {/* Files inside the group */} {open && (
    {list.map((d) => { const isSelected = d.id === selectedId; const dot = STATUS_DOT[d.status] ?? STATUS_DOT.draft; const slug = d.title .trim() .replace(/\s+/g, "-") .replace(/[^a-zA-Z0-9._-]/g, "") .toLowerCase(); const fileName = slug.length > 0 ? `${slug}.md` : `${d.id.slice(0, 8)}.md`; const orchestratorRunning = !!d.orchestratorTaskId; return (
  • ); })}
)}
); }) )}
); } // ============================================================================= // Editor shell — wraps DocumentEditor and handles the "no document selected" // and loading states. // ============================================================================= interface EditorShellProps { selectedId: string | undefined; hasDirectives: boolean; listLoading: boolean; } function EditorShell({ selectedId, hasDirectives, listLoading }: EditorShellProps) { const { directive, loading, updateGoal, cleanup, createPR, pickUpOrders, } = useDirective(selectedId); if (!selectedId) { return (

{listLoading ? "Loading documents..." : hasDirectives ? "Select a document from the sidebar" : "No documents yet — create one from the legacy UI"}

); } if (loading && !directive) { return (

Loading document...

); } if (!directive) { return (

Document not found

); } return (
{/* Document header — breadcrumb-like, mirrors a code editor's tab bar */}
directives / {directive.id.slice(0, 8)} {!!directive.orchestratorTaskId && ( orchestrator running )}
{/* Lexical editor body */} { await updateGoal(goal); }} onCleanup={async () => { await cleanup(); }} onCreatePR={async () => { await createPR(); }} onPickUpOrders={async () => { await pickUpOrders(); }} />
); } // ============================================================================= // Page // ============================================================================= export default function DocumentDirectivesPage() { const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); const navigate = useNavigate(); const { id: selectedId } = useParams<{ id: string }>(); const { directives, loading: listLoading } = useDirectives(); useEffect(() => { if (!authLoading && isAuthConfigured && !isAuthenticated) { navigate("/login"); } }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); if (authLoading) { return (

Loading...

); } return (
{/* Left: file-tree sidebar */}
navigate(`/directives/${id}`)} />
{/* Right: Lexical editor */} 0} listLoading={listLoading} />
); }