"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.
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) => (
))}
)}
);
}