Files
fusion/packages/dashboard/app/components/command-center/AgentActivityPanel.tsx
gsxdsm b7604a94bf FN-9041: remove agent state-change activity logs
Remove durable agent state-change events and hide retained state-change rows from activity displays.

- Stop AgentStore update and reconciliation paths from writing state-change activity events.
- Filter historical state-change events in activity stores and panels while retaining live roster notifications.
- Document the retained activity contract, add coverage, and include a patch changeset.

Files changed:
 .changeset/fn-9041-removal.md                      |  7 ++++
 docs/agent-activity-contract.md                    |  4 ++
 docs/architecture.md                               |  2 +-
 docs/dashboard-guide.md                            |  2 +-
 .../src/__tests__/agent-activity-writers.test.ts   | 23 ++++++-----
 packages/core/src/agents/agent-store.ts            | 28 +++++--------
 .../app/components/agentsOrgChartActivity.ts       | 12 ++++++
 .../command-center/AgentActivityPanel.tsx          | 25 +++++++----
 .../__tests__/AgentActivityPanel.test.tsx          | 48 ++++++++++++++++++++++
 .../app/hooks/__tests__/agentActivityStore.test.ts | 39 +++++++++++++++++-
 packages/dashboard/app/hooks/agentActivityStore.ts |  3 +-
 11 files changed, 153 insertions(+), 40 deletions(-)

Fusion-Task-Id: FN-9041

Fusion-Task-Lineage: 532724b1-5945-4904-b5cd-2c27909805de

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-14 12:41:03 -07:00

111 lines
7.8 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import type { AgentActivityEventType } from "../../api";
import { ActivityFeedRowPresentation } from "../ActivityFeed";
import { isHiddenAgentActivityType } from "../agentsOrgChartActivity";
import { AreaShell } from "./areas/AreaShell";
import { AGENT_ACTIVITY_TYPE_CONFIG, DEFAULT_AGENT_ACTIVITY_CONFIG } from "./agentActivityPresentation";
import type { DateRange } from "./DateRangePicker";
import { useAgentActivity, type AgentActivityFilters } from "./useAgentActivity";
import "./AgentActivityPanel.css";
const LIVE_RENDER_LIMIT = 100;
const TIMELINE_WINDOW_SIZE = 100;
const EVENT_TYPES = (Object.keys(AGENT_ACTIVITY_TYPE_CONFIG) as AgentActivityEventType[])
.filter((type) => !isHiddenAgentActivityType(type));
export interface AgentActivityPanelProps {
projectId?: string;
range: DateRange;
onOpenAgent?: (agentId: string) => void;
onOpenTask?: (taskId: string) => void;
}
/*
FNXC:CommandCenterAgentActivity 2026-08-10-02:03:
This panel provides the org-wide live log and manual-windowed scroll-back timeline without a virtualization dependency. It reuses the ActivityFeed icon/time vocabulary, fetches only `/api/agent-activity`, and keeps DateRange client-side because that route has no time-range parameter.
*/
export function AgentActivityPanel({ projectId, range, onOpenAgent, onOpenTask }: AgentActivityPanelProps) {
const { t } = useTranslation("app");
const [mode, setMode] = useState<"live" | "timeline">("live");
const [filters, setFilters] = useState<AgentActivityFilters>({});
const [timelineLimit, setTimelineLimit] = useState(TIMELINE_WINDOW_SIZE);
const activity = useAgentActivity({ projectId, filters, range });
useEffect(() => {
setTimelineLimit(TIMELINE_WINDOW_SIZE);
}, [filters.agentId, filters.taskId, filters.type, range.from, range.to]);
const agentOptions = useMemo(
() => [...new Set(activity.events.map((event) => event.agentId).filter(Boolean))].sort(),
[activity.events],
);
/*
FNXC:AgentActivityStream 2026-08-14-19:18:
FN-9041 hides historical state churn at this render boundary because the cursor hook must retain
every wire row for correct no-progress and exhaustion accounting. Filter before each display limit
so hidden rows cannot consume a visible slot, while paging can still load older real activity.
*/
const visibleEvents = activity.visibleEvents.filter((event) => !isHiddenAgentActivityType(event.type));
const rows = mode === "live"
? activity.events.filter((event) => !isHiddenAgentActivityType(event.type)).slice(0, LIVE_RENDER_LIMIT)
: visibleEvents.slice(0, timelineLimit);
return (
<section className="cc-agent-activity">
<div className="cc-agent-activity-mode" role="group" aria-label={t("commandCenter.agentActivity.mode", "Activity mode")}>
<button type="button" className="btn btn-sm" aria-pressed={mode === "live"} onClick={() => setMode("live")}>{t("commandCenter.agentActivity.live", "Live")}</button>
<button type="button" className="btn btn-sm" aria-pressed={mode === "timeline"} onClick={() => setMode("timeline")}>{t("commandCenter.agentActivity.timeline", "Timeline")}</button>
</div>
{mode === "timeline" ? (
<div className="cc-agent-activity-filters">
<select className="input" aria-label={t("commandCenter.agentActivity.agentFilter", "Filter by agent")} value={filters.agentId ?? ""} onChange={(event) => setFilters((current) => ({ ...current, agentId: event.target.value || undefined }))}>
<option value="">{t("commandCenter.agentActivity.allAgents", "All agents")}</option>
{agentOptions.map((agentId) => <option key={agentId} value={agentId}>{agentId}</option>)}
</select>
<input className="input" aria-label={t("commandCenter.agentActivity.taskFilter", "Filter by task")} value={filters.taskId ?? ""} placeholder={t("commandCenter.agentActivity.taskFilter", "Filter by task")} onChange={(event) => setFilters((current) => ({ ...current, taskId: event.target.value || undefined }))} />
<select className="input" aria-label={t("commandCenter.agentActivity.typeFilter", "Filter by event type")} value={filters.type ?? ""} onChange={(event) => setFilters((current) => ({ ...current, type: (event.target.value || undefined) as AgentActivityEventType | undefined }))}>
<option value="">{t("commandCenter.agentActivity.allTypes", "All event types")}</option>
{EVENT_TYPES.map((type) => <option key={type} value={type}>{AGENT_ACTIVITY_TYPE_CONFIG[type].label}</option>)}
</select>
<span className="cc-agent-activity-range-note">{t("commandCenter.agentActivity.rangeNote", "Time range filters loaded events; it is not sent to the server.")}</span>
</div>
) : null}
<AreaShell testId="agent-activity" isLoading={activity.isLoading} error={activity.error} isEmpty={!rows.length && !activity.hasMore} emptyMessage={t("commandCenter.agentActivity.empty", "No agent activity yet.")}>
{!rows.length ? <div className="cc-area-empty" data-testid="cc-area-agent-activity-empty"><p>{t("commandCenter.agentActivity.empty", "No agent activity yet.")}</p></div> : (
<div className="cc-agent-activity-list">
{rows.map((row) => <AgentActivityRow key={row.eventId} event={row} onOpenAgent={onOpenAgent} onOpenTask={onOpenTask} />)}
</div>
)}
{mode === "timeline" && timelineLimit < visibleEvents.length ? (
<button type="button" className="btn" onClick={() => setTimelineLimit((value) => value + TIMELINE_WINDOW_SIZE)}>{t("commandCenter.agentActivity.showMore", "Show more loaded activity")}</button>
) : null}
{mode === "timeline" && activity.hasMore ? (
<button type="button" className="btn" onClick={activity.loadOlder} disabled={activity.isLoadingOlder}>
{activity.isLoadingOlder ? t("commandCenter.agentActivity.loadingOlder", "Loading…") : t("commandCenter.agentActivity.loadOlder", "Load older")}
</button>
) : null}
{mode === "timeline" && !activity.hasMore && activity.exhaustedReason ? <p className="cc-agent-activity-end">{t("commandCenter.agentActivity.end", "End of activity history")}</p> : null}
</AreaShell>
</section>
);
}
function AgentActivityRow({ event, onOpenAgent, onOpenTask }: { event: ReturnType<typeof useAgentActivity>["events"][number]; onOpenAgent?: (agentId: string) => void; onOpenTask?: (taskId: string) => void }) {
const { t } = useTranslation("app");
const baseConfig = AGENT_ACTIVITY_TYPE_CONFIG[event.type] ?? DEFAULT_AGENT_ACTIVITY_CONFIG;
const config = { ...baseConfig, label: t(`commandCenter.agentActivity.eventTypes.${event.type}`, baseConfig.label) };
const openTaskLabel = t("commandCenter.agentActivity.openTask", "Open task {{taskId}}", { taskId: event.taskId });
const openAgentLabel = t("commandCenter.agentActivity.openAgent", "Open agent {{agentId}}", { agentId: event.agentId });
const content = <ActivityFeedRowPresentation
config={config}
timestamp={event.occurredAt}
details={<span className="activity-feed-description">{event.summary}</span>}
/>;
if (event.taskId && onOpenTask) {
return <div className="cc-agent-activity-target"><button type="button" className="cc-agent-activity-row" aria-label={openTaskLabel} onClick={() => onOpenTask(event.taskId!)}>{content}</button>{onOpenAgent ? <button type="button" className="btn btn-sm" aria-label={openAgentLabel} onClick={() => onOpenAgent(event.agentId)}>{event.agentId}</button> : null}</div>;
}
if (event.agentId && onOpenAgent) return <button type="button" className="cc-agent-activity-row" aria-label={openAgentLabel} onClick={() => onOpenAgent(event.agentId)}>{content}</button>;
return <div className="cc-agent-activity-row">{content}</div>;
}