mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Update app_spec.txt and coding_prompt.md for improved clarity and consistency
- Updated references to `app_spec.txt` and `feature_list.json` in app_spec.txt to include the correct path. - Enhanced coding_prompt.md by incorporating testing utilities for better test management and readability. - Added new utility functions in tests/utils.ts to streamline test interactions. This commit aims to improve documentation accuracy and maintainability of testing practices.
This commit is contained in:
310
app/src/components/ui/feature-image-upload.tsx
Normal file
310
app/src/components/ui/feature-image-upload.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useCallback } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ImageIcon, X, Upload } from "lucide-react";
|
||||
|
||||
export interface FeatureImage {
|
||||
id: string;
|
||||
data: string; // base64 encoded
|
||||
mimeType: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface FeatureImageUploadProps {
|
||||
images: FeatureImage[];
|
||||
onImagesChange: (images: FeatureImage[]) => void;
|
||||
maxFiles?: number;
|
||||
maxFileSize?: number; // in bytes, default 10MB
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ACCEPTED_IMAGE_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
];
|
||||
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
export function FeatureImageUpload({
|
||||
images,
|
||||
onImagesChange,
|
||||
maxFiles = 5,
|
||||
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
||||
className,
|
||||
disabled = false,
|
||||
}: FeatureImageUploadProps) {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const fileToBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error("Failed to read file as base64"));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error("Failed to read file"));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
const processFiles = useCallback(
|
||||
async (files: FileList) => {
|
||||
if (disabled || isProcessing) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
const newImages: FeatureImage[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const file of Array.from(files)) {
|
||||
// Validate file type
|
||||
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
||||
errors.push(
|
||||
`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > maxFileSize) {
|
||||
const maxSizeMB = maxFileSize / (1024 * 1024);
|
||||
errors.push(
|
||||
`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we've reached max files
|
||||
if (newImages.length + images.length >= maxFiles) {
|
||||
errors.push(`Maximum ${maxFiles} images allowed.`);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = await fileToBase64(file);
|
||||
const imageAttachment: FeatureImage = {
|
||||
id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
data: base64,
|
||||
mimeType: file.type,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
};
|
||||
newImages.push(imageAttachment);
|
||||
} catch (error) {
|
||||
errors.push(`${file.name}: Failed to process image.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.warn("Image upload errors:", errors);
|
||||
}
|
||||
|
||||
if (newImages.length > 0) {
|
||||
onImagesChange([...images, ...newImages]);
|
||||
}
|
||||
|
||||
setIsProcessing(false);
|
||||
},
|
||||
[disabled, isProcessing, images, maxFiles, maxFileSize, onImagesChange]
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
|
||||
if (disabled) return;
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
processFiles(files);
|
||||
}
|
||||
},
|
||||
[disabled, processFiles]
|
||||
);
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!disabled) {
|
||||
setIsDragOver(true);
|
||||
}
|
||||
},
|
||||
[disabled]
|
||||
);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const handleFileSelect = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
processFiles(files);
|
||||
}
|
||||
// Reset the input so the same file can be selected again
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
},
|
||||
[processFiles]
|
||||
);
|
||||
|
||||
const handleBrowseClick = useCallback(() => {
|
||||
if (!disabled && fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const removeImage = useCallback(
|
||||
(imageId: string) => {
|
||||
onImagesChange(images.filter((img) => img.id !== imageId));
|
||||
},
|
||||
[images, onImagesChange]
|
||||
);
|
||||
|
||||
const clearAllImages = useCallback(() => {
|
||||
onImagesChange([]);
|
||||
}, [onImagesChange]);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept={ACCEPTED_IMAGE_TYPES.join(",")}
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
disabled={disabled}
|
||||
data-testid="feature-image-input"
|
||||
/>
|
||||
|
||||
{/* Drop zone */}
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={handleBrowseClick}
|
||||
className={cn(
|
||||
"relative rounded-lg border-2 border-dashed transition-all duration-200 cursor-pointer",
|
||||
{
|
||||
"border-blue-400 bg-blue-50 dark:bg-blue-950/20":
|
||||
isDragOver && !disabled,
|
||||
"border-muted-foreground/25": !isDragOver && !disabled,
|
||||
"border-muted-foreground/10 opacity-50 cursor-not-allowed": disabled,
|
||||
"hover:border-blue-400 hover:bg-blue-50/50 dark:hover:bg-blue-950/10":
|
||||
!disabled && !isDragOver,
|
||||
}
|
||||
)}
|
||||
data-testid="feature-image-dropzone"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center p-4 text-center">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-full p-2 mb-2",
|
||||
isDragOver && !disabled
|
||||
? "bg-blue-100 dark:bg-blue-900/30"
|
||||
: "bg-muted"
|
||||
)}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<Upload className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<ImageIcon className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isDragOver && !disabled
|
||||
? "Drop images here"
|
||||
: "Click or drag images here"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Up to {maxFiles} images, max{" "}
|
||||
{Math.round(maxFileSize / (1024 * 1024))}MB each
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image previews */}
|
||||
{images.length > 0 && (
|
||||
<div className="mt-3 space-y-2" data-testid="feature-image-previews">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
{images.length} image{images.length > 1 ? "s" : ""} selected
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearAllImages}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
disabled={disabled}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{images.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="relative group rounded-md border border-muted bg-muted/50 overflow-hidden"
|
||||
data-testid={`feature-image-preview-${image.id}`}
|
||||
>
|
||||
{/* Image thumbnail */}
|
||||
<div className="w-16 h-16 flex items-center justify-center">
|
||||
<img
|
||||
src={image.data}
|
||||
alt={image.filename}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
{/* Remove button */}
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeImage(image.id);
|
||||
}}
|
||||
className="absolute top-0.5 right-0.5 p-0.5 rounded-full bg-destructive text-destructive-foreground opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
data-testid={`remove-image-${image.id}`}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
{/* Filename tooltip on hover */}
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1 py-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<p className="text-[10px] text-white truncate">
|
||||
{image.filename}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
app/src/components/ui/textarea.tsx
Normal file
20
app/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-h-[80px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-none",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -106,6 +106,11 @@ export function AgentOutputModal({
|
||||
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") {
|
||||
|
||||
@@ -79,6 +79,7 @@ export function BoardView() {
|
||||
const [showActivityLog, setShowActivityLog] = useState(false);
|
||||
const [showOutputModal, setShowOutputModal] = useState(false);
|
||||
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
|
||||
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(new Set());
|
||||
|
||||
// Make current project available globally for modal
|
||||
useEffect(() => {
|
||||
@@ -185,6 +186,32 @@ export function BoardView() {
|
||||
loadFeatures();
|
||||
}, [loadFeatures]);
|
||||
|
||||
// Check which features have context files
|
||||
useEffect(() => {
|
||||
const checkAllContexts = async () => {
|
||||
const inProgressFeatures = features.filter((f) => f.status === "in_progress");
|
||||
const contextChecks = await Promise.all(
|
||||
inProgressFeatures.map(async (f) => ({
|
||||
id: f.id,
|
||||
hasContext: await checkContextExists(f.id),
|
||||
}))
|
||||
);
|
||||
|
||||
const newSet = new Set<string>();
|
||||
contextChecks.forEach(({ id, hasContext }) => {
|
||||
if (hasContext) {
|
||||
newSet.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
setFeaturesWithContext(newSet);
|
||||
};
|
||||
|
||||
if (features.length > 0 && !isLoading) {
|
||||
checkAllContexts();
|
||||
}
|
||||
}, [features, isLoading]);
|
||||
|
||||
// Save features to file
|
||||
const saveFeatures = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
@@ -360,6 +387,59 @@ export function BoardView() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResumeFeature = async (feature: Feature) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
console.log("[Board] Resuming feature:", { id: feature.id, description: feature.description });
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
console.error("Auto mode API not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the API to resume this specific feature by ID with context
|
||||
const result = await api.autoMode.resumeFeature(
|
||||
currentProject.path,
|
||||
feature.id
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Board] Feature resume started successfully");
|
||||
// The feature status will be updated by the auto mode service
|
||||
// and the UI will reload features when resume completes
|
||||
} else {
|
||||
console.error("[Board] Failed to resume feature:", result.error);
|
||||
await loadFeatures();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Board] Error resuming feature:", error);
|
||||
await loadFeatures();
|
||||
}
|
||||
};
|
||||
|
||||
const checkContextExists = async (featureId: string): Promise<boolean> => {
|
||||
if (!currentProject) return false;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.contextExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await api.autoMode.contextExists(
|
||||
currentProject.path,
|
||||
featureId
|
||||
);
|
||||
|
||||
return result.success && result.exists === true;
|
||||
} catch (error) {
|
||||
console.error("[Board] Error checking context:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getColumnFeatures = (columnId: ColumnId) => {
|
||||
return features.filter((f) => f.status === columnId);
|
||||
};
|
||||
@@ -504,6 +584,8 @@ export function BoardView() {
|
||||
onDelete={() => handleDeleteFeature(feature.id)}
|
||||
onViewOutput={() => handleViewOutput(feature)}
|
||||
onVerify={() => handleVerifyFeature(feature)}
|
||||
onResume={() => handleResumeFeature(feature)}
|
||||
hasContext={featuresWithContext.has(feature.id)}
|
||||
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -295,11 +295,14 @@ export function InterviewView() {
|
||||
await api.mkdir(fullProjectPath);
|
||||
|
||||
// Write app_spec.txt with generated content
|
||||
await api.writeFile(`${fullProjectPath}/app_spec.txt`, generatedSpec);
|
||||
await api.writeFile(
|
||||
`${fullProjectPath}/.automaker/app_spec.txt`,
|
||||
generatedSpec
|
||||
);
|
||||
|
||||
// Create initial .automaker/feature_list.json
|
||||
await api.writeFile(
|
||||
`${fullProjectPath}/feature_list.json`,
|
||||
`${fullProjectPath}/.automaker/feature_list.json`,
|
||||
JSON.stringify(
|
||||
[
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Feature } from "@/store/app-store";
|
||||
import { GripVertical, Edit, CheckCircle2, Circle, Loader2, Trash2, Eye, PlayCircle } from "lucide-react";
|
||||
import { GripVertical, Edit, CheckCircle2, Circle, Loader2, Trash2, Eye, PlayCircle, RotateCcw } from "lucide-react";
|
||||
|
||||
interface KanbanCardProps {
|
||||
feature: Feature;
|
||||
@@ -20,10 +20,12 @@ interface KanbanCardProps {
|
||||
onDelete: () => void;
|
||||
onViewOutput?: () => void;
|
||||
onVerify?: () => void;
|
||||
onResume?: () => void;
|
||||
hasContext?: boolean;
|
||||
isCurrentAutoTask?: boolean;
|
||||
}
|
||||
|
||||
export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify, isCurrentAutoTask }: KanbanCardProps) {
|
||||
export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify, onResume, hasContext, isCurrentAutoTask }: KanbanCardProps) {
|
||||
// Disable dragging if the feature is in progress or verified
|
||||
const isDraggable = feature.status === "backlog";
|
||||
const {
|
||||
@@ -127,7 +129,21 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "in_progress" && (
|
||||
<>
|
||||
{onVerify && (
|
||||
{hasContext && onResume ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-blue-600 hover:bg-blue-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
}}
|
||||
data-testid={`resume-feature-${feature.id}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
) : onVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
@@ -139,9 +155,9 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
|
||||
data-testid={`verify-feature-${feature.id}`}
|
||||
>
|
||||
<PlayCircle className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
Implement
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
{onViewOutput && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -12,10 +12,25 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { FolderOpen, Plus, Cpu, Folder, Clock, Sparkles, MessageSquare, ChevronDown } from "lucide-react";
|
||||
import {
|
||||
FolderOpen,
|
||||
Plus,
|
||||
Cpu,
|
||||
Folder,
|
||||
Clock,
|
||||
Sparkles,
|
||||
MessageSquare,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -24,7 +39,8 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function WelcomeView() {
|
||||
const { projects, addProject, setCurrentProject, setCurrentView } = useAppStore();
|
||||
const { projects, addProject, setCurrentProject, setCurrentView } =
|
||||
useAppStore();
|
||||
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState("");
|
||||
const [newProjectPath, setNewProjectPath] = useState("");
|
||||
@@ -101,13 +117,17 @@ export function WelcomeView() {
|
||||
);
|
||||
|
||||
await api.writeFile(
|
||||
`${projectPath}/feature_list.json`,
|
||||
`${projectPath}/.automaker/feature_list.json`,
|
||||
JSON.stringify(
|
||||
[
|
||||
{
|
||||
category: "Core",
|
||||
description: "First feature to implement",
|
||||
steps: ["Step 1: Define requirements", "Step 2: Implement", "Step 3: Test"],
|
||||
steps: [
|
||||
"Step 1: Define requirements",
|
||||
"Step 2: Implement",
|
||||
"Step 3: Test",
|
||||
],
|
||||
passes: false,
|
||||
},
|
||||
],
|
||||
@@ -151,8 +171,12 @@ export function WelcomeView() {
|
||||
<Cpu className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Welcome to Automaker</h1>
|
||||
<p className="text-sm text-zinc-400">Your autonomous AI development studio</p>
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
Welcome to Automaker
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Your autonomous AI development studio
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,9 +198,12 @@ export function WelcomeView() {
|
||||
<Plus className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">New Project</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-1">
|
||||
New Project
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Create a new project from scratch with AI-powered development
|
||||
Create a new project from scratch with AI-powered
|
||||
development
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +250,9 @@ export function WelcomeView() {
|
||||
<FolderOpen className="w-6 h-6 text-zinc-400 group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">Open Project</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-1">
|
||||
Open Project
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Open an existing project folder to continue working
|
||||
</p>
|
||||
@@ -246,7 +275,9 @@ export function WelcomeView() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Clock className="w-5 h-5 text-zinc-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Recent Projects</h2>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Recent Projects
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{recentProjects.map((project) => (
|
||||
@@ -266,10 +297,14 @@ export function WelcomeView() {
|
||||
<p className="font-medium text-white truncate group-hover:text-brand-500 transition-colors">
|
||||
{project.name}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-500 truncate mt-0.5">{project.path}</p>
|
||||
<p className="text-xs text-zinc-500 truncate mt-0.5">
|
||||
{project.path}
|
||||
</p>
|
||||
{project.lastOpened && (
|
||||
<p className="text-xs text-zinc-600 mt-1">
|
||||
{new Date(project.lastOpened).toLocaleDateString()}
|
||||
{new Date(
|
||||
project.lastOpened
|
||||
).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -287,7 +322,9 @@ export function WelcomeView() {
|
||||
<div className="w-16 h-16 rounded-2xl bg-zinc-900/50 border border-white/10 flex items-center justify-center mb-4">
|
||||
<Sparkles className="w-8 h-8 text-zinc-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">No projects yet</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
No projects yet
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400 max-w-md">
|
||||
Get started by creating a new project or opening an existing one
|
||||
</p>
|
||||
@@ -297,7 +334,10 @@ export function WelcomeView() {
|
||||
</div>
|
||||
|
||||
{/* New Project Dialog */}
|
||||
<Dialog open={showNewProjectDialog} onOpenChange={setShowNewProjectDialog}>
|
||||
<Dialog
|
||||
open={showNewProjectDialog}
|
||||
onOpenChange={setShowNewProjectDialog}
|
||||
>
|
||||
<DialogContent
|
||||
className="bg-zinc-900 border-white/10"
|
||||
data-testid="new-project-dialog"
|
||||
|
||||
@@ -11,16 +11,20 @@ export function useAutoMode() {
|
||||
const {
|
||||
isAutoModeRunning,
|
||||
setAutoModeRunning,
|
||||
currentAutoTask,
|
||||
setCurrentAutoTask,
|
||||
runningAutoTasks,
|
||||
addRunningTask,
|
||||
removeRunningTask,
|
||||
clearRunningTasks,
|
||||
currentProject,
|
||||
addAutoModeActivity,
|
||||
} = useAppStore(
|
||||
useShallow((state) => ({
|
||||
isAutoModeRunning: state.isAutoModeRunning,
|
||||
setAutoModeRunning: state.setAutoModeRunning,
|
||||
currentAutoTask: state.currentAutoTask,
|
||||
setCurrentAutoTask: state.setCurrentAutoTask,
|
||||
runningAutoTasks: state.runningAutoTasks,
|
||||
addRunningTask: state.addRunningTask,
|
||||
removeRunningTask: state.removeRunningTask,
|
||||
clearRunningTasks: state.clearRunningTasks,
|
||||
currentProject: state.currentProject,
|
||||
addAutoModeActivity: state.addAutoModeActivity,
|
||||
}))
|
||||
@@ -36,7 +40,7 @@ export function useAutoMode() {
|
||||
|
||||
switch (event.type) {
|
||||
case "auto_mode_feature_start":
|
||||
setCurrentAutoTask(event.featureId);
|
||||
addRunningTask(event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "start",
|
||||
@@ -45,13 +49,14 @@ export function useAutoMode() {
|
||||
break;
|
||||
|
||||
case "auto_mode_feature_complete":
|
||||
// Feature completed - UI will reload features on its own
|
||||
// Feature completed - remove from running tasks and UI will reload features on its own
|
||||
console.log(
|
||||
"[AutoMode] Feature completed:",
|
||||
event.featureId,
|
||||
"passes:",
|
||||
event.passes
|
||||
);
|
||||
removeRunningTask(event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "complete",
|
||||
@@ -65,7 +70,7 @@ export function useAutoMode() {
|
||||
case "auto_mode_complete":
|
||||
// All features completed
|
||||
setAutoModeRunning(false);
|
||||
setCurrentAutoTask(null);
|
||||
clearRunningTasks();
|
||||
console.log("[AutoMode] All features completed!");
|
||||
break;
|
||||
|
||||
@@ -115,7 +120,7 @@ export function useAutoMode() {
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [setCurrentAutoTask, setAutoModeRunning, addAutoModeActivity]);
|
||||
}, [addRunningTask, removeRunningTask, clearRunningTasks, setAutoModeRunning, addAutoModeActivity]);
|
||||
|
||||
// Start auto mode
|
||||
const start = useCallback(async () => {
|
||||
@@ -158,7 +163,7 @@ export function useAutoMode() {
|
||||
|
||||
if (result.success) {
|
||||
setAutoModeRunning(false);
|
||||
setCurrentAutoTask(null);
|
||||
clearRunningTasks();
|
||||
console.log("[AutoMode] Stopped successfully");
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to stop:", result.error);
|
||||
@@ -168,11 +173,11 @@ export function useAutoMode() {
|
||||
console.error("[AutoMode] Error stopping:", error);
|
||||
throw error;
|
||||
}
|
||||
}, [setAutoModeRunning, setCurrentAutoTask]);
|
||||
}, [setAutoModeRunning, clearRunningTasks]);
|
||||
|
||||
return {
|
||||
isRunning: isAutoModeRunning,
|
||||
currentTask: currentAutoTask,
|
||||
runningTasks: runningAutoTasks,
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface AutoModeAPI {
|
||||
status: () => Promise<{ success: boolean; isRunning?: boolean; currentFeatureId?: string | null; error?: string }>;
|
||||
runFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
verifyFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
resumeFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
contextExists: (projectPath: string, featureId: string) => Promise<{ success: boolean; exists?: boolean; error?: string }>;
|
||||
onEvent: (callback: (event: AutoModeEvent) => void) => () => void;
|
||||
}
|
||||
|
||||
@@ -288,8 +290,9 @@ export const getElectronAPI = (): ElectronAPI => {
|
||||
|
||||
// Mock Auto Mode state and implementation
|
||||
let mockAutoModeRunning = false;
|
||||
let mockRunningFeatures = new Set<string>(); // Track multiple concurrent feature verifications
|
||||
let mockAutoModeCallbacks: ((event: AutoModeEvent) => void)[] = [];
|
||||
let mockAutoModeTimeout: NodeJS.Timeout | null = null;
|
||||
let mockAutoModeTimeouts = new Map<string, NodeJS.Timeout>(); // Track timeouts per feature
|
||||
|
||||
function createMockAutoModeAPI(): AutoModeAPI {
|
||||
return {
|
||||
@@ -299,19 +302,21 @@ function createMockAutoModeAPI(): AutoModeAPI {
|
||||
}
|
||||
|
||||
mockAutoModeRunning = true;
|
||||
const featureId = "auto-mode-0";
|
||||
mockRunningFeatures.add(featureId);
|
||||
|
||||
// Simulate auto mode with Plan-Act-Verify phases
|
||||
simulateAutoModeLoop(projectPath);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
stop: async () => {
|
||||
mockAutoModeRunning = false;
|
||||
if (mockAutoModeTimeout) {
|
||||
clearTimeout(mockAutoModeTimeout);
|
||||
mockAutoModeTimeout = null;
|
||||
}
|
||||
mockRunningFeatures.clear();
|
||||
// Clear all timeouts
|
||||
mockAutoModeTimeouts.forEach(timeout => clearTimeout(timeout));
|
||||
mockAutoModeTimeouts.clear();
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
@@ -324,27 +329,44 @@ function createMockAutoModeAPI(): AutoModeAPI {
|
||||
},
|
||||
|
||||
runFeature: async (projectPath: string, featureId: string) => {
|
||||
if (mockAutoModeRunning) {
|
||||
return { success: false, error: "Auto mode is already running" };
|
||||
if (mockRunningFeatures.has(featureId)) {
|
||||
return { success: false, error: `Feature ${featureId} is already running` };
|
||||
}
|
||||
|
||||
mockAutoModeRunning = true;
|
||||
simulateAutoModeLoop(projectPath);
|
||||
mockRunningFeatures.add(featureId);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
return { success: true, passes: true };
|
||||
},
|
||||
|
||||
verifyFeature: async (projectPath: string, featureId: string) => {
|
||||
if (mockAutoModeRunning) {
|
||||
return { success: false, error: "Auto mode is already running" };
|
||||
if (mockRunningFeatures.has(featureId)) {
|
||||
return { success: false, error: `Feature ${featureId} is already running` };
|
||||
}
|
||||
|
||||
mockAutoModeRunning = true;
|
||||
simulateAutoModeLoop(projectPath);
|
||||
mockRunningFeatures.add(featureId);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
return { success: true, passes: true };
|
||||
},
|
||||
|
||||
resumeFeature: async (projectPath: string, featureId: string) => {
|
||||
if (mockRunningFeatures.has(featureId)) {
|
||||
return { success: false, error: `Feature ${featureId} is already running` };
|
||||
}
|
||||
|
||||
mockRunningFeatures.add(featureId);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
return { success: true, passes: true };
|
||||
},
|
||||
|
||||
contextExists: async (projectPath: string, featureId: string) => {
|
||||
// Mock implementation - simulate that context exists for some features
|
||||
const exists = mockFileSystem[`${projectPath}/.automaker/context/${featureId}.md`] !== undefined;
|
||||
return { success: true, exists };
|
||||
},
|
||||
|
||||
onEvent: (callback: (event: AutoModeEvent) => void) => {
|
||||
mockAutoModeCallbacks.push(callback);
|
||||
return () => {
|
||||
@@ -358,8 +380,7 @@ function emitAutoModeEvent(event: AutoModeEvent) {
|
||||
mockAutoModeCallbacks.forEach(cb => cb(event));
|
||||
}
|
||||
|
||||
async function simulateAutoModeLoop(projectPath: string) {
|
||||
const featureId = "feature-0";
|
||||
async function simulateAutoModeLoop(projectPath: string, featureId: string) {
|
||||
const mockFeature = {
|
||||
id: featureId,
|
||||
category: "Core",
|
||||
@@ -375,8 +396,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
feature: mockFeature,
|
||||
});
|
||||
|
||||
await delay(300);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(300, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
// Phase 1: PLANNING
|
||||
emitAutoModeEvent({
|
||||
@@ -392,8 +413,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
content: "Analyzing codebase structure and creating implementation plan...",
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(500, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
// Phase 2: ACTION
|
||||
emitAutoModeEvent({
|
||||
@@ -409,8 +430,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
content: "Starting code implementation...",
|
||||
});
|
||||
|
||||
await delay(300);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(300, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
// Simulate tool use
|
||||
emitAutoModeEvent({
|
||||
@@ -420,8 +441,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
input: { file: "package.json" },
|
||||
});
|
||||
|
||||
await delay(300);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(300, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
emitAutoModeEvent({
|
||||
type: "auto_mode_tool",
|
||||
@@ -430,8 +451,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
input: { file: "src/feature.ts", content: "// Feature code" },
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(500, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
// Phase 3: VERIFICATION
|
||||
emitAutoModeEvent({
|
||||
@@ -447,8 +468,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
content: "Verifying implementation and checking test results...",
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(500, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
emitAutoModeEvent({
|
||||
type: "auto_mode_progress",
|
||||
@@ -464,21 +485,15 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
message: "Feature implemented successfully",
|
||||
});
|
||||
|
||||
await delay(300);
|
||||
if (!mockAutoModeRunning) return;
|
||||
|
||||
// All features complete
|
||||
emitAutoModeEvent({
|
||||
type: "auto_mode_complete",
|
||||
message: "All features completed!",
|
||||
});
|
||||
|
||||
mockAutoModeRunning = false;
|
||||
// Clean up this feature from running set
|
||||
mockRunningFeatures.delete(featureId);
|
||||
mockAutoModeTimeouts.delete(featureId);
|
||||
}
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
function delay(ms: number, featureId: string): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
mockAutoModeTimeout = setTimeout(resolve, ms);
|
||||
const timeout = setTimeout(resolve, ms);
|
||||
mockAutoModeTimeouts.set(featureId, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,12 +36,21 @@ export interface ChatSession {
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface FeatureImage {
|
||||
id: string;
|
||||
data: string; // base64 encoded
|
||||
mimeType: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
id: string;
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
status: "backlog" | "in_progress" | "verified";
|
||||
images?: FeatureImage[];
|
||||
}
|
||||
|
||||
export interface FileTreeNode {
|
||||
@@ -96,7 +105,7 @@ export interface AppState {
|
||||
|
||||
// Auto Mode
|
||||
isAutoModeRunning: boolean;
|
||||
currentAutoTask: string | null; // Feature ID being worked on
|
||||
runningAutoTasks: string[]; // Feature IDs being worked on (supports concurrent tasks)
|
||||
autoModeActivityLog: AutoModeActivity[];
|
||||
}
|
||||
|
||||
@@ -160,7 +169,9 @@ export interface AppActions {
|
||||
|
||||
// Auto Mode actions
|
||||
setAutoModeRunning: (running: boolean) => void;
|
||||
setCurrentAutoTask: (taskId: string | null) => void;
|
||||
addRunningTask: (taskId: string) => void;
|
||||
removeRunningTask: (taskId: string) => void;
|
||||
clearRunningTasks: () => void;
|
||||
addAutoModeActivity: (activity: Omit<AutoModeActivity, "id" | "timestamp">) => void;
|
||||
clearAutoModeActivity: () => void;
|
||||
|
||||
@@ -187,7 +198,7 @@ const initialState: AppState = {
|
||||
currentChatSession: null,
|
||||
chatHistoryOpen: false,
|
||||
isAutoModeRunning: false,
|
||||
currentAutoTask: null,
|
||||
runningAutoTasks: [],
|
||||
autoModeActivityLog: [],
|
||||
};
|
||||
|
||||
@@ -375,7 +386,19 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
|
||||
// Auto Mode actions
|
||||
setAutoModeRunning: (running) => set({ isAutoModeRunning: running }),
|
||||
setCurrentAutoTask: (taskId) => set({ currentAutoTask: taskId }),
|
||||
|
||||
addRunningTask: (taskId) => {
|
||||
const current = get().runningAutoTasks;
|
||||
if (!current.includes(taskId)) {
|
||||
set({ runningAutoTasks: [...current, taskId] });
|
||||
}
|
||||
},
|
||||
|
||||
removeRunningTask: (taskId) => {
|
||||
set({ runningAutoTasks: get().runningAutoTasks.filter(id => id !== taskId) });
|
||||
},
|
||||
|
||||
clearRunningTasks: () => set({ runningAutoTasks: [] }),
|
||||
|
||||
addAutoModeActivity: (activity) => {
|
||||
const id = `activity-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
Reference in New Issue
Block a user