'use client'; import React, { useEffect, useState, useMemo } from 'react'; import type { BeadIssue } from '../../lib/types'; import type { ActivityEvent } from '../../lib/activity'; import { useArchetypes } from '../../hooks/use-archetypes'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn, getArchetypeDisplayChar } from '@/lib/utils'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { getEventTone, formatRelativeTime, getInitials } from './activity-panel'; export interface SwarmCommandFeedProps { epicId: string; issues: BeadIssue[]; projectRoot: string; } export function SwarmCommandFeed({ epicId, issues, projectRoot }: SwarmCommandFeedProps) { const [activities, setActivities] = useState([]); const { archetypes } = useArchetypes(projectRoot); // 1. Compute Contextual Tasks const contextBeads = useMemo(() => { return issues.filter(issue => { const parent = issue.dependencies.find(d => d.type === 'parent'); return parent?.target === epicId; }); }, [issues, epicId]); const contextBeadIds = useMemo(() => new Set(contextBeads.map(b => b.id)), [contextBeads]); // 2. Compute "Active Squad Roster" (Unique assignees working on in_progress tasks for THIS epic) const rosterEntries = useMemo(() => { const activeAssignees = new Set(); const entries: { assignee: string, currentTask: string, archetype?: any }[] = []; contextBeads.forEach(b => { if (b.status === 'in_progress' && b.assignee && !activeAssignees.has(b.assignee)) { activeAssignees.add(b.assignee); const assigneeStr = b.assignee.toLowerCase(); const matchedArchetype = archetypes.find(a => assigneeStr.includes(a.id.toLowerCase()) || assigneeStr.includes(a.name.toLowerCase()) ); entries.push({ assignee: b.assignee, currentTask: b.title, archetype: matchedArchetype }); } }); return entries; }, [contextBeads, archetypes]); // 3. Subscribe to real-time activity, filtering ONLY for this epic's children useEffect(() => { const source = new EventSource(`/api/events?projectRoot=${encodeURIComponent(projectRoot)}`); const onActivity = (event: MessageEvent) => { try { const data = JSON.parse(event.data) as ActivityEvent; // ONLY accept events for beads that belong to this Epic if (data?.beadId && contextBeadIds.has(data.beadId)) { setActivities(prev => [data, ...prev].slice(0, 100)); // Keep a healthy buffer for terminal feel } } catch (e) { // Ignore parse errors } }; source.addEventListener('activity', onActivity as EventListener); return () => { source.removeEventListener('activity', onActivity as EventListener); source.close(); }; }, [projectRoot, contextBeadIds]); return (
{/* SQUAD ROSTER SECTION */}

Active Squad

{rosterEntries.length} DEPLOYED
{rosterEntries.length === 0 ? (
No agents currently operating on this Epic.
) : (
{rosterEntries.map((agent, i) => (
{agent.archetype ? getArchetypeDisplayChar(agent.archetype) : getInitials(agent.assignee)}
{agent.assignee}
> {agent.currentTask}
))}
)}
{/* STREAMING LOG / TERMINAL SECTION */}

Live Command Feed

Tailing Logs
{activities.length === 0 ? (

Waiting for agent signals...

) : (
{activities.map((activity) => { const eventTone = getEventTone(activity.kind); return (
[{formatRelativeTime(activity.timestamp)}]
{activity.actor && ( {activity.actor.split(' ')[0]} )} {eventTone.label.toLowerCase()} {activity.beadId}
{activity.beadTitle}
); })}
)}
); }