mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat: add TodoWrite support in log viewer for enhanced task management
- Introduced a new TodoListRenderer component to display parsed todo items with status indicators and colors. - Implemented a parseTodoContent function to extract todo items from TodoWrite JSON content. - Enhanced LogEntryItem to conditionally render todo items when a TodoWrite entry is detected, improving log entry clarity and usability. - Updated UI to visually differentiate between todo item statuses, enhancing user experience in task tracking.
This commit is contained in:
@@ -22,6 +22,9 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
X,
|
X,
|
||||||
Filter,
|
Filter,
|
||||||
|
Circle,
|
||||||
|
Play,
|
||||||
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
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 <CheckCircle2 className="w-4 h-4 text-emerald-400" />;
|
||||||
|
case "in_progress":
|
||||||
|
return <Loader2 className="w-4 h-4 text-amber-400 animate-spin" />;
|
||||||
|
case "pending":
|
||||||
|
return <Circle className="w-4 h-4 text-zinc-500" />;
|
||||||
|
default:
|
||||||
|
return <Circle className="w-4 h-4 text-zinc-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-400 ml-auto">
|
||||||
|
Done
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "in_progress":
|
||||||
|
return (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400 ml-auto">
|
||||||
|
In Progress
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{todos.map((todo, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
"flex items-start gap-2 p-2 rounded-md transition-colors",
|
||||||
|
todo.status === "in_progress" && "bg-amber-500/5 border border-amber-500/20",
|
||||||
|
todo.status === "completed" && "bg-emerald-500/5",
|
||||||
|
todo.status === "pending" && "bg-zinc-800/30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 flex-shrink-0">{getStatusIcon(todo.status)}</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className={cn("text-sm", getStatusColor(todo.status))}>
|
||||||
|
{todo.content}
|
||||||
|
</p>
|
||||||
|
{todo.status === "in_progress" && todo.activeForm && (
|
||||||
|
<p className="text-xs text-amber-400/70 mt-0.5 italic">
|
||||||
|
{todo.activeForm}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{getStatusBadge(todo.status)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface LogEntryItemProps {
|
interface LogEntryItemProps {
|
||||||
entry: LogEntry;
|
entry: LogEntry;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
@@ -126,6 +235,13 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) {
|
|||||||
const toolCategory = entry.metadata?.toolCategory;
|
const toolCategory = entry.metadata?.toolCategory;
|
||||||
const toolCategoryColors = isToolCall ? getToolCategoryColor(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
|
// Get the appropriate icon based on entry type and tool category
|
||||||
const icon = isToolCall ? getToolCategoryIcon(toolCategory) : getLogIcon(entry.type);
|
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"
|
className="px-4 pb-3 pt-1"
|
||||||
data-testid={`log-entry-content-${entry.id}`}
|
data-testid={`log-entry-content-${entry.id}`}
|
||||||
>
|
>
|
||||||
<div className="font-mono text-xs space-y-1">
|
{/* Render TodoWrite entries with special formatting */}
|
||||||
{formattedContent.map((part, index) => (
|
{parsedTodos ? (
|
||||||
<div key={index}>
|
<TodoListRenderer todos={parsedTodos} />
|
||||||
{part.type === "json" ? (
|
) : (
|
||||||
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-primary">
|
<div className="font-mono text-xs space-y-1">
|
||||||
{part.content}
|
{formattedContent.map((part, index) => (
|
||||||
</pre>
|
<div key={index}>
|
||||||
) : (
|
{part.type === "json" ? (
|
||||||
<pre
|
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-primary">
|
||||||
className={cn(
|
{part.content}
|
||||||
"whitespace-pre-wrap break-words",
|
</pre>
|
||||||
textColor
|
) : (
|
||||||
)}
|
<pre
|
||||||
>
|
className={cn(
|
||||||
{part.content}
|
"whitespace-pre-wrap break-words",
|
||||||
</pre>
|
textColor
|
||||||
)}
|
)}
|
||||||
</div>
|
>
|
||||||
))}
|
{part.content}
|
||||||
</div>
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
// Use fullPrompt (already built above) with model and all images
|
||||||
|
// Pass previousContext so the history is preserved in the output file
|
||||||
await this.runAgent(
|
await this.runAgent(
|
||||||
workDir,
|
workDir,
|
||||||
featureId,
|
featureId,
|
||||||
fullPrompt,
|
fullPrompt,
|
||||||
abortController,
|
abortController,
|
||||||
allImagePaths.length > 0 ? allImagePaths : imagePaths,
|
allImagePaths.length > 0 ? allImagePaths : imagePaths,
|
||||||
model
|
model,
|
||||||
|
previousContext || undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mark as waiting_approval for user review
|
// Mark as waiting_approval for user review
|
||||||
@@ -1169,7 +1171,8 @@ This helps parse your summary correctly in the output logs.`;
|
|||||||
prompt: string,
|
prompt: string,
|
||||||
abortController: AbortController,
|
abortController: AbortController,
|
||||||
imagePaths?: string[],
|
imagePaths?: string[],
|
||||||
model?: string
|
model?: string,
|
||||||
|
previousContent?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// CI/CD Mock Mode: Return early with mock response when AUTOMAKER_MOCK_AGENT is set
|
// CI/CD Mock Mode: Return early with mock response when AUTOMAKER_MOCK_AGENT is set
|
||||||
// This prevents actual API calls during automated testing
|
// 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
|
// Execute via provider
|
||||||
const stream = provider.executeQuery(options);
|
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
|
// Agent output goes to .automaker directory
|
||||||
// Note: We use the original projectPath here (from config), not workDir
|
// Note: We use the original projectPath here (from config), not workDir
|
||||||
// because workDir might be a worktree path
|
// because workDir might be a worktree path
|
||||||
|
|||||||
Reference in New Issue
Block a user