Render todo lists in agent stream

This commit is contained in:
Mohamed Boudra
2025-11-15 12:43:01 +01:00
parent 1d9f6a0df3
commit 6da27613aa
5 changed files with 578 additions and 48 deletions
+177
View File
@@ -1,5 +1,7 @@
import { View, Text, Pressable, Animated } from "react-native";
import { useState, useEffect, useRef, memo, useMemo, useCallback } from "react";
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
import Markdown from "react-native-markdown-display";
import {
Circle,
@@ -23,6 +25,7 @@ import { StyleSheet } from "react-native-unistyles";
import { baseColors, theme } from "@/styles/theme";
import { Colors } from "@/constants/theme";
import * as Clipboard from "expo-clipboard";
import type { TodoEntry } from "@/types/stream";
interface UserMessageProps {
message: string;
@@ -599,6 +602,180 @@ export const ActivityLog = memo(function ActivityLog({
);
});
interface TodoListCardProps {
provider: AgentProvider;
timestamp: number;
items: TodoEntry[];
}
function formatPlanTimestamp(timestamp: number): string {
try {
return new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
}).format(new Date(timestamp));
} catch {
return new Date(timestamp).toLocaleTimeString();
}
}
const todoListCardStylesheet = StyleSheet.create((theme) => ({
container: {
marginHorizontal: theme.spacing[2],
marginBottom: theme.spacing[2],
},
card: {
backgroundColor: theme.colors.card,
borderRadius: theme.borderRadius.lg,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
padding: theme.spacing[3],
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: theme.spacing[2],
},
headerMeta: {
flexDirection: "column",
gap: theme.spacing[0],
},
title: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
textTransform: "uppercase",
letterSpacing: 0.6,
},
timestamp: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
},
providerBadge: {
backgroundColor: "rgba(59, 130, 246, 0.15)",
borderRadius: theme.borderRadius.full,
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[1],
},
providerText: {
color: "#93c5fd",
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold,
},
progressText: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
marginBottom: theme.spacing[2],
},
list: {
gap: theme.spacing[2],
},
itemRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
checkbox: {
width: 24,
height: 24,
borderRadius: theme.borderRadius.base,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
alignItems: "center",
justifyContent: "center",
backgroundColor: "transparent",
},
checkboxCompleted: {
backgroundColor: theme.colors.primary,
borderColor: theme.colors.primary,
},
itemText: {
flex: 1,
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
itemTextCompleted: {
color: theme.colors.mutedForeground,
textDecorationLine: "line-through",
},
emptyText: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.sm,
fontStyle: "italic",
},
}));
export const TodoListCard = memo(function TodoListCard({
provider,
timestamp,
items,
}: TodoListCardProps) {
const providerLabel = useMemo(() => {
const definition = getAgentProviderDefinition(provider);
return definition?.label ?? provider;
}, [provider]);
const completedCount = useMemo(
() => items.filter((item) => item.completed).length,
[items]
);
const timestampLabel = useMemo(() => formatPlanTimestamp(timestamp), [timestamp]);
const iconColor = theme.colors.background;
return (
<View style={todoListCardStylesheet.container}>
<View style={todoListCardStylesheet.card}>
<View style={todoListCardStylesheet.header}>
<View style={todoListCardStylesheet.headerMeta}>
<Text style={todoListCardStylesheet.title}>Plan</Text>
<Text style={todoListCardStylesheet.timestamp}>{timestampLabel}</Text>
</View>
<View style={todoListCardStylesheet.providerBadge}>
<Text style={todoListCardStylesheet.providerText}>{providerLabel}</Text>
</View>
</View>
<Text style={todoListCardStylesheet.progressText}>
{items.length > 0
? `${completedCount}/${items.length} completed`
: "Waiting for tasks..."}
</Text>
<View style={todoListCardStylesheet.list}>
{items.length === 0 ? (
<Text style={todoListCardStylesheet.emptyText}>
No todo items shared yet.
</Text>
) : (
items.map((item, idx) => (
<View key={`${item.text}-${idx}`} style={todoListCardStylesheet.itemRow}>
<View
style={[
todoListCardStylesheet.checkbox,
item.completed && todoListCardStylesheet.checkboxCompleted,
]}
>
{item.completed && <Check size={14} color={iconColor} />}
</View>
<Text
style={[
todoListCardStylesheet.itemText,
item.completed && todoListCardStylesheet.itemTextCompleted,
]}
>
{item.text}
</Text>
</View>
))
)}
</View>
</View>
</View>
);
});
interface AgentThoughtMessageProps {
message: string;
}