"use client" /** * /projects — Dual-source project view with inline task+agent expansion * * PRIMARY → Tiger's PROJECTS.md (source of truth, read-only) * SECONDARY → SQLite projects (dashboard-queued, waiting for Tiger) * * Click any project → expands to show tasks from TASKS.md for that project, * each task row showing: stage name, assigned agent (emoji+name), status badge. */ import * as React from "react" import useSWR from "swr" import { FolderOpen, Plus, Loader2, ChevronDown, ChevronRight, Inbox, Trash2, MoreVertical, } from "lucide-react" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" // ─── Types ──────────────────────────────────────────────────────────────────── interface FileProject { id: string name: string description: string created: string tasks_count: string status: string } interface FileTask { id: string title: string status: string status_raw: string assigned_agent: string project: string isProject?: boolean isSubTask?: boolean parentId?: string } interface DbProject { id: string name: string description: string status: string priority: string created_at: string } // ─── Constants ──────────────────────────────────────────────────────────────── const PRIORITY_COLORS: Record = { low: "bg-gray-500/10 text-gray-400 border-gray-500/20", medium: "bg-blue-500/10 text-blue-400 border-blue-500/20", high: "bg-amber-500/10 text-amber-400 border-amber-500/20", urgent: "bg-red-500/10 text-red-400 border-red-500/20", } const TASK_STATUS_COLORS: Record = { "in-progress": "bg-amber-500/10 text-amber-400", review: "bg-purple-500/10 text-purple-400", done: "bg-emerald-500/10 text-emerald-400", backlog: "bg-zinc-500/10 text-zinc-400", } const AGENT_EMOJI: Record = { tiger: "🐯", main: "🐯", cody: "💻", coder: "💻", ethan: "🔍", researcher: "🔍", cathy: "✍️", writer: "✍️", elon: "📊", pm: "📊", } const AGENT_COLORS: Record = { tiger: "text-orange-400", main: "text-orange-400", cody: "text-blue-400", coder: "text-blue-400", ethan: "text-green-400", researcher: "text-green-400", cathy: "text-pink-400", writer: "text-pink-400", elon: "text-violet-400", pm: "text-violet-400", } // ─── Helpers ────────────────────────────────────────────────────────────────── const fetcher = (url: string) => fetch(url).then((r) => r.json()) function statusBadgeClass(status: string) { const s = status.toLowerCase() if (s.includes("progress") || s.includes("active") || s.includes("🔴") || s.includes("🔄")) return "bg-green-500/10 text-green-400 border-green-500/20" if (s.includes("review")) return "bg-purple-500/10 text-purple-400 border-purple-500/20" if (s.includes("done") || s.includes("complete") || s.includes("approved")) return "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" return "bg-zinc-500/10 text-zinc-400 border-zinc-500/20" } function cleanText(s: string) { // Strip emoji and markdown bold markers for display return s.replace(/\*\*/g, "").replace(/[✅⏳🔄❌🛢️🔍💻📊✍️🐯]/g, "").trim() } // ─── Task row inside expanded project ───────────────────────────────────────── function TaskRow({ task }: { task: FileTask }) { const agentKey = task.assigned_agent?.toLowerCase() ?? "" return (
{/* Status badge */} {task.status_raw ? cleanText(task.status_raw).slice(0, 20) : task.status} {/* Stage/task name */} {cleanText(task.title)} {/* Agent */} {agentKey && agentKey !== "unassigned" && ( {AGENT_EMOJI[agentKey] ?? "🤖"} {agentKey} )}
) } // ─── Tiger's project card (read-only, with task expand) ─────────────────────── function FileProjectCard({ project }: { project: FileProject }) { const [expanded, setExpanded] = React.useState(false) // Fetch tasks from TASKS.md filtered to this project name const projectName = cleanText(project.name) const { data: tasksData, isLoading: tasksLoading } = useSWR<{ ok: boolean; tasks: FileTask[] }>( expanded ? `/api/tiger/file-tasks?project=${encodeURIComponent(projectName)}` : null, fetcher ) // Show sub-tasks (stage rows) and the project-level task (agent + status) const allTasks = tasksData?.tasks ?? [] const projectTask = allTasks.find((t) => t.isProject) const subTasks = allTasks.filter((t) => t.isSubTask) return (
{/* Expand toggle + title */} {/* Status badge */} {cleanText(project.status).replace(/in progress/i, "Active")}
{project.description && ( {project.description} )}
{/* Meta row */}
Created {project.created} · {project.tasks_count} {/* Show primary agent when collapsed */} {!expanded && projectTask?.assigned_agent && ( <> · {AGENT_EMOJI[projectTask.assigned_agent] ?? ""} {projectTask.assigned_agent} )}
{/* Expanded task list */} {expanded && (
{tasksLoading ? (
) : allTasks.length === 0 ? (

No tasks found in TASKS.md for this project.

) : (
{/* Project-level agent row */} {projectTask && (
Lead: {AGENT_EMOJI[projectTask.assigned_agent] ?? ""} {projectTask.assigned_agent} {cleanText(projectTask.status_raw || projectTask.status)}
)} {/* Sub-task rows (review pipeline stages etc) */} {subTasks.length > 0 ? ( subTasks.map((t) => ) ) : (

No task breakdown available — Tiger tracks this at project level.

)}
)}
)}
) } // ─── Dashboard-queued project card ─────────────────────────────────────────── function DbProjectCard({ project, onDelete }: { project: DbProject; onDelete: (id: string) => void }) { return (
{project.name} {project.description && ( {project.description} )}
onDelete(project.id)} className="text-destructive"> Delete
{project.priority} Queued — waiting for Tiger
) } // ─── Main page ──────────────────────────────────────────────────────────────── export default function ProjectsPage() { const { data: fileData, isLoading: fileLoading } = useSWR<{ ok: boolean; projects: FileProject[] }>("/api/tiger/file-tasks/projects", fetcher, { refreshInterval: 60_000 }) const { data: dbData, isLoading: dbLoading, mutate: mutateDb } = useSWR<{ ok: boolean; projects: DbProject[] }>("/api/tiger/projects", fetcher, { refreshInterval: 60_000 }) const fileProjects = fileData?.projects ?? [] const dbProjects = dbData?.projects ?? [] const isLoading = fileLoading || dbLoading const [isCreateOpen, setIsCreateOpen] = React.useState(false) const [form, setForm] = React.useState({ name: "", seed: "", description: "", priority: "medium" }) const [creating, setCreating] = React.useState(false) const [createError, setCreateError] = React.useState("") const handleCreate = async () => { const payload: Record = { priority: form.priority } if (form.name.trim()) payload.name = form.name.trim() if (form.seed.trim()) payload.seed = form.seed.trim() if (form.description.trim()) payload.description = form.description.trim() if (!payload.name && !payload.seed) { setCreateError("Enter a project name or seed text."); return } setCreating(true); setCreateError("") try { const res = await fetch("/api/tiger/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) const result = await res.json() if (!result.ok) throw new Error(result.error ?? "Failed") setForm({ name: "", seed: "", description: "", priority: "medium" }) setIsCreateOpen(false) mutateDb() } catch (e: any) { setCreateError(e.message) } finally { setCreating(false) } } const handleDelete = async (id: string) => { await fetch(`/api/tiger/projects/${id}`, { method: "DELETE" }) mutateDb() } return (
{/* Header */}

Projects

Tiger's projects from PROJECTS.md. Click a project to see its tasks and agents.

Queue a Project for Tiger Tiger will pick this up and add it to PROJECTS.md.
setForm((f) => ({ ...f, name: e.target.value }))} placeholder="e.g. BESS Economics Model" className="mt-1" />