@@ -11,6 +11,7 @@ import {
1111 Check ,
1212 X ,
1313 Cog ,
14+ ListTodo ,
1415} from "lucide-react" ;
1516import ReactMarkdown from "react-markdown" ;
1617import 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+
29100const 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 => {
308379const 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