"use client"; export const dynamic = "force-dynamic"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { Activity, ArrowUpRight, MessageSquare, Pause, Plus, Pencil, Play, RefreshCcw, Settings, ShieldCheck, X, } from "lucide-react"; import { Markdown } from "@/components/atoms/Markdown"; import { StatusDot } from "@/components/atoms/StatusDot"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { TaskBoard } from "@/components/organisms/TaskBoard"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { BoardChatComposer } from "@/components/BoardChatComposer"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import DropdownSelect, { type DropdownSelectOption, } from "@/components/ui/dropdown-select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { ApiError } from "@/api/mutator"; import { streamAgentsApiV1AgentsStreamGet } from "@/api/generated/agents/agents"; import { streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet, updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch, } from "@/api/generated/approvals/approvals"; import { listTaskCommentFeedApiV1ActivityTaskCommentsGet, streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet, } from "@/api/generated/activity/activity"; import { getBoardGroupSnapshotApiV1BoardsBoardIdGroupSnapshotGet, getBoardSnapshotApiV1BoardsBoardIdSnapshotGet, } from "@/api/generated/boards/boards"; import { createBoardMemoryApiV1BoardsBoardIdMemoryPost, streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet, } from "@/api/generated/board-memory/board-memory"; import { type getMyMembershipApiV1OrganizationsMeMemberGetResponse, useGetMyMembershipApiV1OrganizationsMeMemberGet, } from "@/api/generated/organizations/organizations"; import { createTaskApiV1BoardsBoardIdTasksPost, createTaskCommentApiV1BoardsBoardIdTasksTaskIdCommentsPost, deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete, listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet, streamTasksApiV1BoardsBoardIdTasksStreamGet, updateTaskApiV1BoardsBoardIdTasksTaskIdPatch, } from "@/api/generated/tasks/tasks"; import type { AgentRead, ApprovalRead, BoardGroupSnapshot, BoardMemoryRead, BoardRead, OrganizationMemberRead, TaskCardRead, TaskCommentRead, TaskRead, } from "@/api/generated/model"; import { createExponentialBackoff } from "@/lib/backoff"; import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime"; import { cn } from "@/lib/utils"; import { usePageActive } from "@/hooks/usePageActive"; type Board = BoardRead; type TaskStatus = Exclude; type Task = Omit< TaskCardRead, "status" | "priority" | "approvals_count" | "approvals_pending_count" > & { status: TaskStatus; priority: string; approvals_count: number; approvals_pending_count: number; }; type Agent = AgentRead & { status: string }; type TaskComment = TaskCommentRead; type Approval = ApprovalRead & { status: string }; type BoardChatMessage = BoardMemoryRead; const normalizeTask = (task: TaskCardRead): Task => ({ ...task, status: task.status ?? "inbox", priority: task.priority ?? "medium", approvals_count: task.approvals_count ?? 0, approvals_pending_count: task.approvals_pending_count ?? 0, }); const normalizeAgent = (agent: AgentRead): Agent => ({ ...agent, status: agent.status ?? "offline", }); const normalizeApproval = (approval: ApprovalRead): Approval => ({ ...approval, status: approval.status ?? "pending", }); const priorities = [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, ]; const statusOptions = [ { value: "inbox", label: "Inbox" }, { value: "in_progress", label: "In progress" }, { value: "review", label: "Review" }, { value: "done", label: "Done" }, ]; const EMOJI_GLYPHS: Record = { ":gear:": "⚙️", ":sparkles:": "✨", ":rocket:": "🚀", ":megaphone:": "📣", ":chart_with_upwards_trend:": "📈", ":bulb:": "💡", ":wrench:": "🔧", ":shield:": "🛡️", ":memo:": "📝", ":brain:": "🧠", }; const SSE_RECONNECT_BACKOFF = { baseMs: 1_000, factor: 2, jitter: 0.2, maxMs: 5 * 60_000, } as const; const formatShortTimestamp = (value: string) => { const date = parseApiDatetime(value); if (!date) return "—"; return date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; type ToastMessage = { id: number; message: string; tone: "error" | "success"; }; const formatActionError = (err: unknown, fallback: string) => { if (err instanceof ApiError) { if (err.status === 403) { return "Read-only access. You do not have permission to make changes."; } return err.message || fallback; } if (err instanceof Error && err.message) { return err.message; } return fallback; }; const resolveBoardAccess = ( member: OrganizationMemberRead | null, boardId?: string | null, ) => { if (!member || !boardId) { return { canRead: false, canWrite: false }; } if (member.all_boards_write) { return { canRead: true, canWrite: true }; } if (member.all_boards_read) { return { canRead: true, canWrite: false }; } const entry = member.board_access?.find( (access) => access.board_id === boardId, ); if (!entry) { return { canRead: false, canWrite: false }; } const canWrite = Boolean(entry.can_write); const canRead = Boolean(entry.can_read || entry.can_write); return { canRead, canWrite }; }; const TaskCommentCard = memo(function TaskCommentCard({ comment, authorLabel, }: { comment: TaskComment; authorLabel: string; }) { const message = (comment.message ?? "").trim(); return (
{authorLabel} {formatShortTimestamp(comment.created_at)}
{message ? (
) : (

)}
); }); TaskCommentCard.displayName = "TaskCommentCard"; const ChatMessageCard = memo(function ChatMessageCard({ message, }: { message: BoardChatMessage; }) { return (

{message.source ?? "User"}

{formatShortTimestamp(message.created_at)}
); }); ChatMessageCard.displayName = "ChatMessageCard"; const LiveFeedCard = memo(function LiveFeedCard({ comment, taskTitle, authorName, authorRole, authorAvatar, onViewTask, isNew, }: { comment: TaskComment; taskTitle: string; authorName: string; authorRole?: string | null; authorAvatar: string; onViewTask?: () => void; isNew?: boolean; }) { const message = (comment.message ?? "").trim(); return (
{authorAvatar}
{onViewTask ? ( ) : null}
{authorName} {authorRole ? ( <> · {authorRole} ) : null} · {formatShortTimestamp(comment.created_at)}
{message ? (
) : (

)}
); }); LiveFeedCard.displayName = "LiveFeedCard"; export default function BoardDetailPage() { const router = useRouter(); const params = useParams(); const searchParams = useSearchParams(); const boardIdParam = params?.boardId; const boardId = Array.isArray(boardIdParam) ? boardIdParam[0] : boardIdParam; const { isSignedIn } = useAuth(); const isPageActive = usePageActive(); const taskIdFromUrl = searchParams.get("taskId"); const membershipQuery = useGetMyMembershipApiV1OrganizationsMeMemberGet< getMyMembershipApiV1OrganizationsMeMemberGetResponse, ApiError >({ query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", }, }); const boardAccess = useMemo( () => resolveBoardAccess( membershipQuery.data?.status === 200 ? membershipQuery.data.data : null, boardId, ), [membershipQuery.data, boardId], ); const isOrgAdmin = useMemo(() => { const member = membershipQuery.data?.status === 200 ? membershipQuery.data.data : null; return member ? ["owner", "admin"].includes(member.role) : false; }, [membershipQuery.data]); const canWrite = boardAccess.canWrite; const [board, setBoard] = useState(null); const [tasks, setTasks] = useState([]); const [agents, setAgents] = useState([]); const [groupSnapshot, setGroupSnapshot] = useState( null, ); const [groupSnapshotError, setGroupSnapshotError] = useState( null, ); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const selectedTaskIdRef = useRef(null); const openedTaskIdFromUrlRef = useRef(null); const [comments, setComments] = useState([]); const [liveFeed, setLiveFeed] = useState([]); const liveFeedRef = useRef([]); const liveFeedFlashTimersRef = useRef>({}); const [liveFeedFlashIds, setLiveFeedFlashIds] = useState< Record >({}); const [isLiveFeedHistoryLoading, setIsLiveFeedHistoryLoading] = useState(false); const [liveFeedHistoryError, setLiveFeedHistoryError] = useState< string | null >(null); const liveFeedHistoryLoadedRef = useRef(false); const [isCommentsLoading, setIsCommentsLoading] = useState(false); const [commentsError, setCommentsError] = useState(null); const [newComment, setNewComment] = useState(""); const taskCommentInputRef = useRef(null); const [isPostingComment, setIsPostingComment] = useState(false); const [postCommentError, setPostCommentError] = useState(null); const [isDetailOpen, setIsDetailOpen] = useState(false); const tasksRef = useRef([]); const approvalsRef = useRef([]); const agentsRef = useRef([]); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [approvals, setApprovals] = useState([]); const [isApprovalsLoading, setIsApprovalsLoading] = useState(false); const [approvalsError, setApprovalsError] = useState(null); const [approvalsUpdatingId, setApprovalsUpdatingId] = useState( null, ); const [isChatOpen, setIsChatOpen] = useState(false); const [chatMessages, setChatMessages] = useState([]); const [isChatSending, setIsChatSending] = useState(false); const [chatError, setChatError] = useState(null); const chatMessagesRef = useRef([]); const chatEndRef = useRef(null); const [isAgentsControlDialogOpen, setIsAgentsControlDialogOpen] = useState(false); const [agentsControlAction, setAgentsControlAction] = useState< "pause" | "resume" >("pause"); const [isAgentsControlSending, setIsAgentsControlSending] = useState(false); const [agentsControlError, setAgentsControlError] = useState( null, ); const [isDeletingTask, setIsDeletingTask] = useState(false); const [deleteTaskError, setDeleteTaskError] = useState(null); const [viewMode, setViewMode] = useState<"board" | "list">("board"); const [isLiveFeedOpen, setIsLiveFeedOpen] = useState(false); const [toasts, setToasts] = useState([]); const isLiveFeedOpenRef = useRef(false); const toastIdRef = useRef(0); const toastTimersRef = useRef>({}); const pushLiveFeed = useCallback((comment: TaskComment) => { const alreadySeen = liveFeedRef.current.some( (item) => item.id === comment.id, ); setLiveFeed((prev) => { if (prev.some((item) => item.id === comment.id)) { return prev; } const next = [comment, ...prev]; return next.slice(0, 50); }); if (alreadySeen) return; if (!isLiveFeedOpenRef.current) return; setLiveFeedFlashIds((prev) => prev[comment.id] ? prev : { ...prev, [comment.id]: true }, ); if (typeof window === "undefined") return; const existingTimer = liveFeedFlashTimersRef.current[comment.id]; if (existingTimer !== undefined) { window.clearTimeout(existingTimer); } liveFeedFlashTimersRef.current[comment.id] = window.setTimeout(() => { delete liveFeedFlashTimersRef.current[comment.id]; setLiveFeedFlashIds((prev) => { if (!prev[comment.id]) return prev; const next = { ...prev }; delete next[comment.id]; return next; }); }, 2200); }, []); const dismissToast = useCallback((id: number) => { setToasts((prev) => prev.filter((toast) => toast.id !== id)); const timer = toastTimersRef.current[id]; if (timer !== undefined) { window.clearTimeout(timer); delete toastTimersRef.current[id]; } }, []); const pushToast = useCallback( (message: string, tone: ToastMessage["tone"] = "error") => { const trimmed = message.trim(); if (!trimmed) return; const id = toastIdRef.current + 1; toastIdRef.current = id; setToasts((prev) => [...prev, { id, message: trimmed, tone }]); if (typeof window !== "undefined") { toastTimersRef.current[id] = window.setTimeout(() => { dismissToast(id); }, 3500); } }, [dismissToast], ); useEffect(() => { liveFeedHistoryLoadedRef.current = false; setIsLiveFeedHistoryLoading(false); setLiveFeedHistoryError(null); setLiveFeed([]); setLiveFeedFlashIds({}); if (typeof window !== "undefined") { Object.values(liveFeedFlashTimersRef.current).forEach((timerId) => { window.clearTimeout(timerId); }); } liveFeedFlashTimersRef.current = {}; }, [boardId]); useEffect(() => { return () => { if (typeof window !== "undefined") { Object.values(liveFeedFlashTimersRef.current).forEach((timerId) => { window.clearTimeout(timerId); }); } liveFeedFlashTimersRef.current = {}; }; }, []); useEffect(() => { return () => { if (typeof window !== "undefined") { Object.values(toastTimersRef.current).forEach((timerId) => { window.clearTimeout(timerId); }); } toastTimersRef.current = {}; }; }, []); useEffect(() => { if (!isLiveFeedOpen) return; if (!isSignedIn || !boardId) return; if (liveFeedHistoryLoadedRef.current) return; let cancelled = false; setIsLiveFeedHistoryLoading(true); setLiveFeedHistoryError(null); const fetchHistory = async () => { try { const result = await listTaskCommentFeedApiV1ActivityTaskCommentsGet({ board_id: boardId, limit: 200, }); if (cancelled) return; if (result.status !== 200) { throw new Error("Unable to load live feed."); } const items = result.data.items ?? []; liveFeedHistoryLoadedRef.current = true; const mapped: TaskComment[] = items.map((item) => ({ id: item.id, message: item.message ?? null, agent_id: item.agent_id ?? null, task_id: item.task_id ?? null, created_at: item.created_at, })); setLiveFeed((prev) => { const map = new Map(); [...prev, ...mapped].forEach((item) => map.set(item.id, item)); const merged = [...map.values()]; merged.sort((a, b) => { const aTime = apiDatetimeToMs(a.created_at) ?? 0; const bTime = apiDatetimeToMs(b.created_at) ?? 0; return bTime - aTime; }); return merged.slice(0, 50); }); } catch (err) { if (cancelled) return; setLiveFeedHistoryError( err instanceof Error ? err.message : "Unable to load live feed.", ); } finally { if (cancelled) return; setIsLiveFeedHistoryLoading(false); } }; void fetchHistory(); return () => { cancelled = true; }; }, [boardId, isLiveFeedOpen, isSignedIn]); const [isDialogOpen, setIsDialogOpen] = useState(false); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [priority, setPriority] = useState("medium"); const [createError, setCreateError] = useState(null); const [isCreating, setIsCreating] = useState(false); const [editTitle, setEditTitle] = useState(""); const [editDescription, setEditDescription] = useState(""); const [editStatus, setEditStatus] = useState("inbox"); const [editPriority, setEditPriority] = useState("medium"); const [editAssigneeId, setEditAssigneeId] = useState(""); const [editDependsOnTaskIds, setEditDependsOnTaskIds] = useState( [], ); const [isSavingTask, setIsSavingTask] = useState(false); const [saveTaskError, setSaveTaskError] = useState(null); const isSidePanelOpen = isDetailOpen || isChatOpen || isLiveFeedOpen; const titleLabel = useMemo( () => (board ? `${board.name} board` : "Board"), [board], ); useEffect(() => { if (!isSidePanelOpen) return; const { body, documentElement } = document; const originalHtmlOverflow = documentElement.style.overflow; const originalBodyOverflow = body.style.overflow; const originalBodyPaddingRight = body.style.paddingRight; const scrollbarWidth = window.innerWidth - documentElement.clientWidth; documentElement.style.overflow = "hidden"; body.style.overflow = "hidden"; if (scrollbarWidth > 0) { body.style.paddingRight = `${scrollbarWidth}px`; } return () => { documentElement.style.overflow = originalHtmlOverflow; body.style.overflow = originalBodyOverflow; body.style.paddingRight = originalBodyPaddingRight; }; }, [isSidePanelOpen]); const latestTaskTimestamp = (items: Task[]) => { let latestTime = 0; items.forEach((task) => { const value = task.updated_at ?? task.created_at; if (!value) return; const time = apiDatetimeToMs(value); if (time !== null && time > latestTime) { latestTime = time; } }); return latestTime ? new Date(latestTime).toISOString() : null; }; const latestApprovalTimestamp = (items: Approval[]) => { let latestTime = 0; items.forEach((approval) => { const value = approval.resolved_at ?? approval.created_at; if (!value) return; const time = apiDatetimeToMs(value); if (time !== null && time > latestTime) { latestTime = time; } }); return latestTime ? new Date(latestTime).toISOString() : null; }; const latestAgentTimestamp = (items: Agent[]) => { let latestTime = 0; items.forEach((agent) => { const value = agent.updated_at ?? agent.last_seen_at; if (!value) return; const time = apiDatetimeToMs(value); if (time !== null && time > latestTime) { latestTime = time; } }); return latestTime ? new Date(latestTime).toISOString() : null; }; const loadBoard = useCallback(async () => { if (!isSignedIn || !boardId) return; setIsLoading(true); setIsApprovalsLoading(true); setError(null); setApprovalsError(null); setChatError(null); setGroupSnapshotError(null); try { const snapshotResult = await getBoardSnapshotApiV1BoardsBoardIdSnapshotGet(boardId); if (snapshotResult.status !== 200) { throw new Error("Unable to load board snapshot."); } const snapshot = snapshotResult.data; setBoard(snapshot.board); setTasks((snapshot.tasks ?? []).map(normalizeTask)); setAgents((snapshot.agents ?? []).map(normalizeAgent)); setApprovals((snapshot.approvals ?? []).map(normalizeApproval)); setChatMessages(snapshot.chat_messages ?? []); try { const groupResult = await getBoardGroupSnapshotApiV1BoardsBoardIdGroupSnapshotGet( boardId, { include_self: false, include_done: false, per_board_task_limit: 5, }, ); if (groupResult.status === 200) { setGroupSnapshot(groupResult.data); } else { setGroupSnapshot(null); } } catch (groupErr) { const message = groupErr instanceof Error ? groupErr.message : "Unable to load board group snapshot."; setGroupSnapshotError(message); setGroupSnapshot(null); } } catch (err) { const message = err instanceof Error ? err.message : "Something went wrong."; setError(message); setApprovalsError(message); setChatError(message); setGroupSnapshotError(message); setGroupSnapshot(null); } finally { setIsLoading(false); setIsApprovalsLoading(false); } }, [boardId, isSignedIn]); useEffect(() => { void loadBoard(); }, [loadBoard]); useEffect(() => { tasksRef.current = tasks; }, [tasks]); useEffect(() => { approvalsRef.current = approvals; }, [approvals]); useEffect(() => { agentsRef.current = agents; }, [agents]); useEffect(() => { selectedTaskIdRef.current = selectedTask?.id ?? null; }, [selectedTask?.id]); useEffect(() => { chatMessagesRef.current = chatMessages; }, [chatMessages]); useEffect(() => { liveFeedRef.current = liveFeed; }, [liveFeed]); useEffect(() => { isLiveFeedOpenRef.current = isLiveFeedOpen; }, [isLiveFeedOpen]); useEffect(() => { if (!isChatOpen) return; const timeout = window.setTimeout(() => { chatEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); }, 50); return () => window.clearTimeout(timeout); }, [chatMessages, isChatOpen]); const latestChatTimestamp = (items: BoardChatMessage[]) => { if (!items.length) return undefined; const latest = items.reduce((max, item) => { const ts = apiDatetimeToMs(item.created_at); return ts === null ? max : Math.max(max, ts); }, 0); if (!latest) return undefined; return new Date(latest).toISOString(); }; const lastAgentControlCommand = useMemo(() => { for (let i = chatMessages.length - 1; i >= 0; i -= 1) { const value = (chatMessages[i]?.content ?? "").trim().toLowerCase(); if (value === "/pause" || value === "/resume") { return value; } } return null; }, [chatMessages]); const isAgentsPaused = lastAgentControlCommand === "/pause"; useEffect(() => { if (!isPageActive) return; if (!isSignedIn || !boardId || !board) return; if (!isChatOpen) return; let isCancelled = false; const abortController = new AbortController(); const backoff = createExponentialBackoff(SSE_RECONNECT_BACKOFF); let reconnectTimeout: number | undefined; const connect = async () => { try { const since = latestChatTimestamp(chatMessagesRef.current); const params = { is_chat: true, ...(since ? { since } : {}) }; const streamResult = await streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet( boardId, params, { headers: { Accept: "text/event-stream" }, signal: abortController.signal, }, ); if (streamResult.status !== 200) { throw new Error("Unable to connect board chat stream."); } const response = streamResult.data as Response; if (!(response instanceof Response) || !response.body) { throw new Error("Unable to connect board chat stream."); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (!isCancelled) { const { value, done } = await reader.read(); if (done) break; if (value && value.length) { // Consider the stream "healthy" once we receive any bytes (including pings), // then reset the backoff for future reconnects. backoff.reset(); } buffer += decoder.decode(value, { stream: true }); buffer = buffer.replace(/\r\n/g, "\n"); let boundary = buffer.indexOf("\n\n"); while (boundary !== -1) { const raw = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); const lines = raw.split("\n"); let eventType = "message"; let data = ""; for (const line of lines) { if (line.startsWith("event:")) { eventType = line.slice(6).trim(); } else if (line.startsWith("data:")) { data += line.slice(5).trim(); } } if (eventType === "memory" && data) { try { const payload = JSON.parse(data) as { memory?: BoardChatMessage; }; if (payload.memory?.tags?.includes("chat")) { setChatMessages((prev) => { const exists = prev.some( (item) => item.id === payload.memory?.id, ); if (exists) return prev; const next = [...prev, payload.memory as BoardChatMessage]; next.sort((a, b) => { const aTime = apiDatetimeToMs(a.created_at) ?? 0; const bTime = apiDatetimeToMs(b.created_at) ?? 0; return aTime - bTime; }); return next; }); } } catch { // ignore malformed } } boundary = buffer.indexOf("\n\n"); } } } catch { // Reconnect handled below. } if (!isCancelled) { if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } const delay = backoff.nextDelayMs(); reconnectTimeout = window.setTimeout(() => { reconnectTimeout = undefined; void connect(); }, delay); } }; void connect(); return () => { isCancelled = true; abortController.abort(); if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } }; }, [board, boardId, isChatOpen, isPageActive, isSignedIn]); useEffect(() => { if (!isPageActive) return; if (!isLiveFeedOpen) return; if (!isSignedIn || !boardId) return; let isCancelled = false; const abortController = new AbortController(); const backoff = createExponentialBackoff(SSE_RECONNECT_BACKOFF); let reconnectTimeout: number | undefined; const connect = async () => { try { const since = (() => { let latestTime = 0; liveFeedRef.current.forEach((comment) => { const time = apiDatetimeToMs(comment.created_at); if (time !== null && time > latestTime) { latestTime = time; } }); return latestTime ? new Date(latestTime).toISOString() : null; })(); const streamResult = await streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet( { board_id: boardId, since: since ?? null, }, { headers: { Accept: "text/event-stream" }, signal: abortController.signal, }, ); if (streamResult.status !== 200) { throw new Error("Unable to connect live feed stream."); } const response = streamResult.data as Response; if (!(response instanceof Response) || !response.body) { throw new Error("Unable to connect live feed stream."); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (!isCancelled) { const { value, done } = await reader.read(); if (done) break; if (value && value.length) { backoff.reset(); } buffer += decoder.decode(value, { stream: true }); buffer = buffer.replace(/\r\n/g, "\n"); let boundary = buffer.indexOf("\n\n"); while (boundary !== -1) { const raw = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); const lines = raw.split("\n"); let eventType = "message"; let data = ""; for (const line of lines) { if (line.startsWith("event:")) { eventType = line.slice(6).trim(); } else if (line.startsWith("data:")) { data += line.slice(5).trim(); } } if (eventType === "comment" && data) { try { const payload = JSON.parse(data) as { comment?: { id: string; created_at: string; message?: string | null; agent_id?: string | null; task_id?: string | null; }; }; if (payload.comment) { pushLiveFeed({ id: payload.comment.id, created_at: payload.comment.created_at, message: payload.comment.message ?? null, agent_id: payload.comment.agent_id ?? null, task_id: payload.comment.task_id ?? null, }); } } catch { // ignore malformed } } boundary = buffer.indexOf("\n\n"); } } } catch { // Reconnect handled below. } if (!isCancelled) { if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } const delay = backoff.nextDelayMs(); reconnectTimeout = window.setTimeout(() => { reconnectTimeout = undefined; void connect(); }, delay); } }; void connect(); return () => { isCancelled = true; abortController.abort(); if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } }; }, [boardId, isLiveFeedOpen, isPageActive, isSignedIn, pushLiveFeed]); useEffect(() => { if (!isPageActive) return; if (!isSignedIn || !boardId || !board) return; let isCancelled = false; const abortController = new AbortController(); const backoff = createExponentialBackoff(SSE_RECONNECT_BACKOFF); let reconnectTimeout: number | undefined; const connect = async () => { try { const since = latestApprovalTimestamp(approvalsRef.current); const streamResult = await streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet( boardId, since ? { since } : undefined, { headers: { Accept: "text/event-stream" }, signal: abortController.signal, }, ); if (streamResult.status !== 200) { throw new Error("Unable to connect approvals stream."); } const response = streamResult.data as Response; if (!(response instanceof Response) || !response.body) { throw new Error("Unable to connect approvals stream."); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (!isCancelled) { const { value, done } = await reader.read(); if (done) break; if (value && value.length) { backoff.reset(); } buffer += decoder.decode(value, { stream: true }); buffer = buffer.replace(/\r\n/g, "\n"); let boundary = buffer.indexOf("\n\n"); while (boundary !== -1) { const raw = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); const lines = raw.split("\n"); let eventType = "message"; let data = ""; for (const line of lines) { if (line.startsWith("event:")) { eventType = line.slice(6).trim(); } else if (line.startsWith("data:")) { data += line.slice(5).trim(); } } if (eventType === "approval" && data) { try { const payload = JSON.parse(data) as { approval?: ApprovalRead; task_counts?: | { task_id?: string; approvals_count?: number; approvals_pending_count?: number; } | Array<{ task_id?: string; approvals_count?: number; approvals_pending_count?: number; }>; pending_approvals_count?: number; }; if (payload.approval) { const normalized = normalizeApproval(payload.approval); setApprovals((prev) => { const index = prev.findIndex( (item) => item.id === normalized.id, ); if (index === -1) { return [normalized, ...prev]; } const next = [...prev]; next[index] = { ...next[index], ...normalized, }; return next; }); } const taskCounts = Array.isArray(payload.task_counts) ? payload.task_counts : payload.task_counts ? [payload.task_counts] : []; if (taskCounts.length > 0) { setTasks((prev) => { const countsByTaskId = new Map( taskCounts .filter((row) => Boolean(row.task_id)) .map((row) => [row.task_id as string, row]), ); return prev.map((task) => { const counts = countsByTaskId.get(task.id); if (!counts) return task; return { ...task, approvals_count: counts.approvals_count ?? task.approvals_count, approvals_pending_count: counts.approvals_pending_count ?? task.approvals_pending_count, }; }); }); } } catch { // Ignore malformed payloads. } } boundary = buffer.indexOf("\n\n"); } } } catch { // Reconnect handled below. } if (!isCancelled) { if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } const delay = backoff.nextDelayMs(); reconnectTimeout = window.setTimeout(() => { reconnectTimeout = undefined; void connect(); }, delay); } }; void connect(); return () => { isCancelled = true; abortController.abort(); if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } }; }, [board, boardId, isPageActive, isSignedIn]); useEffect(() => { if (!selectedTask) { setEditTitle(""); setEditDescription(""); setEditStatus("inbox"); setEditPriority("medium"); setEditAssigneeId(""); setEditDependsOnTaskIds([]); setSaveTaskError(null); return; } setEditTitle(selectedTask.title); setEditDescription(selectedTask.description ?? ""); setEditStatus(selectedTask.status); setEditPriority(selectedTask.priority); setEditAssigneeId(selectedTask.assigned_agent_id ?? ""); setEditDependsOnTaskIds(selectedTask.depends_on_task_ids ?? []); setSaveTaskError(null); }, [selectedTask]); useEffect(() => { if (!isPageActive) return; if (!isSignedIn || !boardId || !board) return; let isCancelled = false; const abortController = new AbortController(); const backoff = createExponentialBackoff(SSE_RECONNECT_BACKOFF); let reconnectTimeout: number | undefined; const connect = async () => { try { const since = latestTaskTimestamp(tasksRef.current); const streamResult = await streamTasksApiV1BoardsBoardIdTasksStreamGet( boardId, since ? { since } : undefined, { headers: { Accept: "text/event-stream" }, signal: abortController.signal, }, ); if (streamResult.status !== 200) { throw new Error("Unable to connect task stream."); } const response = streamResult.data as Response; if (!(response instanceof Response) || !response.body) { throw new Error("Unable to connect task stream."); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (!isCancelled) { const { value, done } = await reader.read(); if (done) break; if (value && value.length) { backoff.reset(); } buffer += decoder.decode(value, { stream: true }); buffer = buffer.replace(/\r\n/g, "\n"); let boundary = buffer.indexOf("\n\n"); while (boundary !== -1) { const raw = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); const lines = raw.split("\n"); let eventType = "message"; let data = ""; for (const line of lines) { if (line.startsWith("event:")) { eventType = line.slice(6).trim(); } else if (line.startsWith("data:")) { data += line.slice(5).trim(); } } if (eventType === "task" && data) { try { const payload = JSON.parse(data) as { type?: string; task?: TaskRead; comment?: TaskCommentRead; }; if ( payload.comment?.task_id && payload.type === "task.comment" ) { pushLiveFeed(payload.comment); setComments((prev) => { if ( selectedTaskIdRef.current !== payload.comment?.task_id ) { return prev; } const exists = prev.some( (item) => item.id === payload.comment?.id, ); if (exists) { return prev; } const createdMs = apiDatetimeToMs( payload.comment?.created_at, ); if (prev.length === 0 || createdMs === null) { return [payload.comment as TaskComment, ...prev]; } const first = prev[0]; const firstMs = apiDatetimeToMs(first?.created_at); if (firstMs !== null && createdMs >= firstMs) { return [payload.comment as TaskComment, ...prev]; } const last = prev[prev.length - 1]; const lastMs = apiDatetimeToMs(last?.created_at); if (lastMs !== null && createdMs <= lastMs) { return [...prev, payload.comment as TaskComment]; } const next = [...prev, payload.comment as TaskComment]; next.sort((a, b) => { const aTime = apiDatetimeToMs(a.created_at) ?? 0; const bTime = apiDatetimeToMs(b.created_at) ?? 0; return bTime - aTime; }); return next; }); } else if (payload.task) { setTasks((prev) => { const index = prev.findIndex( (item) => item.id === payload.task?.id, ); if (index === -1) { const assignee = payload.task?.assigned_agent_id ? (agentsRef.current.find( (agent) => agent.id === payload.task?.assigned_agent_id, )?.name ?? null) : null; const created = normalizeTask({ ...payload.task, assignee, approvals_count: 0, approvals_pending_count: 0, } as TaskCardRead); return [created, ...prev]; } const next = [...prev]; const existing = next[index]; const assignee = payload.task?.assigned_agent_id ? (agentsRef.current.find( (agent) => agent.id === payload.task?.assigned_agent_id, )?.name ?? null) : null; const updated = normalizeTask({ ...existing, ...payload.task, assignee, approvals_count: existing.approvals_count, approvals_pending_count: existing.approvals_pending_count, } as TaskCardRead); next[index] = { ...existing, ...updated }; return next; }); } } catch { // Ignore malformed payloads. } } boundary = buffer.indexOf("\n\n"); } } } catch { // Reconnect handled below. } if (!isCancelled) { if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } const delay = backoff.nextDelayMs(); reconnectTimeout = window.setTimeout(() => { reconnectTimeout = undefined; void connect(); }, delay); } }; void connect(); return () => { isCancelled = true; abortController.abort(); if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } }; }, [board, boardId, isPageActive, isSignedIn, pushLiveFeed]); useEffect(() => { if (!isPageActive) return; if (!isSignedIn || !boardId || !isOrgAdmin) return; let isCancelled = false; const abortController = new AbortController(); const backoff = createExponentialBackoff(SSE_RECONNECT_BACKOFF); let reconnectTimeout: number | undefined; const connect = async () => { try { const since = latestAgentTimestamp(agentsRef.current); const streamResult = await streamAgentsApiV1AgentsStreamGet( { board_id: boardId, since: since ?? null, }, { headers: { Accept: "text/event-stream" }, signal: abortController.signal, }, ); if (streamResult.status !== 200) { throw new Error("Unable to connect agent stream."); } const response = streamResult.data as Response; if (!(response instanceof Response) || !response.body) { throw new Error("Unable to connect agent stream."); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (!isCancelled) { const { value, done } = await reader.read(); if (done) break; if (value && value.length) { backoff.reset(); } buffer += decoder.decode(value, { stream: true }); buffer = buffer.replace(/\r\n/g, "\n"); let boundary = buffer.indexOf("\n\n"); while (boundary !== -1) { const raw = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); const lines = raw.split("\n"); let eventType = "message"; let data = ""; for (const line of lines) { if (line.startsWith("event:")) { eventType = line.slice(6).trim(); } else if (line.startsWith("data:")) { data += line.slice(5).trim(); } } if (eventType === "agent" && data) { try { const payload = JSON.parse(data) as { agent?: AgentRead }; if (payload.agent) { const normalized = normalizeAgent(payload.agent); setAgents((prev) => { const index = prev.findIndex( (item) => item.id === normalized.id, ); if (index === -1) { return [normalized, ...prev]; } const next = [...prev]; next[index] = { ...next[index], ...normalized, }; return next; }); } } catch { // Ignore malformed payloads. } } boundary = buffer.indexOf("\n\n"); } } } catch { // Reconnect handled below. } if (!isCancelled) { if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } const delay = backoff.nextDelayMs(); reconnectTimeout = window.setTimeout(() => { reconnectTimeout = undefined; void connect(); }, delay); } }; void connect(); return () => { isCancelled = true; abortController.abort(); if (reconnectTimeout !== undefined) { window.clearTimeout(reconnectTimeout); } }; }, [board, boardId, isOrgAdmin, isPageActive, isSignedIn]); const resetForm = () => { setTitle(""); setDescription(""); setPriority("medium"); setCreateError(null); }; const handleCreateTask = async () => { if (!isSignedIn || !boardId) return; const trimmed = title.trim(); if (!trimmed) { setCreateError("Add a task title to continue."); return; } setIsCreating(true); setCreateError(null); try { const result = await createTaskApiV1BoardsBoardIdTasksPost(boardId, { title: trimmed, description: description.trim() || null, status: "inbox", priority, }); if (result.status !== 200) throw new Error("Unable to create task."); const created = normalizeTask({ ...result.data, assignee: result.data.assigned_agent_id ? (assigneeById.get(result.data.assigned_agent_id) ?? null) : null, approvals_count: 0, approvals_pending_count: 0, } as TaskCardRead); setTasks((prev) => [created, ...prev]); setIsDialogOpen(false); resetForm(); } catch (err) { const message = formatActionError(err, "Something went wrong."); setCreateError(message); pushToast(message); } finally { setIsCreating(false); } }; const postBoardChatMessage = useCallback( async (content: string): Promise<{ ok: boolean; error: string | null }> => { if (!isSignedIn || !boardId) { return { ok: false, error: "Sign in to send messages." }; } const trimmed = content.trim(); if (!trimmed) return { ok: false, error: null }; try { const result = await createBoardMemoryApiV1BoardsBoardIdMemoryPost( boardId, { content: trimmed, tags: ["chat"], }, ); if (result.status !== 200) { throw new Error("Unable to send message."); } const created = result.data; if (created.tags?.includes("chat")) { setChatMessages((prev) => { const exists = prev.some((item) => item.id === created.id); if (exists) return prev; const next = [...prev, created]; next.sort((a, b) => { const aTime = apiDatetimeToMs(a.created_at) ?? 0; const bTime = apiDatetimeToMs(b.created_at) ?? 0; return aTime - bTime; }); return next; }); } return { ok: true, error: null }; } catch (err) { const message = formatActionError(err, "Unable to send message."); return { ok: false, error: message }; } }, [boardId, isSignedIn], ); const handleSendChat = useCallback( async (content: string): Promise => { const trimmed = content.trim(); if (!trimmed) return false; setIsChatSending(true); setChatError(null); try { const result = await postBoardChatMessage(trimmed); if (!result.ok) { if (result.error) { setChatError(result.error); pushToast(result.error); } return false; } return true; } finally { setIsChatSending(false); } }, [postBoardChatMessage, pushToast], ); const openAgentsControlDialog = (action: "pause" | "resume") => { setAgentsControlAction(action); setAgentsControlError(null); setIsAgentsControlDialogOpen(true); }; const handleConfirmAgentsControl = useCallback(async () => { const command = agentsControlAction === "pause" ? "/pause" : "/resume"; setIsAgentsControlSending(true); setAgentsControlError(null); try { const result = await postBoardChatMessage(command); if (!result.ok) { const message = result.error ?? `Unable to send ${command} command.`; setAgentsControlError(message); pushToast(message); return; } setIsAgentsControlDialogOpen(false); } finally { setIsAgentsControlSending(false); } }, [agentsControlAction, postBoardChatMessage, pushToast]); const assigneeById = useMemo(() => { const map = new Map(); agents .filter((agent) => !boardId || agent.board_id === boardId) .forEach((agent) => { map.set(agent.id, agent.name); }); return map; }, [agents, boardId]); const taskTitleById = useMemo(() => { const map = new Map(); tasks.forEach((task) => { map.set(task.id, task.title); }); return map; }, [tasks]); const taskById = useMemo(() => { const map = new Map(); tasks.forEach((task) => { map.set(task.id, task); }); return map; }, [tasks]); const orderedLiveFeed = useMemo(() => { return [...liveFeed].sort((a, b) => { const aTime = apiDatetimeToMs(a.created_at) ?? 0; const bTime = apiDatetimeToMs(b.created_at) ?? 0; return bTime - aTime; }); }, [liveFeed]); const assignableAgents = useMemo( () => agents.filter((agent) => !agent.is_board_lead), [agents], ); const dependencyOptions = useMemo(() => { if (!selectedTask) return []; const alreadySelected = new Set(editDependsOnTaskIds); return tasks .filter((task) => task.id !== selectedTask.id) .map((task) => ({ value: task.id, label: `${task.title} (${task.status.replace(/_/g, " ")})`, disabled: alreadySelected.has(task.id), })); }, [editDependsOnTaskIds, selectedTask, tasks]); const addTaskDependency = useCallback((dependencyId: string) => { setEditDependsOnTaskIds((prev) => prev.includes(dependencyId) ? prev : [...prev, dependencyId], ); }, []); const removeTaskDependency = useCallback((dependencyId: string) => { setEditDependsOnTaskIds((prev) => prev.filter((value) => value !== dependencyId), ); }, []); const hasTaskChanges = useMemo(() => { if (!selectedTask) return false; const normalizedTitle = editTitle.trim(); const normalizedDescription = editDescription.trim(); const currentDescription = (selectedTask.description ?? "").trim(); const currentAssignee = selectedTask.assigned_agent_id ?? ""; const currentDeps = [...(selectedTask.depends_on_task_ids ?? [])] .sort() .join("|"); const nextDeps = [...editDependsOnTaskIds].sort().join("|"); return ( normalizedTitle !== selectedTask.title || normalizedDescription !== currentDescription || editStatus !== selectedTask.status || editPriority !== selectedTask.priority || editAssigneeId !== currentAssignee || currentDeps !== nextDeps ); }, [ editAssigneeId, editDependsOnTaskIds, editDescription, editPriority, editStatus, editTitle, selectedTask, ]); const pendingApprovals = useMemo( () => approvals.filter((approval) => approval.status === "pending"), [approvals], ); const taskApprovals = useMemo(() => { if (!selectedTask) return []; const taskId = selectedTask.id; const taskIdsForApproval = (approval: Approval) => { const payload = approval.payload ?? {}; const payloadValue = (key: string) => { const value = (payload as Record)[key]; if (typeof value === "string" || typeof value === "number") { return String(value); } return null; }; const payloadArray = (key: string) => { const value = (payload as Record)[key]; if (!Array.isArray(value)) return []; return value.filter((item): item is string => typeof item === "string"); }; const linkedTaskIds = ( approval as Approval & { task_ids?: string[] | null } ).task_ids; const singleTaskId = approval.task_id ?? payloadValue("task_id") ?? payloadValue("taskId") ?? payloadValue("taskID"); const merged = [ ...(Array.isArray(linkedTaskIds) ? linkedTaskIds : []), ...payloadArray("task_ids"), ...payloadArray("taskIds"), ...payloadArray("taskIDs"), ...(singleTaskId ? [singleTaskId] : []), ]; return [...new Set(merged)]; }; return approvals.filter((approval) => taskIdsForApproval(approval).includes(taskId), ); }, [approvals, selectedTask]); 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 [...agents].sort((a, b) => { const diff = rank(a) - rank(b); if (diff !== 0) return diff; return a.name.localeCompare(b.name); }); }, [agents, workingAgentIds]); const boardLead = useMemo( () => agents.find((agent) => agent.is_board_lead) ?? null, [agents], ); const isBoardLeadProvisioning = boardLead?.status === "provisioning"; const loadComments = useCallback( async (taskId: string) => { if (!isSignedIn || !boardId) return; setIsCommentsLoading(true); setCommentsError(null); try { const result = await listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet( boardId, taskId, ); if (result.status !== 200) throw new Error("Unable to load comments."); const items = [...(result.data.items ?? [])]; items.sort((a, b) => { const aTime = apiDatetimeToMs(a.created_at) ?? 0; const bTime = apiDatetimeToMs(b.created_at) ?? 0; return bTime - aTime; }); setComments(items); } catch (err) { setCommentsError( err instanceof Error ? err.message : "Something went wrong.", ); } finally { setIsCommentsLoading(false); } }, [boardId, isSignedIn], ); const openComments = useCallback( (task: { id: string }) => { setIsChatOpen(false); setIsLiveFeedOpen(false); const fullTask = tasksRef.current.find((item) => item.id === task.id); if (!fullTask) return; selectedTaskIdRef.current = fullTask.id; setSelectedTask(fullTask); setIsDetailOpen(true); void loadComments(task.id); }, [loadComments], ); useEffect(() => { if (!taskIdFromUrl) return; if (openedTaskIdFromUrlRef.current === taskIdFromUrl) return; const exists = tasks.some((task) => task.id === taskIdFromUrl); if (!exists) return; openedTaskIdFromUrlRef.current = taskIdFromUrl; openComments({ id: taskIdFromUrl }); }, [openComments, taskIdFromUrl, tasks]); const closeComments = () => { setIsDetailOpen(false); selectedTaskIdRef.current = null; setSelectedTask(null); setComments([]); setCommentsError(null); setNewComment(""); setPostCommentError(null); setIsEditDialogOpen(false); }; const openBoardChat = () => { if (isDetailOpen) { closeComments(); } setIsLiveFeedOpen(false); setIsChatOpen(true); }; const closeBoardChat = () => { setIsChatOpen(false); setChatError(null); }; const openLiveFeed = () => { if (isDetailOpen) { closeComments(); } if (isChatOpen) { closeBoardChat(); } setIsLiveFeedOpen(true); }; const closeLiveFeed = () => { setIsLiveFeedOpen(false); }; const handlePostComment = async () => { if (!selectedTask || !boardId || !isSignedIn) return; const trimmed = newComment.trim(); if (!trimmed) { setPostCommentError("Write a message before sending."); return; } setIsPostingComment(true); setPostCommentError(null); try { const result = await createTaskCommentApiV1BoardsBoardIdTasksTaskIdCommentsPost( boardId, selectedTask.id, { message: trimmed }, ); if (result.status !== 200) throw new Error("Unable to send message."); const created = result.data; setComments((prev) => [created, ...prev]); setNewComment(""); } catch (err) { const message = formatActionError(err, "Unable to send message."); setPostCommentError(message); pushToast(message); } finally { setIsPostingComment(false); taskCommentInputRef.current?.focus(); } }; const handleTaskSave = async (closeOnSuccess = false) => { if (!selectedTask || !isSignedIn || !boardId) return; const trimmedTitle = editTitle.trim(); if (!trimmedTitle) { setSaveTaskError("Title is required."); return; } setIsSavingTask(true); setSaveTaskError(null); try { const currentDeps = [...(selectedTask.depends_on_task_ids ?? [])] .sort() .join("|"); const nextDeps = [...editDependsOnTaskIds].sort().join("|"); const depsChanged = currentDeps !== nextDeps; const updatePayload: Parameters< typeof updateTaskApiV1BoardsBoardIdTasksTaskIdPatch >[2] = { title: trimmedTitle, description: editDescription.trim() || null, status: editStatus, priority: editPriority, assigned_agent_id: editAssigneeId || null, }; if (depsChanged && selectedTask.status !== "done") { updatePayload.depends_on_task_ids = editDependsOnTaskIds; } const result = await updateTaskApiV1BoardsBoardIdTasksTaskIdPatch( boardId, selectedTask.id, updatePayload, ); if (result.status === 409) { const blockedIds = result.data.detail.blocked_by_task_ids ?? []; const blockedTitles = blockedIds .map((id) => taskTitleById.get(id) ?? id) .join(", "); setSaveTaskError( blockedTitles ? `${result.data.detail.message} Blocked by: ${blockedTitles}` : result.data.detail.message, ); return; } if (result.status === 422) { setSaveTaskError( result.data.detail?.[0]?.msg ?? "Validation error while saving task.", ); return; } const previous = tasksRef.current.find((task) => task.id === selectedTask.id) ?? selectedTask; const updated = normalizeTask({ ...previous, ...result.data, assignee: result.data.assigned_agent_id ? (assigneeById.get(result.data.assigned_agent_id) ?? null) : null, approvals_count: previous.approvals_count, approvals_pending_count: previous.approvals_pending_count, } as TaskCardRead); setTasks((prev) => prev.map((task) => task.id === updated.id ? { ...task, ...updated } : task, ), ); setSelectedTask(updated); if (closeOnSuccess) { setIsEditDialogOpen(false); } } catch (err) { const message = formatActionError(err, "Something went wrong."); setSaveTaskError(message); pushToast(message); } finally { setIsSavingTask(false); } }; const handleTaskReset = () => { if (!selectedTask) return; setEditTitle(selectedTask.title); setEditDescription(selectedTask.description ?? ""); setEditStatus(selectedTask.status); setEditPriority(selectedTask.priority); setEditAssigneeId(selectedTask.assigned_agent_id ?? ""); setEditDependsOnTaskIds(selectedTask.depends_on_task_ids ?? []); setSaveTaskError(null); }; const handleDeleteTask = async () => { if (!selectedTask || !boardId || !isSignedIn) return; setIsDeletingTask(true); setDeleteTaskError(null); try { const result = await deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete( boardId, selectedTask.id, ); if (result.status !== 200) throw new Error("Unable to delete task."); setTasks((prev) => prev.filter((task) => task.id !== selectedTask.id)); setIsDeleteDialogOpen(false); closeComments(); } catch (err) { const message = formatActionError(err, "Something went wrong."); setDeleteTaskError(message); pushToast(message); } finally { setIsDeletingTask(false); } }; const handleTaskMove = useCallback( async (taskId: string, status: TaskStatus) => { if (!isSignedIn || !boardId) return; const currentTask = tasksRef.current.find((task) => task.id === taskId); if (!currentTask || currentTask.status === status) return; if (currentTask.is_blocked && status !== "inbox") { setError("Task is blocked by incomplete dependencies."); return; } const previousTasks = tasksRef.current; setTasks((prev) => prev.map((task) => task.id === taskId ? { ...task, status, assigned_agent_id: status === "inbox" ? null : task.assigned_agent_id, assignee: status === "inbox" ? null : task.assignee, } : task, ), ); try { const result = await updateTaskApiV1BoardsBoardIdTasksTaskIdPatch( boardId, taskId, { status }, ); if (result.status === 409) { const blockedIds = result.data.detail.blocked_by_task_ids ?? []; const blockedTitles = blockedIds .map((id) => taskTitleById.get(id) ?? id) .join(", "); throw new Error( blockedTitles ? `${result.data.detail.message} Blocked by: ${blockedTitles}` : result.data.detail.message, ); } if (result.status === 422) { throw new Error( result.data.detail?.[0]?.msg ?? "Validation error while moving task.", ); } const assignee = result.data.assigned_agent_id ? (agentsRef.current.find( (agent) => agent.id === result.data.assigned_agent_id, )?.name ?? null) : null; const updated = normalizeTask({ ...currentTask, ...result.data, assignee, approvals_count: currentTask.approvals_count, approvals_pending_count: currentTask.approvals_pending_count, } as TaskCardRead); setTasks((prev) => prev.map((task) => task.id === updated.id ? { ...task, ...updated } : task, ), ); } catch (err) { setTasks(previousTasks); const message = formatActionError(err, "Unable to move task."); setError(message); pushToast(message); } }, [boardId, isSignedIn, pushToast, taskTitleById], ); const agentInitials = (agent: Agent) => agent.name .split(" ") .filter(Boolean) .slice(0, 2) .map((part) => part[0]) .join("") .toUpperCase(); const resolveEmoji = (value?: string | null) => { if (!value) return null; const trimmed = value.trim(); if (!trimmed) return null; if (EMOJI_GLYPHS[trimmed]) return EMOJI_GLYPHS[trimmed]; if (trimmed.startsWith(":") && trimmed.endsWith(":")) return null; return trimmed; }; const agentAvatarLabel = (agent: Agent) => { if (agent.is_board_lead) return "⚙️"; let emojiValue: string | null = null; if (agent.identity_profile && typeof agent.identity_profile === "object") { const rawEmoji = (agent.identity_profile as Record) .emoji; emojiValue = typeof rawEmoji === "string" ? rawEmoji : null; } const emoji = resolveEmoji(emojiValue); return emoji ?? agentInitials(agent); }; const agentRoleLabel = (agent: Agent) => { // Prefer the configured identity role from the API. if (agent.identity_profile && typeof agent.identity_profile === "object") { const rawRole = (agent.identity_profile as Record).role; if (typeof rawRole === "string") { const trimmed = rawRole.trim(); if (trimmed) return trimmed; } } if (agent.is_board_lead) return "Board lead"; if (agent.is_gateway_main) return "Gateway main"; return "Agent"; }; const formatTaskTimestamp = (value?: string | null) => { if (!value) return "—"; const date = parseApiDatetime(value); if (!date) return "—"; return date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; const statusBadgeClass = (value?: string) => { switch (value) { case "in_progress": return "bg-purple-100 text-purple-700"; case "review": return "bg-indigo-100 text-indigo-700"; case "done": return "bg-emerald-100 text-emerald-700"; default: return "bg-slate-100 text-slate-600"; } }; const priorityBadgeClass = (value?: string) => { switch (value?.toLowerCase()) { case "high": return "bg-rose-100 text-rose-700"; case "medium": return "bg-amber-100 text-amber-700"; case "low": return "bg-emerald-100 text-emerald-700"; default: return "bg-slate-100 text-slate-600"; } }; const formatApprovalTimestamp = (value?: string | null) => { if (!value) return "—"; const date = parseApiDatetime(value); if (!date) return value; return date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; const humanizeApprovalAction = (value: string) => value .split(".") .map((part) => part.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()), ) .join(" · "); const approvalPayloadValue = (payload: Approval["payload"], key: string) => { if (!payload || typeof payload !== "object") return null; const value = (payload as Record)[key]; if (typeof value === "string" || typeof value === "number") { return String(value); } return null; }; const approvalPayloadValues = (payload: Approval["payload"], key: string) => { if (!payload || typeof payload !== "object") return []; const value = (payload as Record)[key]; if (!Array.isArray(value)) return []; return value.filter((item): item is string => typeof item === "string"); }; const approvalTaskIds = (approval: Approval) => { const payload = approval.payload ?? {}; const linkedTaskIds = ( approval as Approval & { task_ids?: string[] | null } ).task_ids; const singleTaskId = approval.task_id ?? approvalPayloadValue(payload, "task_id") ?? approvalPayloadValue(payload, "taskId") ?? approvalPayloadValue(payload, "taskID"); const manyTaskIds = [ ...approvalPayloadValues(payload, "task_ids"), ...approvalPayloadValues(payload, "taskIds"), ...approvalPayloadValues(payload, "taskIDs"), ]; const merged = [ ...(Array.isArray(linkedTaskIds) ? linkedTaskIds : []), ...manyTaskIds, ...(singleTaskId ? [singleTaskId] : []), ]; const deduped: string[] = []; const seen = new Set(); merged.forEach((value) => { if (seen.has(value)) return; seen.add(value); deduped.push(value); }); return deduped; }; const approvalRows = (approval: Approval) => { const payload = approval.payload ?? {}; const taskIds = approvalTaskIds(approval); const assignedAgentId = approvalPayloadValue(payload, "assigned_agent_id") ?? approvalPayloadValue(payload, "assignedAgentId"); const title = approvalPayloadValue(payload, "title"); const role = approvalPayloadValue(payload, "role"); const isAssign = approval.action_type.includes("assign"); const rows: Array<{ label: string; value: string }> = []; if (taskIds.length === 1) rows.push({ label: "Task", value: taskIds[0] }); if (taskIds.length > 1) rows.push({ label: "Tasks", value: taskIds.join(", ") }); if (isAssign) { rows.push({ label: "Assignee", value: assignedAgentId ?? "Unassigned", }); } if (title) rows.push({ label: "Title", value: title }); if (role) rows.push({ label: "Role", value: role }); return rows; }; const approvalReason = (approval: Approval) => approvalPayloadValue(approval.payload ?? {}, "reason"); const handleApprovalDecision = useCallback( async (approvalId: string, status: "approved" | "rejected") => { if (!isSignedIn || !boardId) return; if (!canWrite) { pushToast( "Read-only access. You do not have permission to update approvals.", ); return; } setApprovalsUpdatingId(approvalId); setApprovalsError(null); try { const result = await updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch( boardId, approvalId, { status }, ); if (result.status !== 200) { throw new Error("Unable to update approval."); } const updated = normalizeApproval(result.data); setApprovals((prev) => prev.map((item) => (item.id === approvalId ? updated : item)), ); } catch (err) { const message = formatActionError(err, "Unable to update approval."); setApprovalsError(message); pushToast(message); } finally { setApprovalsUpdatingId(null); } }, [boardId, canWrite, isSignedIn, pushToast], ); return (

Sign in to view boards.

{board?.name ?? "Board"}

Keep tasks moving through your workflow.

{isBoardLeadProvisioning ? (
Provisioning board lead…
) : null}
{isOrgAdmin ? ( ) : null} {isOrgAdmin ? ( ) : null}
{isOrgAdmin ? ( ) : null}
{error && (
{error}
)} {isLoading ? (
Loading {titleLabel}…
) : ( <> {viewMode === "list" ? ( <> {groupSnapshotError ? (
{groupSnapshotError}
) : null} {groupSnapshot?.group ? (

Related boards

{groupSnapshot.group.name}

{groupSnapshot.group.description ? (

{groupSnapshot.group.description}

) : null}
{isOrgAdmin ? ( ) : null}
{groupSnapshot.boards && groupSnapshot.boards.length ? (
{groupSnapshot.boards.map((item) => (
Inbox {item.task_counts?.inbox ?? 0} In progress{" "} {item.task_counts?.in_progress ?? 0} Review {item.task_counts?.review ?? 0}
{item.tasks && item.tasks.length ? (
    {item.tasks.slice(0, 3).map((task) => (
  • {task.status.replace( /_/g, " ", )} {task.priority}

    {task.title}

    {formatTaskTimestamp( task.updated_at, )}

    Assignee:{" "} {task.assignee ?? "Unassigned"}

  • ))} {item.tasks.length > 3 ? (
  • +{item.tasks.length - 3} more…
  • ) : null}
) : (

No tasks in this snapshot.

)}
))}
) : (

No other boards in this group yet.

)}
) : groupSnapshot ? (

No board group configured

Assign this board to a group to give agents visibility into related work.

) : null} ) : null} {viewMode === "board" ? ( ) : (

All tasks

{tasks.length} tasks in this board

{tasks.length === 0 ? (
No tasks yet. Create your first task to get started.
) : ( tasks.map((task) => ( )) )}
)} )}
{isDetailOpen || isChatOpen || isLiveFeedOpen ? (
{ if (isChatOpen) { closeBoardChat(); } else if (isLiveFeedOpen) { closeLiveFeed(); } else { closeComments(); } }} /> ) : null}