Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions components/frontend/src/components/ui/tool-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Check,
X,
Cog,
ListTodo,
} from "lucide-react";
import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";
Expand All @@ -26,6 +27,76 @@ export type ToolMessageProps = {
timestamp?: string;
};

// TodoWrite types and helpers
type TodoItem = {
id?: string;
content: string;
status: "pending" | "in_progress" | "completed";
priority?: "high" | "medium" | "low";
};

const parseTodoItems = (input?: Record<string, unknown>): TodoItem[] | null => {
if (!input) return null;
const todos = input.todos;
if (!Array.isArray(todos) || todos.length === 0) return null;
return todos.filter(
(item): item is TodoItem =>
item != null &&
typeof item === "object" &&
typeof (item as Record<string, unknown>).content === "string" &&
typeof (item as Record<string, unknown>).status === "string"
);
};
Comment on lines +38 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unsafe type assertion may cause runtime errors with malformed data.

parseTodoItems casts the array directly to TodoItem[] without validating that each element has the required content and status properties. If the backend sends incomplete data, accessing todo.status or todo.content in TodoListView will silently fail or render incorrectly.

🛡️ Proposed fix to add validation
 const parseTodoItems = (input?: Record<string, unknown>): TodoItem[] | null => {
   if (!input) return null;
   const todos = input.todos;
   if (!Array.isArray(todos) || todos.length === 0) return null;
-  return todos as TodoItem[];
+  return todos
+    .filter(
+      (t): t is TodoItem =>
+        t != null &&
+        typeof t === "object" &&
+        typeof (t as Record<string, unknown>).content === "string" &&
+        ["pending", "in_progress", "completed"].includes(
+          (t as Record<string, unknown>).status as string
+        )
+    );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parseTodoItems = (input?: Record<string, unknown>): TodoItem[] | null => {
if (!input) return null;
const todos = input.todos;
if (!Array.isArray(todos) || todos.length === 0) return null;
return todos as TodoItem[];
};
const parseTodoItems = (input?: Record<string, unknown>): TodoItem[] | null => {
if (!input) return null;
const todos = input.todos;
if (!Array.isArray(todos) || todos.length === 0) return null;
return todos
.filter(
(t): t is TodoItem =>
t != null &&
typeof t === "object" &&
typeof (t as Record<string, unknown>).content === "string" &&
["pending", "in_progress", "completed"].includes(
(t as Record<string, unknown>).status as string
)
);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/ui/tool-message.tsx` around lines 38 - 43,
The parseTodoItems function unsafely casts input.todos to TodoItem[]; update
parseTodoItems to validate each array element has the required fields (e.g.,
content as string and status as expected enum/string) before returning; either
filter out invalid entries or return null if any element is malformed. Reference
parseTodoItems and ensure the output shape matches what TodoListView expects
(accessing todo.content and todo.status) by performing a type guard check on
each item and returning a correctly typed TodoItem[] or null.


const isTodoWriteTool = (toolName: string) =>
toolName.toLowerCase() === "todowrite";

const TodoListView: React.FC<{ todos: TodoItem[] }> = ({ todos }) => (
<div className="space-y-1">
{todos.map((todo, idx) => (
<div key={todo.id ?? idx} className="flex items-start gap-2 py-0.5">
<div className="flex-shrink-0 mt-0.5">
{todo.status === "completed" && (
<Check className="w-3.5 h-3.5 text-green-500" />
)}
{todo.status === "in_progress" && (
<Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />
)}
{todo.status === "pending" && (
<div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
)}
Comment on lines +59 to +67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing fallback for unexpected status values.

If todo.status contains an unexpected value (not one of the three defined statuses), no icon will be rendered. Consider adding a fallback for robustness.

🛡️ Proposed fix to add fallback
           {todo.status === "pending" && (
             <div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
           )}
+          {!["completed", "in_progress", "pending"].includes(todo.status) && (
+            <div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
+          )}

Note: If the validation fix in parseTodoItems is applied, this becomes redundant.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{todo.status === "completed" && (
<Check className="w-3.5 h-3.5 text-green-500" />
)}
{todo.status === "in_progress" && (
<Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />
)}
{todo.status === "pending" && (
<div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
)}
{todo.status === "completed" && (
<Check className="w-3.5 h-3.5 text-green-500" />
)}
{todo.status === "in_progress" && (
<Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />
)}
{todo.status === "pending" && (
<div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
)}
{!["completed", "in_progress", "pending"].includes(todo.status) && (
<div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/ui/tool-message.tsx` around lines 53 - 61,
The rendering logic for todo.status (checked in the component that uses
todo.status and icons Check and Loader2) lacks a fallback for unexpected values;
update the JSX to include a default branch that renders a sensible fallback icon
or placeholder (e.g., a neutral icon or aria-hidden empty circle) when
todo.status is not "completed", "in_progress", or "pending", and ensure it
provides appropriate accessibility attributes (aria-label or title) so
unexpected statuses still display meaningfully.

{todo.status !== "completed" && todo.status !== "in_progress" && todo.status !== "pending" && (
<div className="w-3.5 h-3.5 rounded-full border-2 border-muted-foreground/40" />
)}
</div>
<span
className={cn(
"text-xs flex-1 leading-tight",
todo.status === "completed" && "line-through text-muted-foreground",
todo.status === "in_progress" && "text-foreground font-medium",
todo.status === "pending" && "text-muted-foreground"
)}
>
{todo.content}
</span>
{todo.priority && (
<Badge
variant="outline"
className={cn(
"text-[9px] px-1 py-0 flex-shrink-0 leading-tight",
todo.priority === "high" && "border-red-300 text-red-600 dark:border-red-700 dark:text-red-400",
todo.priority === "medium" && "border-yellow-300 text-yellow-600 dark:border-yellow-700 dark:text-yellow-400",
todo.priority === "low" && "border-border text-muted-foreground"
)}
>
{todo.priority}
</Badge>
)}
</div>
))}
</div>
);

const formatToolName = (toolName?: string) => {
if (!toolName) return "Unknown Tool";
// Remove mcp__ prefix and format nicely
Expand Down Expand Up @@ -308,6 +379,22 @@ const extractTextFromResultContent = (content: unknown): string => {
const generateToolSummary = (toolName: string, input?: Record<string, unknown>): string => {
if (!input || Object.keys(input).length === 0) return formatToolName(toolName);

// TodoWrite - summarize task counts by status
if (isTodoWriteTool(toolName)) {
const todos = parseTodoItems(input);
if (todos) {
const total = todos.length;
const completed = todos.filter((t) => t.status === "completed").length;
const inProgress = todos.filter((t) => t.status === "in_progress").length;
const pending = todos.filter((t) => t.status === "pending").length;
const parts: string[] = [];
if (completed > 0) parts.push(`${completed} done`);
if (inProgress > 0) parts.push(`${inProgress} in progress`);
if (pending > 0) parts.push(`${pending} pending`);
return `${total} task${total !== 1 ? "s" : ""}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`;
}
}

// AskUserQuestion - show first question text
if (toolName.toLowerCase().replace(/[^a-z]/g, "") === "askuserquestion") {
const questions = input.questions as Array<{ question: string }> | undefined;
Expand All @@ -318,7 +405,6 @@ const generateToolSummary = (toolName: string, input?: Record<string, unknown>):
return "Asking a question";
}


// WebSearch - show query
if (toolName.toLowerCase().includes("websearch") || toolName.toLowerCase().includes("web_search")) {
const query = input.query as string | undefined;
Expand Down Expand Up @@ -537,6 +623,10 @@ export const ToolMessage = React.forwardRef<HTMLDivElement, ToolMessageProps>(
{getInitials(subagentType)}
</span>
</div>
) : isTodoWriteTool(toolUseBlock?.name ?? "") ? (
<div className="w-8 h-8 rounded-full flex items-center justify-center bg-indigo-600">
<ListTodo className="w-4 h-4 text-white" />
</div>
) : (
<div className="w-8 h-8 rounded-full flex items-center justify-center bg-purple-600">
<Cog className="w-4 h-4 text-white" />
Expand Down Expand Up @@ -701,7 +791,25 @@ export const ToolMessage = React.forwardRef<HTMLDivElement, ToolMessageProps>(
// Default tool rendering (existing behavior)
isExpanded && (
<div className="px-3 pb-3 space-y-3 bg-muted/50">
{toolUseBlock?.input && (
{/* TodoWrite: render structured task list */}
{isTodoWriteTool(toolUseBlock?.name ?? "") && (() => {
const todos = parseTodoItems(inputData);
if (!todos) return null;
return (
<div>
<h4 className="text-xs font-medium text-foreground/80 mb-2 flex items-center gap-1.5">
<ListTodo className="w-3.5 h-3.5" />
Tasks
</h4>
<div className="rounded border border-border bg-card p-2">
<TodoListView todos={todos} />
</div>
</div>
);
})()}

{/* Generic input for non-TodoWrite tools */}
{toolUseBlock?.input && !isTodoWriteTool(toolUseBlock.name) && (
<div>
<h4 className="text-xs font-medium text-foreground/80 mb-1">Input</h4>
<div className="bg-slate-950 dark:bg-black rounded text-xs p-2 overflow-x-auto">
Expand Down
Loading