A duplicate agent id slipping through useAgents (race between initial fetch and an SSE refresh, or backend pagination edge case) was flooding React with "Encountered two children with the same key" warnings. With the active panel re-rendering on every transcript event the warning fired every few ms and snowballed the console buffer until the page crashed with OOM. Dedupe by id at the hook (so every consumer benefits) and again in ActiveAgentsPanel as belt-and-braces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import { Activity } from "lucide-react";
|
|
import type { Agent } from "../api";
|
|
import "./ActiveAgentsPanel.css";
|
|
import { useLiveTranscript } from "../hooks/useLiveTranscript";
|
|
|
|
interface LiveAgentCardProps {
|
|
agent: Agent;
|
|
projectId?: string;
|
|
onSelect?: (agentId: string) => void;
|
|
}
|
|
|
|
function LiveAgentCard({ agent, projectId, onSelect }: LiveAgentCardProps) {
|
|
const { entries, isConnected } = useLiveTranscript(agent.taskId, projectId);
|
|
const elapsed = agent.lastHeartbeatAt
|
|
? Math.floor((Date.now() - new Date(agent.lastHeartbeatAt).getTime()) / 1000)
|
|
: 0;
|
|
|
|
const handleSelect = () => {
|
|
if (onSelect) {
|
|
onSelect(agent.id);
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
handleSelect();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="live-agent-card"
|
|
onClick={handleSelect}
|
|
onKeyDown={handleKeyDown}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label={`Select agent ${agent.name}`}
|
|
>
|
|
<div className="live-agent-card-header">
|
|
<div className="live-agent-card-name">
|
|
<span className="live-agent-pulse" />
|
|
<span>{agent.name}</span>
|
|
</div>
|
|
{agent.taskId && (
|
|
<span className="live-agent-task badge">{agent.taskId}</span>
|
|
)}
|
|
</div>
|
|
<div className="live-agent-card-transcript">
|
|
{entries.length === 0 ? (
|
|
<div className="live-agent-card-empty">
|
|
{isConnected ? "Waiting for output..." : "Connecting..."}
|
|
</div>
|
|
) : (
|
|
entries.slice(0, 20).map((entry, i) => (
|
|
<div key={i} className="live-agent-card-line">
|
|
{entry.text}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
<div className="live-agent-card-footer">
|
|
<span className="text-secondary">{formatElapsed(elapsed)}</span>
|
|
{isConnected && <Activity size={12} className="live-agent-streaming-dot" />}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatElapsed(seconds: number): string {
|
|
if (seconds < 60) return `${seconds}s`;
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
}
|
|
|
|
interface ActiveAgentsPanelProps {
|
|
agents: Agent[];
|
|
projectId?: string;
|
|
onAgentSelect?: (agentId: string) => void;
|
|
}
|
|
|
|
export function ActiveAgentsPanel({ agents, projectId, onAgentSelect }: ActiveAgentsPanelProps) {
|
|
// Dedupe by id defensively. The store should return unique agents but a race
|
|
// between the initial fetch and an SSE refresh can briefly surface the same
|
|
// agent twice — without this guard React floods the console with duplicate
|
|
// key warnings (which previously snowballed into OOM).
|
|
const uniqueAgents = Array.from(new Map(agents.map((a) => [a.id, a])).values());
|
|
|
|
if (uniqueAgents.length === 0) return null;
|
|
|
|
return (
|
|
<div className="active-agents-panel">
|
|
<div className="active-agents-panel-header">
|
|
<Activity size={16} />
|
|
<span>Active Agents ({uniqueAgents.length})</span>
|
|
</div>
|
|
<div className="active-agents-grid">
|
|
{uniqueAgents.map(agent => (
|
|
<LiveAgentCard key={agent.id} agent={agent} projectId={projectId} onSelect={onAgentSelect} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|