fix(activity): use API route metadata for feed links

This commit is contained in:
Abhimanyu Saharan
2026-03-04 16:25:16 +05:30
parent 4378d354f4
commit 81b9a586ed

View File

@@ -89,9 +89,11 @@ type FeedItem = {
actor_role: string | null;
board_id: string | null;
board_name: string | null;
board_href: string | null;
task_id: string | null;
task_title: string | null;
title: string;
context_href: string | null;
};
type TaskMeta = {
@@ -99,6 +101,10 @@ type TaskMeta = {
boardId: string | null;
};
type ActivityRouteParams = Record<string, string>;
const ACTIVITY_FEED_PATH = "/activity";
const TASK_EVENT_TYPES = new Set<TaskEventType>([
"task.comment",
"task.created",
@@ -120,6 +126,73 @@ const formatShortTimestamp = (value: string) => {
});
};
const normalizeRouteParams = (
params: ActivityEventRead["route_params"] | ActivityRouteParams | null | undefined,
): ActivityRouteParams => {
if (!params || typeof params !== "object") return {};
return Object.entries(params).reduce<ActivityRouteParams>((acc, [key, value]) => {
if (typeof value === "string" && value.length > 0) {
acc[key] = value;
}
return acc;
}, {});
};
const buildRouteHref = (
routeName: string | null | undefined,
routeParams: ActivityRouteParams,
fallback: {
eventId: string;
eventType: string;
createdAt: string;
taskId: string | null;
},
): string => {
if (routeName === "board.approvals") {
const boardId = routeParams.boardId;
if (boardId) {
return `/boards/${encodeURIComponent(boardId)}/approvals`;
}
}
if (routeName === "board") {
const boardId = routeParams.boardId;
if (boardId) {
const params = new URLSearchParams();
Object.entries(routeParams).forEach(([key, value]) => {
if (key !== "boardId") params.set(key, value);
});
const query = params.toString();
return query
? `/boards/${encodeURIComponent(boardId)}?${query}`
: `/boards/${encodeURIComponent(boardId)}`;
}
}
const params = new URLSearchParams(
Object.keys(routeParams).length > 0
? routeParams
: {
eventId: fallback.eventId,
eventType: fallback.eventType,
createdAt: fallback.createdAt,
},
);
if (fallback.taskId && !params.has("taskId")) {
params.set("taskId", fallback.taskId);
}
return `${ACTIVITY_FEED_PATH}?${params.toString()}`;
};
const buildBoardHref = (
routeParams: ActivityRouteParams,
boardId: string | null,
): string | null => {
const resolved = routeParams.boardId ?? boardId;
if (!resolved) return null;
return `/boards/${encodeURIComponent(resolved)}`;
};
const feedItemElementId = (id: string): string =>
`activity-item-${id.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
@@ -219,11 +292,6 @@ const FeedCard = memo(function FeedCard({
}) {
const message = (item.message ?? "").trim();
const authorAvatar = (item.actor_name[0] ?? "A").toUpperCase();
const taskHref =
item.board_id && item.task_id
? `/boards/${item.board_id}?taskId=${item.task_id}`
: null;
const boardHref = item.board_id ? `/boards/${item.board_id}` : null;
return (
<div
@@ -241,9 +309,9 @@ const FeedCard = memo(function FeedCard({
</div>
<div className="min-w-0 flex-1">
<div className="min-w-0">
{taskHref ? (
{item.context_href ? (
<Link
href={taskHref}
href={item.context_href}
className="block text-sm font-semibold leading-snug text-slate-900 transition hover:text-slate-950 hover:underline"
title={item.title}
style={{
@@ -269,9 +337,9 @@ const FeedCard = memo(function FeedCard({
>
{eventLabel(item.event_type)}
</span>
{boardHref && item.board_name ? (
{item.board_href && item.board_name ? (
<Link
href={boardHref}
href={item.board_href}
className="font-semibold text-slate-700 hover:text-slate-900 hover:underline"
>
{item.board_name}
@@ -424,12 +492,30 @@ export default function ActivityPage() {
);
const mapTaskActivity = useCallback(
(event: ActivityEventRead): FeedItem | null => {
(
event: ActivityEventRead,
fallbackBoardId: string | null = null,
): FeedItem | null => {
if (!isTaskEventType(event.event_type)) return null;
const meta = event.task_id
? taskMetaByIdRef.current.get(event.task_id)
: null;
const boardId = meta?.boardId ?? null;
const routeName = event.route_name ?? null;
const routeParams = normalizeRouteParams(event.route_params);
const taskId = event.task_id ?? routeParams.taskId ?? null;
const boardId =
meta?.boardId ??
event.board_id ??
routeParams.boardId ??
fallbackBoardId ??
null;
const fallbackRouteParams: ActivityRouteParams = {};
if (boardId) fallbackRouteParams.boardId = boardId;
if (taskId) fallbackRouteParams.taskId = taskId;
const effectiveRouteParams =
Object.keys(routeParams).length > 0 ? routeParams : fallbackRouteParams;
const effectiveRouteName =
routeName ?? (boardId ? "board" : "activity");
const author = resolveAuthor(event.agent_id, currentUserDisplayName);
return {
id: `activity:${event.id}`,
@@ -442,10 +528,17 @@ export default function ActivityPage() {
actor_role: author.role,
board_id: boardId,
board_name: boardNameForId(boardId),
task_id: event.task_id ?? null,
board_href: buildBoardHref(effectiveRouteParams, boardId),
task_id: taskId,
task_title: meta?.title ?? null,
title:
meta?.title ?? (event.task_id ? "Unknown task" : "Task activity"),
meta?.title ?? (taskId ? "Unknown task" : "Task activity"),
context_href: buildRouteHref(effectiveRouteName, effectiveRouteParams, {
eventId: event.id,
eventType: event.event_type,
createdAt: event.created_at,
taskId,
}),
};
},
[boardNameForId, currentUserDisplayName, resolveAuthor],
@@ -457,6 +550,11 @@ export default function ActivityPage() {
? taskMetaByIdRef.current.get(comment.task_id)
: null;
const boardId = meta?.boardId ?? fallbackBoardId;
const taskId = comment.task_id ?? null;
const routeParams: ActivityRouteParams = {};
if (boardId) routeParams.boardId = boardId;
if (taskId) routeParams.taskId = taskId;
routeParams.commentId = comment.id;
const author = resolveAuthor(comment.agent_id, currentUserDisplayName);
return {
id: `comment:${comment.id}`,
@@ -469,10 +567,17 @@ export default function ActivityPage() {
actor_role: author.role,
board_id: boardId,
board_name: boardNameForId(boardId),
task_id: comment.task_id ?? null,
board_href: buildBoardHref(routeParams, boardId),
task_id: taskId,
task_title: meta?.title ?? null,
title:
meta?.title ?? (comment.task_id ? "Unknown task" : "Task activity"),
meta?.title ?? (taskId ? "Unknown task" : "Task activity"),
context_href: buildRouteHref("board", routeParams, {
eventId: comment.id,
eventType: "task.comment",
createdAt: comment.created_at,
taskId,
}),
};
},
[boardNameForId, currentUserDisplayName, resolveAuthor],
@@ -525,6 +630,8 @@ export default function ActivityPage() {
const taskMeta = approval.task_id
? taskMetaByIdRef.current.get(approval.task_id)
: null;
const routeParams: ActivityRouteParams = { boardId };
const taskId = approval.task_id ?? null;
return {
id: `approval:${approval.id}:${kind}:${stamp}`,
@@ -537,9 +644,16 @@ export default function ActivityPage() {
actor_role: author.role,
board_id: boardId,
board_name: boardNameForId(boardId),
task_id: approval.task_id ?? null,
board_href: buildBoardHref(routeParams, boardId),
task_id: taskId,
task_title: taskMeta?.title ?? null,
title: `Approval · ${action}`,
context_href: buildRouteHref("board.approvals", routeParams, {
eventId: approval.id,
eventType: kind,
createdAt: stamp,
taskId,
}),
};
},
[boardNameForId, currentUserDisplayName, resolveAuthor],
@@ -553,6 +667,7 @@ export default function ActivityPage() {
currentUserDisplayName,
);
const command = content.startsWith("/");
const routeParams: ActivityRouteParams = { boardId, panel: "chat" };
return {
id: `chat:${memory.id}`,
created_at: memory.created_at,
@@ -564,9 +679,16 @@ export default function ActivityPage() {
actor_role: null,
board_id: boardId,
board_name: boardNameForId(boardId),
board_href: buildBoardHref(routeParams, boardId),
task_id: null,
task_title: null,
title: command ? "Board command" : "Board chat",
context_href: buildRouteHref("board", routeParams, {
eventId: memory.id,
eventType: command ? "board.command" : "board.chat",
createdAt: memory.created_at,
taskId: null,
}),
};
},
[boardNameForId, currentUserDisplayName],
@@ -618,6 +740,10 @@ export default function ActivityPage() {
: kind === "agent.offline"
? `${agent.name} is offline.`
: `${agent.name} updated (${humanizeStatus(nextStatus)}).`;
const boardId = agent.board_id ?? null;
const routeParams: ActivityRouteParams = boardId
? { boardId }
: {};
return {
id: `agent:${agent.id}:${isSnapshot ? "snapshot" : kind}:${stamp}`,
@@ -628,11 +754,21 @@ export default function ActivityPage() {
agent_id: agent.id,
actor_name: agent.name,
actor_role: roleFromAgent(agent),
board_id: agent.board_id ?? null,
board_name: boardNameForId(agent.board_id),
board_id: boardId,
board_name: boardNameForId(boardId),
board_href: buildBoardHref(routeParams, boardId),
task_id: null,
task_title: null,
title: `Agent · ${agent.name}`,
context_href:
boardId === null
? null
: buildRouteHref("board", routeParams, {
eventId: agent.id,
eventType: kind,
createdAt: stamp,
taskId: null,
}),
};
},
[boardNameForId],
@@ -871,12 +1007,8 @@ export default function ActivityPage() {
updateTaskMeta(payload.task, boardId);
}
if (payload.activity) {
const mapped = mapTaskActivity(payload.activity);
const mapped = mapTaskActivity(payload.activity, boardId);
if (mapped) {
if (!mapped.board_id) {
mapped.board_id = boardId;
mapped.board_name = boardNameForId(boardId);
}
if (!mapped.task_title && payload.task?.title) {
mapped.task_title = payload.task.title;
mapped.title = payload.task.title;