import { useEffect, useMemo, useState } from 'react'; import { api } from '@renderer/api'; import { MemberExecutionLog } from '@renderer/components/team/members/MemberExecutionLog'; import { asEnhancedChunkArray } from '@renderer/types/data'; import { AlertCircle, Clock, FileText, Loader2 } from 'lucide-react'; import type { BoardTaskLogActor, BoardTaskLogSegment, BoardTaskLogStreamResponse, } from '@shared/types'; interface TaskLogStreamSectionProps { teamName: string; taskId: string; } function formatRelativeTime(isoString: string): string { const date = new Date(isoString); const diffMs = Date.now() - date.getTime(); const diffMin = Math.floor(diffMs / 60_000); const diffHours = Math.floor(diffMin / 60); const diffDays = Math.floor(diffHours / 24); if (!Number.isFinite(diffMs)) return '--'; if (diffMin < 1) return 'just now'; if (diffMin < 60) return `${diffMin}m ago`; if (diffHours < 24) return `${diffHours}h ago`; return `${diffDays}d ago`; } function actorLabel(actor: BoardTaskLogActor): string { if (actor.memberName) { return actor.memberName; } if (actor.role === 'lead' || actor.isSidechain === false) { return 'lead session'; } if (actor.agentId) { return `member ${actor.agentId.slice(0, 8)}`; } return `member session ${actor.sessionId.slice(0, 8)}`; } function normalizeResponse(response: BoardTaskLogStreamResponse): BoardTaskLogStreamResponse { return { participants: response.participants, defaultFilter: response.defaultFilter, segments: response.segments.map((segment) => ({ ...segment, chunks: asEnhancedChunkArray(segment.chunks) ?? [], })), }; } function SegmentMarker({ segment }: { segment: BoardTaskLogSegment }): React.JSX.Element { return (
{actorLabel(segment.actor)} {formatRelativeTime(segment.endTimestamp)}
); } function SegmentBlock({ segment, showHeader, }: { segment: BoardTaskLogSegment; showHeader: boolean; }): React.JSX.Element { return (
{showHeader ? : null}
); } export function TaskLogStreamSection({ teamName, taskId, }: TaskLogStreamSectionProps): React.JSX.Element { const [stream, setStream] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedParticipantKey, setSelectedParticipantKey] = useState<'all' | string>('all'); useEffect(() => { let cancelled = false; const run = async (): Promise => { try { setLoading(true); setError(null); const response = normalizeResponse(await api.teams.getTaskLogStream(teamName, taskId)); if (cancelled) return; setStream(response); setSelectedParticipantKey(response.defaultFilter); } catch (loadError) { if (cancelled) return; setError(loadError instanceof Error ? loadError.message : 'Failed to load task log stream'); setStream(null); } finally { if (!cancelled) { setLoading(false); } } }; void run(); return () => { cancelled = true; }; }, [taskId, teamName]); const participants = stream?.participants ?? []; const showChips = participants.length > 1; const visibleSegments = useMemo(() => { const source = stream?.segments ?? []; const filtered = selectedParticipantKey === 'all' ? source : source.filter((segment) => segment.participantKey === selectedParticipantKey); return [...filtered].reverse(); }, [selectedParticipantKey, stream?.segments]); const showSegmentHeaders = participants.length > 1 || (selectedParticipantKey !== 'all' && visibleSegments.length > 1); if (loading) { return (

Task Log Stream

Loading task log stream...
); } if (error) { return (

Task Log Stream

{error}
); } return (

Task Log Stream

Task-scoped transcript logs rendered with the same execution-log components used in Logs.

{showChips ? (
{participants.map((participant) => ( ))}
) : null} {visibleSegments.length === 0 ? (
No task log stream yet

Task-linked transcript logs will appear here when explicit task-linked transcript metadata is available.

) : (
{visibleSegments.map((segment) => ( ))}
)}
); }