import { useState, useEffect, useRef } from 'react'; import { View, Text, ScrollView, Pressable } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import type { AgentStatus } from '@server/server/acp/types'; import type { SessionNotification } from '@agentclientprotocol/sdk'; export interface AgentStreamViewProps { agentId: string; agent: { id: string; status: AgentStatus; createdAt: Date; type: 'claude'; }; updates: Array<{ timestamp: Date; notification: SessionNotification; }>; onBack: () => void; onKillAgent: (agentId: string) => void; onCancelAgent: (agentId: string) => void; } function getStatusColor(status: AgentStatus): string { switch (status) { case 'initializing': return '#f59e0b'; case 'ready': return '#3b82f6'; case 'processing': return '#fbbf24'; case 'completed': return '#22c55e'; case 'failed': return '#ef4444'; case 'killed': return '#9ca3af'; default: return '#9ca3af'; } } function getStatusIcon(status: AgentStatus): string { switch (status) { case 'initializing': case 'processing': return '⏳'; case 'ready': case 'completed': return '✓'; case 'failed': case 'killed': return '✗'; default: return '•'; } } function formatTimestamp(date: Date): string { return new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true, }).format(date); } function AgentUpdate({ update, }: { update: { timestamp: Date; notification: SessionNotification }; }) { const [isExpanded, setIsExpanded] = useState(true); const notification = update.notification as any; // Parse different notification types if (notification.type === 'sessionUpdate') { const sessionUpdate = notification.sessionUpdate || {}; // Handle message chunks if (sessionUpdate.kind === 'agent_message_chunk') { return ( {formatTimestamp(update.timestamp)} {sessionUpdate.chunk || ''} ); } // Handle tool calls if (sessionUpdate.kind === 'tool_call') { return ( setIsExpanded(!isExpanded)} className="px-3 py-2 active:bg-zinc-700" > {formatTimestamp(update.timestamp)} Tool Call: {sessionUpdate.toolName || 'unknown'} {isExpanded && sessionUpdate.arguments && ( {JSON.stringify(sessionUpdate.arguments, null, 2)} )} ); } // Handle tool call updates if (sessionUpdate.kind === 'tool_call_update') { return ( {formatTimestamp(update.timestamp)} {sessionUpdate.status || 'updating'} ); } // Handle available commands if (sessionUpdate.kind === 'available_commands_update') { const commands = sessionUpdate.commands || []; return ( setIsExpanded(!isExpanded)} className="px-3 py-2 active:bg-zinc-700" > {formatTimestamp(update.timestamp)} Available Commands ({commands.length}) {isExpanded && ( {commands.map((cmd: any, idx: number) => ( • {cmd.name || cmd} ))} )} ); } // Generic session update return ( {formatTimestamp(update.timestamp)} {JSON.stringify(sessionUpdate, null, 2)} ); } // Fallback for unknown notification types return ( {formatTimestamp(update.timestamp)} {JSON.stringify(notification, null, 2)} ); } export function AgentStreamView({ agentId, agent, updates, onBack, onKillAgent, onCancelAgent, }: AgentStreamViewProps) { const scrollViewRef = useRef(null); const insets = useSafeAreaInsets(); // Auto-scroll to bottom when new updates arrive useEffect(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); }, [updates]); const canCancel = agent.status === 'processing'; const canKill = agent.status !== 'killed' && agent.status !== 'completed'; return ( {/* Header */} {/* Back button */} ← Back to Chat {/* Agent info */} Agent: {agentId.substring(0, 8)} {getStatusIcon(agent.status)} {agent.status} Created: {formatTimestamp(agent.createdAt)} {/* Control buttons */} {(canCancel || canKill) && ( {canCancel && ( onCancelAgent(agentId)} className="flex-1 bg-orange-600 px-4 py-2 rounded-lg active:bg-orange-700" > Cancel )} {canKill && ( onKillAgent(agentId)} className="flex-1 bg-red-600 px-4 py-2 rounded-lg active:bg-red-700" > Kill )} )} {/* Updates list */} {updates.length === 0 ? ( No updates yet.{'\n'}Waiting for agent activity... ) : ( updates.map((update, idx) => ) )} ); }