diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index c45593f..bf32dbc 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { X } from "lucide-react"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { TaskBoard } from "@/components/organisms/TaskBoard"; @@ -27,6 +28,7 @@ import { } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { getApiBaseUrl } from "@/lib/api-base"; +import { cn } from "@/lib/utils"; type Board = { id: string; @@ -47,6 +49,7 @@ type Task = { type Agent = { id: string; name: string; + status: string; board_id?: string | null; }; @@ -82,7 +85,7 @@ export default function BoardDetailPage() { const [comments, setComments] = useState([]); const [isCommentsLoading, setIsCommentsLoading] = useState(false); const [commentsError, setCommentsError] = useState(null); - const [isCommentsOpen, setIsCommentsOpen] = useState(false); + const [isDetailOpen, setIsDetailOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); const [title, setTitle] = useState(""); @@ -216,6 +219,35 @@ export default function BoardDetailPage() { [tasks, assigneeById], ); + const boardAgents = useMemo( + () => agents.filter((agent) => !boardId || agent.board_id === boardId), + [agents, boardId], + ); + + const workingAgentIds = useMemo(() => { + const working = new Set(); + tasks.forEach((task) => { + if (task.status === "in_progress" && task.assigned_agent_id) { + working.add(task.assigned_agent_id); + } + }); + return working; + }, [tasks]); + + const sortedAgents = useMemo(() => { + const rank = (agent: Agent) => { + if (workingAgentIds.has(agent.id)) return 0; + if (agent.status === "online") return 1; + if (agent.status === "provisioning") return 2; + return 3; + }; + return [...boardAgents].sort((a, b) => { + const diff = rank(a) - rank(b); + if (diff !== 0) return diff; + return a.name.localeCompare(b.name); + }); + }, [boardAgents, workingAgentIds]); + const loadComments = async (taskId: string) => { if (!isSignedIn || !boardId) return; setIsCommentsLoading(true); @@ -242,17 +274,33 @@ export default function BoardDetailPage() { const openComments = (task: Task) => { setSelectedTask(task); - setIsCommentsOpen(true); + setIsDetailOpen(true); void loadComments(task.id); }; const closeComments = () => { - setIsCommentsOpen(false); + setIsDetailOpen(false); setSelectedTask(null); setComments([]); setCommentsError(null); }; + const agentInitials = (name: string) => + name + .split(" ") + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]) + .join("") + .toUpperCase(); + + const agentStatusLabel = (agent: Agent) => { + if (workingAgentIds.has(agent.id)) return "Working"; + if (agent.status === "online") return "Active"; + if (agent.status === "provisioning") return "Provisioning"; + return "Offline"; + }; + const formatCommentTimestamp = (value: string) => { const date = new Date(value); if (Number.isNaN(date.getTime())) return "—"; @@ -318,62 +366,147 @@ export default function BoardDetailPage() { -
- {error && ( -
- {error} +
+ - {isLoading ? ( -
- Loading {titleLabel}… -
- ) : ( - setIsDialogOpen(true)} - isCreateDisabled={isCreating} - onTaskSelect={openComments} - /> - )} +
+ {error && ( +
+ {error} +
+ )} + + {isLoading ? ( +
+ Loading {titleLabel}… +
+ ) : ( + setIsDialogOpen(true)} + isCreateDisabled={isCreating} + onTaskSelect={openComments} + /> + )} +
- - { - if (!open) { - closeComments(); - } - }}> - - - {selectedTask?.title ?? "Task"} - - {selectedTask?.description || "Task details and discussion."} - - -
+ {isDetailOpen ? ( +
+ ) : null} +
+
+