import React, { useMemo, useState } from 'react' import type { ProgressEntry, ProgressEntryType } from '../types/kanban' import { progressEntryKey } from '../lib/progressEntryKey' import { IconBrain, IconTool, IconChevron, IconSparkle, IconShield, IconArrowRight, IconGate, IconZap, IconWorkItem, IconGatePass, IconGateReject, IconClock, IconHandoff } from './SvgIcons' interface AgentProgressBlockProps { entries: ProgressEntry[] agentStatus?: string currentTool?: string toolElapsedMs?: number lastToolSummary?: string sessionStatus?: string expandedByDefault?: boolean } const TERMINAL_STATUSES = new Set(['done', 'failed', 'cancelled']) export const INLINE_PROGRESS_ENTRY_TYPES = new Set(['thinking', 'tool_call', 'autonomy', 'needs_input', 'verification']) const ENTRY_CONFIG: Record = { thinking: { icon: , color: 'var(--accent)', label: 'Thinking' }, tool_call: { icon: , color: 'var(--green)', label: 'Tool' }, autonomy: { icon: , color: 'var(--yellow)', label: 'Autonomy' }, handoff: { icon: , color: 'var(--accent)', label: 'Handoff' }, gate_result: { icon: , color: 'var(--green)', label: 'Gate' }, status_change: { icon: , color: 'var(--text-secondary)', label: 'Status' }, work_item_started: { icon: , color: 'var(--accent)', label: 'Work item' }, gate_approved: { icon: , color: 'var(--green)', label: 'Gate Passed' }, gate_rejected: { icon: , color: 'var(--red)', label: 'Rejected' }, awaiting_manager_review: { icon: , color: 'var(--yellow)', label: 'Awaiting Manager Review' }, awaiting_human: { icon: , color: 'var(--yellow)', label: 'Awaiting Human Review' }, awaiting_review: { icon: , color: 'var(--yellow)', label: 'Awaiting Review' }, awaiting_peer: { icon: , color: 'var(--yellow)', label: 'Awaiting Peer' }, work_item_failed: { icon: , color: 'var(--red)', label: 'Failed' }, deadlock: { icon: , color: 'var(--red)', label: 'Deadlock' }, needs_input: { icon: , color: 'var(--yellow)', label: 'Needs Input' }, verification: { icon: , color: 'var(--accent)', label: 'Verification' }, } const COLLAPSED_COUNT = 5 function elapsed(ts: number): string { const sec = Math.floor((Date.now() - ts) / 1000) if (sec < 5) return 'just now' if (sec < 60) return `${sec}s ago` const min = Math.floor(sec / 60) if (min < 60) return `${min}m ago` return `${Math.floor(min / 60)}h ago` } function normalizeNestedJson(value: unknown): unknown { if (typeof value === 'string') { const trimmed = value.trim() if ( (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')) ) { try { return normalizeNestedJson(JSON.parse(trimmed)) } catch { return value } } return value } if (Array.isArray(value)) { return value.map(normalizeNestedJson) } if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value).map(([key, nestedValue]) => [key, normalizeNestedJson(nestedValue)]), ) } return value } function formatToolDetail(detail: string): string { const trimmed = detail.trim() if (!trimmed) return detail if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return detail try { return JSON.stringify(normalizeNestedJson(JSON.parse(trimmed)), null, 2) } catch { return detail } } export function AgentProgressBlock({ entries, agentStatus, currentTool, toolElapsedMs, lastToolSummary, sessionStatus, expandedByDefault }: AgentProgressBlockProps) { const [expanded, setExpanded] = useState(!!expandedByDefault) const isTerminal = !!sessionStatus && TERMINAL_STATUSES.has(sessionStatus) const isThinking = !isTerminal && agentStatus === 'reflecting' const isToolActive = !isTerminal && agentStatus === 'tool_active' const isWorking = isThinking || isToolActive const filteredEntries = useMemo(() => { return entries }, [entries]) const visibleEntries = useMemo(() => { if (expanded || filteredEntries.length <= COLLAPSED_COUNT) return filteredEntries return filteredEntries.slice(-COLLAPSED_COUNT) }, [filteredEntries, expanded]) const hiddenCount = filteredEntries.length - visibleEntries.length if (filteredEntries.length === 0 && !isWorking && !isTerminal) return null return (
{/* ── Live status indicator ──────────────────────── */} {isWorking && (
{isToolActive ? : } {isToolActive ? 'Running' : 'Thinking'} {isToolActive && currentTool && ( {currentTool} )} {isToolActive && typeof toolElapsedMs === 'number' && toolElapsedMs > 0 && ( {toolElapsedMs < 1000 ? `${toolElapsedMs}ms` : `${(toolElapsedMs / 1000).toFixed(1)}s`} )}
)} {/* ── Last tool result summary ──────────────────── */} {lastToolSummary && !isToolActive && (
Last tool result: {lastToolSummary}
)} {/* ── Collapse toggle (above timeline) ──────────── */} {hiddenCount > 0 && ( )} {/* ── Timeline entries ───────────────────────────── */} {visibleEntries.length > 0 && (
{visibleEntries.map((entry, i) => { const isLast = i === visibleEntries.length - 1 const cfg = ENTRY_CONFIG[entry.type] || ENTRY_CONFIG.status_change return (
{cfg.icon}
{!isLast &&
}
) })}
)} {/* ── Terminal state completion indicator ──────────── */} {isTerminal && !isWorking && (
{sessionStatus === 'done' ? '\u2713' : sessionStatus === 'failed' ? '\u2717' : '\u2014'} {sessionStatus === 'done' ? 'Completed' : sessionStatus === 'failed' ? 'Failed' : 'Cancelled'}
)} {/* ── Collapse button (when expanded) ────────────── */} {expanded && filteredEntries.length > COLLAPSED_COUNT && ( )}
) } export const AgentProgressEntryCard = React.memo(function AgentProgressEntryCard({ entry }: { entry: ProgressEntry }) { const [expanded, setExpanded] = useState(false) const cfg = ENTRY_CONFIG[entry.type] || ENTRY_CONFIG.status_change const hasToolDetail = entry.type === 'tool_call' && !!entry.detail if (entry.type === 'tool_call') { return (
{expanded && entry.detail && (
{formatToolDetail(entry.detail)}
)}
) } if (entry.type === 'autonomy') { const hasDetail = !!entry.detail return (
{expanded && entry.detail && (
{entry.detail}
)}
) } if (entry.type === 'thinking') { return (
{expanded && (
{entry.detail || entry.summary}
)}
) } if (entry.type === 'verification') { const hasDetail = !!entry.detail return (
{expanded && entry.detail && (
{entry.detail}
)}
) } return ( <>
{cfg.label} {entry.summary} {elapsed(entry.timestamp)}
{entry.detail && (
{entry.detail}
)} ) })