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],
},
}));