"use client"; import * as React from "react"; import { useParams, useRouter } from "next/navigation"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; import { ChevronDown, ChevronRight, FileText, Folder, FolderKanban, Loader2, Plus, Search, Rocket, X } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet"; import { api } from "@/lib/trpc"; import { cn } from "@/lib/utils"; const STATUS_VARIANT: Record = { draft: "bg-muted text-muted-foreground", ready: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", in_progress: "bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/20", blocked: "bg-destructive/10 text-destructive border-destructive/20", done: "bg-sky-500/15 text-sky-600 dark:text-sky-400 border-sky-500/20", cancelled: "bg-muted text-muted-foreground", }; function StatusChip({ status }: { status: string | null }) { const s = status ?? "unknown"; const cls = STATUS_VARIANT[s] ?? STATUS_VARIANT.draft; return ( {s} ); } function slugify(value: string): string { return value .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .replace(/--+/g, "-"); } type DialogState = | { kind: "plan" } | { kind: "epic"; planSlug: string } | { kind: "task"; planSlug: string; epicSlug: string } | null; export default function PlansPage() { const params = useParams(); const router = useRouter(); const workspaceSlug = params?.workspaceSlug as string | undefined; const [search, setSearch] = React.useState(""); const [expandedPlans, setExpandedPlans] = React.useState>(new Set()); const [expandedEpics, setExpandedEpics] = React.useState>(new Set()); const [selectedId, setSelectedId] = React.useState(null); const [dialog, setDialog] = React.useState(null); const [formSlug, setFormSlug] = React.useState(""); const [formTitle, setFormTitle] = React.useState(""); const [formBody, setFormBody] = React.useState(""); const [formAgentPrompt, setFormAgentPrompt] = React.useState(""); const [formError, setFormError] = React.useState(null); const treeQuery = api.backlog.listTree.useQuery({ workspace: workspaceSlug! }, { enabled: Boolean(workspaceSlug) }); const detailQuery = api.backlog.getBacklogItem.useQuery( { workspace: workspaceSlug!, backlogItemId: selectedId! }, { enabled: Boolean(workspaceSlug && selectedId) }, ); const promptQuery = api.backlog.getWorkflowPrompt.useQuery( { workspace: workspaceSlug!, backlogItemId: selectedId! }, { enabled: Boolean(workspaceSlug && selectedId) }, ); const utils = api.useUtils(); const createPlan = api.backlog.createPlan.useMutation({ onSuccess: () => { void utils.backlog.listTree.invalidate(); closeDialog(); }, onError: (e) => setFormError(e.message), }); const createEpic = api.backlog.createEpic.useMutation({ onSuccess: () => { void utils.backlog.listTree.invalidate(); closeDialog(); }, onError: (e) => setFormError(e.message), }); const createTask = api.backlog.createTask.useMutation({ onSuccess: () => { void utils.backlog.listTree.invalidate(); closeDialog(); }, onError: (e) => setFormError(e.message), }); const startConvoy = api.backlog.startConvoy.useMutation({ onSuccess: () => { void utils.convoys.kanban.invalidate(); if (workspaceSlug) router.push(`/${workspaceSlug}/mission-control`); }, onError: (e) => setFormError(e.message), }); // Direct convoy.create path required by brief 4 — Start → Convoy via convoys.create with planSlug/epicSlug const createConvoyDirect = api.convoys.create.useMutation({ onSuccess: () => { void utils.convoys.kanban.invalidate(); if (workspaceSlug) router.push(`/${workspaceSlug}/mission-control`); }, onError: (e) => setFormError(e.message), }); function closeDialog() { setDialog(null); setFormSlug(""); setFormTitle(""); setFormBody(""); setFormAgentPrompt(""); setFormError(null); } function openPlanDialog() { setFormError(null); setFormSlug(""); setFormTitle(""); setFormBody(""); setFormAgentPrompt(""); setDialog({ kind: "plan" }); } function openEpicDialog(planSlug: string) { setFormError(null); setFormSlug(""); setFormTitle(""); setFormBody(""); setFormAgentPrompt(""); setDialog({ kind: "epic", planSlug }); } function openTaskDialog(planSlug: string, epicSlug: string) { setFormError(null); setFormSlug(""); setFormTitle(""); setFormBody(""); setFormAgentPrompt(""); setDialog({ kind: "task", planSlug, epicSlug }); } const tree = React.useMemo(() => treeQuery.data?.plans ?? [], [treeQuery.data?.plans]); const filtered = React.useMemo(() => { if (!search.trim()) return tree; const q = search.trim().toLowerCase(); return tree .map((plan) => { const planMatches = plan.title.toLowerCase().includes(q) || plan.slug.toLowerCase().includes(q); const epics = plan.epics .map((epic) => { const epicMatches = epic.title.toLowerCase().includes(q) || epic.slug.toLowerCase().includes(q); const tasks = epic.tasks.filter( (t) => t.title.toLowerCase().includes(q) || t.slug.toLowerCase().includes(q) || (t.status ?? "").toLowerCase().includes(q), ); if (epicMatches || tasks.length > 0 || planMatches) { return { ...epic, tasks: epicMatches && tasks.length === 0 ? epic.tasks : tasks }; } return null; }) .filter(Boolean) as typeof plan.epics; if (planMatches) return { ...plan, epics: epics.length ? epics : plan.epics }; if (epics.length) return { ...plan, epics }; return null; }) .filter(Boolean) as typeof tree; }, [tree, search]); React.useEffect(() => { if (tree.length && expandedPlans.size === 0) { setExpandedPlans(new Set(tree.slice(0, 2).map((p) => p.slug))); } }, [tree, expandedPlans.size]); if (!workspaceSlug) return null; const isMutating = createPlan.isPending || createEpic.isPending || createTask.isPending; return (

Plans

Plan → Epic → Task tree from plans/ markdown

setSearch(e.target.value)} className="w-64 pl-8" />
{treeQuery.isLoading ? (
Loading plans…
) : filtered.length === 0 ? (

No plans yet

{search.trim() ? "No results for your search." : "Create your first plan or run pnpm --filter @tasks/database watch:markdown-backlog to import plans/ after seeding."}

{!search.trim() && ( )}
) : (
{filtered.map((plan) => { const planOpen = expandedPlans.has(plan.slug); return ( setExpandedPlans((prev) => { const next = new Set(prev); if (open) next.add(plan.slug); else next.delete(plan.slug); return next; }) } >
{plan.slug}
{plan.epics.length === 0 ? (

No epics — add one.

) : ( plan.epics.map((epic) => { const epicKey = `${plan.slug}/${epic.slug}`; const epicOpen = expandedEpics.has(epicKey); return ( setExpandedEpics((prev) => { const next = new Set(prev); if (open) next.add(epicKey); else next.delete(epicKey); return next; }) } >
{epic.slug}
    {epic.tasks.length === 0 ? (
  • No tasks.
  • ) : ( epic.tasks.map((task) => (
  • {task.priority && ( {task.priority} )} {task.repoPath}
  • )) )}
); }) )}
); })}
)}
!open && setSelectedId(null)}> {detailQuery.data?.title ?? "Details"} {detailQuery.data?.repoPath ?? ""} {detailQuery.data ? ` · ${detailQuery.data.kind}` : ""}
{detailQuery.isLoading ? (
Loading…
) : detailQuery.data ? ( <>
{detailQuery.data.priority && {detailQuery.data.priority}} {detailQuery.data.kind}

Repo path

{detailQuery.data.repoPath}

Slugs

Plan: {detailQuery.data.planSlug} {detailQuery.data.epicSlug ? ( <> {" "} Epic: {detailQuery.data.epicSlug} ) : null}{" "} Slug: {detailQuery.data.slug}

Body

                        {detailQuery.data.bodyMarkdown || "(empty)"}
                      

Effective prompt

Source: {promptQuery.data?.source ?? "—"}

                        {promptQuery.data?.effectivePrompt ?? promptQuery.data?.ownOverride ?? "(no prompt)"}
                      
{promptQuery.data?.ownOverride &&

Own override present

}
{detailQuery.data.kind === "task" && (
)} {(startConvoy.error || createConvoyDirect.error) && (

{(startConvoy.error ?? createConvoyDirect.error)?.message}

)} {(startConvoy.isSuccess || createConvoyDirect.isSuccess) && (

Convoy {(startConvoy.data?.convoySlug ?? createConvoyDirect.data?.convoyId) ?? ""} created — check Mission Control.

)} ) : (

Select an item to see details.

)}
!open && closeDialog()}>
{dialog?.kind === "plan" ? "New Plan" : dialog?.kind === "epic" ? "New Epic" : dialog?.kind === "task" ? "New Task" : "New"}
{dialog?.kind === "epic" && (
{dialog.planSlug}
)} {dialog?.kind === "task" && (
{dialog.planSlug}
{dialog.epicSlug}
)}
{ const v = e.target.value.toLowerCase(); setFormSlug(v); if (!formTitle.trim() || slugify(formTitle) === formSlug) { // keep title in sync helper? no auto } }} />

Must match ^[a-z0-9-]+$

setFormTitle(e.target.value)} onBlur={() => { if (!formSlug.trim() && formTitle.trim()) setFormSlug(slugify(formTitle)); }} />