From 4db2e91cdd3dc15698e59e84c7eeb5f66150997e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Feb 2026 22:42:21 +0700 Subject: [PATCH] refactor: simplify tasks display and improve spacing in stream view --- .../app/src/components/agent-stream-view.tsx | 18 +- packages/app/src/components/message.tsx | 220 +++++------------- packages/app/src/types/stream.ts | 5 + 3 files changed, 82 insertions(+), 161 deletions(-) diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index f9c388a7d..ba9d7db14 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -54,7 +54,7 @@ import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf"; const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message"; const isToolSequenceItem = (item?: StreamItem) => - item?.kind === "tool_call" || item?.kind === "thought"; + item?.kind === "tool_call" || item?.kind === "thought" || item?.kind === "todo_list"; const AGENT_STREAM_LOG_TAG = "[AgentStreamView]"; const STREAM_ITEM_LOG_MIN_COUNT = 200; const STREAM_ITEM_LOG_DELTA_THRESHOLD = 50; @@ -232,6 +232,16 @@ export function AgentStreamView({ return tightGap; } + // Keep tool sequences visually connected to the preceding user message / tasks. + if ( + isToolSequenceItem(item) && + (aboveItem.kind === "user_message" || + aboveItem.kind === "assistant_message" || + aboveItem.kind === "todo_list") + ) { + return tightGap; + } + // Different types get loose gap (16px) return looseGap; }, @@ -328,8 +338,6 @@ export function AgentStreamView({ case "todo_list": return ( ); @@ -368,6 +376,8 @@ export function AgentStreamView({ return null; } + const gapAbove = getGapAbove(item, index); + // Check if this is the end of a turn (before a user message or end of stream when not running) // In inverted list: index-1 is the next item (newer in time) const nextItem = flatListData[index - 1]; @@ -379,7 +389,7 @@ export function AgentStreamView({ const getContent = () => collectTurnContent(index); return ( - + {content} {isEndOfTurn ? : null} diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 957846886..1b86d9d3b 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -17,8 +17,6 @@ import { useContext, } from "react"; import type { ReactNode, ComponentType } from "react"; -import type { AgentProvider } from "@server/server/agent/agent-sdk-types"; -import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest"; import Markdown, { MarkdownIt } from "react-native-markdown-display"; import * as Linking from "expo-linking"; import { @@ -31,6 +29,7 @@ import { ChevronDown, Loader2, Check, + CheckSquare, X, Wrench, Pencil, @@ -871,101 +870,38 @@ export const ActivityLog = memo(function ActivityLog({ }); interface TodoListCardProps { - provider: AgentProvider; - timestamp: number; items: TodoEntry[]; disableOuterSpacing?: boolean; } -function formatTasksTimestamp(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], - }, - containerSpacing: { - marginBottom: theme.spacing[2], - }, - card: { - backgroundColor: theme.colors.surface2, - 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.foregroundMuted, - 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.foregroundMuted, - fontSize: theme.fontSize.xs, - marginBottom: theme.spacing[2], - }, list: { - gap: theme.spacing[2], + gap: theme.spacing[1], }, itemRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, - checkbox: { - width: 24, - height: 24, - borderRadius: theme.borderRadius.base, + radioOuter: { + width: 16, + height: 16, + borderRadius: 8, borderWidth: theme.borderWidth[1], - borderColor: theme.colors.border, + borderColor: theme.colors.foregroundMuted, alignItems: "center", justifyContent: "center", - backgroundColor: "transparent", }, - checkboxCompleted: { - backgroundColor: theme.colors.primary, - borderColor: theme.colors.primary, + radioInner: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: theme.colors.foregroundMuted, }, itemText: { flex: 1, color: theme.colors.foreground, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.base, }, itemTextCompleted: { color: theme.colors.foregroundMuted, @@ -973,95 +909,56 @@ const todoListCardStylesheet = StyleSheet.create((theme) => ({ }, emptyText: { color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.sm, - fontStyle: "italic", + fontSize: theme.fontSize.base, }, })); export const TodoListCard = memo(function TodoListCard({ - provider, - timestamp, items, disableOuterSpacing, }: TodoListCardProps) { - const resolvedDisableOuterSpacing = - useDisableOuterSpacing(disableOuterSpacing); - const providerLabel = useMemo(() => { - const definition = getAgentProviderDefinition(provider); - return definition?.label ?? provider; - }, [provider]); + const { theme: unistylesTheme } = useUnistyles(); + const [isExpanded, setIsExpanded] = useState(true); - const completedCount = useMemo( - () => items.filter((item) => item.completed).length, - [items] - ); + const handleToggle = useCallback(() => { + setIsExpanded((prev) => !prev); + }, []); - const timestampLabel = useMemo( - () => formatTasksTimestamp(timestamp), - [timestamp] - ); - - const iconColor = theme.colors.surface0; + const renderDetails = useCallback(() => { + return ( + + {items.length === 0 ? ( + No tasks yet. + ) : ( + items.map((item, idx) => ( + + + {item.completed ? : null} + + + {item.text} + + + )) + )} + + ); + }, [items]); return ( - - - - - Tasks - - {timestampLabel} - - - - - {providerLabel} - - - - - {items.length > 0 - ? `${completedCount}/${items.length} completed` - : "Waiting for tasks..."} - - - {items.length === 0 ? ( - - No tasks shared yet. - - ) : ( - items.map((item, idx) => ( - - - {item.completed && } - - - {item.text} - - - )) - )} - - - + ); }); @@ -1152,7 +1049,17 @@ const ExpandableBadge = memo(function ExpandableBadge({ } return ( - + ); diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index 4c340f0c1..328cca579 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -797,6 +797,11 @@ export function reduceStreamUpdate( break; } case "todo": { + if (event.provider === "claude") { + // Claude plan mode is rendered via permission prompts + TodoWrite tool calls. + // Avoid rendering legacy plan-mode todo timeline items as Tasks. + break; + } const items = (item.items ?? []) as TodoEntry[]; nextState = appendTodoList(state, event.provider, items, timestamp); break;