import React, { useEffect, useRef, useState } from 'react'; /* Left-border accent colors per thought type — no emojis */ const BORDER_COLORS: Record = { thought: '#5b8fb9', tool_call: '#d47c3a', tool_result: '#4a7c59', observation: '#5b8fb9', decision: '#8b6cc1', user_action: '#8b6cc1', warning: '#c0392b', }; interface ActivityEvent { type: string; message: string; state: string; timestamp: number | string; agent_name?: string; thought_type?: string; content?: string; stage_name?: string; summary?: string; artifacts?: Array<{ name: string; path: string; description: string }>; decisions?: string[]; warnings?: string[]; next_stage_name?: string; next_stage_preview?: string; } interface Props { events: ActivityEvent[]; thinkingData?: { agent_name: string; message: string } | null; } /* Noise patterns to suppress from the live log */ const NOISE_PATTERNS = [ /^Transitioning:/i, /^Beginning stage:/i, /^Generating approval summary/i, /^Waiting for user approval/i, /^User approved stage/i, /^User rejected stage/i, /^Human-in-the-loop mode enabled/i, /^LLM backend:/i, /^Approved:/i, /^AgentIC Compute Engine selected/i, /^Compute engine ready/i, /^Starting Build Process for/i, /^Build started for/i, /^Stage \w+ complete.*awaiting approval/i, /^\[Orchestrator\] (Beginning|Generating|Waiting|Human-in)/i, ]; /* Filter noise: skip internal transitions, raw file paths, empty messages, known noise */ function isNoise(evt: ActivityEvent): boolean { const msg = (evt.content || evt.message || '').trim(); if (!msg) return true; if (/^\/[a-zA-Z0-9/_.\-]+$/.test(msg)) return true; if (evt.type === 'transition') return true; for (const pattern of NOISE_PATTERNS) { if (pattern.test(msg)) return true; } return false; } /* Truncate full file paths to just the filename */ function shortenPaths(text: string): string { return text.replace(/\/(?:home|tmp|var|opt)\/[^\s]+\/([^\s/]+)/g, '$1'); } /* Truncate long messages at ~120 chars */ function truncate(text: string, max = 120): { display: string; isTruncated: boolean } { if (text.length <= max) return { display: text, isTruncated: false }; return { display: text.slice(0, max) + '…', isTruncated: true }; } export const ActivityFeed: React.FC = ({ events, thinkingData }) => { const ref = useRef(null); const [autoScroll, setAutoScroll] = useState(true); const [expanded, setExpanded] = useState>(new Set()); const lastTop = useRef(0); useEffect(() => { if (autoScroll && ref.current) { ref.current.scrollTop = ref.current.scrollHeight; } }, [events, autoScroll]); const onScroll = () => { if (!ref.current) return; const el = ref.current; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60; if (el.scrollTop < lastTop.current && !atBottom) setAutoScroll(false); else if (atBottom) setAutoScroll(true); lastTop.current = el.scrollTop; }; const toggle = (i: number) => setExpanded(prev => { const s = new Set(prev); s.has(i) ? s.delete(i) : s.add(i); return s; }); /* Filter + deduplicate consecutive identical messages */ const filtered = events.filter(e => { if (e.type === 'ping' || e.type === 'stream_end' || e.type === 'stage_complete') return false; if (isNoise(e)) return false; return true; }); const rows: typeof filtered = []; for (const evt of filtered) { const prev = rows[rows.length - 1]; if (prev && (prev.content || prev.message) === (evt.content || evt.message)) continue; rows.push(evt); } return (
Live Log {!autoScroll && ( )}
{rows.length === 0 ? (
Waiting for agent activity…
) : ( rows.map((evt, i) => { const type = evt.thought_type || 'thought'; const border = BORDER_COLORS[type] || BORDER_COLORS.thought; const raw = shortenPaths(evt.content || evt.message || ''); const isExp = expanded.has(i); const { display, isTruncated } = isExp ? { display: raw, isTruncated: false } : truncate(raw); const ts = typeof evt.timestamp === 'string' ? evt.timestamp.split('T')[1]?.substring(0, 8) || evt.timestamp : new Date((evt.timestamp as number) * 1000).toLocaleTimeString('en-US', { hour12: false, }); const stage = evt.state?.replace(/_/g, ' ') || ''; return (
(isTruncated || isExp) && toggle(i)} > {ts} {stage && {stage}} {display} {isTruncated && }
); }) )} {/* Thinking indicator */} {thinkingData && (
{thinkingData.agent_name || 'Agent'} {thinkingData.message}
)}
); };