Skip to content

Commit 30feeb0

Browse files
Amber Agentclaude
authored andcommitted
feat(frontend): add TodoWrite tool visualization with task list UI
Renders TodoWrite tool calls as a structured task list with status icons (completed/in_progress/pending), priority badges, and a smart summary showing task counts by status in the collapsed view. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4c8486e commit 30feeb0

File tree

1 file changed

+110
-2
lines changed

1 file changed

+110
-2
lines changed

components/frontend/src/components/ui/tool-message.tsx

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Check,
1212
X,
1313
Cog,
14+
ListTodo,
1415
} from "lucide-react";
1516
import ReactMarkdown from "react-markdown";
1617
import type { Components } from "react-markdown";
@@ -26,6 +27,76 @@ export type ToolMessageProps = {
2627
timestamp?: string;
2728
};
2829

30+
// TodoWrite types and helpers
31+
type TodoItem = {
32+
id?: string;
33+
content: string;
34+
status: "pending" | "in_progress" | "completed";
35+
priority?: "high" | "medium" | "low";
36+
};
37+
38+
const parseTodoItems = (input?: Record<string, unknown>): TodoItem[] | null => {
39+
if (!input) return null;
40+
const todos = input.todos;
41+
if (!Array.isArray(todos) || todos.length === 0) return null;
42+
return todos.filter(
43+
(item): item is TodoItem =>
44+
item != null &&
45+
typeof item === "object" &&
46+
typeof (item as Record<string, unknown>).content === "string" &&
47+
typeof (item as Record<string, unknown>).status === "string"
48+
);
49+
};
50+
51+
const isTodoWriteTool = (toolName: string) =>
52+
toolName.toLowerCase() === "todowrite";
53+
54+
const TodoListView: React.FC<{ todos: TodoItem[] }> = ({ todos }) => (
55+
<div className="space-y-1">
56+
{todos.map((todo, idx) => (
57+
<div key={todo.id ?? idx} className="flex items-start gap-2 py-0.5">
58+
<div className="flex-shrink-0 mt-0.5">
59+
{todo.status === "completed" && (
60+
<Check className="w-3.5 h-3.5 text-green-500" />
61+
)}
62+
{todo.status === "in_progress" && (
63+
<Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />
64+
)}
65+
{todo.status === "pending" && (
66+
<div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
67+
)}
68+
{todo.status !== "completed" && todo.status !== "in_progress" && todo.status !== "pending" && (
69+
<div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
70+
)}
71+
</div>
72+
<span
73+
className={cn(
74+
"text-xs flex-1 leading-tight",
75+
todo.status === "completed" && "line-through text-muted-foreground",
76+
todo.status === "in_progress" && "text-foreground font-medium",
77+
todo.status === "pending" && "text-muted-foreground"
78+
)}
79+
>
80+
{todo.content}
81+
</span>
82+
{todo.priority && (
83+
<Badge
84+
variant="outline"
85+
className={cn(
86+
"text-[9px] px-1 py-0 flex-shrink-0 leading-tight",
87+
todo.priority === "high" && "border-red-300 text-red-600 dark:border-red-700 dark:text-red-400",
88+
todo.priority === "medium" && "border-yellow-300 text-yellow-600 dark:border-yellow-700 dark:text-yellow-400",
89+
todo.priority === "low" && "border-border text-muted-foreground"
90+
)}
91+
>
92+
{todo.priority}
93+
</Badge>
94+
)}
95+
</div>
96+
))}
97+
</div>
98+
);
99+
29100
const formatToolName = (toolName?: string) => {
30101
if (!toolName) return "Unknown Tool";
31102
// Remove mcp__ prefix and format nicely
@@ -308,6 +379,22 @@ const extractTextFromResultContent = (content: unknown): string => {
308379
const generateToolSummary = (toolName: string, input?: Record<string, unknown>): string => {
309380
if (!input || Object.keys(input).length === 0) return formatToolName(toolName);
310381

382+
// TodoWrite - summarize task counts by status
383+
if (isTodoWriteTool(toolName)) {
384+
const todos = parseTodoItems(input);
385+
if (todos) {
386+
const total = todos.length;
387+
const completed = todos.filter((t) => t.status === "completed").length;
388+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
389+
const pending = todos.filter((t) => t.status === "pending").length;
390+
const parts: string[] = [];
391+
if (completed > 0) parts.push(`${completed} done`);
392+
if (inProgress > 0) parts.push(`${inProgress} in progress`);
393+
if (pending > 0) parts.push(`${pending} pending`);
394+
return `${total} task${total !== 1 ? "s" : ""}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`;
395+
}
396+
}
397+
311398
// AskUserQuestion - show first question text
312399
if (toolName.toLowerCase().replace(/[^a-z]/g, "") === "askuserquestion") {
313400
const questions = input.questions as Array<{ question: string }> | undefined;
@@ -318,7 +405,6 @@ const generateToolSummary = (toolName: string, input?: Record<string, unknown>):
318405
return "Asking a question";
319406
}
320407

321-
322408
// WebSearch - show query
323409
if (toolName.toLowerCase().includes("websearch") || toolName.toLowerCase().includes("web_search")) {
324410
const query = input.query as string | undefined;
@@ -537,6 +623,10 @@ export const ToolMessage = React.forwardRef<HTMLDivElement, ToolMessageProps>(
537623
{getInitials(subagentType)}
538624
</span>
539625
</div>
626+
) : isTodoWriteTool(toolUseBlock?.name ?? "") ? (
627+
<div className="w-8 h-8 rounded-full flex items-center justify-center bg-indigo-600">
628+
<ListTodo className="w-4 h-4 text-white" />
629+
</div>
540630
) : (
541631
<div className="w-8 h-8 rounded-full flex items-center justify-center bg-purple-600">
542632
<Cog className="w-4 h-4 text-white" />
@@ -701,7 +791,25 @@ export const ToolMessage = React.forwardRef<HTMLDivElement, ToolMessageProps>(
701791
// Default tool rendering (existing behavior)
702792
isExpanded && (
703793
<div className="px-3 pb-3 space-y-3 bg-muted/50">
704-
{toolUseBlock?.input && (
794+
{/* TodoWrite: render structured task list */}
795+
{isTodoWriteTool(toolUseBlock?.name ?? "") && (() => {
796+
const todos = parseTodoItems(inputData);
797+
if (!todos) return null;
798+
return (
799+
<div>
800+
<h4 className="text-xs font-medium text-foreground/80 mb-2 flex items-center gap-1.5">
801+
<ListTodo className="w-3.5 h-3.5" />
802+
Tasks
803+
</h4>
804+
<div className="rounded border border-border bg-card p-2">
805+
<TodoListView todos={todos} />
806+
</div>
807+
</div>
808+
);
809+
})()}
810+
811+
{/* Generic input for non-TodoWrite tools */}
812+
{toolUseBlock?.input && !isTodoWriteTool(toolUseBlock.name) && (
705813
<div>
706814
<h4 className="text-xs font-medium text-foreground/80 mb-1">Input</h4>
707815
<div className="bg-slate-950 dark:bg-black rounded text-xs p-2 overflow-x-auto">

0 commit comments

Comments
 (0)