feat: refactor app structure and components for improved organization and functionality

- Moved components and hooks into a new directory structure under `src/app` for better organization.
- Updated the main entry point to use `index.ts` and adjusted routing accordingly.
- Introduced new components such as `ActiveProcesses`, `AgentStreamView`, and `ConnectionStatus` to enhance the app's functionality.
- Removed deprecated components and hooks to streamline the codebase.
- Updated styles to align with the new theme structure and ensure consistency across the app.

This refactor aims to improve maintainability and scalability of the application.
This commit is contained in:
Mohamed Boudra
2025-10-22 15:48:53 +02:00
parent 2d1ab5bf79
commit 89641a18e7
31 changed files with 8915 additions and 522 deletions
@@ -0,0 +1,453 @@
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 (
<View style={stylesheet.updateCard}>
<Text style={stylesheet.timestampText}>
{formatTimestamp(update.timestamp)}
</Text>
<Text style={stylesheet.messageText}>{sessionUpdate.chunk || ''}</Text>
</View>
);
}
// Handle tool calls
if (sessionUpdate.kind === 'tool_call') {
return (
<View style={stylesheet.collapsibleCard}>
<Pressable
onPress={() => setIsExpanded(!isExpanded)}
style={stylesheet.collapsibleHeader}
>
<Text style={stylesheet.timestampText}>
{formatTimestamp(update.timestamp)}
</Text>
<Text style={stylesheet.toolCallText}>
Tool Call: {sessionUpdate.toolName || 'unknown'}
</Text>
</Pressable>
{isExpanded && sessionUpdate.arguments && (
<View style={stylesheet.collapsibleContent}>
<Text style={stylesheet.codeText}>
{JSON.stringify(sessionUpdate.arguments, null, 2)}
</Text>
</View>
)}
</View>
);
}
// Handle tool call updates
if (sessionUpdate.kind === 'tool_call_update') {
return (
<View style={stylesheet.updateCard}>
<Text style={stylesheet.timestampText}>
{formatTimestamp(update.timestamp)}
</Text>
<Text style={stylesheet.updateStatusText}>
{sessionUpdate.status || 'updating'}
</Text>
</View>
);
}
// Handle available commands
if (sessionUpdate.kind === 'available_commands_update') {
const commands = sessionUpdate.commands || [];
return (
<View style={stylesheet.collapsibleCard}>
<Pressable
onPress={() => setIsExpanded(!isExpanded)}
style={stylesheet.collapsibleHeader}
>
<Text style={stylesheet.timestampText}>
{formatTimestamp(update.timestamp)}
</Text>
<Text style={stylesheet.commandsText}>
Available Commands ({commands.length})
</Text>
</Pressable>
{isExpanded && (
<View style={stylesheet.collapsibleContent}>
{commands.map((cmd: any, idx: number) => (
<Text key={idx} style={stylesheet.commandItem}>
• {cmd.name || cmd}
</Text>
))}
</View>
)}
</View>
);
}
// Generic session update
return (
<View style={stylesheet.updateCard}>
<Text style={stylesheet.timestampText}>
{formatTimestamp(update.timestamp)}
</Text>
<Text style={stylesheet.codeText}>
{JSON.stringify(sessionUpdate, null, 2)}
</Text>
</View>
);
}
// Fallback for unknown notification types
return (
<View style={stylesheet.updateCard}>
<Text style={stylesheet.timestampText}>
{formatTimestamp(update.timestamp)}
</Text>
<Text style={stylesheet.codeText}>
{JSON.stringify(notification, null, 2)}
</Text>
</View>
);
}
export function AgentStreamView({
agentId,
agent,
updates,
onBack,
onKillAgent,
onCancelAgent,
}: AgentStreamViewProps) {
const scrollViewRef = useRef<ScrollView>(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 (
<View style={stylesheet.container}>
{/* Header */}
<View style={[stylesheet.header, { paddingTop: insets.top + 16 }]}>
{/* Back button */}
<Pressable
onPress={onBack}
style={stylesheet.backButton}
>
<Text style={stylesheet.backButtonText}>
← Back to Chat
</Text>
</Pressable>
{/* Agent info */}
<View style={stylesheet.agentInfoRow}>
<View style={stylesheet.agentIdRow}>
<Text style={stylesheet.agentLabel}>Agent:</Text>
<Text style={stylesheet.agentId}>
{agentId.substring(0, 8)}
</Text>
</View>
<View
style={[
stylesheet.statusBadge,
{ backgroundColor: getStatusColor(agent.status) }
]}
>
<Text style={stylesheet.statusIcon}>{getStatusIcon(agent.status)}</Text>
<Text style={stylesheet.statusText}>{agent.status}</Text>
</View>
</View>
<Text style={stylesheet.createdText}>
Created: {formatTimestamp(agent.createdAt)}
</Text>
{/* Control buttons */}
{(canCancel || canKill) && (
<View style={stylesheet.controlButtons}>
{canCancel && (
<Pressable
onPress={() => onCancelAgent(agentId)}
style={stylesheet.cancelButton}
>
<Text style={stylesheet.controlButtonText}>
Cancel
</Text>
</Pressable>
)}
{canKill && (
<Pressable
onPress={() => onKillAgent(agentId)}
style={stylesheet.killButton}
>
<Text style={stylesheet.controlButtonText}>
Kill
</Text>
</Pressable>
)}
</View>
)}
</View>
{/* Updates list */}
<ScrollView
ref={scrollViewRef}
style={stylesheet.scrollView}
contentContainerStyle={{ paddingTop: 16, paddingBottom: Math.max(insets.bottom, 32) }}
>
{updates.length === 0 ? (
<View style={stylesheet.emptyState}>
<Text style={stylesheet.emptyStateText}>
No updates yet.{'\n'}Waiting for agent activity...
</Text>
</View>
) : (
updates.map((update, idx) => <AgentUpdate key={idx} update={update} />)
)}
</ScrollView>
</View>
);
}
const stylesheet = StyleSheet.create((theme) => ({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
header: {
backgroundColor: theme.colors.card,
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: theme.colors.border,
paddingHorizontal: theme.spacing[4],
paddingBottom: theme.spacing[4],
},
backButton: {
backgroundColor: theme.colors.muted,
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
marginBottom: theme.spacing[4],
},
backButtonText: {
color: theme.colors.foreground,
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.mutedForeground,
fontSize: theme.fontSize.sm,
},
agentId: {
color: theme.colors.foreground,
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.foreground,
fontSize: theme.fontSize.xs,
},
statusText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold,
},
createdText: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
},
controlButtons: {
flexDirection: "row",
gap: theme.spacing[2],
marginTop: theme.spacing[4],
},
cancelButton: {
flex: 1,
backgroundColor: theme.colors.palette.orange[600],
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
},
killButton: {
flex: 1,
backgroundColor: theme.colors.destructive,
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
},
controlButtonText: {
color: theme.colors.foreground,
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.mutedForeground,
fontSize: theme.fontSize.sm,
textAlign: "center",
},
updateCard: {
backgroundColor: theme.colors.card,
borderRadius: theme.borderRadius.lg,
padding: theme.spacing[3],
marginBottom: theme.spacing[2],
},
collapsibleCard: {
backgroundColor: theme.colors.card,
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.border,
},
timestampText: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
marginBottom: theme.spacing[1],
},
messageText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
toolCallText: {
color: theme.colors.palette.blue[400],
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
},
updateStatusText: {
color: theme.colors.palette.yellow[400],
fontSize: theme.fontSize.sm,
},
commandsText: {
color: theme.colors.palette.green[400],
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
},
commandItem: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
marginTop: theme.spacing[1],
},
codeText: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
fontFamily: "monospace",
marginTop: theme.spacing[2],
},
}));