import { useState, useEffect, useRef } from 'react'; import { View, Text, ScrollView, Pressable } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { StyleSheet } from 'react-native-unistyles'; 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)} style={stylesheet.collapsibleHeader} > {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)} style={stylesheet.collapsibleHeader} > {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)} style={stylesheet.cancelButton} > Cancel )} {canKill && ( onKillAgent(agentId)} style={stylesheet.killButton} > Kill )} )} {/* Updates list */} {updates.length === 0 ? ( No updates yet.{'\n'}Waiting for agent activity... ) : ( updates.map((update, idx) => ) )} ); } const stylesheet = StyleSheet.create((theme: import('../styles/theme').Theme) => ({ container: { flex: 1, backgroundColor: theme.colors.black, }, header: { backgroundColor: theme.colors.zinc[900], borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.zinc[800], paddingHorizontal: theme.spacing[4], paddingBottom: theme.spacing[4], }, backButton: { backgroundColor: theme.colors.zinc[800], paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.lg, marginBottom: theme.spacing[4], }, backButtonText: { color: theme.colors.white, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, textAlign: "center", }, agentInfoRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: theme.spacing[3], }, agentIdRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, agentLabel: { color: theme.colors.zinc[400], fontSize: theme.fontSize.sm, }, agentId: { color: theme.colors.white, fontSize: theme.fontSize.sm, fontFamily: "monospace", }, statusBadge: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[1], borderRadius: theme.borderRadius.full, }, statusIcon: { color: theme.colors.white, fontSize: theme.fontSize.xs, }, statusText: { color: theme.colors.white, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, }, createdText: { color: theme.colors.zinc[500], fontSize: theme.fontSize.xs, }, controlButtons: { flexDirection: "row", gap: theme.spacing[2], marginTop: theme.spacing[4], }, cancelButton: { flex: 1, backgroundColor: theme.colors.orange[600], paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.lg, }, killButton: { flex: 1, backgroundColor: theme.colors.red[600], paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.lg, }, controlButtonText: { color: theme.colors.white, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, textAlign: "center", }, scrollView: { flex: 1, paddingHorizontal: theme.spacing[4], }, emptyState: { flex: 1, alignItems: "center", justifyContent: "center", paddingVertical: theme.spacing[12], }, emptyStateText: { color: theme.colors.zinc[500], fontSize: theme.fontSize.sm, textAlign: "center", }, updateCard: { backgroundColor: theme.colors.zinc[800], borderRadius: theme.borderRadius.lg, padding: theme.spacing[3], marginBottom: theme.spacing[2], }, collapsibleCard: { backgroundColor: theme.colors.zinc[800], borderRadius: theme.borderRadius.lg, marginBottom: theme.spacing[2], overflow: "hidden", }, collapsibleHeader: { paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], }, collapsibleContent: { paddingHorizontal: theme.spacing[3], paddingBottom: theme.spacing[3], borderTopWidth: theme.borderWidth[1], borderTopColor: theme.colors.zinc[700], }, timestampText: { color: theme.colors.zinc[500], fontSize: theme.fontSize.xs, marginBottom: theme.spacing[1], }, messageText: { color: theme.colors.white, fontSize: theme.fontSize.sm, }, toolCallText: { color: "#60a5fa", fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, }, updateStatusText: { color: "#fbbf24", fontSize: theme.fontSize.sm, }, commandsText: { color: "#4ade80", fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, }, commandItem: { color: theme.colors.zinc[400], fontSize: theme.fontSize.xs, marginTop: theme.spacing[1], }, codeText: { color: theme.colors.zinc[400], fontSize: theme.fontSize.xs, fontFamily: "monospace", marginTop: theme.spacing[2], }, }));