"use client"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; import { ArrowUpRight, Activity as ActivityIcon } from "lucide-react"; import { ApiError } from "@/api/mutator"; import { type listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse, streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet, useListTaskCommentFeedApiV1ActivityTaskCommentsGet, } from "@/api/generated/activity/activity"; import type { ActivityTaskCommentFeedItemRead } from "@/api/generated/model"; import { Markdown } from "@/components/atoms/Markdown"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button } from "@/components/ui/button"; import { createExponentialBackoff } from "@/lib/backoff"; import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime"; import { cn } from "@/lib/utils"; 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", }); }; const latestTimestamp = (items: ActivityTaskCommentFeedItemRead[]) => { let latest = 0; for (const item of items) { const time = apiDatetimeToMs(item.created_at) ?? 0; latest = Math.max(latest, time); } return latest ? new Date(latest).toISOString() : null; }; const FeedCard = memo(function FeedCard({ item, }: { item: ActivityTaskCommentFeedItemRead; }) { const message = (item.message ?? "").trim(); const authorName = item.agent_name?.trim() || "Admin"; const authorRole = item.agent_role?.trim() || null; const authorAvatar = (authorName[0] ?? "A").toUpperCase(); const taskHref = `/boards/${item.board_id}?taskId=${item.task_id}`; const boardHref = `/boards/${item.board_id}`; return (
{authorAvatar}
{item.task_title}
{item.board_name} · {authorName} {authorRole ? ( <> · {authorRole} ) : null} · {formatShortTimestamp(item.created_at)}
View task
{message ? (
) : (

)}
); }); FeedCard.displayName = "FeedCard"; export default function ActivityPage() { const { isSignedIn } = useAuth(); const feedQuery = useListTaskCommentFeedApiV1ActivityTaskCommentsGet< listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse, ApiError >( { limit: 200 }, { query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", refetchOnWindowFocus: false, retry: false, }, }, ); const [feedItems, setFeedItems] = useState( [], ); const feedItemsRef = useRef([]); const seenIdsRef = useRef>(new Set()); const initializedRef = useRef(false); useEffect(() => { feedItemsRef.current = feedItems; }, [feedItems]); useEffect(() => { if (initializedRef.current) return; if (feedQuery.data?.status !== 200) return; const items = feedQuery.data.data.items ?? []; initializedRef.current = true; setFeedItems((prev) => { const map = new Map(); [...prev, ...items].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; }); const next = merged.slice(0, 200); seenIdsRef.current = new Set(next.map((item) => item.id)); return next; }); }, [feedQuery.data]); const pushFeedItem = useCallback((item: ActivityTaskCommentFeedItemRead) => { setFeedItems((prev) => { if (seenIdsRef.current.has(item.id)) return prev; seenIdsRef.current.add(item.id); const next = [item, ...prev]; return next.slice(0, 200); }); }, []); useEffect(() => { if (!isSignedIn) return; let isCancelled = false; const abortController = new AbortController(); const backoff = createExponentialBackoff(SSE_RECONNECT_BACKOFF); let reconnectTimeout: number | undefined; const connect = async () => { try { const since = latestTimestamp(feedItemsRef.current); const streamResult = await streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet( since ? { since } : undefined, { headers: { Accept: "text/event-stream" }, signal: abortController.signal, }, ); if (streamResult.status !== 200) { throw new Error("Unable to connect task comment feed stream."); } const response = streamResult.data as Response; if (!(response instanceof Response) || !response.body) { throw new Error("Unable to connect task comment 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?: ActivityTaskCommentFeedItemRead; }; if (payload.comment) { pushFeedItem(payload.comment); } } 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); } }; }, [isSignedIn, pushFeedItem]); const orderedFeed = useMemo(() => { return [...feedItems].sort((a, b) => { const aTime = apiDatetimeToMs(a.created_at) ?? 0; const bTime = apiDatetimeToMs(b.created_at) ?? 0; return bTime - aTime; }); }, [feedItems]); return (

Sign in to view the feed.

Live feed

Realtime task comments across all boards.

{feedQuery.isLoading && feedItems.length === 0 ? (

Loading feed…

) : feedQuery.error ? (
{feedQuery.error.message || "Unable to load feed."}
) : orderedFeed.length === 0 ? (

Waiting for new comments…

When agents post updates, they will show up here.

) : (
{orderedFeed.map((item) => ( ))}
)}
); }