Add a Command Center live agent activity log with historical navigation. - Stream and paginate organization-wide agent activity with timeline filters. - Link activity rows to agent details and task views while reusing feed presentation. - Add localized copy, documentation, a minor changeset, and coverage for live and scroll-back behavior. Files changed: .changeset/fn-8866-agent-activity-surfaces.md | 7 + docs/dashboard-guide.md | 1 + packages/dashboard/app/App.tsx | 12 ++ packages/dashboard/app/components/ActivityFeed.tsx | 77 ++++---- packages/dashboard/app/components/AgentsView.tsx | 13 +- .../__tests__/AgentsView.focus-agent.test.tsx | 59 ++++++ .../command-center/AgentActivityPanel.css | 98 +++++++++ .../command-center/AgentActivityPanel.tsx | 99 ++++++++++ .../components/command-center/CommandCenter.tsx | 9 + .../__tests__/AgentActivityPanel.mobile.test.ts | 15 ++ .../AgentActivityPanel.sse-integration.test.tsx | 96 +++++++++ .../__tests__/AgentActivityPanel.test.tsx | 121 ++++++++++++ .../__tests__/CommandCenter.test.tsx | 27 ++- .../__tests__/agentActivityCursor.test.ts | 47 +++++ .../__tests__/useAgentActivity.test.tsx | 14 ++ .../command-center/agentActivityCursor.ts | 75 +++++++ .../command-center/agentActivityPresentation.tsx | 8 + .../components/command-center/useAgentActivity.ts | 218 +++++++++++++++++++++ .../app/components/dashboard/MainContent.tsx | 19 ++ .../MainContent.agent-activity-navigation.test.tsx | 127 ++++++++++++ .../dashboard/app/components/dashboard/types.ts | 3 + packages/i18n/locales/en/app.json | 30 ++- packages/i18n/locales/es/app.json | 30 ++- packages/i18n/locales/fr/app.json | 30 ++- packages/i18n/locales/ko/app.json | 30 ++- packages/i18n/locales/pt-BR/app.json | 30 ++- packages/i18n/locales/zh-CN/app.json | 30 ++- packages/i18n/locales/zh-TW/app.json | 30 ++- 28 files changed, 1312 insertions(+), 43 deletions(-) Fusion-Task-Id: FN-8866 Fusion-Task-Lineage: 817ae76e-52c6-4221-8642-df757e773627 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
76 lines
3.7 KiB
TypeScript
76 lines
3.7 KiB
TypeScript
import type { AgentActivityEvent, AgentActivityPage } from "../../api";
|
|
|
|
/*
|
|
FNXC:CommandCenterAgentActivity 2026-08-10-01:13:
|
|
The agent-activity route orders unique per-project `seq` values descending and applies `before` exclusively,
|
|
returning `{ events, nextCursor }`. Echoing that cursor is therefore exact: offset and timestamp tiebreakers
|
|
would only weaken this total order. Sequences are decimal strings and may exceed Number's safe range, so all
|
|
ordering uses BigInt. The no-progress counter is only a bounded backstop for a regressed server contract.
|
|
*/
|
|
|
|
/** Bounded page size sent as `limit`; must stay <= the server hard max of 1000. */
|
|
export const AGENT_ACTIVITY_PAGE_SIZE = 50;
|
|
/** Bounded retry backstop for a server whose cursor does not advance. */
|
|
export const MAX_CONSECUTIVE_NO_PROGRESS_PAGES = 3;
|
|
|
|
export type PageExhaustionReason = "end-of-history" | "no-progress" | "cursor-rewind" | "attempt-cap";
|
|
|
|
export interface ResolvePageInput {
|
|
requestedCursor: string | null;
|
|
existing: readonly AgentActivityEvent[];
|
|
page: AgentActivityPage;
|
|
consecutiveNoProgress: number;
|
|
}
|
|
|
|
export interface ResolvePageOutcome {
|
|
merged: AgentActivityEvent[];
|
|
nextCursor: string | null;
|
|
hasMore: boolean;
|
|
reason: PageExhaustionReason | null;
|
|
consecutiveNoProgress: number;
|
|
}
|
|
|
|
/** Numeric-safe descending compare on decimal `seq` strings. */
|
|
export function compareAgentActivityDesc(a: AgentActivityEvent, b: AgentActivityEvent): number {
|
|
const left = BigInt(a.seq);
|
|
const right = BigInt(b.seq);
|
|
return left === right ? 0 : left > right ? -1 : 1;
|
|
}
|
|
|
|
/** eventId-keyed dedupe where the newest incoming representation wins. */
|
|
export function mergeAgentActivity(existing: readonly AgentActivityEvent[], incoming: readonly AgentActivityEvent[]): AgentActivityEvent[] {
|
|
const rows = new Map<string, AgentActivityEvent>();
|
|
for (const event of existing) rows.set(event.eventId, event);
|
|
for (const event of incoming) rows.set(event.eventId, event);
|
|
return [...rows.values()].sort(compareAgentActivityDesc);
|
|
}
|
|
|
|
export function isStrictlyOlderCursor(candidate: string, requested: string | null): boolean {
|
|
return requested === null || BigInt(candidate) < BigInt(requested);
|
|
}
|
|
|
|
/** The sole owner of the HTTP continuation parameter name. */
|
|
export function cursorToQuery(cursor: string | null): Record<string, string> {
|
|
return cursor === null ? {} : { before: cursor };
|
|
}
|
|
|
|
export function isTruncationFrame(frame: unknown): frame is { truncated: true; fromSeq: string; toSeq: string } {
|
|
return typeof frame === "object" && frame !== null && (frame as { truncated?: unknown }).truncated === true;
|
|
}
|
|
|
|
export function resolvePageOutcome(input: ResolvePageInput): ResolvePageOutcome {
|
|
const { requestedCursor, existing, page, consecutiveNoProgress } = input;
|
|
const merged = mergeAgentActivity(existing, page.events);
|
|
const newRowCount = merged.length - existing.length;
|
|
if (page.nextCursor === null) return { merged, nextCursor: null, hasMore: false, reason: "end-of-history", consecutiveNoProgress: 0 };
|
|
|
|
const nextCount = consecutiveNoProgress + 1;
|
|
const cursorAdvances = isStrictlyOlderCursor(page.nextCursor, requestedCursor);
|
|
if ((newRowCount === 0 || !cursorAdvances) && nextCount >= MAX_CONSECUTIVE_NO_PROGRESS_PAGES) {
|
|
return { merged, nextCursor: requestedCursor, hasMore: false, reason: "attempt-cap", consecutiveNoProgress: nextCount };
|
|
}
|
|
if (newRowCount === 0) return { merged, nextCursor: page.nextCursor, hasMore: true, reason: "no-progress", consecutiveNoProgress: nextCount };
|
|
if (!cursorAdvances) return { merged, nextCursor: requestedCursor, hasMore: true, reason: "cursor-rewind", consecutiveNoProgress: nextCount };
|
|
return { merged, nextCursor: page.nextCursor, hasMore: true, reason: null, consecutiveNoProgress: 0 };
|
|
}
|