refactor: deduplicate user messages and sync agent history on reconnection

This commit is contained in:
Mohamed Boudra
2025-10-22 22:38:15 +02:00
parent b86e5fbb3f
commit 70bdbd74eb
5 changed files with 142 additions and 289 deletions
+22 -271
View File
@@ -3,7 +3,7 @@ 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 { AssistantMessage } from './message';
import { AssistantMessage, UserMessage, ActivityLog, ToolCall } from './message';
import type { StreamItem } from '@/types/stream';
export interface AgentStreamViewProps {
@@ -17,106 +17,6 @@ export interface AgentStreamViewProps {
streamItems: StreamItem[];
}
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 {
// Handle invalid dates gracefully
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return '--:--:--';
}
return new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true,
}).format(date);
}
function UserMessageCard({ text, timestamp }: { text: string; timestamp: Date }) {
return (
<View style={stylesheet.userMessageCard}>
<Text style={stylesheet.timestamp}>
{formatTimestamp(timestamp)}
</Text>
<Text style={stylesheet.userMessageLabel}>You</Text>
<Text style={stylesheet.userMessageText}>{text}</Text>
</View>
);
}
function ThoughtCard({ text, timestamp }: { text: string; timestamp: Date }) {
return (
<View style={stylesheet.thoughtCard}>
<Text style={stylesheet.timestamp}>
{formatTimestamp(timestamp)}
</Text>
<Text style={stylesheet.thoughtLabel}>Thinking</Text>
<Text style={stylesheet.thoughtText}>{text}</Text>
</View>
);
}
function ToolCallCard({
title,
status,
toolKind,
timestamp
}: {
title: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
toolKind?: string;
timestamp: Date;
}) {
return (
<View style={stylesheet.toolCallCard}>
<Text style={stylesheet.timestamp}>
{formatTimestamp(timestamp)}
</Text>
<View style={stylesheet.toolCallHeader}>
<Text style={stylesheet.toolCallLabel}>{toolKind || 'Tool'}</Text>
<Text style={[stylesheet.toolCallStatus, { color: getStatusColor(status as any) }]}>
{status}
</Text>
</View>
<Text style={stylesheet.toolCallTitle}>{title}</Text>
</View>
);
}
export function AgentStreamView({
agentId,
agent,
@@ -149,10 +49,10 @@ export function AgentStreamView({
switch (item.kind) {
case 'user_message':
return (
<UserMessageCard
<UserMessage
key={item.id}
text={item.text}
timestamp={item.timestamp}
message={item.text}
timestamp={item.timestamp.getTime()}
/>
);
@@ -167,23 +67,32 @@ export function AgentStreamView({
case 'thought':
return (
<ThoughtCard
<ActivityLog
key={item.id}
text={item.text}
timestamp={item.timestamp}
type="info"
message={item.text}
timestamp={item.timestamp.getTime()}
/>
);
case 'tool_call':
case 'tool_call': {
// Map status: pending/in_progress -> executing, completed -> completed, failed -> failed
const toolStatus = item.status === 'pending' || item.status === 'in_progress'
? 'executing' as const
: item.status === 'completed'
? 'completed' as const
: 'failed' as const;
return (
<ToolCallCard
<ToolCall
key={item.id}
title={item.title}
status={item.status}
toolKind={item.toolKind}
timestamp={item.timestamp}
toolName={item.title}
args={item.rawInput}
result={item.rawOutput}
status={toolStatus}
/>
);
}
case 'plan':
// TODO: Render plan component
@@ -209,164 +118,6 @@ const stylesheet = StyleSheet.create((theme) => ({
flex: 1,
backgroundColor: theme.colors.background,
},
userMessageCard: {
backgroundColor: theme.colors.card,
borderRadius: theme.borderRadius.lg,
padding: theme.spacing[3],
marginBottom: theme.spacing[2],
borderLeftWidth: 3,
borderLeftColor: theme.colors.primary,
},
userMessageLabel: {
color: theme.colors.primary,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold,
marginBottom: theme.spacing[1],
},
userMessageText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
lineHeight: 20,
},
timestamp: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
marginBottom: theme.spacing[1],
},
thoughtCard: {
backgroundColor: theme.colors.card,
borderRadius: theme.borderRadius.lg,
padding: theme.spacing[3],
marginBottom: theme.spacing[2],
borderLeftWidth: 3,
borderLeftColor: theme.colors.palette.purple[500],
},
thoughtLabel: {
color: theme.colors.palette.purple[500],
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold,
marginBottom: theme.spacing[1],
},
thoughtText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
lineHeight: 20,
fontStyle: 'italic',
},
toolCallCard: {
backgroundColor: theme.colors.card,
borderRadius: theme.borderRadius.lg,
padding: theme.spacing[3],
marginBottom: theme.spacing[2],
borderLeftWidth: 3,
borderLeftColor: theme.colors.palette.blue[500],
},
toolCallHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: theme.spacing[1],
},
toolCallLabel: {
color: theme.colors.palette.blue[500],
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold,
},
toolCallStatus: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.medium,
},
toolCallTitle: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
lineHeight: 20,
},
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],