@@ -329,11 +329,30 @@ export const AgentTodoTool = memo(function AgentTodoTool({
329329 const rawOldTodos = part . output ?. oldTodos || [ ]
330330 const newTodos = part . input ?. todos || part . output ?. newTodos || [ ]
331331
332+ // Check if we're still streaming input (data not yet complete)
333+ const isStreaming = part . state === "input-streaming"
334+
332335 // Determine if this is the creation tool call
333336 // A tool call is the "creation" if:
334337 // 1. It's the first tool call (creationToolCallId is null) OR
335338 // 2. It matches the stored creationToolCallId
336- const isCreationToolCall = creationToolCallId === null || creationToolCallId === part . toolCallId
339+ // 3. NEW: This is a new generation - detected when:
340+ // - output.oldTodos explicitly exists and is empty (server confirmed this is a new list)
341+ // - we have newTodos (creation always has new todos)
342+ // - there are existing syncedTodos from previous generation
343+ // - this is a different tool call than the stored creation one
344+ // IMPORTANT: Check if output.oldTodos is explicitly an empty array, not just missing
345+ // If output doesn't exist yet or oldTodos is undefined, we can't determine if it's new generation
346+ const hasOutputWithEmptyOldTodos = part . output !== undefined &&
347+ 'oldTodos' in part . output &&
348+ Array . isArray ( part . output . oldTodos ) &&
349+ part . output . oldTodos . length === 0
350+ const isNewGeneration = hasOutputWithEmptyOldTodos &&
351+ newTodos . length > 0 &&
352+ syncedTodos . length > 0 &&
353+ creationToolCallId !== null &&
354+ creationToolCallId !== part . toolCallId
355+ const isCreationToolCall = creationToolCallId === null || creationToolCallId === part . toolCallId || isNewGeneration
337356
338357 // Use syncedTodos as fallback for oldTodos when output hasn't arrived yet
339358 // This prevents flickering: without this, when a new tool call arrives with
@@ -398,37 +417,61 @@ export const AgentTodoTool = memo(function AgentTodoTool({
398417 // During streaming, JSON parsing may return partial arrays, causing temporary drops in length
399418 const shouldUpdate = isCreationToolCall || newTodos . length >= currentSyncedTodos . length
400419
420+ // If this is a new generation, reset the creationToolCallId to this tool call
421+ const newCreationId = isNewGeneration ? part . toolCallId : ( creationToolCallId === null ? part . toolCallId : creationToolCallId )
422+
401423 if ( shouldUpdate ) {
402424 // Prevent infinite loop: check if todos actually changed before updating
403425 // Compare by serializing to JSON - if content is the same, skip update
404426 const newTodosJson = JSON . stringify ( newTodos )
405427 const syncedTodosJson = JSON . stringify ( currentSyncedTodos )
406428
407429 if ( newTodosJson !== syncedTodosJson ) {
408- const newCreationId = creationToolCallId === null ? part . toolCallId : creationToolCallId
409430 setTodoState ( { todos : newTodos , creationToolCallId : newCreationId } )
410431 }
411432 }
412433 }
413- } , [ newTodos , setTodoState , creationToolCallId , part . toolCallId , isCreationToolCall ] )
434+ } , [ newTodos , setTodoState , creationToolCallId , part . toolCallId , isCreationToolCall , isNewGeneration ] )
414435
415- // Check if we're still streaming input (data not yet complete)
416- const isStreaming = part . state === "input-streaming"
436+ // For UPDATE tool calls while streaming, show "Updating..." placeholder
437+ // This check MUST come BEFORE the newTodos.length === 0 check
438+ // Otherwise we return null when newTodos is empty during streaming updates
439+ if ( ! isCreationToolCall && isStreaming ) {
440+ return (
441+ < div className = "flex items-start gap-1.5 py-0.5 rounded-md px-2" >
442+ < div className = "flex-1 min-w-0 flex items-center gap-1.5" >
443+ < div className = "text-xs text-muted-foreground flex items-center gap-1.5 min-w-0" >
444+ < span className = "font-medium whitespace-nowrap flex-shrink-0" >
445+ < TextShimmer
446+ as = "span"
447+ duration = { 1.2 }
448+ className = "inline-flex items-center text-xs leading-none h-4 m-0"
449+ >
450+ Updating to-dos...
451+ </ TextShimmer >
452+ </ span >
453+ </ div >
454+ </ div >
455+ </ div >
456+ )
457+ }
417458
418- // Early streaming state - show placeholder
459+ // Early streaming state - show placeholder for CREATION only
419460 if (
420461 newTodos . length === 0 ||
421462 ( isStreaming && ! part . input ?. todos )
422463 ) {
423464 // For update tool calls (not creation), return null to avoid showing placeholder
465+ // Note: This branch is only reached when !isStreaming (update streaming handled above)
424466 if ( ! isCreationToolCall ) {
425467 return null
426468 }
427469
428470 // For creation tool calls, show the placeholder - also sticky with top offset
471+ // z-[5] ensures todo stays below user message (z-10) when both are sticky
429472 return (
430473 < div
431- className = "mx-2 sticky bg-background"
474+ className = "mx-2 sticky z-[5] bg-background"
432475 style = { { top : 'calc(var(--user-message-height, 28px) - 29px)' } }
433476 >
434477 < div className = "rounded-lg border border-border bg-muted/30 px-2.5 py-1.5" >
@@ -453,28 +496,6 @@ export const AgentTodoTool = memo(function AgentTodoTool({
453496 )
454497 }
455498
456- // For UPDATE tool calls while streaming, show "Updating..." placeholder
457- // This prevents showing intermediate/incorrect states during streaming
458- if ( ! isCreationToolCall && isStreaming ) {
459- return (
460- < div className = "flex items-start gap-1.5 py-0.5 rounded-md px-2" >
461- < div className = "flex-1 min-w-0 flex items-center gap-1.5" >
462- < div className = "text-xs text-muted-foreground flex items-center gap-1.5 min-w-0" >
463- < span className = "font-medium whitespace-nowrap flex-shrink-0" >
464- < TextShimmer
465- as = "span"
466- duration = { 1.2 }
467- className = "inline-flex items-center text-xs leading-none h-4 m-0"
468- >
469- Updating todos...
470- </ TextShimmer >
471- </ span >
472- </ div >
473- </ div >
474- </ div >
475- )
476- }
477-
478499 // COMPACT MODE: Single update - render as simple tool call
479500 if ( changes . type === "single" ) {
480501 const change = changes . items [ 0 ]
@@ -508,7 +529,7 @@ export const AgentTodoTool = memo(function AgentTodoTool({
508529 ) . length
509530
510531 // Build summary title
511- let summaryTitle = "Updated todos "
532+ let summaryTitle = "Updated to-dos "
512533 if ( completedChanges > 0 && startedChanges === 0 ) {
513534 summaryTitle = `Finished ${ completedChanges } ${ completedChanges === 1 ? "task" : "tasks" } `
514535 } else if ( startedChanges > 0 && completedChanges === 0 ) {
@@ -581,7 +602,8 @@ export const AgentTodoTool = memo(function AgentTodoTool({
581602 className = { cn (
582603 "mx-2" ,
583604 // Make entire creation todo sticky
584- isCreationToolCall && "sticky bg-background"
605+ // z-[5] ensures todo stays below user message (z-10) when both are sticky
606+ isCreationToolCall && "sticky z-[5] bg-background"
585607 ) }
586608 style = { isCreationToolCall ? {
587609 // Offset so TOP BLOCK (title) goes fully under user message
@@ -595,7 +617,7 @@ export const AgentTodoTool = memo(function AgentTodoTool({
595617 onClick = { handleToggleExpand }
596618 role = "button"
597619 aria-expanded = { isExpanded }
598- aria-label = { `Todo list with ${ totalTodos } items. Click to ${ isExpanded ? "collapse" : "expand" } ` }
620+ aria-label = { `To-do list with ${ totalTodos } items. Click to ${ isExpanded ? "collapse" : "expand" } ` }
599621 tabIndex = { 0 }
600622 onKeyDown = { handleKeyDown }
601623 >
@@ -605,7 +627,7 @@ export const AgentTodoTool = memo(function AgentTodoTool({
605627 To-dos
606628 </ span >
607629 < span className = "text-xs text-muted-foreground truncate flex-1" >
608- { displayTodos [ 0 ] ?. content || "Todo List " }
630+ { displayTodos [ 0 ] ?. content || "To-do list " }
609631 </ span >
610632 { /* Expand/Collapse icon */ }
611633 < div className = "relative w-4 h-4 flex-shrink-0" >
0 commit comments