import { useRef, useEffect, useState, useCallback } from "react"; import { SimpleMarkdown } from "../SimpleMarkdown"; import type { MultiTaskOutputEntry } from "../../hooks/useMultiTaskSubscription"; interface DirectiveLogStreamProps { entries: MultiTaskOutputEntry[]; /** Map of taskId -> label for display */ taskMap: Map; /** Whether the WebSocket is connected */ connected: boolean; /** Filter: set of visible task IDs (null = show all) */ visibleTaskIds: Set | null; /** Current search query */ searchQuery: string; /** Whether the panel is collapsed */ isCollapsed: boolean; /** Toggle collapse state */ onToggleCollapse: () => void; /** Update visible task filter */ onSetVisibleTaskIds: (ids: Set | null) => void; /** Update search query */ onSetSearchQuery: (query: string) => void; /** Clear all entries */ onClear: () => void; } // Assign stable colors to tasks const TASK_COLORS = [ "#75aafc", // blue "#4ade80", // green "#f59e0b", // amber "#a78bfa", // violet "#f472b6", // pink "#22d3ee", // cyan "#fb923c", // orange "#34d399", // emerald ]; function getTaskColor(index: number): string { return TASK_COLORS[index % TASK_COLORS.length]; } export function DirectiveLogStream({ entries, taskMap, connected, visibleTaskIds, searchQuery, isCollapsed, onToggleCollapse, onSetVisibleTaskIds, onSetSearchQuery, onClear, }: DirectiveLogStreamProps) { const containerRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); const [showFilters, setShowFilters] = useState(false); // Build task color map const taskColorMap = new Map(); let colorIdx = 0; for (const [taskId] of taskMap) { taskColorMap.set(taskId, getTaskColor(colorIdx++)); } // Filter entries const filteredEntries = entries.filter((entry) => { // Filter by visible task IDs if (visibleTaskIds && !visibleTaskIds.has(entry.taskId)) return false; // Filter by search query if (searchQuery) { const q = searchQuery.toLowerCase(); const matchesContent = entry.content?.toLowerCase().includes(q); const matchesLabel = entry.taskLabel?.toLowerCase().includes(q); const matchesTool = entry.toolName?.toLowerCase().includes(q); if (!matchesContent && !matchesLabel && !matchesTool) return false; } return true; }); // Handle scroll const handleScroll = useCallback(() => { if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; setAutoScroll(isAtBottom); }, []); // Auto-scroll when entries change useEffect(() => { if (autoScroll && containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [filteredEntries.length, autoScroll]); // Count active (running) tasks const activeTaskCount = Array.from(taskMap.keys()).length; if (isCollapsed) { return ( ); } return (
{/* Header */}
{connected && ( Live )} {filteredEntries.length} entries
{entries.length > 0 && ( )} {!autoScroll && ( )}
{/* Filters */} {showFilters && (
{/* Search */} onSetSearchQuery(e.target.value)} placeholder="Search logs..." className="bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-0.5 text-[10px] font-mono text-white w-[160px] placeholder-[#556677]" /> {/* Task filter buttons */} {Array.from(taskMap.entries()).map(([taskId, label]) => { const isVisible = visibleTaskIds === null || visibleTaskIds.has(taskId); const color = taskColorMap.get(taskId) || "#75aafc"; return ( ); })}
)} {/* Log output */}
{filteredEntries.length === 0 ? (
{entries.length === 0 ? connected ? "Waiting for output..." : "No tasks subscribed" : "No entries match filter"}
) : (
{filteredEntries.map((entry, idx) => ( ))}
)}
); } function LogEntry({ entry, color, }: { entry: MultiTaskOutputEntry; color: string; }) { const [expanded, setExpanded] = useState(false); // Skip empty content for tool results if (entry.messageType === "tool_result" && !entry.content) return null; return (
{/* Task label */} {entry.taskLabel} | {/* Content */}
{entry.messageType === "assistant" && ( )} {entry.messageType === "tool_use" && (
* {entry.toolName || "unknown"} {entry.toolInput && Object.keys(entry.toolInput).length > 0 && ( )} {expanded && entry.toolInput && (
                {JSON.stringify(entry.toolInput, null, 2)}
              
)}
)} {entry.messageType === "tool_result" && (
{entry.isError ? "x" : "+"} {" "} {entry.content.split("\n")[0]} {entry.content.includes("\n") && "..."}
)} {entry.messageType === "result" && (
Done {entry.costUsd !== undefined && ( ${entry.costUsd.toFixed(4)} )} {entry.durationMs !== undefined && ( {(entry.durationMs / 1000).toFixed(1)}s )}
)} {entry.messageType === "error" && ( {entry.content} )} {entry.messageType === "system" && ( {entry.content} )} {!["assistant", "tool_use", "tool_result", "result", "error", "system"].includes( entry.messageType ) && ( {entry.content} )}
); }