1→/** 2→ * TerminalPanel Component 3→ * ======================= 4→ * 5→ * Real-time terminal display for agent output with timestamps, auto-scroll, 6→ * and color-coded log types. 7→ */ 8→ 9→import { useState, useEffect, useRef, useCallback } from 'react' 10→import { useWebSocket, type WebSocketMessage } from '../hooks/useWebSocket' 11→ 12→export type LogType = 'info' | 'error' | 'tool' | 'permission' | 'system' 13→ 14→export interface LogEntry { 15→ id: string 16→ timestamp: string 17→ agentId: string 18→ type: LogType 19→ content: string 20→} 21→ 22→interface TerminalPanelProps { 23→ projectName: string 24→ agentId?: 'A' | 'B' | 'all' 25→ maxLines?: number 26→ showTimestamps?: boolean 27→ autoScroll?: boolean 28→ onLogEntry?: (entry: LogEntry) => void 29→} 30→ 31→const LOG_TYPE_COLORS: Record = { 32→ info: 'text-text-primary', 33→ error: 'text-error', 34→ tool: 'text-accent', 35→ permission: 'text-stalled', 36→ system: 'text-text-secondary', 37→} 38→ 39→const LOG_TYPE_ICONS: Record = { 40→ info: 'INFO', 41→ error: 'ERR ', 42→ tool: 'TOOL', 43→ permission: 'PERM', 44→ system: 'SYS ', 45→} 46→ 47→export function TerminalPanel({ 48→ projectName, 49→ agentId = 'all', 50→ maxLines = 1000, 51→ showTimestamps = true, 52→ autoScroll: initialAutoScroll = true, 53→ onLogEntry, 54→}: TerminalPanelProps) { 55→ const [logs, setLogs] = useState([]) 56→ const [autoScroll, setAutoScroll] = useState(initialAutoScroll) 57→ const [filter, setFilter] = useState('all') 58→ const [searchQuery, setSearchQuery] = useState('') 59→ const terminalRef = useRef(null) 60→ const bottomRef = useRef(null) 61→ const userScrolledRef = useRef(false) 62→ 63→ // Generate unique ID for log entries 64→ const generateId = useCallback(() => { 65→ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` 66→ }, []) 67→ 68→ // Handle WebSocket messages 69→ const handleMessage = useCallback((message: WebSocketMessage) => { 70→ if (message.type === 'agent_log') { 71→ const logAgentId = message.agent_id as string 72→ 73→ // Filter by agent if specified 74→ if (agentId !== 'all' && logAgentId !== agentId) { 75→ return 76→ } 77→ 78→ const entry: LogEntry = { 79→ id: generateId(), 80→ timestamp: message.timestamp as string, 81→ agentId: logAgentId, 82→ type: (message.log_type as LogType) || 'info', 83→ content: message.content as string, 84→ } 85→ 86→ setLogs((prev) => { 87→ const newLogs = [...prev, entry] 88→ // Trim to max lines 89→ if (newLogs.length > maxLines) { 90→ return newLogs.slice(-maxLines) 91→ } 92→ return newLogs 93→ }) 94→ 95→ onLogEntry?.(entry) 96→ } else if (message.type === 'connected') { 97→ // Add system message for connection 98→ const entry: LogEntry = { 99→ id: generateId(), 100→ timestamp: message.timestamp as string, 101→ agentId: 'SYS', 102→ type: 'system', 103→ content: `Connected to project "${projectName}"`, 104→ } 105→ setLogs((prev) => [...prev, entry]) 106→ } else if (message.type === 'agent_status') { 107→ // Add system message for status changes 108→ const entry: LogEntry = { 109→ id: generateId(), 110→ timestamp: message.timestamp as string, 111→ agentId: message.agent_id as string, 112→ type: 'system', 113→ content: `Agent ${message.agent_id} status changed to ${message.status}`, 114→ } 115→ setLogs((prev) => [...prev, entry]) 116→ } 117→ }, [agentId, maxLines, generateId, onLogEntry, projectName]) 118→ 119→ // WebSocket connection 120→ const { status: wsStatus, reconnectAttempts } = useWebSocket({ 121→ projectName, 122→ onMessage: handleMessage, 123→ autoReconnect: true, 124→ maxReconnectAttempts: 0, // Infinite retries 125→ }) 126→ 127→ // Auto-scroll effect 128→ useEffect(() => { 129→ if (autoScroll && !userScrolledRef.current && bottomRef.current) { 130→ bottomRef.current.scrollIntoView({ behavior: 'smooth' }) 131→ } 132→ }, [logs, autoScroll]) 133→ 134→ // Handle manual scroll 135→ const handleScroll = useCallback(() => { 136→ if (!terminalRef.current) return 137→ 138→ const { scrollTop, scrollHeight, clientHeight } = terminalRef.current 139→ const isAtBottom = scrollHeight - scrollTop - clientHeight < 50 140→ 141→ userScrolledRef.current = !isAtBottom 142→ 143→ if (isAtBottom && !autoScroll) { 144→ setAutoScroll(true) 145→ } 146→ }, [autoScroll]) 147→ 148→ // Clear logs 149→ const clearLogs = useCallback(() => { 150→ setLogs([]) 151→ }, []) 152→ 153→ // Copy logs to clipboard 154→ const copyLogs = useCallback(() => { 155→ const text = filteredLogs 156→ .map((log) => { 157→ const timestamp = showTimestamps ? `[${formatTimestamp(log.timestamp)}] ` : '' 158→ return `${timestamp}[${log.agentId}] [${LOG_TYPE_ICONS[log.type]}] ${log.content}` 159→ }) 160→ .join('\n') 161→ 162→ navigator.clipboard.writeText(text).then(() => { 163→ // Could add a toast notification here 164→ console.log('Logs copied to clipboard') 165→ }) 166→ }, [logs, showTimestamps]) 167→ 168→ // Format timestamp 169→ const formatTimestamp = (timestamp: string): string => { 170→ try { 171→ const date = new Date(timestamp) 172→ return date.toLocaleTimeString('en-US', { 173→ hour12: false, 174→ hour: '2-digit', 175→ minute: '2-digit', 176→ second: '2-digit', 177→ fractionalSecondDigits: 3, 178→ }) 179→ } catch { 180→ return timestamp 181→ } 182→ } 183→ 184→ // Filter logs 185→ const filteredLogs = logs.filter((log) => { 186→ if (filter !== 'all' && log.type !== filter) return false 187→ if (searchQuery && !log.content.toLowerCase().includes(searchQuery.toLowerCase())) return false 188→ return true 189→ }) 190→ 191→ // Connection status indicator 192→ const getStatusIndicator = () => { 193→ switch (wsStatus) { 194→ case 'connected': 195→ return { color: 'bg-success', text: 'Connected' } 196→ case 'connecting': 197→ return { color: 'bg-warning animate-pulse', text: 'Connecting...' } 198→ case 'reconnecting': 199→ return { color: 'bg-warning animate-pulse', text: `Reconnecting (${reconnectAttempts})...` } 200→ case 'disconnected': 201→ return { color: 'bg-error', text: 'Disconnected' } 202→ default: 203→ return { color: 'bg-text-secondary', text: 'Unknown' } 204→ } 205→ } 206→ 207→ const statusIndicator = getStatusIndicator() 208→ 209→ return ( 210→
211→ {/* Terminal Header */} 212→
213→
214→
215→
216→ {statusIndicator.text} 217→
218→ 219→ Agent: {agentId === 'all' ? 'All' : agentId} 220→ 221→ 222→ Lines: {filteredLogs.length} 223→ 224→
225→ 226→
227→ {/* Filter Dropdown */} 228→ 241→ 242→ {/* Search Input */} 243→
244→ 245→ 246→ 247→ setSearchQuery(e.target.value)} 251→ placeholder="Search..." 252→ className="text-xs bg-surface border border-border rounded pl-7 pr-2 py-1 w-32 text-text-primary placeholder-text-secondary focus:outline-none focus:ring-1 focus:ring-accent" 253→ data-testid="terminal-search" 254→ /> 255→
256→ 257→ {/* Auto-scroll Toggle */} 258→ 275→ 276→ {/* Copy Button */} 277→ 287→ 288→ {/* Clear Button */} 289→ 299→
300→
301→ 302→ {/* Terminal Content */} 303→
309→ {filteredLogs.length === 0 ? ( 310→
311→
312→ 313→ 314→ 315→

No log entries yet

316→

Start an agent to see output here

317→
318→
319→ ) : ( 320→ filteredLogs.map((log) => ( 321→
326→ {/* Timestamp */} 327→ {showTimestamps && ( 328→ 329→ [{formatTimestamp(log.timestamp)}] 330→ 331→ )} 332→ 333→ {/* Agent ID */} 334→ 339→ [{log.agentId}] 340→ 341→ 342→ {/* Log Type */} 343→ 344→ [{LOG_TYPE_ICONS[log.type]}] 345→ 346→ 347→ {/* Content */} 348→ 349→ {log.content} 350→ 351→
352→ )) 353→ )} 354→
355→
356→
357→ ) 358→} 359→ 360→export default TerminalPanel 361→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.