mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
- Improved error handling in AutoModeService to log errors and update feature status to "waiting_approval" when an error occurs during feature execution. - Added error message writing to the context file for better debugging. - Updated FeatureLoader to include an optional error message parameter when updating feature status. - Enhanced prompt generation to include detailed context about attached images for better user guidance during feature implementation. - Modified KanbanCard component to visually indicate features with errors, improving user awareness of issues. These changes significantly enhance the robustness of the auto mode feature and improve the user experience by providing clearer feedback on feature execution status.
281 lines
8.7 KiB
TypeScript
281 lines
8.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Loader2, List, FileText } from "lucide-react";
|
|
import { getElectronAPI } from "@/lib/electron";
|
|
import { LogViewer } from "@/components/ui/log-viewer";
|
|
|
|
interface AgentOutputModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
featureDescription: string;
|
|
featureId: string;
|
|
/** Called when a number key (0-9) is pressed while the modal is open */
|
|
onNumberKeyPress?: (key: string) => void;
|
|
}
|
|
|
|
type ViewMode = "parsed" | "raw";
|
|
|
|
export function AgentOutputModal({
|
|
open,
|
|
onClose,
|
|
featureDescription,
|
|
featureId,
|
|
onNumberKeyPress,
|
|
}: AgentOutputModalProps) {
|
|
const [output, setOutput] = useState<string>("");
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [viewMode, setViewMode] = useState<ViewMode>("parsed");
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const autoScrollRef = useRef(true);
|
|
const projectPathRef = useRef<string>("");
|
|
|
|
// Auto-scroll to bottom when output changes
|
|
useEffect(() => {
|
|
if (autoScrollRef.current && scrollRef.current) {
|
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
}
|
|
}, [output]);
|
|
|
|
// Load existing output from file
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const loadOutput = async () => {
|
|
const api = getElectronAPI();
|
|
if (!api) return;
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// Get current project path from store (we'll need to pass this)
|
|
const currentProject = (window as any).__currentProject;
|
|
if (!currentProject?.path) {
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
projectPathRef.current = currentProject.path;
|
|
|
|
// Ensure context directory exists
|
|
const contextDir = `${currentProject.path}/.automaker/agents-context`;
|
|
await api.mkdir(contextDir);
|
|
|
|
// Try to read existing output file
|
|
const outputPath = `${contextDir}/${featureId}.md`;
|
|
const result = await api.readFile(outputPath);
|
|
|
|
if (result.success && result.content) {
|
|
setOutput(result.content);
|
|
} else {
|
|
setOutput("");
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load output:", error);
|
|
setOutput("");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
loadOutput();
|
|
}, [open, featureId]);
|
|
|
|
// Save output to file
|
|
const saveOutput = async (newContent: string) => {
|
|
if (!projectPathRef.current) return;
|
|
|
|
const api = getElectronAPI();
|
|
if (!api) return;
|
|
|
|
try {
|
|
const contextDir = `${projectPathRef.current}/.automaker/agents-context`;
|
|
const outputPath = `${contextDir}/${featureId}.md`;
|
|
|
|
await api.writeFile(outputPath, newContent);
|
|
} catch (error) {
|
|
console.error("Failed to save output:", error);
|
|
}
|
|
};
|
|
|
|
// Listen to auto mode events and update output
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const api = getElectronAPI();
|
|
if (!api?.autoMode) return;
|
|
|
|
const unsubscribe = api.autoMode.onEvent((event) => {
|
|
// Filter events for this specific feature only
|
|
if (event.featureId !== featureId) {
|
|
return;
|
|
}
|
|
|
|
let newContent = "";
|
|
|
|
if (event.type === "auto_mode_progress") {
|
|
newContent = event.content || "";
|
|
} else if (event.type === "auto_mode_tool") {
|
|
const toolName = event.tool || "Unknown Tool";
|
|
const toolInput = event.input
|
|
? JSON.stringify(event.input, null, 2)
|
|
: "";
|
|
newContent = `\n🔧 Tool: ${toolName}\n${
|
|
toolInput ? `Input: ${toolInput}` : ""
|
|
}`;
|
|
} else if (event.type === "auto_mode_phase") {
|
|
const phaseEmoji =
|
|
event.phase === "planning"
|
|
? "📋"
|
|
: event.phase === "action"
|
|
? "⚡"
|
|
: "✅";
|
|
newContent = `\n${phaseEmoji} ${event.message}\n`;
|
|
} else if (event.type === "auto_mode_error") {
|
|
newContent = `\n❌ Error: ${event.error}\n`;
|
|
} else if (event.type === "auto_mode_feature_complete") {
|
|
const emoji = event.passes ? "✅" : "⚠️";
|
|
newContent = `\n${emoji} Task completed: ${event.message}\n`;
|
|
|
|
// Close the modal when the feature is verified (passes = true)
|
|
if (event.passes) {
|
|
// Small delay to show the completion message before closing
|
|
setTimeout(() => {
|
|
onClose();
|
|
}, 1500);
|
|
}
|
|
}
|
|
|
|
if (newContent) {
|
|
setOutput((prev) => {
|
|
const updated = prev + newContent;
|
|
saveOutput(updated);
|
|
return updated;
|
|
});
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
};
|
|
}, [open, featureId]);
|
|
|
|
// Handle scroll to detect if user scrolled up
|
|
const handleScroll = () => {
|
|
if (!scrollRef.current) return;
|
|
|
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
autoScrollRef.current = isAtBottom;
|
|
};
|
|
|
|
// Handle number key presses while modal is open
|
|
useEffect(() => {
|
|
if (!open || !onNumberKeyPress) return;
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
// Check if a number key (0-9) was pressed without modifiers
|
|
if (
|
|
!event.ctrlKey &&
|
|
!event.altKey &&
|
|
!event.metaKey &&
|
|
/^[0-9]$/.test(event.key)
|
|
) {
|
|
event.preventDefault();
|
|
onNumberKeyPress(event.key);
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
window.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [open, onNumberKeyPress]);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent
|
|
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
|
|
data-testid="agent-output-modal"
|
|
>
|
|
<DialogHeader className="flex-shrink-0">
|
|
<div className="flex items-center justify-between">
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
|
Agent Output
|
|
</DialogTitle>
|
|
<div className="flex items-center gap-1 bg-zinc-900/50 rounded-lg p-1">
|
|
<button
|
|
onClick={() => setViewMode("parsed")}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
|
viewMode === "parsed"
|
|
? "bg-primary/20 text-primary shadow-sm"
|
|
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50"
|
|
}`}
|
|
data-testid="view-mode-parsed"
|
|
>
|
|
<List className="w-3.5 h-3.5" />
|
|
Parsed
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode("raw")}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
|
viewMode === "raw"
|
|
? "bg-primary/20 text-primary shadow-sm"
|
|
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50"
|
|
}`}
|
|
data-testid="view-mode-raw"
|
|
>
|
|
<FileText className="w-3.5 h-3.5" />
|
|
Raw
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<DialogDescription
|
|
className="mt-1 max-h-24 overflow-y-auto break-words"
|
|
data-testid="agent-output-description"
|
|
>
|
|
{featureDescription}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div
|
|
ref={scrollRef}
|
|
onScroll={handleScroll}
|
|
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[400px] max-h-[60vh]"
|
|
>
|
|
{isLoading && !output ? (
|
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
|
Loading output...
|
|
</div>
|
|
) : !output ? (
|
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
No output yet. The agent will stream output here as it works.
|
|
</div>
|
|
) : viewMode === "parsed" ? (
|
|
<LogViewer output={output} />
|
|
) : (
|
|
<div className="whitespace-pre-wrap break-words text-zinc-300">
|
|
{output}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-xs text-muted-foreground text-center flex-shrink-0">
|
|
{autoScrollRef.current
|
|
? "Auto-scrolling enabled"
|
|
: "Scroll to bottom to enable auto-scroll"}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|