summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/document-directives.tsx
blob: 9cb984b725a1547a7df00bb6a85abbf4ee7c2991 (plain) (tree)
1
2
3
4
5
6
7
8

                                                                          


                                                                     
                                                                                
                                                                         
                                                                                 










                                                                                     




                     










                                                                             








                                                                        

  






















































                                                                                                                                                                                                                                            

































                                                                                                            

















                                                                                        

















                                                                                































































































































                                                                                                                                                 














                                                                                  


                                 

                                            

 

                                                                            




                                                                            






                          

                         

                         





                                            



                                                                        

                                                                                          












                                                                                        



                                                                        




                                 
                                                                   



                                                                                                                                          

                                                                            






                                                                               























                                                                                                                              









































                                                                                                                                                


                                                                 






















                                                                                                                                           





             



































                                                                                                

                         

                                                                                 

















                                                                            
                   











                                                                   
                   











                                                                             
                        



                            
     









                                            

                                                                                          

 







                          














                                                                               













                                                                                
       

                   























                                                                             












                                                                                                                         
                                                                                     















                                                                                                  







                                               

                                                                      

                                                             

              












                                                                                
                                

                         
                          

 






                      






































                                                                                   









                                                                         







                                                                                                              













                                                                                                                          







                                                                                                    





















                                                         







                                                                                










                                                                            



                                                                                  

                                                            

                                                                                     






                                                                 















                                                    



















                                                                                      










                                                                             



                                                         







                                                                           
                                                                                                                                             


                                   

                                 

                                                           


              
                                                   

                                 
                                         

                                               
                                   

             




















































































                                                                                  


          
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router";
import { Masthead } from "../components/Masthead";
import { useDirective, useDirectives } from "../hooks/useDirectives";
import { useAuth } from "../contexts/AuthContext";
import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext";
import { DocumentEditor } from "../components/directives/DocumentEditor";
import { DocumentTaskStream } from "../components/directives/DocumentTaskStream";
import { DirectiveContextMenu } from "../components/directives/DirectiveContextMenu";
import {
  startDirective,
  pauseDirective,
  updateDirective,
  deleteDirective,
  completeDirectiveStep,
  failDirectiveStep,
  skipDirectiveStep,
  stopTask,
} from "../lib/api";
import type {
  DirectiveStatus,
  DirectiveSummary,
  DirectiveWithSteps,
} 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<DirectiveStatus, string> = {
  draft: "bg-[#556677]",
  active: "bg-green-400",
  idle: "bg-yellow-400",
  paused: "bg-orange-400",
  archived: "bg-[#3a4a6a]",
};

// Per-task dot color for the sidebar entries inside a directive folder.
// Matches the StepsBlockNode palette.
const STEP_STATUS_DOT: Record<string, string> = {
  pending: "bg-[#556677]",
  ready: "bg-[#9bc3ff]",
  running: "bg-yellow-400",
  done: "bg-green-400",
  failed: "bg-red-400",
  skipped: "bg-[#3a4a6a]",
};

// =============================================================================
// Sidebar icons (inline SVG, no new deps)
// =============================================================================

function FolderIcon({ open = false }: { open?: boolean }) {
  return (
    <svg
      viewBox="0 0 16 16"
      width={12}
      height={12}
      className="shrink-0"
      aria-hidden
    >
      {open ? (
        <path
          d="M1.5 3.5a1 1 0 0 1 1-1h3.382a1 1 0 0 1 .894.553l.448.894a1 1 0 0 0 .894.553H13.5a1 1 0 0 1 1 1V6H1.5V3.5z M1 6.5h13.382a.5.5 0 0 1 .49.598l-.9 5A.5.5 0 0 1 13.482 12.5H2.518a.5.5 0 0 1-.49-.402l-.9-5A.5.5 0 0 1 1.62 6.5H1z"
          fill="#75aafc"
          opacity="0.85"
        />
      ) : (
        <path
          d="M1.5 4a1 1 0 0 1 1-1h3.382a1 1 0 0 1 .894.553l.448.894a1 1 0 0 0 .894.553H13.5a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V4z"
          fill="#75aafc"
          opacity="0.65"
        />
      )}
    </svg>
  );
}

function FileIcon() {
  return (
    <svg
      viewBox="0 0 16 16"
      width={12}
      height={12}
      className="shrink-0"
      aria-hidden
    >
      <path
        d="M3 1.5h6.293a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 13.293 5.5H13V14a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V1.5z"
        fill="none"
        stroke="#9bc3ff"
        strokeWidth="1"
      />
      <path
        d="M9.5 1.5v3h3"
        fill="none"
        stroke="#9bc3ff"
        strokeWidth="1"
      />
    </svg>
  );
}

/** Terminal/prompt icon for orchestrator and step tasks. */
function TaskIcon() {
  return (
    <svg
      viewBox="0 0 16 16"
      width={12}
      height={12}
      className="shrink-0"
      aria-hidden
    >
      <rect x="1.5" y="3" width="13" height="10" rx="1" fill="none" stroke="#9bc3ff" strokeWidth="1" />
      <path d="M3.5 6l2 2-2 2 M7 10h4" stroke="#9bc3ff" strokeWidth="1" fill="none" strokeLinecap="round" />
    </svg>
  );
}

/** PR-bracket icon for the completion task. */
function CompletionIcon() {
  return (
    <svg
      viewBox="0 0 16 16"
      width={12}
      height={12}
      className="shrink-0"
      aria-hidden
    >
      <circle cx="4" cy="4" r="1.4" fill="none" stroke="#9bc3ff" strokeWidth="1" />
      <circle cx="4" cy="12" r="1.4" fill="none" stroke="#9bc3ff" strokeWidth="1" />
      <circle cx="12" cy="12" r="1.4" fill="none" stroke="#9bc3ff" strokeWidth="1" />
      <path d="M4 5.4v5.2 M4 12h6.6 M12 4l0 6.6" stroke="#9bc3ff" strokeWidth="1" fill="none" />
    </svg>
  );
}

function PinIcon() {
  return (
    <svg
      viewBox="0 0 16 16"
      width={10}
      height={10}
      className="shrink-0"
      aria-hidden
    >
      <path
        d="M8 1.5l1.6 3.6 3.9.4-2.95 2.7.85 3.9L8 10.2 4.6 12.1l.85-3.9L2.5 5.5l3.9-.4z"
        fill="#75aafc"
        opacity="0.7"
      />
    </svg>
  );
}

function Caret({ open }: { open: boolean }) {
  return (
    <svg
      viewBox="0 0 8 8"
      width={8}
      height={8}
      className={`shrink-0 transition-transform ${open ? "rotate-90" : ""}`}
      aria-hidden
    >
      <path d="M2 1l4 3-4 3z" fill="#7788aa" />
    </svg>
  );
}

// =============================================================================
// Sidebar
// =============================================================================

// =============================================================================
// Task row context menu — sits next to DirectiveContextMenu and offers the
// task-level controls (interrupt for orchestrator/completion, complete/fail/
// skip for step tasks).
// =============================================================================

interface TaskContextMenuProps {
  x: number;
  y: number;
  task: FolderTaskRow;
  onClose: () => void;
  onInterrupt: () => void;
  onComplete?: () => void;
  onFail?: () => void;
  onSkip?: () => void;
}

function TaskContextMenu({
  x,
  y,
  task,
  onClose,
  onInterrupt,
  onComplete,
  onFail,
  onSkip,
}: TaskContextMenuProps) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const click = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) onClose();
    };
    const key = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("mousedown", click);
    document.addEventListener("keydown", key);
    return () => {
      document.removeEventListener("mousedown", click);
      document.removeEventListener("keydown", key);
    };
  }, [onClose]);

  useEffect(() => {
    if (!ref.current) return;
    const rect = ref.current.getBoundingClientRect();
    if (rect.right > window.innerWidth) ref.current.style.left = `${x - rect.width}px`;
    if (rect.bottom > window.innerHeight) ref.current.style.top = `${y - rect.height}px`;
  }, [x, y]);

  const item =
    "w-full px-3 py-1.5 text-left text-xs font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] flex items-center gap-2";
  const divider = "border-t border-[rgba(117,170,252,0.2)] my-1";

  // Interrupt is meaningful for live tasks (orchestrator-active or running steps).
  const showInterrupt =
    task.kind === "orchestrator-active" ||
    task.kind === "completion" ||
    task.status === "running";
  // Step lifecycle controls only apply to step tasks.
  const isStep = task.kind === "step";
  const showComplete = isStep && task.status !== "done";
  const showFail = isStep && task.status !== "failed";
  const showSkip = isStep && task.status !== "skipped";

  return (
    <div
      ref={ref}
      className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]"
      style={{ left: x, top: y }}
    >
      <div className="px-3 py-1.5 text-[10px] font-mono text-[#556677] uppercase border-b border-[rgba(117,170,252,0.2)] truncate max-w-[220px]">
        {task.kind === "orchestrator-active" ? "Orchestrator" : task.kind === "completion" ? "Completion" : task.label}
      </div>
      {showInterrupt && (
        <button
          className={item}
          onClick={() => {
            onInterrupt();
            onClose();
          }}
        >
          <span className="text-amber-300">⏹</span>
          Interrupt
        </button>
      )}
      {(showComplete || showFail || showSkip) && <div className={divider} />}
      {showComplete && (
        <button
          className={item}
          onClick={() => {
            onComplete?.();
            onClose();
          }}
        >
          <span className="text-emerald-400">✓</span>
          Mark complete
        </button>
      )}
      {showFail && (
        <button
          className={item}
          onClick={() => {
            onFail?.();
            onClose();
          }}
        >
          <span className="text-red-400">✗</span>
          Mark failed
        </button>
      )}
      {showSkip && (
        <button
          className={item}
          onClick={() => {
            onSkip?.();
            onClose();
          }}
        >
          <span className="text-[#7788aa]">⤳</span>
          Skip
        </button>
      )}
    </div>
  );
}

function slugify(title: string, fallback: string): string {
  const slug = title
    .trim()
    .replace(/\s+/g, "-")
    .replace(/[^a-zA-Z0-9._-]/g, "")
    .toLowerCase();
  return slug.length > 0 ? slug : fallback;
}

interface SidebarSelection {
  directiveId: string;
  /** null = the directive's document; otherwise a task id (orchestrator/step). */
  taskId: string | null;
}

interface SidebarProps {
  directives: DirectiveSummary[];
  loading: boolean;
  selection: SidebarSelection | null;
  onSelect: (sel: SidebarSelection) => void;
}

/**
 * Per-directive folder. Renders the directive as a collapsible folder whose
 * body is the pinned document entry (always first) followed by a `tasks/`
 * subfolder containing the orchestrator, completion, and step tasks.
 *
 * Status dot lives on the right side only (single-side, per the v2 design).
 * If a directive or task has a pending user question, its icon glows.
 */
function DirectiveFolder({
  directive,
  open,
  onToggle,
  selection,
  onSelect,
  pendingTaskIds,
  hasPendingForDirective,
  onDirectiveContextMenu,
  onTaskContextMenu,
}: {
  directive: DirectiveSummary;
  open: boolean;
  onToggle: () => void;
  selection: SidebarSelection | null;
  onSelect: (sel: SidebarSelection) => void;
  /** Set of task ids that currently have pending user questions. */
  pendingTaskIds: Set<string>;
  /** Whether any pending question is associated with this directive. */
  hasPendingForDirective: boolean;
  onDirectiveContextMenu: (e: React.MouseEvent, d: DirectiveSummary) => void;
  onTaskContextMenu: (e: React.MouseEvent, t: FolderTaskRow, directiveId: string) => void;
}) {
  const dotColor = STATUS_DOT[directive.status] ?? STATUS_DOT.draft;
  const fileName = `${slugify(directive.title, directive.id.slice(0, 8))}.md`;

  // Lazy fetch full directive (with steps) only when folder is open.
  const { directive: detailed } = useDirective(open ? directive.id : undefined);

  const docSelected =
    selection?.directiveId === directive.id && selection.taskId === null;

  // Collect the tasks to surface in the folder body.
  const tasks = useMemo(() => collectTasks(detailed, directive), [detailed, directive]);

  const orchestratorRunning = !!directive.orchestratorTaskId;
  // Tasks subfolder open state — independent of the directive folder.
  const [tasksOpen, setTasksOpen] = useState<boolean>(true);

  return (
    <div className="select-none">
      <button
        type="button"
        onClick={onToggle}
        onContextMenu={(e) => onDirectiveContextMenu(e, directive)}
        title={directive.title}
        className="w-full flex items-center gap-1.5 pl-3 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]"
      >
        <Caret open={open} />
        <FolderIcon open={open} />
        <span className="truncate flex-1 text-left">{directive.title}</span>
        {/* Status dot — RIGHT side only. Glows when this directive has a
            pending user question, or pulses when the orchestrator is live. */}
        <StatusDot
          color={dotColor}
          live={orchestratorRunning}
          glow={hasPendingForDirective}
          status={directive.status}
        />
      </button>

      {open && (
        <ul className="py-0.5">
          {/* Pinned document entry — always at the top of the folder. */}
          <li>
            <button
              type="button"
              onClick={() =>
                onSelect({ directiveId: directive.id, taskId: null })
              }
              className={`w-full text-left flex items-center gap-1.5 pl-8 pr-3 py-1 font-mono text-[11px] transition-colors ${
                docSelected
                  ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]"
                  : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent"
              }`}
            >
              <PinIcon />
              <FileIcon />
              <span className="truncate flex-1">{fileName}</span>
            </button>
          </li>

          {/* tasks/ subfolder — collapsible, contains orchestrator/completion/steps. */}
          <li>
            <button
              type="button"
              onClick={() => setTasksOpen((p) => !p)}
              className="w-full flex items-center gap-1.5 pl-8 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]"
            >
              <Caret open={tasksOpen} />
              <FolderIcon open={tasksOpen} />
              <span className="truncate flex-1 text-left">tasks/</span>
              {tasks.length > 0 && (
                <span className="text-[10px] text-[#556677]">{tasks.length}</span>
              )}
            </button>

            {tasksOpen && (
              <ul className="py-0.5">
                {tasks.length === 0 ? (
                  <li className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677]">
                    No tasks yet
                  </li>
                ) : (
                  tasks.map((t) => {
                    const isSelected =
                      selection?.directiveId === directive.id &&
                      selection?.taskId === t.taskId;
                    const tdot = STEP_STATUS_DOT[t.status] ?? STEP_STATUS_DOT.pending;
                    const live =
                      t.status === "running" || t.kind === "orchestrator-active";
                    const glow = pendingTaskIds.has(t.taskId);
                    const Icon =
                      t.kind === "completion" ? CompletionIcon : TaskIcon;
                    return (
                      <li key={t.taskId}>
                        <button
                          type="button"
                          onClick={() =>
                            onSelect({
                              directiveId: directive.id,
                              taskId: t.taskId,
                            })
                          }
                          onContextMenu={(e) =>
                            onTaskContextMenu(e, t, directive.id)
                          }
                          title={t.label}
                          className={`w-full text-left flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] transition-colors ${
                            isSelected
                              ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]"
                              : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent"
                          }`}
                        >
                          <Icon />
                          <span className="truncate flex-1">{t.label}</span>
                          <StatusDot
                            color={tdot}
                            live={live}
                            glow={glow}
                            status={t.status}
                          />
                        </button>
                      </li>
                    );
                  })
                )}
              </ul>
            )}
          </li>
        </ul>
      )}
    </div>
  );
}

/**
 * Right-side status indicator. Composes the colored status dot with optional
 * "live" pulse (orchestrator running) and "glow" attention ring (pending user
 * question waiting on a response).
 */
function StatusDot({
  color,
  live,
  glow,
  status,
}: {
  color: string;
  live: boolean;
  glow: boolean;
  status: string;
}) {
  // The glow is a soft amber ring pulsed via box-shadow. Keep it subtle so it
  // doesn't fight the live pulse for attention when both are present.
  const ring = glow
    ? "shadow-[0_0_0_2px_rgba(251,191,36,0.45),0_0_8px_2px_rgba(251,191,36,0.55)] animate-pulse"
    : "";
  const livePulse = live && !glow ? "animate-pulse" : "";
  const title = glow
    ? `${status} — needs response`
    : live
    ? `${status} — running`
    : `status: ${status}`;
  return (
    <span
      className={`inline-block w-2 h-2 rounded-full shrink-0 ${color} ${ring} ${livePulse}`}
      aria-label={title}
      title={title}
    />
  );
}

interface FolderTaskRow {
  taskId: string;
  /** Directive step id for step kinds — needed for complete/fail/skip APIs. */
  stepId: string | null;
  label: string;
  status: string;
  kind: "orchestrator-active" | "completion" | "step";
}

function collectTasks(
  detailed: DirectiveWithSteps | null,
  summary: DirectiveSummary,
): FolderTaskRow[] {
  const rows: FolderTaskRow[] = [];

  // Orchestrator (planner) — surfaces only while it's actively running so
  // the folder is not flooded with stale orchestrator entries.
  const orchestratorId =
    detailed?.orchestratorTaskId ?? summary.orchestratorTaskId ?? null;
  if (orchestratorId) {
    rows.push({
      taskId: orchestratorId,
      stepId: null,
      label: "orchestrator",
      status: "running",
      kind: "orchestrator-active",
    });
  }

  // Completion (PR creation) task.
  const completionId =
    detailed?.completionTaskId ?? summary.completionTaskId ?? null;
  if (completionId) {
    rows.push({
      taskId: completionId,
      stepId: null,
      label: "completion",
      status: "running",
      kind: "completion",
    });
  }

  // Step tasks — only steps that have actually been started have a taskId.
  if (detailed) {
    for (const step of detailed.steps) {
      if (!step.taskId) continue;
      rows.push({
        taskId: step.taskId,
        stepId: step.id,
        label: step.name,
        status: step.status,
        kind: "step",
      });
    }
  }

  return rows;
}

interface SidebarProps {
  directives: DirectiveSummary[];
  loading: boolean;
  selection: SidebarSelection | null;
  onSelect: (sel: SidebarSelection) => void;
  onDirectiveContextMenu: (e: React.MouseEvent, d: DirectiveSummary) => void;
  onTaskContextMenu: (e: React.MouseEvent, t: FolderTaskRow, directiveId: string) => void;
}

function DocumentSidebar({
  directives,
  loading,
  selection,
  onSelect,
  onDirectiveContextMenu,
  onTaskContextMenu,
}: SidebarProps) {
  // Pending user questions — drives the "glow" attention ring. We split into
  // two indices so the directive folder header glows whenever ANY of its
  // tasks has a pending question, while individual task rows glow only for
  // their own question.
  const { pendingQuestions } = useSupervisorQuestions();
  const { directivesWithPending, tasksWithPending } = useMemo(() => {
    const dirs = new Set<string>();
    const tasks = new Set<string>();
    for (const q of pendingQuestions) {
      if (q.directiveId) dirs.add(q.directiveId);
      if (q.taskId) tasks.add(q.taskId);
    }
    return { directivesWithPending: dirs, tasksWithPending: tasks };
  }, [pendingQuestions]);

  // Sort active first, then idle, then paused, then archived.
  const sorted = useMemo(() => {
    const order: Record<DirectiveStatus, number> = {
      active: 0,
      paused: 1,
      idle: 2,
      draft: 3,
      archived: 4,
    };
    return [...directives].sort((a, b) => {
      const oa = order[a.status] ?? 99;
      const ob = order[b.status] ?? 99;
      if (oa !== ob) return oa - ob;
      return a.title.localeCompare(b.title, undefined, { sensitivity: "base" });
    });
  }, [directives]);

  // Track which directive folders are open. The currently selected directive
  // is forced open so deep links land on something visible.
  const [openIds, setOpenIds] = useState<Set<string>>(new Set());
  const lastSelectedRef = useRef<string | null>(null);
  useEffect(() => {
    if (selection && selection.directiveId !== lastSelectedRef.current) {
      lastSelectedRef.current = selection.directiveId;
      setOpenIds((prev) => {
        if (prev.has(selection.directiveId)) return prev;
        const next = new Set(prev);
        next.add(selection.directiveId);
        return next;
      });
    }
  }, [selection]);

  const toggleOpen = useCallback((id: string) => {
    setOpenIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }, []);

  return (
    <div className="flex flex-col h-full">
      {/* Sidebar header */}
      <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]">
        <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide">
          Documents
        </span>
        <span className="text-[10px] font-mono text-[#556677]">
          {directives.length}
        </span>
      </div>

      {/* Top-level "directives/" folder header (informational, non-interactive). */}
      <div className="flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-mono text-[#9bc3ff]">
        <FolderIcon open />
        <span>directives/</span>
      </div>

      {/* Body */}
      <div className="flex-1 overflow-y-auto pb-4">
        {loading && directives.length === 0 ? (
          <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
            Loading...
          </div>
        ) : directives.length === 0 ? (
          <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
            No directives yet
          </div>
        ) : (
          sorted.map((d) => (
            <DirectiveFolder
              key={d.id}
              directive={d}
              open={openIds.has(d.id)}
              onToggle={() => toggleOpen(d.id)}
              selection={selection}
              onSelect={onSelect}
              pendingTaskIds={tasksWithPending}
              hasPendingForDirective={directivesWithPending.has(d.id)}
              onDirectiveContextMenu={onDirectiveContextMenu}
              onTaskContextMenu={onTaskContextMenu}
            />
          ))
        )}
      </div>
    </div>
  );
}

// =============================================================================
// Editor shell — wraps DocumentEditor and handles the "no document selected"
// and loading states.
// =============================================================================

interface EditorShellProps {
  selectedId: string | undefined;
  selectedTaskId: string | null;
  hasDirectives: boolean;
  listLoading: boolean;
  onClearTask: () => void;
}

function EditorShell({
  selectedId,
  selectedTaskId,
  hasDirectives,
  listLoading,
  onClearTask,
}: EditorShellProps) {
  const {
    directive,
    loading,
    updateGoal,
    cleanup,
    createPR,
    pickUpOrders,
  } = useDirective(selectedId);

  if (!selectedId) {
    return (
      <div className="flex-1 flex items-center justify-center h-full">
        <p className="text-[#556677] font-mono text-[12px]">
          {listLoading
            ? "Loading documents..."
            : hasDirectives
            ? "Select a document from the sidebar"
            : "No documents yet — create one from the legacy UI"}
        </p>
      </div>
    );
  }

  if (loading && !directive) {
    return (
      <div className="flex-1 flex items-center justify-center h-full">
        <p className="text-[#556677] font-mono text-[12px]">Loading document...</p>
      </div>
    );
  }

  if (!directive) {
    return (
      <div className="flex-1 flex items-center justify-center h-full">
        <p className="text-[#7788aa] font-mono text-[12px]">Document not found</p>
      </div>
    );
  }

  // Resolve the label for the breadcrumb when a task is selected.
  const taskLabel = selectedTaskId
    ? selectedTaskId === directive.orchestratorTaskId
      ? "orchestrator"
      : selectedTaskId === directive.completionTaskId
      ? "completion"
      : directive.steps.find((s) => s.taskId === selectedTaskId)?.name ??
        selectedTaskId.slice(0, 8)
    : null;

  return (
    <div className="flex-1 flex flex-col h-full overflow-hidden">
      {/* Document header — breadcrumb-like, mirrors a code editor's tab bar */}
      <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
        <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
          <FileIcon />
          <span>directives /</span>
          <span className="text-[#9bc3ff]">{directive.id.slice(0, 8)}</span>
          {selectedTaskId && (
            <>
              <span>/</span>
              <span className="text-[#9bc3ff]">{taskLabel}</span>
              <button
                type="button"
                onClick={onClearTask}
                className="ml-2 px-1.5 py-0.5 text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded normal-case"
              >
                back to document
              </button>
            </>
          )}
          {!selectedTaskId && !!directive.orchestratorTaskId && (
            <span className="ml-2 inline-flex items-center gap-1 text-yellow-400">
              <span className="inline-block w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse" />
              orchestrator running
            </span>
          )}
        </div>
      </div>

      {selectedTaskId ? (
        <DocumentTaskStream
          taskId={selectedTaskId}
          label={taskLabel ?? selectedTaskId.slice(0, 8)}
        />
      ) : (
        <DocumentEditor
          directive={directive}
          onUpdateGoal={async (goal) => {
            await updateGoal(goal);
          }}
          onCleanup={async () => {
            await cleanup();
          }}
          onCreatePR={async () => {
            await createPR();
          }}
          onPickUpOrders={async () => {
            await pickUpOrders();
          }}
        />
      )}
    </div>
  );
}

// =============================================================================
// Page
// =============================================================================

type ContextMenuState =
  | { kind: "directive"; x: number; y: number; directive: DirectiveSummary }
  | {
      kind: "task";
      x: number;
      y: number;
      task: FolderTaskRow;
      directiveId: string;
    }
  | null;

export default function DocumentDirectivesPage() {
  const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
  const navigate = useNavigate();
  const { id: selectedId } = useParams<{ id: string }>();
  const [searchParams, setSearchParams] = useSearchParams();
  const selectedTaskId = searchParams.get("task");
  const { directives, loading: listLoading, refresh: refreshList } = useDirectives();
  const [contextMenu, setContextMenu] = useState<ContextMenuState>(null);

  useEffect(() => {
    if (!authLoading && isAuthConfigured && !isAuthenticated) {
      navigate("/login");
    }
  }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);

  const onSelect = useCallback(
    (sel: SidebarSelection) => {
      const next = `/directives/${sel.directiveId}${
        sel.taskId ? `?task=${sel.taskId}` : ""
      }`;
      navigate(next);
    },
    [navigate],
  );

  const onClearTask = useCallback(() => {
    const next = new URLSearchParams(searchParams);
    next.delete("task");
    setSearchParams(next, { replace: true });
  }, [searchParams, setSearchParams]);

  const onDirectiveContextMenu = useCallback(
    (e: React.MouseEvent, d: DirectiveSummary) => {
      e.preventDefault();
      e.stopPropagation();
      setContextMenu({ kind: "directive", x: e.clientX, y: e.clientY, directive: d });
    },
    [],
  );

  const onTaskContextMenu = useCallback(
    (e: React.MouseEvent, task: FolderTaskRow, directiveId: string) => {
      e.preventDefault();
      e.stopPropagation();
      setContextMenu({ kind: "task", x: e.clientX, y: e.clientY, task, directiveId });
    },
    [],
  );

  const closeContextMenu = useCallback(() => setContextMenu(null), []);

  if (authLoading) {
    return (
      <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
        <Masthead showNav />
        <main className="flex-1 flex items-center justify-center">
          <p className="text-[#7788aa] font-mono text-sm">Loading...</p>
        </main>
      </div>
    );
  }

  const selection: SidebarSelection | null = selectedId
    ? { directiveId: selectedId, taskId: selectedTaskId }
    : null;

  return (
    <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
      <Masthead showNav />
      <main
        className="flex-1 flex overflow-hidden"
        style={{ height: "calc(100vh - 80px)" }}
      >
        {/* Left: file-tree sidebar */}
        <div className="w-[260px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col bg-[#091428]">
          <DocumentSidebar
            directives={directives}
            loading={listLoading}
            selection={selection}
            onSelect={onSelect}
            onDirectiveContextMenu={onDirectiveContextMenu}
            onTaskContextMenu={onTaskContextMenu}
          />
        </div>

        {/* Right: Lexical editor / task stream */}
        <EditorShell
          selectedId={selectedId}
          selectedTaskId={selectedTaskId}
          hasDirectives={directives.length > 0}
          listLoading={listLoading}
          onClearTask={onClearTask}
        />
      </main>

      {/* Context menus — rendered at page level so they overlay everything. */}
      {contextMenu?.kind === "directive" && (
        <DirectiveContextMenu
          x={contextMenu.x}
          y={contextMenu.y}
          directive={contextMenu.directive}
          onClose={closeContextMenu}
          onStart={async () => {
            await startDirective(contextMenu.directive.id);
            await refreshList();
          }}
          onPause={async () => {
            await pauseDirective(contextMenu.directive.id);
            await refreshList();
          }}
          onArchive={async () => {
            await updateDirective(contextMenu.directive.id, {
              status: "archived",
            });
            await refreshList();
          }}
          onDelete={async () => {
            if (
              !window.confirm(
                `Delete "${contextMenu.directive.title}"? This cannot be undone.`,
              )
            ) {
              return;
            }
            await deleteDirective(contextMenu.directive.id);
            await refreshList();
            // If the deleted one was selected, clear selection.
            if (selectedId === contextMenu.directive.id) {
              navigate("/directives");
            }
          }}
          onGoToPR={() => {
            if (contextMenu.directive.prUrl) {
              window.open(contextMenu.directive.prUrl, "_blank", "noreferrer");
            }
          }}
        />
      )}
      {contextMenu?.kind === "task" && (
        <TaskContextMenu
          x={contextMenu.x}
          y={contextMenu.y}
          task={contextMenu.task}
          onClose={closeContextMenu}
          onInterrupt={async () => {
            try {
              await stopTask(contextMenu.task.taskId);
            } catch (err) {
              // eslint-disable-next-line no-console
              console.error("[makima] failed to interrupt task", err);
            }
            await refreshList();
          }}
          onComplete={async () => {
            if (!contextMenu.task.stepId) return;
            await completeDirectiveStep(
              contextMenu.directiveId,
              contextMenu.task.stepId,
            );
            await refreshList();
          }}
          onFail={async () => {
            if (!contextMenu.task.stepId) return;
            await failDirectiveStep(
              contextMenu.directiveId,
              contextMenu.task.stepId,
            );
            await refreshList();
          }}
          onSkip={async () => {
            if (!contextMenu.task.stepId) return;
            await skipDirectiveStep(
              contextMenu.directiveId,
              contextMenu.task.stepId,
            );
            await refreshList();
          }}
        />
      )}
    </div>
  );
}