mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
feat: Update feature list and add project initialization utilities
- Removed obsolete feature from feature_list.json related to context file deletion. - Added new features for keyboard shortcuts in Kanban and context management. - Introduced project initialization utilities to create necessary .automaker directory structure and files when opening a new project. - Updated the AgentOutputModal and other components to reference the new agents-context directory. - Enhanced Playwright tests for context file management and project initialization. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,7 @@ export function AgentOutputModal({
|
||||
projectPathRef.current = currentProject.path;
|
||||
|
||||
// Ensure context directory exists
|
||||
const contextDir = `${currentProject.path}/.automaker/context`;
|
||||
const contextDir = `${currentProject.path}/.automaker/agents-context`;
|
||||
await api.mkdir(contextDir);
|
||||
|
||||
// Try to read existing output file
|
||||
@@ -89,7 +89,7 @@ export function AgentOutputModal({
|
||||
if (!api) return;
|
||||
|
||||
try {
|
||||
const contextDir = `${projectPathRef.current}/.automaker/context`;
|
||||
const contextDir = `${projectPathRef.current}/.automaker/agents-context`;
|
||||
const outputPath = `${contextDir}/${featureId}.md`;
|
||||
|
||||
await api.writeFile(outputPath, newContent);
|
||||
@@ -120,9 +120,16 @@ export function AgentOutputModal({
|
||||
const toolInput = event.input
|
||||
? JSON.stringify(event.input, null, 2)
|
||||
: "";
|
||||
newContent = `\n🔧 Tool: ${toolName}\n${toolInput ? `Input: ${toolInput}` : ""}`;
|
||||
newContent = `\n🔧 Tool: ${toolName}\n${
|
||||
toolInput ? `Input: ${toolInput}` : ""
|
||||
}`;
|
||||
} else if (event.type === "auto_mode_phase") {
|
||||
const phaseEmoji = event.phase === "planning" ? "📋" : event.phase === "action" ? "⚡" : "✅";
|
||||
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`;
|
||||
@@ -164,7 +171,10 @@ export function AgentOutputModal({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col" data-testid="agent-output-modal">
|
||||
<DialogContent
|
||||
className="max-w-4xl max-h-[80vh] flex flex-col"
|
||||
data-testid="agent-output-modal"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { initializeProject } from "@/lib/project-init";
|
||||
import {
|
||||
FolderOpen,
|
||||
Plus,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
Sparkles,
|
||||
MessageSquare,
|
||||
ChevronDown,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -37,6 +39,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function WelcomeView() {
|
||||
const { projects, addProject, setCurrentProject, setCurrentView } =
|
||||
@@ -45,14 +48,30 @@ export function WelcomeView() {
|
||||
const [newProjectName, setNewProjectName] = useState("");
|
||||
const [newProjectPath, setNewProjectPath] = useState("");
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isOpening, setIsOpening] = useState(false);
|
||||
const [showInitDialog, setShowInitDialog] = useState(false);
|
||||
const [initStatus, setInitStatus] = useState<{
|
||||
isNewProject: boolean;
|
||||
createdFiles: string[];
|
||||
projectName: string;
|
||||
projectPath: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleOpenProject = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.openDirectory();
|
||||
/**
|
||||
* Initialize project and optionally kick off project analysis agent
|
||||
*/
|
||||
const initializeAndOpenProject = useCallback(async (path: string, name: string) => {
|
||||
setIsOpening(true);
|
||||
try {
|
||||
// Initialize the .automaker directory structure
|
||||
const initResult = await initializeProject(path);
|
||||
|
||||
if (!result.canceled && result.filePaths[0]) {
|
||||
const path = result.filePaths[0];
|
||||
const name = path.split("/").pop() || "Untitled Project";
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const project = {
|
||||
id: `project-${Date.now()}`,
|
||||
@@ -63,9 +82,53 @@ export function WelcomeView() {
|
||||
|
||||
addProject(project);
|
||||
setCurrentProject(project);
|
||||
|
||||
// Show initialization dialog if files were created
|
||||
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
|
||||
setInitStatus({
|
||||
isNewProject: initResult.isNewProject,
|
||||
createdFiles: initResult.createdFiles,
|
||||
projectName: name,
|
||||
projectPath: path,
|
||||
});
|
||||
setShowInitDialog(true);
|
||||
|
||||
// TODO: Kick off agent to analyze the project and update app_spec.txt
|
||||
// This will be implemented in a future iteration with the auto-mode service
|
||||
console.log("[Welcome] Project initialized, created files:", initResult.createdFiles);
|
||||
} else {
|
||||
toast.success("Project opened", {
|
||||
description: `Opened ${name}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Welcome] Failed to open project:", error);
|
||||
toast.error("Failed to open project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
} finally {
|
||||
setIsOpening(false);
|
||||
}
|
||||
}, [addProject, setCurrentProject]);
|
||||
|
||||
const handleOpenProject = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.openDirectory();
|
||||
|
||||
if (!result.canceled && result.filePaths[0]) {
|
||||
const path = result.filePaths[0];
|
||||
const name = path.split("/").pop() || "Untitled Project";
|
||||
await initializeAndOpenProject(path, name);
|
||||
}
|
||||
}, [initializeAndOpenProject]);
|
||||
|
||||
/**
|
||||
* Handle clicking on a recent project
|
||||
*/
|
||||
const handleRecentProjectClick = useCallback(async (project: { id: string; name: string; path: string }) => {
|
||||
await initializeAndOpenProject(project.path, project.name);
|
||||
}, [initializeAndOpenProject]);
|
||||
|
||||
const handleNewProject = () => {
|
||||
setNewProjectName("");
|
||||
setNewProjectPath("");
|
||||
@@ -96,44 +159,39 @@ export function WelcomeView() {
|
||||
// Create project directory
|
||||
await api.mkdir(projectPath);
|
||||
|
||||
// Create initial files
|
||||
// Initialize .automaker directory with all necessary files
|
||||
const initResult = await initializeProject(projectPath);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the app_spec.txt with the project name
|
||||
await api.writeFile(
|
||||
`${projectPath}/app_spec.txt`,
|
||||
`${projectPath}/.automaker/app_spec.txt`,
|
||||
`<project_specification>
|
||||
<project_name>${newProjectName}</project_name>
|
||||
|
||||
<overview>
|
||||
Describe your project here...
|
||||
Describe your project here. This file will be analyzed by an AI agent
|
||||
to understand your project structure and tech stack.
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
<!-- Define your tech stack -->
|
||||
<!-- The AI agent will fill this in after analyzing your project -->
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
<!-- List core features -->
|
||||
<!-- List core features and capabilities -->
|
||||
</core_capabilities>
|
||||
</project_specification>`
|
||||
);
|
||||
|
||||
await api.writeFile(
|
||||
`${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",
|
||||
],
|
||||
passes: false,
|
||||
},
|
||||
],
|
||||
null,
|
||||
2
|
||||
)
|
||||
<implemented_features>
|
||||
<!-- The AI agent will populate this based on code analysis -->
|
||||
</implemented_features>
|
||||
</project_specification>`
|
||||
);
|
||||
|
||||
const project = {
|
||||
@@ -146,8 +204,24 @@ export function WelcomeView() {
|
||||
addProject(project);
|
||||
setCurrentProject(project);
|
||||
setShowNewProjectDialog(false);
|
||||
|
||||
toast.success("Project created", {
|
||||
description: `Created ${newProjectName} with .automaker directory`,
|
||||
});
|
||||
|
||||
// Set init status to show the dialog
|
||||
setInitStatus({
|
||||
isNewProject: true,
|
||||
createdFiles: initResult.createdFiles || [],
|
||||
projectName: newProjectName,
|
||||
projectPath: projectPath,
|
||||
});
|
||||
setShowInitDialog(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to create project:", error);
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
@@ -284,7 +358,7 @@ export function WelcomeView() {
|
||||
<div
|
||||
key={project.id}
|
||||
className="group relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md hover:bg-zinc-900/70 hover:border-brand-500/50 transition-all duration-200 cursor-pointer"
|
||||
onClick={() => setCurrentProject(project)}
|
||||
onClick={() => handleRecentProjectClick(project)}
|
||||
data-testid={`recent-project-${project.id}`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/0 to-purple-600/0 group-hover:from-brand-500/5 group-hover:to-purple-600/5 transition-all"></div>
|
||||
@@ -405,6 +479,79 @@ export function WelcomeView() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Project Initialization Dialog */}
|
||||
<Dialog open={showInitDialog} onOpenChange={setShowInitDialog}>
|
||||
<DialogContent
|
||||
className="bg-zinc-900 border-white/10"
|
||||
data-testid="project-init-dialog"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-brand-500" />
|
||||
{initStatus?.isNewProject ? "Project Initialized" : "Project Updated"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-zinc-400">
|
||||
{initStatus?.isNewProject
|
||||
? `Created .automaker directory structure for ${initStatus?.projectName}`
|
||||
: `Updated missing files in .automaker for ${initStatus?.projectName}`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-zinc-300 font-medium">Created files:</p>
|
||||
<ul className="space-y-1.5">
|
||||
{initStatus?.createdFiles.map((file) => (
|
||||
<li
|
||||
key={file}
|
||||
className="flex items-center gap-2 text-sm text-zinc-400"
|
||||
>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
||||
<code className="text-xs bg-zinc-800 px-2 py-0.5 rounded">
|
||||
{file}
|
||||
</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{initStatus?.isNewProject && (
|
||||
<div className="mt-4 p-3 rounded-lg bg-zinc-800/50 border border-white/5">
|
||||
<p className="text-sm text-zinc-400">
|
||||
<span className="text-brand-400">Tip:</span> Edit the{" "}
|
||||
<code className="text-xs bg-zinc-800 px-1.5 py-0.5 rounded">
|
||||
app_spec.txt
|
||||
</code>{" "}
|
||||
file to describe your project. The AI agent will use this to
|
||||
understand your project structure.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => setShowInitDialog(false)}
|
||||
className="bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
|
||||
data-testid="close-init-dialog"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Loading overlay when opening project */}
|
||||
{isOpening && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
data-testid="project-opening-overlay"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-zinc-900 border border-white/10">
|
||||
<Loader2 className="w-8 h-8 text-brand-500 animate-spin" />
|
||||
<p className="text-white font-medium">Initializing project...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -281,9 +281,18 @@ export const getElectronAPI = (): ElectronAPI => {
|
||||
},
|
||||
|
||||
exists: async (filePath: string) => {
|
||||
return mockFileSystem[filePath] !== undefined ||
|
||||
filePath.endsWith("feature_list.json") ||
|
||||
filePath.endsWith("app_spec.txt");
|
||||
// Check if file exists in mock file system (including newly created files)
|
||||
if (mockFileSystem[filePath] !== undefined) {
|
||||
return true;
|
||||
}
|
||||
// Legacy mock files for backwards compatibility
|
||||
if (filePath.endsWith("feature_list.json") && !filePath.includes(".automaker")) {
|
||||
return true;
|
||||
}
|
||||
if (filePath.endsWith("app_spec.txt") && !filePath.includes(".automaker")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
stat: async () => {
|
||||
|
||||
208
app/src/lib/project-init.ts
Normal file
208
app/src/lib/project-init.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Project initialization utilities
|
||||
*
|
||||
* Handles the setup of the .automaker directory structure when opening
|
||||
* new or existing projects.
|
||||
*/
|
||||
|
||||
import { getElectronAPI } from "./electron";
|
||||
|
||||
export interface ProjectInitResult {
|
||||
success: boolean;
|
||||
isNewProject: boolean;
|
||||
error?: string;
|
||||
createdFiles?: string[];
|
||||
existingFiles?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Default app_spec.txt template for new projects
|
||||
*/
|
||||
const DEFAULT_APP_SPEC = `<project_specification>
|
||||
<project_name>Untitled Project</project_name>
|
||||
|
||||
<overview>
|
||||
Describe your project here. This file will be analyzed by an AI agent
|
||||
to understand your project structure and tech stack.
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
<!-- The AI agent will fill this in after analyzing your project -->
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
<!-- List core features and capabilities -->
|
||||
</core_capabilities>
|
||||
|
||||
<implemented_features>
|
||||
<!-- The AI agent will populate this based on code analysis -->
|
||||
</implemented_features>
|
||||
</project_specification>
|
||||
`;
|
||||
|
||||
/**
|
||||
* Default feature_list.json template for new projects
|
||||
*/
|
||||
const DEFAULT_FEATURE_LIST = JSON.stringify([], null, 2);
|
||||
|
||||
/**
|
||||
* Default coding_prompt.md template for new projects
|
||||
*/
|
||||
const DEFAULT_CODING_PROMPT = `# Coding Guidelines
|
||||
|
||||
This file contains project-specific coding guidelines and conventions
|
||||
that the AI agent should follow when implementing features.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Follow existing code conventions in the project
|
||||
- Use consistent formatting and naming conventions
|
||||
- Add appropriate comments for complex logic
|
||||
|
||||
## Testing
|
||||
|
||||
- Write tests for new features when applicable
|
||||
- Ensure existing tests pass before marking features complete
|
||||
|
||||
## Git Commits
|
||||
|
||||
- Use clear, descriptive commit messages
|
||||
- Reference feature IDs when relevant
|
||||
|
||||
## Additional Notes
|
||||
|
||||
Add any project-specific guidelines here.
|
||||
`;
|
||||
|
||||
/**
|
||||
* Required files and directories in the .automaker directory
|
||||
*/
|
||||
const REQUIRED_STRUCTURE = {
|
||||
directories: [
|
||||
".automaker",
|
||||
".automaker/context",
|
||||
".automaker/agents-context",
|
||||
],
|
||||
files: {
|
||||
".automaker/app_spec.txt": DEFAULT_APP_SPEC,
|
||||
".automaker/feature_list.json": DEFAULT_FEATURE_LIST,
|
||||
".automaker/coding_prompt.md": DEFAULT_CODING_PROMPT,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the .automaker directory structure for a project
|
||||
*
|
||||
* @param projectPath - The root path of the project
|
||||
* @returns Result indicating what was created or if the project was already initialized
|
||||
*/
|
||||
export async function initializeProject(projectPath: string): Promise<ProjectInitResult> {
|
||||
const api = getElectronAPI();
|
||||
const createdFiles: string[] = [];
|
||||
const existingFiles: string[] = [];
|
||||
|
||||
try {
|
||||
// Create all required directories
|
||||
for (const dir of REQUIRED_STRUCTURE.directories) {
|
||||
const fullPath = `${projectPath}/${dir}`;
|
||||
await api.mkdir(fullPath);
|
||||
}
|
||||
|
||||
// Check and create required files
|
||||
for (const [relativePath, defaultContent] of Object.entries(REQUIRED_STRUCTURE.files)) {
|
||||
const fullPath = `${projectPath}/${relativePath}`;
|
||||
const exists = await api.exists(fullPath);
|
||||
|
||||
if (!exists) {
|
||||
await api.writeFile(fullPath, defaultContent);
|
||||
createdFiles.push(relativePath);
|
||||
} else {
|
||||
existingFiles.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this is a new project (all files were created)
|
||||
const isNewProject = createdFiles.length === Object.keys(REQUIRED_STRUCTURE.files).length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
isNewProject,
|
||||
createdFiles,
|
||||
existingFiles,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[project-init] Failed to initialize project:", error);
|
||||
return {
|
||||
success: false,
|
||||
isNewProject: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error occurred",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a project has the required .automaker structure
|
||||
*
|
||||
* @param projectPath - The root path of the project
|
||||
* @returns true if all required files/directories exist
|
||||
*/
|
||||
export async function isProjectInitialized(projectPath: string): Promise<boolean> {
|
||||
const api = getElectronAPI();
|
||||
|
||||
try {
|
||||
// Check all required files exist
|
||||
for (const relativePath of Object.keys(REQUIRED_STRUCTURE.files)) {
|
||||
const fullPath = `${projectPath}/${relativePath}`;
|
||||
const exists = await api.exists(fullPath);
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[project-init] Error checking project initialization:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a summary of what needs to be initialized for a project
|
||||
*
|
||||
* @param projectPath - The root path of the project
|
||||
* @returns List of missing files/directories
|
||||
*/
|
||||
export async function getProjectInitStatus(projectPath: string): Promise<{
|
||||
initialized: boolean;
|
||||
missingFiles: string[];
|
||||
existingFiles: string[];
|
||||
}> {
|
||||
const api = getElectronAPI();
|
||||
const missingFiles: string[] = [];
|
||||
const existingFiles: string[] = [];
|
||||
|
||||
try {
|
||||
for (const relativePath of Object.keys(REQUIRED_STRUCTURE.files)) {
|
||||
const fullPath = `${projectPath}/${relativePath}`;
|
||||
const exists = await api.exists(fullPath);
|
||||
if (exists) {
|
||||
existingFiles.push(relativePath);
|
||||
} else {
|
||||
missingFiles.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
initialized: missingFiles.length === 0,
|
||||
missingFiles,
|
||||
existingFiles,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[project-init] Error getting project status:", error);
|
||||
return {
|
||||
initialized: false,
|
||||
missingFiles: Object.keys(REQUIRED_STRUCTURE.files),
|
||||
existingFiles: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user