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 { DocumentEditor } from "../components/directives/DocumentEditor";
import {
  type DirectiveSummary,
  type DirectiveStatus,
  type DirectiveContract as Contract,
  type DirectiveContractStatus as ContractStatus,
  type DirectiveStep,
  type Task,
  type DirectiveContractTasksResponse as ContractTasksResponse,
  type DirectiveContractMergeMode as ContractMergeMode,
  listDirectiveContracts as listContracts,
  createDirectiveContract as createContract,
  getDirectiveContract as getContract,
  updateDirectiveContract as updateContract,
  listDirectiveContractTasks as listContractTasks,
  startDirectiveContract,
  pauseDirectiveContract,
  unlockDirectiveContract,
  reopenDirectiveContract,
  reorderDirectiveContract,
  createDirectiveTask,
  startDirective,
  pauseDirective,
  updateDirective,
  deleteDirective,
  createDirectivePR,
  advanceDirective,
  cleanupDirective,
  pickUpOrders,
  stopTask,
  skipDirectiveStep,
} from "../lib/api";
import { SidebarContextMenu, type ContextMenuItem } from "../components/SidebarContextMenu";
import { TaskPage } from "../components/directives/TaskPage";

// 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",
  inactive: "bg-[#75aafc]",
  archived: "bg-[#3a4a6a]",
};

// Per-contract status palette. Active = bright green (currently driving
// daemons); queued = amber (locked, waiting for the active slot); draft
// = grey (editable spec); shipped = muted blue (work done); archived =
// faint navy.
const DOC_STATUS_DOT: Record<ContractStatus, string> = {
  draft: "bg-[#556677]",
  queued: "bg-amber-400",
  active: "bg-green-400",
  shipped: "bg-[#75aafc]",
  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.
// =============================================================================

// Sidebar is now a flat list ordered by status precedence — see
// `sortedDirectives` in DocumentSidebar. Status is shown as a colored dot
// on the right of each row, no per-status grouping.

// Slugify a document title for the displayed `.md` filename, falling back to
// the directive title and finally the document id slice when the title is
// empty. Mirrors the file-naming fix from step 1 (use the user-readable label
// rather than just an id slice). Accepts either a DirectiveSummary or a full
// DirectiveWithSteps — only `title` is read.
function fileLabel(
  doc: Contract,
  directive: { title: string },
): string {
  const docTitle = doc.title.trim();
  if (docTitle.length > 0) return docTitle;
  const dirTitle = directive.title.trim();
  if (dirTitle.length > 0) return dirTitle;
  return doc.id.slice(0, 8);
}

// =============================================================================
// 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>
  );
}

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>
  );
}

// =============================================================================
// SidebarSelection — exactly one of taskId/documentId is non-null. taskId is
// reserved for a future "task selection" feature (we expose it in the URL
// already so the param shape is stable). documentId picks one of the
// directive's documents.
// =============================================================================

interface SidebarSelection {
  directiveId: string;
  taskId: string | null;
  documentId: string | null;
}

// =============================================================================
// Per-directive folder — renders as a collapsible folder containing the
// directive's documents. Loads documents lazily on first open (mirroring the
// pattern from step 1's DirectiveFolder, which fetched the full directive
// only when expanded).
// =============================================================================

interface DirectiveFolderProps {
  directive: DirectiveSummary;
  open: boolean;
  onToggle: () => void;
  /** Called when the user clicks the folder header itself (after toggle). */
  onHeaderClick: () => void;
  selection: SidebarSelection | null;
  onSelectDocument: (directiveId: string, doc: Contract) => void;
  /** Right-click handler — opens the generic context menu with items
   *  built for the given entity type. */
  onContextMenuDirective: (e: React.MouseEvent, directive: DirectiveSummary) => void;
  onContextMenuContract: (e: React.MouseEvent, contract: Contract) => void;
  onContextMenuStep: (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => void;
  onContextMenuTask: (e: React.MouseEvent, task: Task, directiveId: string) => void;
  /** Click handler for task/step rows — navigates to the live transcript. */
  onSelectTask: (directiveId: string, taskId: string) => void;
  /**
   * Document refresh trigger — bumped externally so the folder refetches its
   * document list after a create/update happens elsewhere. Primarily used so
   * a freshly-created document shows up immediately.
   */
  refreshNonce: number;
}

function DirectiveFolder({
  directive,
  open,
  onToggle,
  onHeaderClick,
  selection,
  onSelectDocument,
  onContextMenuDirective,
  onContextMenuContract,
  onContextMenuStep,
  onContextMenuTask,
  onSelectTask,
  refreshNonce,
}: DirectiveFolderProps) {
  const selectedTaskIdForFolder =
    selection && selection.directiveId === directive.id
      ? selection.taskId
      : null;
  const dotColor = STATUS_DOT[directive.status] ?? STATUS_DOT.draft;
  const orchestratorRunning = !!directive.orchestratorTaskId;

  // Documents fetched lazily on open. We deliberately scope the fetch to the
  // open-state so closed folders don't pay the network cost on initial render.
  const [docs, setDocs] = useState<Contract[] | null>(null);
  const [docsLoading, setDocsLoading] = useState(false);
  const [docsError, setDocsError] = useState<string | null>(null);

  // shipped/ subfolder open state — independent of the directive folder.
  const [shippedOpen, setShippedOpen] = useState(false);

  const refresh = useCallback(async () => {
    setDocsLoading(true);
    setDocsError(null);
    try {
      const list = await listContracts(directive.id);
      setDocs(list);
    } catch (e) {
      setDocsError(e instanceof Error ? e.message : "Failed to load documents");
    } finally {
      setDocsLoading(false);
    }
  }, [directive.id]);

  // Fetch on open; refetch when refreshNonce bumps and the folder is open.
  useEffect(() => {
    if (!open) return;
    void refresh();
  }, [open, refresh, refreshNonce]);

  // Split the documents into the two visual groups. Memoised so we don't
  // recompute on every render.
  const { activeDocs, shippedDocs } = useMemo(() => {
    const active: Contract[] = [];
    const shipped: Contract[] = [];
    for (const d of docs ?? []) {
      if (d.status === "shipped" || d.status === "archived") {
        shipped.push(d);
      } else {
        active.push(d);
      }
    }
    // Stable order: by createdAt ascending so the first row is the oldest doc.
    active.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    shipped.sort((a, b) => (b.shippedAt ?? "").localeCompare(a.shippedAt ?? ""));
    return { activeDocs: active, shippedDocs: shipped };
  }, [docs]);

  // When a directive owns more than one contract, the two `tasks/` folders
  // would otherwise be ambiguous. We pass this down to DocumentTasksFolder
  // so it can rename itself to `tasks - <contract name>/` for clarity.
  const multipleContracts = activeDocs.length + shippedDocs.length > 1;

  // Selection helpers — used to highlight the currently-selected doc row.
  const selectedDocumentId =
    selection && selection.directiveId === directive.id
      ? selection.documentId
      : null;

  // Drag-to-reorder state — only active contracts (draft/queued/active) are
  // reorderable. The drag id lives in the folder so the drop target can read
  // it from props. `dragOverId` powers the visual indicator on hover.
  const [dragId, setDragId] = useState<string | null>(null);
  const [dragOverId, setDragOverId] = useState<string | null>(null);

  const handleReorder = useCallback(
    async (draggedId: string, targetDoc: Contract) => {
      setDragId(null);
      setDragOverId(null);
      // No-op if dropping on self.
      if (draggedId === targetDoc.id) return;
      try {
        await reorderDirectiveContract(draggedId, targetDoc.position);
        await refresh();
      } catch (e) {
        // Fail open — sidebar will refresh on next refreshNonce bump.
        // eslint-disable-next-line no-console
        console.error("Reorder failed", e);
      }
    },
    [refresh],
  );

  return (
    <div className="select-none">
      {/* Directive folder header. Status is shown as a colored dot on the
          RIGHT (per the user's spec — flat list, no per-status grouping).
          Right-click opens the context menu (start / pause / archive /
          delete / create-PR / update-PR / etc.).

          Row is a div with onClick (not a <button>) so we can nest a
          real `+` button inside that surfaces on hover for quick
          contract / ephemeral-task creation without leaving the file
          tree. */}
      <div
        role="button"
        tabIndex={0}
        onClick={() => {
          onToggle();
          onHeaderClick();
        }}
        onKeyDown={(e) => {
          if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            onToggle();
            onHeaderClick();
          }
        }}
        onContextMenu={(e) => onContextMenuDirective(e, directive)}
        title={`${directive.title} — ${directive.status}`}
        className="group 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.06)] cursor-pointer"
      >
        <Caret open={open} />
        <FolderIcon open={open} />
        <span className="truncate flex-1 text-left">
          {directive.title.trim().length > 0
            ? directive.title
            : directive.id.slice(0, 8)}
          /
        </span>
        {/* Hover-only quick-add affordance — replaces the old inline
            "+ New document" / "+ New ephemeral task" buttons that used
            to sit inside the folder body. Builds the same items array
            the directive context menu would, so right-click and `+`
            stay in sync. */}
        <button
          type="button"
          onClick={(e) => {
            e.stopPropagation();
            onContextMenuDirective(e, directive);
          }}
          className="opacity-0 group-hover:opacity-100 text-[12px] leading-none text-emerald-300 hover:text-white px-1"
          title="New contract / ephemeral task"
          aria-label="New contract or ephemeral task"
        >
          +
        </button>
        {orchestratorRunning && (
          <span
            className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-yellow-400 animate-pulse"
            title="Orchestrator running"
            aria-label="Orchestrator running"
          />
        )}
        {/* Status indicator on the RIGHT side of the row. */}
        <span
          className={`inline-block w-2 h-2 rounded-full shrink-0 ${dotColor}`}
          aria-label={`status: ${directive.status}`}
          title={`status: ${directive.status}`}
        />
      </div>

      {/* Folder body — rendered only when open */}
      {open && (
        <div className="py-0.5">
          {docsLoading && !docs && (
            <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677]">
              Loading documents…
            </div>
          )}
          {docsError && (
            <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-red-400">
              {docsError}
            </div>
          )}

          {/* Active group */}
          {docs && (
            <>
              {/* "New contract" / "New ephemeral task" used to be rendered
                  here as inline buttons. They're now reachable via the
                  directive folder's right-click context menu and the `+`
                  hover-button on the directive header (see DirectiveFolder
                  row above). Keeping the file tree free of action rows
                  makes the hierarchy easier to scan. */}

              {activeDocs.length === 0 && !docsLoading && (
                <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677] italic">
                  no contracts yet — right-click to add
                </div>
              )}

              {activeDocs.map((doc) => (
                // Each active document gets its own tasks/ subfolder
                // immediately below it. Active docs default-open the
                // folder so the user sees their live work without an
                // extra click.
                <div key={doc.id}>
                  <DocumentRow
                    doc={doc}
                    directive={directive}
                    selected={doc.id === selectedDocumentId}
                    onSelect={() => onSelectDocument(directive.id, doc)}
                    onContextMenu={onContextMenuContract}
                    draggable
                    onDragStart={() => setDragId(doc.id)}
                    onDragEnd={() => {
                      setDragId(null);
                      setDragOverId(null);
                    }}
                    onDragOver={() => {
                      if (dragId && dragId !== doc.id) setDragOverId(doc.id);
                    }}
                    onDragLeave={() => {
                      if (dragOverId === doc.id) setDragOverId(null);
                    }}
                    onDrop={() => {
                      if (dragId) void handleReorder(dragId, doc);
                    }}
                    dragOver={dragOverId === doc.id}
                  />
                  <DocumentTasksFolder
                    documentId={doc.id}
                    directiveId={directive.id}
                    depth="normal"
                    defaultOpen={doc.status === "active"}
                    refreshNonce={refreshNonce}
                    selectedTaskId={selectedTaskIdForFolder}
                    onSelectTask={onSelectTask}
                    onContextMenuStep={onContextMenuStep}
                    onContextMenuTask={onContextMenuTask}
                    contractLabel={fileLabel(doc, directive)}
                    multipleContracts={multipleContracts}
                  />
                </div>
              ))}

              {/* shipped/ subfolder — only rendered when at least one shipped
                  or archived doc exists. Hidden entirely otherwise so empty
                  directives stay tidy. */}
              {shippedDocs.length > 0 && (
                <div>
                  <button
                    type="button"
                    onClick={() => setShippedOpen((v) => !v)}
                    className="w-full flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)]"
                  >
                    <Caret open={shippedOpen} />
                    <FolderIcon open={shippedOpen} />
                    <span>shipped/</span>
                    <span className="ml-auto text-[10px] text-[#556677]">
                      {shippedDocs.length}
                    </span>
                  </button>
                  {shippedOpen &&
                    shippedDocs.map((doc) => (
                      // Shipped docs render the doc row + its frozen
                      // tasks/ subfolder. The tasks/ folder defaults
                      // closed (history) so it doesn't dominate the
                      // sidebar; users can click to inspect what work
                      // produced this shipped contract.
                      <div key={doc.id}>
                        <DocumentRow
                          doc={doc}
                          directive={directive}
                          selected={doc.id === selectedDocumentId}
                          onSelect={() => onSelectDocument(directive.id, doc)}
                          onContextMenu={onContextMenuContract}
                          indent="deep"
                        />
                        <DocumentTasksFolder
                          documentId={doc.id}
                          directiveId={directive.id}
                          depth="deep"
                          defaultOpen={false}
                          refreshNonce={refreshNonce}
                          selectedTaskId={selectedTaskIdForFolder}
                          onSelectTask={onSelectTask}
                          onContextMenuStep={onContextMenuStep}
                          onContextMenuTask={onContextMenuTask}
                          contractLabel={fileLabel(doc, directive)}
                          multipleContracts={multipleContracts}
                        />
                      </div>
                    ))}
                </div>
              )}
            </>
          )}
        </div>
      )}
    </div>
  );
}

// =============================================================================
// DocumentRow — one row inside a directive folder. The indent depth differs
// between active rows (one level deep) and shipped rows (two levels deep).
// =============================================================================

interface DocumentRowProps {
  doc: Contract;
  directive: DirectiveSummary;
  selected: boolean;
  onSelect: () => void;
  onContextMenu: (e: React.MouseEvent, contract: Contract) => void;
  indent?: "normal" | "deep";
  // ----- Drag-to-reorder props (optional — only wired by the active list) ---
  /** Whether this row participates in HTML5 drag (active docs only). */
  draggable?: boolean;
  onDragStart?: () => void;
  onDragEnd?: () => void;
  onDragOver?: () => void;
  onDragLeave?: () => void;
  onDrop?: () => void;
  /** True while a drag is hovering over this row — drives the drop indicator. */
  dragOver?: boolean;
}

function DocumentRow({
  doc,
  directive,
  selected,
  onSelect,
  onContextMenu,
  indent = "normal",
  draggable = false,
  onDragStart,
  onDragEnd,
  onDragOver,
  onDragLeave,
  onDrop,
  dragOver = false,
}: DocumentRowProps) {
  const dot = DOC_STATUS_DOT[doc.status] ?? DOC_STATUS_DOT.draft;
  const padLeft = indent === "deep" ? "pl-[88px]" : "pl-14";
  const name = `${fileLabel(doc, directive)}.md`;

  // Drop-indicator: a top border accent on the hovered target row.
  const dropAccent = dragOver
    ? "border-t-2 border-t-emerald-400"
    : "border-t-2 border-t-transparent";

  return (
    <button
      type="button"
      onClick={onSelect}
      onContextMenu={(e) => onContextMenu(e, doc)}
      title={name}
      draggable={draggable}
      onDragStart={(e) => {
        if (!draggable) return;
        // Required by Firefox to actually start the drag.
        e.dataTransfer.effectAllowed = "move";
        e.dataTransfer.setData("text/plain", doc.id);
        onDragStart?.();
      }}
      onDragEnd={onDragEnd}
      onDragOver={(e) => {
        if (!draggable) return;
        e.preventDefault();
        e.dataTransfer.dropEffect = "move";
        onDragOver?.();
      }}
      onDragLeave={onDragLeave}
      onDrop={(e) => {
        if (!draggable) return;
        e.preventDefault();
        onDrop?.();
      }}
      className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${dropAccent} ${
        selected
          ? "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"
      }`}
    >
      <FileIcon />
      <span
        className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
        aria-hidden
        title={doc.status}
      />
      <span className="truncate flex-1">{name}</span>
      {/* Status chip — only shown for non-active states so the row stays
          uncluttered for the common case. */}
      {doc.status !== "active" && (
        <span className="text-[9px] uppercase tracking-wide text-[#556677]">
          {doc.status}
        </span>
      )}
      {/* PR badge for shipped docs. The link short-circuits the row's
          onClick so clicking the PR doesn't also re-select the doc. */}
      {doc.prUrl && (doc.status === "shipped" || doc.status === "archived") && (
        <a
          href={doc.prUrl}
          target="_blank"
          rel="noreferrer noopener"
          onClick={(e) => e.stopPropagation()}
          className="text-[9px] text-[#75aafc] hover:text-white border border-[#2a3a5a] rounded px-1"
          title={doc.prUrl}
        >
          PR
        </a>
      )}
    </button>
  );
}

// =============================================================================
// Per-document tasks/ subfolder — fetches the steps + ephemeral tasks for a
// single document and renders a collapsible `tasks/` row beneath the
// document. Lazy: fetch only fires once the user opens the folder, and we
// also keep the folder closed by default for shipped docs (where it's
// historical) and open by default for active docs (where it's live work).
// =============================================================================

interface DocumentTasksFolderProps {
  documentId: string;
  /** Parent directive id — needed so a clicked task row can navigate to
   *  /directives/<directiveId>?task=<taskId>. */
  directiveId: string;
  /** Visual indent depth — mirrors the parent DocumentRow's indent so the
   *  tasks/ row sits one level deeper than its parent doc. */
  depth: "normal" | "deep";
  /** Whether to fetch+open by default. Active docs default to open so the
   *  user sees their live tasks immediately; shipped docs default to closed
   *  (historical), and the user can click to expand. */
  defaultOpen: boolean;
  /** Bumped externally so the folder refetches its task list after a save
   *  or status change elsewhere. Same nonce used for the directive folder. */
  refreshNonce: number;
  /** Currently-selected task id (drives row highlight). */
  selectedTaskId: string | null;
  /** Click handler for step/task rows — navigates to the live transcript. */
  onSelectTask: (directiveId: string, taskId: string) => void;
  onContextMenuStep: (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => void;
  onContextMenuTask: (e: React.MouseEvent, task: Task, directiveId: string) => void;
  /** Human-readable contract label (already resolved via fileLabel). Used to
   *  disambiguate multiple tasks/ folders under the same directive. */
  contractLabel: string;
  /** True when the parent directive owns more than one contract — drives the
   *  `tasks - <contractLabel>/` rename so the two sibling tasks/ folders are
   *  distinguishable. Single-contract directives keep the plain `tasks/`. */
  multipleContracts: boolean;
}

function DocumentTasksFolder({
  documentId,
  directiveId,
  depth,
  defaultOpen,
  refreshNonce,
  selectedTaskId,
  onSelectTask,
  onContextMenuStep,
  onContextMenuTask,
  contractLabel,
  multipleContracts,
}: DocumentTasksFolderProps) {
  const [open, setOpen] = useState(defaultOpen);
  const [data, setData] = useState<ContractTasksResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Inner row indent is one level deeper than the folder header. Folder
  // header uses pl-[88px] (deep) or pl-14 (normal); tasks rows go one
  // step beyond that.
  const headerPadLeft = depth === "deep" ? "pl-[88px]" : "pl-14";
  const rowPadLeft = depth === "deep" ? "pl-[112px]" : "pl-[72px]";

  const refresh = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await listContractTasks(documentId);
      setData(res);
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to load tasks");
    } finally {
      setLoading(false);
    }
  }, [documentId]);

  // Fetch when the folder is open (initial open or refresh). We don't
  // pre-fetch on closed folders so we don't waste bandwidth on the long
  // tail of historical shipped docs the user never expands.
  useEffect(() => {
    if (!open) return;
    void refresh();
  }, [open, refresh, refreshNonce]);

  // De-duplicate: a step that has spawned a task appears in `steps[]`
  // with step.taskId === task.id, and the same task ALSO appears in
  // `tasks[]`. Rendering both produces a double-row + double-highlight
  // when the user clicks. Keep the step row (it carries the step name,
  // which is the meaningful label) and drop the matching task row.
  const stepTaskIds = useMemo(
    () => new Set((data?.steps ?? []).map((s) => s.taskId).filter((id): id is string => !!id)),
    [data],
  );
  const ephemeralTasks = useMemo(
    () => (data?.tasks ?? []).filter((t) => !stepTaskIds.has(t.id)),
    [data, stepTaskIds],
  );
  const total = (data?.steps.length ?? 0) + ephemeralTasks.length;

  // Folder always renders (even when empty) so the user can click into a
  // fresh contract's tasks/ folder and see it stay visible. The empty state
  // shows a muted "no tasks yet" placeholder inside the open body — same
  // visual weight as the existing "Loading tasks…" / error placeholders.
  //
  // When the parent directive owns multiple contracts, both tasks/ folders
  // are disambiguated as `tasks - <contractLabel>/` so the user can tell
  // them apart. Single-contract directives keep the plain `tasks/` label.
  const headerLabel = multipleContracts
    ? `tasks - ${contractLabel}/`
    : "tasks/";

  return (
    <div>
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        title={headerLabel}
        className={`w-full flex items-center gap-1.5 ${headerPadLeft} pr-3 py-1 font-mono text-[11px] text-[#7788aa] hover:bg-[rgba(117,170,252,0.06)]`}
      >
        <Caret open={open} />
        <FolderIcon open={open} />
        <span className="truncate text-left">{headerLabel}</span>
        {total > 0 && (
          <span className="ml-auto text-[10px] text-[#556677]">{total}</span>
        )}
      </button>
      {open && (
        <div className="py-0.5">
          {loading && !data && (
            <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-[#556677]`}>
              Loading tasks…
            </div>
          )}
          {error && (
            <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-red-400`}>
              {error}
            </div>
          )}
          {data && total === 0 && !loading && !error && (
            <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-[#556677] italic`}>
              no tasks yet
            </div>
          )}
          {data?.steps.map((step) => (
            <StepRow
              key={`step-${step.id}`}
              step={step}
              directiveId={directiveId}
              selected={!!selectedTaskId && step.taskId === selectedTaskId}
              padLeft={rowPadLeft}
              onSelect={onSelectTask}
              onContextMenu={onContextMenuStep}
            />
          ))}
          {ephemeralTasks.map((task) => (
            <TaskRow
              key={`task-${task.id}`}
              task={task}
              directiveId={directiveId}
              selected={task.id === selectedTaskId}
              padLeft={rowPadLeft}
              onSelect={onSelectTask}
              onContextMenu={onContextMenuTask}
            />
          ))}
        </div>
      )}
    </div>
  );
}

// Step status → coloured dot, mirroring directive status palette so the
// sidebar reads consistently.
const STEP_STATUS_DOT: Record<string, string> = {
  pending: "bg-[#556677]",
  ready: "bg-[#9bc3ff]",
  running: "bg-yellow-400",
  completed: "bg-green-400",
  failed: "bg-red-400",
  skipped: "bg-[#3a4a6a]",
};

// Task status → coloured dot. Statuses come from the Task model; the small
// set we expect in directive context is enough — anything else falls back
// to the muted "draft" colour.
const TASK_STATUS_DOT: Record<string, string> = {
  pending: "bg-[#556677]",
  starting: "bg-yellow-400",
  running: "bg-yellow-400",
  completed: "bg-green-400",
  failed: "bg-red-400",
  cancelled: "bg-[#3a4a6a]",
  interrupted: "bg-orange-400",
};

interface StepRowProps {
  step: DirectiveStep;
  directiveId: string;
  selected: boolean;
  padLeft: string;
  onSelect: (directiveId: string, taskId: string) => void;
  onContextMenu: (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => void;
}

function StepRow({
  step,
  directiveId,
  selected,
  padLeft,
  onSelect,
  onContextMenu,
}: StepRowProps) {
  const dot = STEP_STATUS_DOT[step.status] ?? "bg-[#556677]";
  // Steps without an underlying task can't be opened — the executor
  // hasn't started yet so there's no transcript to show. Render them
  // disabled so the user can see them in the list but knows they're
  // inert. Same for steps stuck in pending/skipped.
  const taskId = step.taskId;
  const clickable = !!taskId;
  return (
    <button
      type="button"
      disabled={!clickable}
      onClick={() => clickable && onSelect(directiveId, taskId!)}
      onContextMenu={(e) => onContextMenu(e, step, directiveId)}
      title={
        clickable
          ? `${step.name} (${step.status})`
          : `${step.name} — no task spawned yet (${step.status})`
      }
      className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${
        selected
          ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]"
          : clickable
          ? "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent"
          : "text-[#556677] border-l-2 border-transparent cursor-not-allowed"
      }`}
    >
      <FileIcon />
      <span
        className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
        aria-hidden
        title={step.status}
      />
      <span className="truncate flex-1">{step.name}</span>
      <span className="text-[9px] uppercase tracking-wide text-[#556677]">
        step
      </span>
    </button>
  );
}

interface TaskRowProps {
  task: Task;
  directiveId: string;
  selected: boolean;
  padLeft: string;
  onSelect: (directiveId: string, taskId: string) => void;
  onContextMenu: (e: React.MouseEvent, task: Task, directiveId: string) => void;
}

function TaskRow({
  task,
  directiveId,
  selected,
  padLeft,
  onSelect,
  onContextMenu,
}: TaskRowProps) {
  const dot = TASK_STATUS_DOT[task.status] ?? "bg-[#556677]";
  // Supervisor tasks get a small "sup" tag so the user can spot
  // contract orchestrators in the list.
  const isSup = task.isSupervisor;
  return (
    <button
      type="button"
      onClick={() => onSelect(directiveId, task.id)}
      onContextMenu={(e) => onContextMenu(e, task, directiveId)}
      title={`${task.name} (${task.status})`}
      className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${
        selected
          ? "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"
      }`}
    >
      <FileIcon />
      <span
        className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
        aria-hidden
        title={task.status}
      />
      <span className="truncate flex-1">{task.name}</span>
      <span className="text-[9px] uppercase tracking-wide text-[#556677]">
        {isSup ? "sup" : "task"}
      </span>
    </button>
  );
}

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

interface SidebarProps {
  directives: DirectiveSummary[];
  loading: boolean;
  selection: SidebarSelection | null;
  onSelectDocument: (directiveId: string, doc: Contract) => void;
  onSelectDirective: (directiveId: string) => void;
  /** Top-level "Contracts" header `+ New` button — opens the
   *  NewContractModal to create a brand-new directive (along with its
   *  first contract). */
  onCreateContract: () => void;
  onContextMenuDirective: (e: React.MouseEvent, directive: DirectiveSummary) => void;
  onContextMenuContract: (e: React.MouseEvent, contract: Contract) => void;
  onContextMenuStep: (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => void;
  onContextMenuTask: (e: React.MouseEvent, task: Task, directiveId: string) => void;
  onSelectTask: (directiveId: string, taskId: string) => void;
  refreshNonce: number;
}

function DocumentSidebar({
  directives,
  loading,
  selection,
  onSelectDocument,
  onSelectDirective,
  onCreateContract,
  onContextMenuDirective,
  onContextMenuContract,
  onContextMenuStep,
  onContextMenuTask,
  onSelectTask,
  refreshNonce,
}: SidebarProps) {
  // Flat sort: active first, then idle, paused, draft, inactive, archived.
  // Status is surfaced as a colored dot on the RIGHT of each contract row
  // (see DirectiveFolder header) — the user explicitly asked NOT to nest
  // contracts inside per-status folders.
  const sortedDirectives: DirectiveSummary[] = useMemo(() => {
    const order: Record<DirectiveStatus, number> = {
      active: 0,
      paused: 1,
      idle: 2,
      draft: 3,
      inactive: 4,
      archived: 5,
    };
    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]);

  // Per-directive open state. We auto-open the directive containing the
  // current selection so the user can see what they're editing.
  const [openDirectives, setOpenDirectives] = useState<Record<string, boolean>>({});

  useEffect(() => {
    if (!selection) return;
    setOpenDirectives((prev) =>
      prev[selection.directiveId] ? prev : { ...prev, [selection.directiveId]: true },
    );
  }, [selection?.directiveId]);

  const toggleDirective = (id: string) =>
    setOpenDirectives((prev) => ({ ...prev, [id]: !prev[id] }));

  return (
    <div className="flex flex-col h-full">
      {/* Sidebar header */}
      <div className="flex items-center justify-between gap-2 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">
          Contracts
        </span>
        <div className="flex items-center gap-2">
          <span className="text-[10px] font-mono text-[#556677]">
            {directives.length}
          </span>
          <button
            type="button"
            onClick={onCreateContract}
            className="text-[11px] font-mono text-emerald-300 hover:text-white border border-emerald-700/60 hover:border-emerald-400 rounded px-1.5 py-0.5 leading-none"
            title="Create a new contract (directive)"
          >
            + New
          </button>
        </div>
      </div>

      {/* Top-level "directives/" folder */}
      <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 — flat list, status is a colored dot on the right of each row. */}
      <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 contracts yet
          </div>
        ) : (
          <div className="py-0.5">
            {sortedDirectives.map((d) => (
              <DirectiveFolder
                key={d.id}
                directive={d}
                open={!!openDirectives[d.id]}
                onToggle={() => toggleDirective(d.id)}
                onHeaderClick={() => onSelectDirective(d.id)}
                selection={selection}
                onSelectDocument={onSelectDocument}
                onContextMenuDirective={onContextMenuDirective}
                onContextMenuContract={onContextMenuContract}
                onContextMenuStep={onContextMenuStep}
                onContextMenuTask={onContextMenuTask}
                onSelectTask={onSelectTask}
                refreshNonce={refreshNonce}
              />
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// =============================================================================
// Contract header — breadcrumb + status badge + lifecycle action buttons +
// merge mode radio. Renders above the spec editor in the document path.
//
// Action visibility is status-driven:
//   * draft     → Lock & Start
//   * queued    → Unlock (back to draft); shows "queued" pill
//   * active    → Pause, Complete, Unlock; shows "active" + pulsing dot
//   * shipped   → reopen via spec edit (no buttons here; backend reactivates)
//   * archived  → no buttons
//
// Merge mode (shared / own_pr) is editable while the contract is in
// `draft` or `queued` — once active, the queue scheduler has already
// claimed the slot, so flipping the toggle would silently change a
// running flow's branch target. Locked rows show the value as readonly.
// =============================================================================

interface ContractHeaderProps {
  directive: { id: string; title: string; orchestratorTaskId: string | null };
  doc: Contract;
  docTitle: string;
  /** Called with the server's response after any status / merge-mode
   *  transition so the parent can refresh the editor + sidebar. */
  onContractChanged: (updated: Contract) => void;
}

function ContractHeader({
  directive,
  doc,
  docTitle,
  onContractChanged,
}: ContractHeaderProps) {
  const [busy, setBusy] = useState<null | "start" | "pause" | "unlock" | "reopen" | "merge_mode">(
    null,
  );
  const [error, setError] = useState<string | null>(null);

  const wrap = useCallback(
    async (tag: typeof busy, op: () => Promise<Contract>) => {
      try {
        setBusy(tag);
        setError(null);
        const updated = await op();
        onContractChanged(updated);
      } catch (e) {
        setError(e instanceof Error ? e.message : "Unknown error");
      } finally {
        setBusy(null);
      }
    },
    [onContractChanged],
  );

  const onStart = useCallback(
    () => wrap("start", () => startDirectiveContract(doc.id)),
    [doc.id, wrap],
  );
  const onPause = useCallback(
    () => wrap("pause", () => pauseDirectiveContract(doc.id)),
    [doc.id, wrap],
  );
  const onUnlock = useCallback(
    () => wrap("unlock", () => unlockDirectiveContract(doc.id)),
    [doc.id, wrap],
  );
  const onReopen = useCallback(
    () => wrap("reopen", () => reopenDirectiveContract(doc.id)),
    [doc.id, wrap],
  );
  const onMergeMode = useCallback(
    (mode: ContractMergeMode) =>
      wrap("merge_mode", () => updateContract(doc.id, { mergeMode: mode })),
    [doc.id, wrap],
  );

  const editableMergeMode = doc.status === "draft" || doc.status === "queued";

  return (
    <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)] flex flex-col gap-2">
      {/* Row 1: breadcrumb + status pill + orchestrator indicator */}
      <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.title.trim().length > 0
            ? directive.title
            : directive.id.slice(0, 8)}
        </span>
        <span>/</span>
        <span className="text-white">{docTitle}</span>
        <ContractStatusPill status={doc.status} />
        {!!directive.orchestratorTaskId && (
          <span className="ml-auto 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>

      {/* Row 2: action buttons (status-driven) + merge mode + error */}
      <div className="flex items-center gap-2 text-[11px] font-mono">
        {doc.status === "draft" && (
          <ContractActionButton onClick={onStart} disabled={busy !== null} variant="primary">
            {busy === "start" ? "Starting…" : "Lock & Start"}
          </ContractActionButton>
        )}
        {doc.status === "queued" && (
          <ContractActionButton onClick={onUnlock} disabled={busy !== null}>
            {busy === "unlock" ? "Unlocking…" : "Unlock"}
          </ContractActionButton>
        )}
        {doc.status === "active" && (
          <>
            <ContractActionButton onClick={onPause} disabled={busy !== null}>
              {busy === "pause" ? "Pausing…" : "Pause"}
            </ContractActionButton>
            <ContractActionButton onClick={onUnlock} disabled={busy !== null}>
              {busy === "unlock" ? "Unlocking…" : "Unlock"}
            </ContractActionButton>
            {/* No "Mark complete" — the contract auto-ships when the
                orchestrator raises a PR (server-side; see
                update_directive's PR-detection branch in handlers/
                directives.rs). */}
          </>
        )}
        {doc.status === "shipped" && (
          <ContractActionButton onClick={onReopen} disabled={busy !== null} variant="primary">
            {busy === "reopen" ? "Reopening…" : "Reopen for amendment"}
          </ContractActionButton>
        )}

        {/* Merge mode radios — visible always, editable only in draft/queued */}
        <div className="ml-auto flex items-center gap-2 text-[#7788aa]">
          <span className="uppercase tracking-wide">merge:</span>
          <MergeModeRadio
            value={doc.mergeMode}
            onChange={onMergeMode}
            disabled={!editableMergeMode || busy !== null}
          />
        </div>
      </div>

      {error && (
        <div className="text-[10px] font-mono text-red-400">{error}</div>
      )}
    </div>
  );
}

function ContractStatusPill({ status }: { status: ContractStatus }) {
  const styles: Record<ContractStatus, { label: string; cls: string }> = {
    draft: { label: "draft", cls: "text-[#556677]" },
    queued: { label: "queued", cls: "text-amber-400" },
    active: { label: "active", cls: "text-green-400" },
    shipped: { label: "shipped", cls: "text-[#75aafc]" },
    archived: { label: "archived", cls: "text-[#7788aa]" },
  };
  const s = styles[status];
  return <span className={`ml-2 normal-case ${s.cls}`}>{s.label}</span>;
}

function ContractActionButton({
  children,
  onClick,
  disabled,
  variant,
}: {
  children: React.ReactNode;
  onClick: () => void;
  disabled?: boolean;
  variant?: "primary";
}) {
  const base =
    "px-2 py-1 border border-[rgba(117,170,252,0.3)] rounded text-[10px] uppercase tracking-wide transition-colors";
  const colors =
    variant === "primary"
      ? "text-green-300 hover:bg-[rgba(120,200,140,0.1)]"
      : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)]";
  const dim = disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer";
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled}
      className={`${base} ${colors} ${dim}`}
    >
      {children}
    </button>
  );
}

function MergeModeRadio({
  value,
  onChange,
  disabled,
}: {
  value: ContractMergeMode;
  onChange: (mode: ContractMergeMode) => void;
  disabled?: boolean;
}) {
  const opt = (mode: ContractMergeMode, label: string) => {
    const selected = value === mode;
    const cls = selected
      ? "text-white border-[rgba(117,170,252,0.6)] bg-[rgba(117,170,252,0.1)]"
      : "text-[#7788aa] border-transparent hover:text-[#9bc3ff]";
    const dim = disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer";
    return (
      <button
        key={mode}
        type="button"
        onClick={() => !disabled && !selected && onChange(mode)}
        disabled={disabled}
        className={`px-2 py-0.5 rounded border ${cls} ${dim} text-[10px] uppercase tracking-wide`}
      >
        {label}
      </button>
    );
  };
  return (
    <div className="flex items-center gap-1">
      {opt("shared", "shared")}
      {opt("own_pr", "own pr")}
    </div>
  );
}

// =============================================================================
// Editor shell — wraps DocumentEditor and handles the "no document selected"
// and loading states. Two modes:
//   1) documentId selected → fetch the Contract and edit doc.body via
//      updateContract (the call that auto-reactivates a shipped doc).
//   2) no documentId (legacy fallback, kept for the "select a directive but
//      not a document" transitional case) → edit directive.goal as before.
// =============================================================================

interface EditorShellProps {
  selection: SidebarSelection | null;
  hasDirectives: boolean;
  listLoading: boolean;
  /** Bumped after a successful document save so the sidebar refetches. */
  onDocumentChanged: () => void;
}

function EditorShell({
  selection,
  hasDirectives,
  listLoading,
  onDocumentChanged,
}: EditorShellProps) {
  const directiveId = selection?.directiveId;
  const documentId = selection?.documentId ?? null;

  // We deliberately don't pull `updateGoal` here — in the multi-document
  // world, edits flow through updateContract (which auto-reactivates
  // a shipped doc when its body changes). The legacy directive.goal is
  // unused on this surface.
  const {
    directive,
    loading: directiveLoading,
    cleanup,
    createPR,
    pickUpOrders,
  } = useDirective(directiveId);

  // Document fetch — only when documentId is selected. Refetched whenever the
  // id changes; not polled (the document stream is too low-traffic to warrant
  // background refresh in this iteration).
  const [doc, setDoc] = useState<Contract | null>(null);
  const [docLoading, setDocLoading] = useState(false);
  const [docError, setDocError] = useState<string | null>(null);

  useEffect(() => {
    if (!documentId) {
      setDoc(null);
      setDocLoading(false);
      setDocError(null);
      return;
    }
    let cancelled = false;
    setDocLoading(true);
    setDocError(null);
    getContract(documentId)
      .then((d) => {
        if (cancelled) return;
        setDoc(d);
      })
      .catch((e) => {
        if (cancelled) return;
        setDocError(e instanceof Error ? e.message : "Failed to load document");
      })
      .finally(() => {
        if (cancelled) return;
        setDocLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, [documentId]);

  // Save callback for the document path. The backend re-stamps a shipped doc
  // back to active when its body changes, so we just optimistically update
  // local state with the server's response.
  const onUpdateDocumentBody = useCallback(
    async (body: string) => {
      if (!documentId) return;
      const updated = await updateContract(documentId, { body });
      setDoc(updated);
      // Tell the sidebar to refetch the directive's document list so the
      // status chip flips from `shipped` back to `active` (and any title
      // changes propagate). Cheap — folders only refetch when open.
      onDocumentChanged();
    },
    [documentId, onDocumentChanged],
  );

  // ---- Empty / error / loading states ------------------------------------
  if (!directiveId) {
    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 directives yet — create one from the legacy UI"}
        </p>
      </div>
    );
  }

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

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

  // --- Task path: task row clicked in the sidebar ------------------------
  // Renders the live transcript via TaskPage. Selection wins over
  // the document path when both are somehow present (defensive).
  if (selection?.taskId) {
    const taskId = selection.taskId;
    // Resolve a human label for the task: orchestrator/completion are
    // labelled by role; step tasks borrow the step name; everything else
    // is an ephemeral and just shows the task id slice. Look-up uses the
    // already-fetched directive (with steps).
    const stepWithTask = directive.steps.find((s) => s.taskId === taskId);
    const label =
      taskId === directive.orchestratorTaskId
        ? "orchestrator"
        : taskId === directive.completionTaskId
        ? "completion"
        : stepWithTask?.name ?? taskId.slice(0, 8);
    const isStepBound =
      taskId === directive.orchestratorTaskId ||
      taskId === directive.completionTaskId ||
      !!stepWithTask;
    return (
      <div className="flex-1 flex flex-col h-full overflow-hidden">
        <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.title.trim().length > 0
                ? directive.title
                : directive.id.slice(0, 8)}
            </span>
            <span>/</span>
            <span className="text-white">{label}</span>
          </div>
        </div>
        <TaskPage
          taskId={taskId}
          label={label}
          ephemeral={!isStepBound}
        />
      </div>
    );
  }

  // --- Document path: documentId selected --------------------------------
  if (documentId) {
    if (docLoading && !doc) {
      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 (docError) {
      return (
        <div className="flex-1 flex items-center justify-center h-full">
          <p className="text-red-400 font-mono text-[12px]">{docError}</p>
        </div>
      );
    }
    if (!doc) {
      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>
      );
    }

    // The contract title is the filename label; the contract body is the
    // editor body. DocumentEditor takes these directly (no more synthesis
    // hack) — `directive` is still passed for orchestrator state and the
    // embedded steps panel via StepsBlockContextProvider.
    const docTitle = `${fileLabel(doc, directive)}.md`;

    return (
      <div className="flex-1 flex flex-col h-full overflow-hidden">
        <ContractHeader
          directive={directive}
          doc={doc}
          docTitle={docTitle}
          onContractChanged={(updated) => {
            setDoc(updated);
            onDocumentChanged();
          }}
        />

        <DocumentEditor
          // Keying by document id ensures the Lexical editor remounts cleanly
          // when the user switches documents, so the previous doc's body
          // doesn't bleed into the new one.
          key={doc.id}
          directive={directive}
          documentId={doc.id}
          title={docTitle}
          body={doc.body}
          onUpdateBody={onUpdateDocumentBody}
          onCleanup={async () => {
            await cleanup();
          }}
          onCreatePR={async () => {
            await createPR();
          }}
          onPickUpOrders={async () => {
            await pickUpOrders();
          }}
          // Locked-and-started (`active`), `queued`, `shipped`, and
          // `archived` contracts must be unlocked before edits are
          // accepted. Only `draft` is freely editable; everything else
          // shows the in-editor Unlock affordance.
          editable={doc.status === "draft"}
          onRequestUnlock={async () => {
            const updated = await unlockDirectiveContract(doc.id);
            setDoc(updated);
            onDocumentChanged();
          }}
        />
      </div>
    );
  }

  // --- Legacy fallback: directive selected but no document chosen --------
  // We only ever land here transiently while the page resolves the default
  // document selection, so we render a thin "loading" placeholder rather
  // than the full goal editor (which would be confusing alongside the new
  // multi-document model).
  return (
    <div className="flex-1 flex flex-col h-full overflow-hidden">
      <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.title.trim().length > 0
              ? directive.title
              : directive.id.slice(0, 8)}
          </span>
        </div>
      </div>
      <div className="flex-1 flex items-center justify-center">
        <p className="text-[#556677] font-mono text-[12px]">
          Select a document, or click "+ New document" to create one.
        </p>
      </div>
    </div>
  );
}

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

export default function DocumentDirectivesPage() {
  const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
  const navigate = useNavigate();
  const { id: routeDirectiveId } = useParams<{ id: string }>();
  const [searchParams, setSearchParams] = useSearchParams();
  const { directives, loading: listLoading } = useDirectives();

  // refreshNonce — bumped to tell open directive folders to refetch their
  // document lists (after a create or save).
  const [refreshNonce, setRefreshNonce] = useState(0);
  const bumpRefresh = useCallback(() => setRefreshNonce((n) => n + 1), []);

  // Derive the SidebarSelection from the URL. The route param is the
  // directive id; ?document=:id and ?task=:id pick a specific child. Exactly
  // one of taskId/documentId can be set; if both happen to be present in the
  // URL (which shouldn't happen via our nav code) we prefer ?task= since
  // task selection is the more disruptive action.
  const selection: SidebarSelection | null = useMemo(() => {
    if (!routeDirectiveId) return null;
    const taskId = searchParams.get("task");
    const documentId = searchParams.get("document");
    if (taskId) return { directiveId: routeDirectiveId, taskId, documentId: null };
    if (documentId) return { directiveId: routeDirectiveId, taskId: null, documentId };
    return { directiveId: routeDirectiveId, taskId: null, documentId: null };
  }, [routeDirectiveId, searchParams]);

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

  // ------------------------------------------------------------------
  // Default-selection: when the user clicks a directive's folder header (or
  // lands on /directives/:id without ?document=) we pick the first active or
  // draft document and update the URL to point at it. This avoids the
  // "directive selected, but nothing in the editor" intermediate state.
  // ------------------------------------------------------------------
  const lastResolvedRef = useRef<string | null>(null);
  useEffect(() => {
    if (!routeDirectiveId) {
      lastResolvedRef.current = null;
      return;
    }
    // Only auto-resolve when no document/task has been picked yet, AND we
    // haven't already resolved this directive in a prior tick (otherwise
    // navigating away from the doc would instantly re-pick the same one).
    if (selection?.documentId || selection?.taskId) {
      lastResolvedRef.current = routeDirectiveId;
      return;
    }
    if (lastResolvedRef.current === routeDirectiveId) return;
    lastResolvedRef.current = routeDirectiveId;

    let cancelled = false;
    listContracts(routeDirectiveId)
      .then((list) => {
        if (cancelled) return;
        // Prefer the first 'active' doc; fall back to the first 'draft'.
        const firstActive = list.find((d) => d.status === "active");
        const firstDraft = list.find((d) => d.status === "draft");
        const pick = firstActive ?? firstDraft;
        if (pick) {
          setSearchParams(
            (prev) => {
              const next = new URLSearchParams(prev);
              next.set("document", pick.id);
              next.delete("task");
              return next;
            },
            { replace: true },
          );
        }
      })
      .catch(() => {
        // Swallow — the editor pane will show "Document not found" and the
        // user can click "+ New document" to recover.
      });
    return () => {
      cancelled = true;
    };
  }, [routeDirectiveId, selection?.documentId, selection?.taskId, setSearchParams]);

  const handleSelectDocument = useCallback(
    (directiveId: string, doc: Contract) => {
      navigate(`/directives/${directiveId}?document=${doc.id}`);
    },
    [navigate],
  );

  // When the user clicks a directive folder header (not a document row), we
  // jump to /directives/:id without ?document= — the default-selection
  // effect above will then pick the first active doc.
  const handleSelectDirective = useCallback(
    (directiveId: string) => {
      if (routeDirectiveId === directiveId) return;
      navigate(`/directives/${directiveId}`);
    },
    [navigate, routeDirectiveId],
  );

  const handleCreateDocument = useCallback(
    async (directive: DirectiveSummary) => {
      const created = await createContract(directive.id, {
        title: "",
        body: "",
      });
      bumpRefresh();
      // Navigate to the new doc so it's selected immediately.
      navigate(`/directives/${directive.id}?document=${created.id}`);
    },
    [bumpRefresh, navigate],
  );

  // Click on a task or step row → open the live transcript pane via
  // ?task=<id>. EditorShell switches to TaskPage when this is set.
  const handleSelectTask = useCallback(
    (directiveId: string, taskId: string) => {
      navigate(`/directives/${directiveId}?task=${taskId}`);
    },
    [navigate],
  );

  // Modal state for the two new creation surfaces in the sidebar:
  //   * + New contract  → opens NewContractModal, calls useDirectives.create
  //   * + New ephemeral task (per directive) → opens NewEphemeralTaskModal
  const { create: createDirective } = useDirectives();
  const [showNewContract, setShowNewContract] = useState(false);
  const [newEphemeralFor, setNewEphemeralFor] = useState<DirectiveSummary | null>(null);

  const handleSubmitNewContract = useCallback(
    async (title: string, body: string, repositoryUrl: string) => {
      const d = await createDirective({
        title,
        contractBody: body,
        repositoryUrl: repositoryUrl.length > 0 ? repositoryUrl : undefined,
      });
      setShowNewContract(false);
      navigate(`/directives/${d.id}`);
    },
    [createDirective, navigate],
  );

  const handleSubmitNewEphemeral = useCallback(
    async (name: string, plan: string) => {
      if (!newEphemeralFor) return;
      const task = await createDirectiveTask(newEphemeralFor.id, { name, plan });
      const target = newEphemeralFor.id;
      setNewEphemeralFor(null);
      bumpRefresh();
      navigate(`/directives/${target}?task=${task.id}`);
    },
    [newEphemeralFor, bumpRefresh, navigate],
  );

  // Right-click context menu — generic. The state holds whichever items
  // array the entity's builder produced; the SidebarContextMenu just
  // renders them. Each entity type has its own builder (directive,
  // contract, step, task) hung off the page so all the action callbacks
  // (start/pause/archive/delete/etc.) live in one place.
  const { refresh: refreshDirectiveList } = useDirectives();
  const [contextMenu, setContextMenu] = useState<{
    x: number;
    y: number;
    items: ContextMenuItem[];
  } | null>(null);

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

  const runAction = useCallback(
    async (action: () => Promise<unknown>, errMsg: string) => {
      try {
        await action();
        await refreshDirectiveList();
        bumpRefresh();
      } catch (err) {
        // eslint-disable-next-line no-console
        console.error(`[makima] ${errMsg}`, err);
        alert(
          err instanceof Error ? `${errMsg}: ${err.message}` : errMsg,
        );
      }
    },
    [refreshDirectiveList, bumpRefresh],
  );

  const openMenu = useCallback(
    (e: React.MouseEvent, items: ContextMenuItem[]) => {
      e.preventDefault();
      e.stopPropagation();
      setContextMenu({ x: e.clientX, y: e.clientY, items });
    },
    [],
  );

  // ---- Per-entity menu builders. Each returns its items array; the
  //      handler wraps them with openMenu so each row can fire its own
  //      right-click with one line of wiring. ----
  const handleContextMenuDirective = useCallback(
    (e: React.MouseEvent, d: DirectiveSummary) => {
      const items: ContextMenuItem[] = [
        {
          label: "+ New contract here",
          onClick: () => {
            void handleCreateDocument(d);
          },
        },
        { label: "+ New ephemeral task", onClick: () => setNewEphemeralFor(d) },
        { label: "", separator: true },
        {
          label: "Start",
          onClick: () => runAction(() => startDirective(d.id), "Failed to start directive"),
          disabled: d.status === "active" || d.status === "archived",
        },
        {
          label: "Pause",
          onClick: () => runAction(() => pauseDirective(d.id), "Failed to pause directive"),
          disabled: d.status !== "active",
        },
        { label: "", separator: true },
        {
          label: d.prUrl ? "Update PR" : "Create PR",
          onClick: () =>
            runAction(
              () => createDirectivePR(d.id),
              d.prUrl ? "Failed to update PR" : "Failed to create PR",
            ),
        },
        ...(d.prUrl
          ? [{
              label: "Go to PR",
              onClick: () => window.open(d.prUrl!, "_blank", "noreferrer"),
            } as ContextMenuItem]
          : []),
        {
          label: "Advance DAG",
          onClick: () => runAction(() => advanceDirective(d.id), "Failed to advance DAG"),
        },
        {
          label: "Cleanup merged steps",
          onClick: () => runAction(() => cleanupDirective(d.id), "Failed to clean up"),
        },
        {
          label: "Pick up orders",
          onClick: () => runAction(() => pickUpOrders(d.id), "Failed to pick up orders"),
        },
        { label: "", separator: true },
        {
          label: "Archive",
          danger: true,
          onClick: () =>
            runAction(
              () => updateDirective(d.id, { status: "archived" }),
              "Failed to archive",
            ),
          disabled: d.status === "archived",
        },
        {
          label: "Delete",
          danger: true,
          onClick: async () => {
            if (!window.confirm(`Delete "${d.title}"? This cannot be undone.`)) return;
            await runAction(() => deleteDirective(d.id), "Failed to delete");
            if (selection?.directiveId === d.id) navigate("/directives");
          },
        },
      ];
      openMenu(e, items);
    },
    [openMenu, runAction, navigate, selection],
  );

  const handleContextMenuContract = useCallback(
    (e: React.MouseEvent, c: Contract) => {
      const items: ContextMenuItem[] = [
        {
          label: "Lock & Start",
          onClick: () =>
            runAction(() => startDirectiveContract(c.id), "Failed to start contract"),
          disabled: c.status !== "draft",
        },
        {
          label: "Pause",
          onClick: () =>
            runAction(() => pauseDirectiveContract(c.id), "Failed to pause contract"),
          disabled: c.status !== "active",
        },
        {
          label: "Unlock",
          onClick: () =>
            runAction(() => unlockDirectiveContract(c.id), "Failed to unlock contract"),
          disabled: c.status !== "active" && c.status !== "queued",
        },
        {
          label: "Reopen for amendment",
          onClick: () =>
            runAction(() => reopenDirectiveContract(c.id), "Failed to reopen contract"),
          disabled: c.status !== "shipped",
        },
      ];
      openMenu(e, items);
    },
    [openMenu, runAction],
  );

  const handleContextMenuStep = useCallback(
    (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => {
      const items: ContextMenuItem[] = [
        {
          label: "Stop task",
          onClick: () =>
            step.taskId
              ? runAction(() => stopTask(step.taskId!), "Failed to stop task")
              : undefined,
          disabled: !step.taskId || step.status !== "running",
        },
        {
          label: "Skip step",
          danger: true,
          onClick: () =>
            runAction(
              () => skipDirectiveStep(directiveId, step.id),
              "Failed to skip step",
            ),
          disabled: step.status === "completed" || step.status === "skipped",
        },
      ];
      openMenu(e, items);
    },
    [openMenu, runAction],
  );

  const handleContextMenuTask = useCallback(
    (e: React.MouseEvent, task: Task, _directiveId: string) => {
      const items: ContextMenuItem[] = [
        {
          label: "Stop task",
          onClick: () => runAction(() => stopTask(task.id), "Failed to stop task"),
          disabled: task.status === "done" || task.status === "failed" || task.status === "merged",
        },
      ];
      openMenu(e, items);
    },
    [openMenu, runAction],
  );

  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>
    );
  }

  return (
    // h-screen + overflow-hidden so the page itself never scrolls; the
    // sidebar and editor pane each manage their own scroll via flex-1
    // children with overflow-y-auto. Previously we set
    // height: calc(100vh - 80px) on <main>, which assumed an 80px masthead
    // and quietly clipped content when the masthead was taller (or pushed
    // the page below the viewport on shorter screens, which made the
    // whole page scroll instead of the sidebar/editor independently).
    <div className="relative z-10 h-screen flex flex-col bg-[#0a1628] overflow-hidden">
      <Masthead showNav />
      <main className="flex-1 flex min-h-0 overflow-hidden">
        {/* Left: file-tree sidebar — independent scroll. */}
        <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}
            onSelectDocument={handleSelectDocument}
            onSelectDirective={handleSelectDirective}
            onCreateContract={() => setShowNewContract(true)}
            onContextMenuDirective={handleContextMenuDirective}
            onContextMenuContract={handleContextMenuContract}
            onContextMenuStep={handleContextMenuStep}
            onContextMenuTask={handleContextMenuTask}
            onSelectTask={handleSelectTask}
            refreshNonce={refreshNonce}
          />
        </div>

        {/* Right: Lexical editor */}
        <EditorShell
          selection={selection}
          hasDirectives={directives.length > 0}
          listLoading={listLoading}
          onDocumentChanged={bumpRefresh}
        />
      </main>

      {showNewContract && (
        <NewContractModal
          onClose={() => setShowNewContract(false)}
          onSubmit={handleSubmitNewContract}
        />
      )}
      {newEphemeralFor && (
        <NewEphemeralTaskModal
          directive={newEphemeralFor}
          onClose={() => setNewEphemeralFor(null)}
          onSubmit={handleSubmitNewEphemeral}
        />
      )}

      {contextMenu && (
        <SidebarContextMenu
          x={contextMenu.x}
          y={contextMenu.y}
          items={contextMenu.items}
          onClose={closeContextMenu}
        />
      )}
    </div>
  );
}

/**
 * Modal for creating a new directive (= "contract" in the doc-mode UI).
 * Title + goal are required; repository_url is optional. On submit calls
 * useDirectives.create and navigates the user into the new directive.
 */
function NewContractModal({
  onClose,
  onSubmit,
}: {
  onClose: () => void;
  onSubmit: (title: string, goal: string, repositoryUrl: string) => Promise<void>;
}) {
  const [title, setTitle] = useState("");
  const [goal, setGoal] = useState("");
  const [repo, setRepo] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [err, setErr] = useState<string | null>(null);
  const titleRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    titleRef.current?.focus();
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    const t = title.trim();
    const g = goal.trim();
    if (!t || !g || submitting) return;
    setSubmitting(true);
    setErr(null);
    try {
      await onSubmit(t, g, repo.trim());
    } catch (caught) {
      setErr(caught instanceof Error ? caught.message : String(caught));
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
      onClick={onClose}
    >
      <form
        onSubmit={submit}
        onClick={(e) => e.stopPropagation()}
        className="w-[520px] max-w-[90vw] bg-[#0a1628] border border-[rgba(117,170,252,0.4)] shadow-xl flex flex-col"
      >
        <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.25)]">
          <p className="text-[10px] font-mono text-[#556677] uppercase tracking-wide">
            New contract
          </p>
        </div>
        <div className="px-4 py-4 space-y-3">
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Title
            </label>
            <input
              ref={titleRef}
              type="text"
              value={title}
              onChange={(e) => setTitle(e.target.value)}
              placeholder="e.g. Migrate auth to Supabase"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]"
            />
          </div>
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Goal
            </label>
            <textarea
              value={goal}
              onChange={(e) => setGoal(e.target.value)}
              onKeyDown={(e) => {
                if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
                  void submit(e as unknown as React.FormEvent);
                }
              }}
              rows={4}
              placeholder="Describe what the contract should achieve"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566] resize-none"
            />
          </div>
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Repository URL (optional)
            </label>
            <input
              type="text"
              value={repo}
              onChange={(e) => setRepo(e.target.value)}
              placeholder="e.g. https://github.com/owner/repo"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]"
            />
          </div>
          {err && (
            <p className="text-[11px] font-mono text-red-400">{err}</p>
          )}
        </div>
        <div className="px-4 py-3 border-t border-dashed border-[rgba(117,170,252,0.25)] flex items-center justify-end gap-2">
          <button
            type="button"
            onClick={onClose}
            className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-[#7788aa] border border-[#2a3a5a] hover:text-white"
          >
            Cancel
          </button>
          <button
            type="submit"
            disabled={!title.trim() || !goal.trim() || submitting}
            className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-emerald-300 border border-emerald-700/60 hover:border-emerald-400 disabled:opacity-40 disabled:cursor-not-allowed"
          >
            {submitting ? "Creating…" : "Create contract"}
          </button>
        </div>
      </form>
    </div>
  );
}

/**
 * Modal for spawning an ephemeral task under a directive. Mirrors the
 * existing right-click "+ New task" flow.
 */
function NewEphemeralTaskModal({
  directive,
  onClose,
  onSubmit,
}: {
  directive: DirectiveSummary;
  onClose: () => void;
  onSubmit: (name: string, plan: string) => Promise<void>;
}) {
  const [name, setName] = useState("");
  const [plan, setPlan] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [err, setErr] = useState<string | null>(null);
  const nameRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    nameRef.current?.focus();
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    const n = name.trim();
    const p = plan.trim();
    if (!n || !p || submitting) return;
    setSubmitting(true);
    setErr(null);
    try {
      await onSubmit(n, p);
    } catch (caught) {
      setErr(caught instanceof Error ? caught.message : String(caught));
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
      onClick={onClose}
    >
      <form
        onSubmit={submit}
        onClick={(e) => e.stopPropagation()}
        className="w-[480px] max-w-[90vw] bg-[#0a1628] border border-[rgba(117,170,252,0.4)] shadow-xl flex flex-col"
      >
        <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.25)]">
          <p className="text-[10px] font-mono text-[#556677] uppercase tracking-wide">
            New ephemeral task in
          </p>
          <p className="text-[12px] font-mono text-white truncate">
            {directive.title}
          </p>
        </div>
        <div className="px-4 py-4 space-y-3">
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Name
            </label>
            <input
              ref={nameRef}
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="e.g. Investigate flaky test"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]"
            />
          </div>
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Plan / instructions
            </label>
            <textarea
              value={plan}
              onChange={(e) => setPlan(e.target.value)}
              onKeyDown={(e) => {
                if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
                  void submit(e as unknown as React.FormEvent);
                }
              }}
              rows={5}
              placeholder="What should the task do?"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566] resize-none"
            />
          </div>
          {err && (
            <p className="text-[11px] font-mono text-red-400">{err}</p>
          )}
        </div>
        <div className="px-4 py-3 border-t border-dashed border-[rgba(117,170,252,0.25)] flex items-center justify-end gap-2">
          <button
            type="button"
            onClick={onClose}
            className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-[#7788aa] border border-[#2a3a5a] hover:text-white"
          >
            Cancel
          </button>
          <button
            type="submit"
            disabled={!name.trim() || !plan.trim() || submitting}
            className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-[#c084fc] border border-[#c084fc]/40 hover:border-[#c084fc] disabled:opacity-40 disabled:cursor-not-allowed"
          >
            {submitting ? "Spawning…" : "Spawn task"}
          </button>
        </div>
      </form>
    </div>
  );
}
