fix: enhance todo list rendering with robust error handling and validation

- Add comprehensive validation in getLatestTodo function to ensure parsed JSON contains valid todo arrays
- Implement normalizedTodos logic in TodoListDisplay component to filter out invalid todo items
- Add fallback handling for edge cases where todo data might be malformed or missing
- Ensure todo items have required properties (id, content, status) before rendering
- Fixes issue #5916 where todo lists would not render properly
This commit is contained in:
Roo Code 2025-07-18 19:10:17 +00:00
parent 8c349767fa
commit 9c02e5dc82
3 changed files with 7714 additions and 27 deletions

7638
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,38 @@
import { ClineMessage } from "@roo-code/types"
export function getLatestTodo(clineMessages: ClineMessage[]) {
const todos = clineMessages
.filter(
(msg) =>
(msg.type === "ask" && msg.ask === "tool") || (msg.type === "say" && msg.say === "user_edit_todos"),
)
.map((msg) => {
try {
return JSON.parse(msg.text ?? "{}")
} catch {
return null
}
})
.filter((item) => item && item.tool === "updateTodoList" && Array.isArray(item.todos))
.map((item) => item.todos)
.pop()
if (todos) {
const filteredMessages = clineMessages.filter(
(msg) => (msg.type === "ask" && msg.ask === "tool") || (msg.type === "say" && msg.say === "user_edit_todos"),
)
const parsedItems = filteredMessages.map((msg) => {
try {
const text = msg.text ?? "{}"
const parsed = JSON.parse(text)
return parsed
} catch (error) {
return null
}
})
const todoItems = parsedItems.filter((item) => {
if (!item) {
return false
}
const hasTool = item.tool === "updateTodoList"
const hasTodos = item.todos !== undefined
const isArrayTodos = Array.isArray(item.todos)
return hasTool && hasTodos && isArrayTodos
})
if (todoItems.length === 0) {
return []
}
const todos = todoItems.map((item) => item.todos).pop()
if (todos && Array.isArray(todos)) {
return todos
} else {
return []

View file

@ -1,21 +1,49 @@
import { useState, useRef, useMemo, useEffect } from "react"
export function TodoListDisplay({ todos }: { todos: any[] }) {
// Normalize todos to ensure we have a valid array
const normalizedTodos = useMemo(() => {
if (!todos) {
return []
}
if (!Array.isArray(todos)) {
// Try to handle case where todos might be a single object
if (typeof todos === "object" && "length" in todos) {
return Array.from(todos)
}
return []
}
// Filter out any invalid todo items
const validTodos = todos.filter((todo) => {
if (!todo || typeof todo !== "object") {
return false
}
if (!todo.content || typeof todo.content !== "string") {
return false
}
return true
})
return validTodos
}, [todos])
const [isCollapsed, setIsCollapsed] = useState(true)
const ulRef = useRef<HTMLUListElement>(null)
const itemRefs = useRef<(HTMLLIElement | null)[]>([])
const scrollIndex = useMemo(() => {
const inProgressIdx = todos.findIndex((todo: any) => todo.status === "in_progress")
const inProgressIdx = normalizedTodos.findIndex((todo: any) => todo.status === "in_progress")
if (inProgressIdx !== -1) return inProgressIdx
return todos.findIndex((todo: any) => todo.status !== "completed")
}, [todos])
return normalizedTodos.findIndex((todo: any) => todo.status !== "completed")
}, [normalizedTodos])
// Find the most important todo to display when collapsed
const mostImportantTodo = useMemo(() => {
const inProgress = todos.find((todo: any) => todo.status === "in_progress")
const inProgress = normalizedTodos.find((todo: any) => todo.status === "in_progress")
if (inProgress) return inProgress
return todos.find((todo: any) => todo.status !== "completed")
}, [todos])
return normalizedTodos.find((todo: any) => todo.status !== "completed")
}, [normalizedTodos])
useEffect(() => {
if (isCollapsed) return
if (!ulRef.current) return
@ -29,11 +57,15 @@ export function TodoListDisplay({ todos }: { todos: any[] }) {
const scrollTo = targetTop - (ulHeight / 2 - targetHeight / 2)
ul.scrollTop = scrollTo
}
}, [todos, isCollapsed, scrollIndex])
if (!Array.isArray(todos) || todos.length === 0) return null
}, [normalizedTodos, isCollapsed, scrollIndex])
const totalCount = todos.length
const completedCount = todos.filter((todo: any) => todo.status === "completed").length
// Enhanced guard clause
if (normalizedTodos.length === 0) {
return null
}
const totalCount = normalizedTodos.length
const completedCount = normalizedTodos.filter((todo: any) => todo.status === "completed").length
const allCompleted = completedCount === totalCount && totalCount > 0
@ -268,7 +300,7 @@ export function TodoListDisplay({ todos }: { todos: any[] }) {
overflowY: "auto",
padding: "12px 16px",
}}>
{todos.map((todo: any, idx: number) => {
{normalizedTodos.map((todo: any, idx: number) => {
let icon
if (todo.status === "completed") {
icon = (