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;