diff --git a/apps/app/src/components/ui/log-viewer.tsx b/apps/app/src/components/ui/log-viewer.tsx index d962a4fc..a926e2d9 100644 --- a/apps/app/src/components/ui/log-viewer.tsx +++ b/apps/app/src/components/ui/log-viewer.tsx @@ -22,6 +22,9 @@ import { Layers, X, Filter, + Circle, + Play, + Loader2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { @@ -111,6 +114,112 @@ const getToolCategoryColor = (category: ToolCategory | undefined): string => { } }; +/** + * Interface for parsed todo items from TodoWrite tool + */ +interface TodoItem { + content: string; + status: "pending" | "in_progress" | "completed"; + activeForm?: string; +} + +/** + * Parses TodoWrite JSON content and extracts todo items + */ +function parseTodoContent(content: string): TodoItem[] | null { + try { + // Find the JSON object in the content + const jsonMatch = content.match(/\{[\s\S]*"todos"[\s\S]*\}/); + if (!jsonMatch) return null; + + const parsed = JSON.parse(jsonMatch[0]) as { todos?: TodoItem[] }; + if (!parsed.todos || !Array.isArray(parsed.todos)) return null; + + return parsed.todos; + } catch { + return null; + } +} + +/** + * Renders a list of todo items with status icons and colors + */ +function TodoListRenderer({ todos }: { todos: TodoItem[] }) { + const getStatusIcon = (status: TodoItem["status"]) => { + switch (status) { + case "completed": + return ; + case "in_progress": + return ; + case "pending": + return ; + default: + return ; + } + }; + + const getStatusColor = (status: TodoItem["status"]) => { + switch (status) { + case "completed": + return "text-emerald-300 line-through opacity-70"; + case "in_progress": + return "text-amber-300"; + case "pending": + return "text-zinc-400"; + default: + return "text-zinc-400"; + } + }; + + const getStatusBadge = (status: TodoItem["status"]) => { + switch (status) { + case "completed": + return ( + + Done + + ); + case "in_progress": + return ( + + In Progress + + ); + default: + return null; + } + }; + + return ( +
+ {todos.map((todo, index) => ( +
+
{getStatusIcon(todo.status)}
+
+

+ {todo.content} +

+ {todo.status === "in_progress" && todo.activeForm && ( +

+ {todo.activeForm} +

+ )} +
+ {getStatusBadge(todo.status)} +
+ ))} +
+ ); +} + interface LogEntryItemProps { entry: LogEntry; isExpanded: boolean; @@ -126,6 +235,13 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) { const toolCategory = entry.metadata?.toolCategory; const toolCategoryColors = isToolCall ? getToolCategoryColor(toolCategory) : ""; + // Check if this is a TodoWrite entry and parse the todos + const isTodoWrite = entry.metadata?.toolName === "TodoWrite"; + const parsedTodos = useMemo(() => { + if (!isTodoWrite) return null; + return parseTodoContent(entry.content); + }, [isTodoWrite, entry.content]); + // Get the appropriate icon based on entry type and tool category const icon = isToolCall ? getToolCategoryIcon(toolCategory) : getLogIcon(entry.type); @@ -256,26 +372,31 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) { className="px-4 pb-3 pt-1" data-testid={`log-entry-content-${entry.id}`} > -
- {formattedContent.map((part, index) => ( -
- {part.type === "json" ? ( -
-                    {part.content}
-                  
- ) : ( -
-                    {part.content}
-                  
- )} -
- ))} -
+ {/* Render TodoWrite entries with special formatting */} + {parsedTodos ? ( + + ) : ( +
+ {formattedContent.map((part, index) => ( +
+ {part.type === "json" ? ( +
+                      {part.content}
+                    
+ ) : ( +
+                      {part.content}
+                    
+ )} +
+ ))} +
+ )} )} diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 40c434c0..1ce206c9 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -549,13 +549,15 @@ Address the follow-up instructions above. Review the previous work and make the } // Use fullPrompt (already built above) with model and all images + // Pass previousContext so the history is preserved in the output file await this.runAgent( workDir, featureId, fullPrompt, abortController, allImagePaths.length > 0 ? allImagePaths : imagePaths, - model + model, + previousContext || undefined ); // Mark as waiting_approval for user review @@ -1169,7 +1171,8 @@ This helps parse your summary correctly in the output logs.`; prompt: string, abortController: AbortController, imagePaths?: string[], - model?: string + model?: string, + previousContent?: string ): Promise { // CI/CD Mock Mode: Return early with mock response when AUTOMAKER_MOCK_AGENT is set // This prevents actual API calls during automated testing @@ -1271,7 +1274,10 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. // Execute via provider const stream = provider.executeQuery(options); - let responseText = ""; + // Initialize with previous content if this is a follow-up, with a separator + let responseText = previousContent + ? `${previousContent}\n\n---\n\n## Follow-up Session\n\n` + : ""; // Agent output goes to .automaker directory // Note: We use the original projectPath here (from config), not workDir // because workDir might be a worktree path