Implement initial project structure and features for Automaker application, including environment setup, auto mode services, and session management. Update port configurations to 3007 and add new UI components for enhanced user interaction.

This commit is contained in:
Cody Seibert
2025-12-08 21:11:00 -05:00
parent 3c8e786f29
commit 9392422d35
67 changed files with 16275 additions and 696 deletions

View File

@@ -0,0 +1,680 @@
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const path = require("path");
const fs = require("fs/promises");
/**
* Agent Service - Runs Claude agents in the Electron main process
* This service survives Next.js restarts and maintains conversation state
*/
class AgentService {
constructor() {
this.sessions = new Map(); // sessionId -> { messages, isRunning, abortController }
this.stateDir = null; // Will be set when app is ready
}
/**
* Initialize the service with app data directory
*/
async initialize(appDataPath) {
this.stateDir = path.join(appDataPath, "agent-sessions");
this.metadataFile = path.join(appDataPath, "sessions-metadata.json");
await fs.mkdir(this.stateDir, { recursive: true });
console.log("[AgentService] Initialized with state dir:", this.stateDir);
}
/**
* Start or resume a conversation
*/
async startConversation({ sessionId, workingDirectory }) {
console.log("[AgentService] Starting conversation:", sessionId);
// Initialize session if it doesn't exist
if (!this.sessions.has(sessionId)) {
const messages = await this.loadSession(sessionId);
this.sessions.set(sessionId, {
messages,
isRunning: false,
abortController: null,
workingDirectory: workingDirectory || process.cwd(),
});
}
const session = this.sessions.get(sessionId);
return {
success: true,
messages: session.messages,
sessionId,
};
}
/**
* Send a message to the agent and stream responses
*/
async sendMessage({
sessionId,
message,
workingDirectory,
imagePaths,
sendToRenderer,
}) {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
if (session.isRunning) {
throw new Error("Agent is already processing a message");
}
// Read images from temp files and convert to base64 for storage
const images = [];
if (imagePaths && imagePaths.length > 0) {
const fs = require("fs/promises");
const path = require("path");
for (const imagePath of imagePaths) {
try {
const imageBuffer = await fs.readFile(imagePath);
const base64Data = imageBuffer.toString("base64");
// Determine media type from file extension
const ext = path.extname(imagePath).toLowerCase();
const mimeTypeMap = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || "image/png";
images.push({
data: base64Data,
mimeType: mediaType,
filename: path.basename(imagePath),
});
console.log(
`[AgentService] Loaded image from ${imagePath} for storage`
);
} catch (error) {
console.error(
`[AgentService] Failed to load image from ${imagePath}:`,
error
);
}
}
}
// Add user message to conversation with base64 images
const userMessage = {
id: this.generateId(),
role: "user",
content: message,
images: images.length > 0 ? images : undefined,
timestamp: new Date().toISOString(),
};
session.messages.push(userMessage);
session.isRunning = true;
session.abortController = new AbortController();
// Send initial user message to renderer
sendToRenderer({
type: "message",
message: userMessage,
});
// Save state with base64 images
await this.saveSession(sessionId, session.messages);
try {
// Configure Claude Agent SDK options
const options = {
// model: "claude-sonnet-4-20250514",
model: "claude-opus-4-5-20251101",
systemPrompt: this.getSystemPrompt(),
maxTurns: 20,
cwd: workingDirectory || session.workingDirectory,
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: session.abortController,
};
// Build prompt content with text and images
let promptContent = message;
// If there are images, create a content array
if (imagePaths && imagePaths.length > 0) {
const contentBlocks = [];
// Add text block
if (message && message.trim()) {
contentBlocks.push({
type: "text",
text: message,
});
}
// Add image blocks
const fs = require("fs");
for (const imagePath of imagePaths) {
try {
const imageBuffer = fs.readFileSync(imagePath);
const base64Data = imageBuffer.toString("base64");
const ext = path.extname(imagePath).toLowerCase();
const mimeTypeMap = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || "image/png";
contentBlocks.push({
type: "image",
source: {
type: "base64",
media_type: mediaType,
data: base64Data,
},
});
} catch (error) {
console.error(
`[AgentService] Failed to load image ${imagePath}:`,
error
);
}
}
// Use content blocks if we have images
if (
contentBlocks.length > 1 ||
(contentBlocks.length === 1 && contentBlocks[0].type === "image")
) {
promptContent = contentBlocks;
}
}
// Build payload for the SDK
const promptPayload = Array.isArray(promptContent)
? (async function* () {
yield {
type: "user",
session_id: "",
message: {
role: "user",
content: promptContent,
},
parent_tool_use_id: null,
};
})()
: promptContent;
// Send the query via the SDK (conversation state handled by the SDK)
const stream = query({ prompt: promptPayload, options });
let currentAssistantMessage = null;
let responseText = "";
const toolUses = [];
// Stream responses from the SDK
for await (const msg of stream) {
if (msg.type === "assistant") {
if (msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
// Create or update assistant message
if (!currentAssistantMessage) {
currentAssistantMessage = {
id: this.generateId(),
role: "assistant",
content: responseText,
timestamp: new Date().toISOString(),
};
session.messages.push(currentAssistantMessage);
} else {
currentAssistantMessage.content = responseText;
}
// Stream to renderer
sendToRenderer({
type: "stream",
messageId: currentAssistantMessage.id,
content: responseText,
isComplete: false,
});
} else if (block.type === "tool_use") {
const toolUse = {
name: block.name,
input: block.input,
};
toolUses.push(toolUse);
// Send tool use notification
sendToRenderer({
type: "tool_use",
tool: toolUse,
});
}
}
}
} else if (msg.type === "result") {
if (msg.subtype === "success" && msg.result) {
// Use the final result
if (currentAssistantMessage) {
currentAssistantMessage.content = msg.result;
responseText = msg.result;
}
}
// Send completion
sendToRenderer({
type: "complete",
messageId: currentAssistantMessage?.id,
content: responseText,
toolUses,
});
}
}
// Save final state
await this.saveSession(sessionId, session.messages);
session.isRunning = false;
session.abortController = null;
return {
success: true,
message: currentAssistantMessage,
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[AgentService] Query aborted");
session.isRunning = false;
session.abortController = null;
return { success: false, aborted: true };
}
console.error("[AgentService] Error:", error);
session.isRunning = false;
session.abortController = null;
// Add error message
const errorMessage = {
id: this.generateId(),
role: "assistant",
content: `Error: ${error.message}`,
timestamp: new Date().toISOString(),
isError: true,
};
session.messages.push(errorMessage);
await this.saveSession(sessionId, session.messages);
sendToRenderer({
type: "error",
error: error.message,
message: errorMessage,
});
throw error;
}
}
/**
* Get conversation history
*/
getHistory(sessionId) {
const session = this.sessions.get(sessionId);
if (!session) {
return { success: false, error: "Session not found" };
}
return {
success: true,
messages: session.messages,
isRunning: session.isRunning,
};
}
/**
* Stop current agent execution
*/
async stopExecution(sessionId) {
const session = this.sessions.get(sessionId);
if (!session) {
return { success: false, error: "Session not found" };
}
if (session.abortController) {
session.abortController.abort();
session.isRunning = false;
session.abortController = null;
}
return { success: true };
}
/**
* Clear conversation history
*/
async clearSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.messages = [];
session.isRunning = false;
await this.saveSession(sessionId, []);
}
return { success: true };
}
/**
* Load session from disk
*/
async loadSession(sessionId) {
if (!this.stateDir) return [];
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
try {
const data = await fs.readFile(sessionFile, "utf-8");
const parsed = JSON.parse(data);
console.log(
`[AgentService] Loaded ${parsed.length} messages for ${sessionId}`
);
return parsed;
} catch (error) {
// Session doesn't exist yet
return [];
}
}
/**
* Save session to disk
*/
async saveSession(sessionId, messages) {
if (!this.stateDir) return;
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
try {
await fs.writeFile(
sessionFile,
JSON.stringify(messages, null, 2),
"utf-8"
);
console.log(
`[AgentService] Saved ${messages.length} messages for ${sessionId}`
);
// Update timestamp
await this.updateSessionTimestamp(sessionId);
} catch (error) {
console.error("[AgentService] Failed to save session:", error);
}
}
/**
* Get system prompt
*/
getSystemPrompt() {
return `You are an AI assistant helping users build software. You are part of the Automaker application,
which is designed to help developers plan, design, and implement software projects autonomously.
Your role is to:
- Help users define their project requirements and specifications
- Ask clarifying questions to better understand their needs
- Suggest technical approaches and architectures
- Guide them through the development process
- Be conversational and helpful
- Write, edit, and modify code files as requested
- Execute commands and tests
- Search and analyze the codebase
When discussing projects, help users think through:
- Core functionality and features
- Technical stack choices
- Data models and architecture
- User experience considerations
- Testing strategies
You have full access to the codebase and can:
- Read files to understand existing code
- Write new files
- Edit existing files
- Run bash commands
- Search for code patterns
- Execute tests and builds
IMPORTANT: When making file changes, be aware that the Next.js development server may restart.
This is normal and expected. Your conversation state is preserved across these restarts.`;
}
/**
* Generate unique ID
*/
generateId() {
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
}
// ============================================================================
// Session Management
// ============================================================================
/**
* Load all session metadata
*/
async loadMetadata() {
if (!this.metadataFile) return {};
try {
const data = await fs.readFile(this.metadataFile, "utf-8");
return JSON.parse(data);
} catch (error) {
return {};
}
}
/**
* Save session metadata
*/
async saveMetadata(metadata) {
if (!this.metadataFile) return;
try {
await fs.writeFile(
this.metadataFile,
JSON.stringify(metadata, null, 2),
"utf-8"
);
} catch (error) {
console.error("[AgentService] Failed to save metadata:", error);
}
}
/**
* List all sessions
*/
async listSessions({ includeArchived = false } = {}) {
const metadata = await this.loadMetadata();
const sessions = [];
for (const [sessionId, meta] of Object.entries(metadata)) {
if (!includeArchived && meta.isArchived) continue;
const messages = await this.loadSession(sessionId);
const lastMessage = messages[messages.length - 1];
sessions.push({
id: sessionId,
name: meta.name || sessionId,
projectPath: meta.projectPath || "",
createdAt: meta.createdAt,
updatedAt: meta.updatedAt,
messageCount: messages.length,
isArchived: meta.isArchived || false,
tags: meta.tags || [],
preview: lastMessage?.content.substring(0, 100) || "",
});
}
// Sort by most recently updated
sessions.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
return sessions;
}
/**
* Create a new session
*/
async createSession({ name, projectPath, workingDirectory }) {
const sessionId = `session_${Date.now()}_${Math.random()
.toString(36)
.substring(2, 11)}`;
const metadata = await this.loadMetadata();
metadata[sessionId] = {
name,
projectPath,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
isArchived: false,
tags: [],
};
await this.saveMetadata(metadata);
this.sessions.set(sessionId, {
messages: [],
isRunning: false,
abortController: null,
workingDirectory: workingDirectory || projectPath,
});
await this.saveSession(sessionId, []);
return {
success: true,
sessionId,
session: metadata[sessionId],
};
}
/**
* Update session metadata
*/
async updateSession({ sessionId, name, tags }) {
const metadata = await this.loadMetadata();
if (!metadata[sessionId]) {
return { success: false, error: "Session not found" };
}
if (name !== undefined) metadata[sessionId].name = name;
if (tags !== undefined) metadata[sessionId].tags = tags;
metadata[sessionId].updatedAt = new Date().toISOString();
await this.saveMetadata(metadata);
return { success: true };
}
/**
* Archive a session
*/
async archiveSession(sessionId) {
const metadata = await this.loadMetadata();
if (!metadata[sessionId]) {
return { success: false, error: "Session not found" };
}
metadata[sessionId].isArchived = true;
metadata[sessionId].updatedAt = new Date().toISOString();
await this.saveMetadata(metadata);
return { success: true };
}
/**
* Unarchive a session
*/
async unarchiveSession(sessionId) {
const metadata = await this.loadMetadata();
if (!metadata[sessionId]) {
return { success: false, error: "Session not found" };
}
metadata[sessionId].isArchived = false;
metadata[sessionId].updatedAt = new Date().toISOString();
await this.saveMetadata(metadata);
return { success: true };
}
/**
* Delete a session permanently
*/
async deleteSession(sessionId) {
const metadata = await this.loadMetadata();
if (!metadata[sessionId]) {
return { success: false, error: "Session not found" };
}
// Remove from metadata
delete metadata[sessionId];
await this.saveMetadata(metadata);
// Remove from memory
this.sessions.delete(sessionId);
// Delete session file
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
try {
await fs.unlink(sessionFile);
} catch (error) {
console.warn("[AgentService] Failed to delete session file:", error);
}
return { success: true };
}
/**
* Update session metadata when messages change
*/
async updateSessionTimestamp(sessionId) {
const metadata = await this.loadMetadata();
if (metadata[sessionId]) {
metadata[sessionId].updatedAt = new Date().toISOString();
await this.saveMetadata(metadata);
}
}
}
// Export singleton instance
module.exports = new AgentService();

View File

@@ -0,0 +1,808 @@
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const path = require("path");
const fs = require("fs/promises");
/**
* Auto Mode Service - Autonomous feature implementation
* Automatically picks and implements features from the kanban board
*/
class AutoModeService {
constructor() {
this.isRunning = false;
this.currentFeatureId = null;
this.abortController = null;
this.currentQuery = null;
this.projectPath = null;
this.sendToRenderer = null;
}
/**
* Start auto mode - continuously implement features
*/
async start({ projectPath, sendToRenderer }) {
if (this.isRunning) {
throw new Error("Auto mode is already running");
}
this.isRunning = true;
this.projectPath = projectPath;
this.sendToRenderer = sendToRenderer;
console.log("[AutoMode] Starting auto mode for project:", projectPath);
// Run the autonomous loop
this.runLoop().catch((error) => {
console.error("[AutoMode] Loop error:", error);
this.stop();
});
return { success: true };
}
/**
* Stop auto mode
*/
async stop() {
console.log("[AutoMode] Stopping auto mode");
this.isRunning = false;
// Abort current agent execution
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
this.currentQuery = null;
this.currentFeatureId = null;
this.projectPath = null;
this.sendToRenderer = null;
return { success: true };
}
/**
* Get status of auto mode
*/
getStatus() {
return {
isRunning: this.isRunning,
currentFeatureId: this.currentFeatureId,
};
}
/**
* Run a specific feature by ID
*/
async runFeature({ projectPath, featureId, sendToRenderer }) {
if (this.isRunning) {
throw new Error("Auto mode is already running");
}
this.isRunning = true;
this.projectPath = projectPath;
this.sendToRenderer = sendToRenderer;
console.log(`[AutoMode] Running specific feature: ${featureId}`);
try {
// Load features
const features = await this.loadFeatures();
const feature = features.find(f => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
console.log(`[AutoMode] Running feature: ${feature.description}`);
this.currentFeatureId = feature.id;
// Update feature status to in_progress
await this.updateFeatureStatus(featureId, "in_progress");
this.sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: feature,
});
// Implement the feature
const result = await this.implementFeature(feature);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "backlog";
await this.updateFeatureStatus(feature.id, newStatus);
this.sendToRenderer({
type: "auto_mode_feature_complete",
featureId: feature.id,
passes: result.passes,
message: result.message,
});
return { success: true, passes: result.passes };
} catch (error) {
console.error("[AutoMode] Error running feature:", error);
this.sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: this.currentFeatureId,
});
throw error;
} finally {
this.isRunning = false;
this.currentFeatureId = null;
this.projectPath = null;
this.sendToRenderer = null;
}
}
/**
* Verify a specific feature by running its tests
*/
async verifyFeature({ projectPath, featureId, sendToRenderer }) {
console.log(`[AutoMode] verifyFeature called with:`, { projectPath, featureId });
if (this.isRunning) {
throw new Error("Auto mode is already running");
}
this.isRunning = true;
this.projectPath = projectPath;
this.sendToRenderer = sendToRenderer;
console.log(`[AutoMode] Verifying feature: ${featureId}`);
try {
// Load features
const features = await this.loadFeatures();
const feature = features.find(f => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
console.log(`[AutoMode] Verifying feature: ${feature.description}`);
this.currentFeatureId = feature.id;
this.sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: feature,
});
// Verify the feature by running tests
const result = await this.verifyFeatureTests(feature);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "in_progress";
await this.updateFeatureStatus(featureId, newStatus);
this.sendToRenderer({
type: "auto_mode_feature_complete",
featureId: feature.id,
passes: result.passes,
message: result.message,
});
return { success: true, passes: result.passes };
} catch (error) {
console.error("[AutoMode] Error verifying feature:", error);
this.sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: this.currentFeatureId,
});
throw error;
} finally {
this.isRunning = false;
this.currentFeatureId = null;
this.projectPath = null;
this.sendToRenderer = null;
}
}
/**
* Main autonomous loop - picks and implements features
*/
async runLoop() {
while (this.isRunning) {
try {
// Load features from feature_list.json
const features = await this.loadFeatures();
// Find highest priority incomplete feature
const nextFeature = this.selectNextFeature(features);
if (!nextFeature) {
console.log("[AutoMode] No more features to implement");
this.sendToRenderer({
type: "auto_mode_complete",
message: "All features completed!",
});
break;
}
console.log(`[AutoMode] Selected feature: ${nextFeature.description}`);
this.currentFeatureId = nextFeature.id;
this.sendToRenderer({
type: "auto_mode_feature_start",
featureId: nextFeature.id,
feature: nextFeature,
});
// Implement the feature
const result = await this.implementFeature(nextFeature);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "backlog";
await this.updateFeatureStatus(nextFeature.id, newStatus);
this.sendToRenderer({
type: "auto_mode_feature_complete",
featureId: nextFeature.id,
passes: result.passes,
message: result.message,
});
// Small delay before next feature
if (this.isRunning) {
await this.sleep(3000);
}
} catch (error) {
console.error("[AutoMode] Error in loop iteration:", error);
this.sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: this.currentFeatureId,
});
// Wait before retrying
await this.sleep(5000);
}
}
console.log("[AutoMode] Loop ended");
this.isRunning = false;
}
/**
* Load features from feature_list.json
*/
async loadFeatures() {
const featuresPath = path.join(this.projectPath, ".automaker", "feature_list.json");
try {
const content = await fs.readFile(featuresPath, "utf-8");
const features = JSON.parse(content);
// Ensure each feature has an ID
return features.map((f, index) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
}));
} catch (error) {
console.error("[AutoMode] Failed to load features:", error);
return [];
}
}
/**
* Select the next feature to implement
* Prioritizes: earlier features in the list that are not verified
*/
selectNextFeature(features) {
// Find first feature that is in backlog or in_progress status
return features.find((f) => f.status !== "verified");
}
/**
* Write output to feature context file
*/
async writeToContextFile(featureId, content) {
if (!this.projectPath) return;
try {
const contextDir = path.join(this.projectPath, ".automaker", "context");
// Ensure directory exists
try {
await fs.access(contextDir);
} catch {
await fs.mkdir(contextDir, { recursive: true });
}
const filePath = path.join(contextDir, `${featureId}.md`);
// Append to existing file or create new one
try {
const existing = await fs.readFile(filePath, "utf-8");
await fs.writeFile(filePath, existing + content, "utf-8");
} catch {
await fs.writeFile(filePath, content, "utf-8");
}
} catch (error) {
console.error("[AutoMode] Failed to write to context file:", error);
}
}
/**
* Implement a single feature using Claude Agent SDK
* Uses a Plan-Act-Verify loop with detailed phase logging
*/
async implementFeature(feature) {
console.log(`[AutoMode] Implementing: ${feature.description}`);
try {
// ========================================
// PHASE 1: PLANNING
// ========================================
const planningMessage = `📋 Planning implementation for: ${feature.description}\n`;
await this.writeToContextFile(feature.id, planningMessage);
this.sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "planning",
message: `Planning implementation for: ${feature.description}`,
});
console.log(`[AutoMode] Phase: PLANNING for ${feature.description}`);
this.abortController = new AbortController();
// Configure options for the SDK query
const options = {
model: "claude-opus-4-5-20251101",
systemPrompt: this.getCodingPrompt(),
maxTurns: 30,
cwd: this.projectPath,
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: this.abortController,
};
// Build the prompt for this specific feature
const prompt = this.buildFeaturePrompt(feature);
// Planning: Analyze the codebase and create implementation plan
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: "Analyzing codebase structure and creating implementation plan...",
});
// Small delay to show planning phase
await this.sleep(500);
// ========================================
// PHASE 2: ACTION
// ========================================
const actionMessage = `⚡ Executing implementation for: ${feature.description}\n`;
await this.writeToContextFile(feature.id, actionMessage);
this.sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "action",
message: `Executing implementation for: ${feature.description}`,
});
console.log(`[AutoMode] Phase: ACTION for ${feature.description}`);
// Send query
this.currentQuery = query({ prompt, options });
// Stream responses
let responseText = "";
let hasStartedToolUse = false;
for await (const msg of this.currentQuery) {
if (!this.isRunning) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
// Write to context file
await this.writeToContextFile(feature.id, block.text);
// Stream progress to renderer
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: block.text,
});
} else if (block.type === "tool_use") {
// First tool use indicates we're actively implementing
if (!hasStartedToolUse) {
hasStartedToolUse = true;
const startMsg = "Starting code implementation...\n";
await this.writeToContextFile(feature.id, startMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: startMsg,
});
}
// Write tool use to context file
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
await this.writeToContextFile(feature.id, toolMsg);
// Notify about tool use
this.sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
this.currentQuery = null;
this.abortController = null;
// ========================================
// PHASE 3: VERIFICATION
// ========================================
const verificationMessage = `✅ Verifying implementation for: ${feature.description}\n`;
await this.writeToContextFile(feature.id, verificationMessage);
this.sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "verification",
message: `Verifying implementation for: ${feature.description}`,
});
console.log(`[AutoMode] Phase: VERIFICATION for ${feature.description}`);
const checkingMsg = "Verifying implementation and checking test results...\n";
await this.writeToContextFile(feature.id, checkingMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: checkingMsg,
});
// Re-load features to check if it was marked as verified
const updatedFeatures = await this.loadFeatures();
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
const passes = updatedFeature?.status === "verified";
// Send verification result
const resultMsg = passes
? "✓ Verification successful: All tests passed\n"
: "✗ Verification: Tests need attention\n";
await this.writeToContextFile(feature.id, resultMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: resultMsg,
});
return {
passes,
message: responseText.substring(0, 500), // First 500 chars
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[AutoMode] Feature run aborted");
this.abortController = null;
this.currentQuery = null;
return {
passes: false,
message: "Auto mode aborted",
};
}
console.error("[AutoMode] Error implementing feature:", error);
// Clean up
this.abortController = null;
this.currentQuery = null;
throw error;
}
}
/**
* Update feature status in feature_list.json
*/
async updateFeatureStatus(featureId, status) {
const features = await this.loadFeatures();
const feature = features.find(f => f.id === featureId);
if (!feature) {
console.error(`[AutoMode] Feature ${featureId} not found`);
return;
}
// Update the status field
feature.status = status;
// Save back to file
const featuresPath = path.join(this.projectPath, ".automaker", "feature_list.json");
const toSave = features.map((f) => ({
id: f.id,
category: f.category,
description: f.description,
steps: f.steps,
status: f.status,
}));
await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8");
console.log(`[AutoMode] Updated feature ${featureId}: status=${status}`);
}
/**
* Verify feature tests (runs tests and checks if they pass)
*/
async verifyFeatureTests(feature) {
console.log(`[AutoMode] Verifying tests for: ${feature.description}`);
try {
const verifyMsg = `\n✅ Verifying tests for: ${feature.description}\n`;
await this.writeToContextFile(feature.id, verifyMsg);
this.sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "verification",
message: `Verifying tests for: ${feature.description}`,
});
this.abortController = new AbortController();
const options = {
model: "claude-opus-4-5-20251101",
systemPrompt: this.getVerificationPrompt(),
maxTurns: 15,
cwd: this.projectPath,
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: this.abortController,
};
const prompt = this.buildVerificationPrompt(feature);
const runningTestsMsg = "Running Playwright tests to verify feature implementation...\n";
await this.writeToContextFile(feature.id, runningTestsMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: runningTestsMsg,
});
this.currentQuery = query({ prompt, options });
let responseText = "";
for await (const msg of this.currentQuery) {
if (!this.isRunning) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
await this.writeToContextFile(feature.id, block.text);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: block.text,
});
} else if (block.type === "tool_use") {
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
await this.writeToContextFile(feature.id, toolMsg);
this.sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
this.currentQuery = null;
this.abortController = null;
// Re-load features to check if it was marked as verified
const updatedFeatures = await this.loadFeatures();
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
const passes = updatedFeature?.status === "verified";
const finalMsg = passes
? "✓ Verification successful: All tests passed\n"
: "✗ Tests failed or not all passing - feature remains in progress\n";
await this.writeToContextFile(feature.id, finalMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: finalMsg,
});
return {
passes,
message: responseText.substring(0, 500),
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[AutoMode] Verification aborted");
this.abortController = null;
this.currentQuery = null;
return {
passes: false,
message: "Verification aborted",
};
}
console.error("[AutoMode] Error verifying feature:", error);
this.abortController = null;
this.currentQuery = null;
throw error;
}
}
/**
* Build the prompt for implementing a specific feature
*/
buildFeaturePrompt(feature) {
return `You are working on a feature implementation task.
**Current Feature to Implement:**
Category: ${feature.category}
Description: ${feature.description}
**Steps to Complete:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Your Task:**
1. Read the project files to understand the current codebase structure
2. Implement the feature according to the description and steps
3. Write Playwright tests to verify the feature works correctly
4. Run the tests and ensure they pass
5. Update feature_list.json to mark this feature as "status": "verified"
6. Commit your changes with git
**Important Guidelines:**
- Focus ONLY on implementing this specific feature
- Write clean, production-quality code
- Add proper error handling
- Write comprehensive Playwright tests
- Ensure all existing tests still pass
- Mark the feature as passing only when all tests are green
- Make a git commit when complete
Begin by reading the project structure and then implementing the feature.`;
}
/**
* Build the prompt for verifying a specific feature
*/
buildVerificationPrompt(feature) {
return `You are verifying that a feature implementation is complete and working correctly.
**Feature to Verify:**
ID: ${feature.id}
Category: ${feature.category}
Description: ${feature.description}
Current Status: ${feature.status}
**Steps that should be implemented:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Your Task:**
1. Read the feature_list.json file to see the current status
2. Look for Playwright tests related to this feature
3. Run the Playwright tests for this feature: npx playwright test
4. Check if all tests pass
5. If ALL tests pass:
- Update feature_list.json to set this feature's "status" to "verified"
- Explain what tests passed
6. If ANY tests fail:
- Keep the feature "status" as "in_progress" in feature_list.json
- Explain what tests failed and why
**Important:**
- Only mark as "verified" if ALL Playwright tests pass
- Do NOT implement new code - only verify existing implementation
- Focus on running tests and updating the status accurately
- Be thorough in checking test results
Begin by reading feature_list.json and finding the appropriate tests to run.`;
}
/**
* Get the system prompt for verification agent
*/
getVerificationPrompt() {
return `You are an AI verification agent focused on testing and validation.
Your role is to:
- Run Playwright tests to verify feature implementations
- Accurately report test results
- Update feature status in feature_list.json based on test outcomes
- Only mark features as "verified" when ALL tests pass
- Keep features as "in_progress" if tests fail
You have access to:
- Read and edit files
- Run bash commands (especially Playwright tests)
- Analyze test output
Be accurate and thorough in your verification process.`;
}
/**
* Get the system prompt for coding agent
*/
getCodingPrompt() {
return `You are an AI coding agent working autonomously to implement features.
Your role is to:
- Implement features exactly as specified
- Write production-quality code
- Create comprehensive Playwright tests
- Ensure all tests pass before marking features complete
- Commit working code to git
- Be thorough and detail-oriented
You have full access to:
- Read and write files
- Run bash commands
- Execute tests
- Make git commits
- Search and analyze the codebase
Focus on one feature at a time and complete it fully before finishing.`;
}
/**
* Sleep helper
*/
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Export singleton instance
module.exports = new AutoModeService();

View File

@@ -1,6 +1,13 @@
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const path = require("path");
// Load environment variables from .env file
require("dotenv").config({ path: path.join(__dirname, "../.env") });
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const fs = require("fs/promises");
const os = require("os");
const agentService = require("./agent-service");
const autoModeService = require("./auto-mode-service");
let mainWindow = null;
@@ -22,7 +29,7 @@ function createWindow() {
// Load Next.js dev server in development or production build
const isDev = !app.isPackaged;
if (isDev) {
mainWindow.loadURL("http://localhost:3000");
mainWindow.loadURL("http://localhost:3007");
// mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
@@ -33,7 +40,11 @@ function createWindow() {
});
}
app.whenReady().then(() => {
app.whenReady().then(async () => {
// Initialize agent service
const appDataPath = app.getPath("userData");
await agentService.initialize(appDataPath);
createWindow();
app.on("activate", () => {
@@ -140,7 +151,286 @@ ipcMain.handle("app:getPath", (_, name) => {
return app.getPath(name);
});
// Save image to temp directory
ipcMain.handle("app:saveImageToTemp", async (_, { data, filename, mimeType }) => {
try {
// Create temp directory for images if it doesn't exist
const tempDir = path.join(os.tmpdir(), "automaker-images");
await fs.mkdir(tempDir, { recursive: true });
// Generate unique filename
const timestamp = Date.now();
const ext = mimeType.split("/")[1] || "png";
const safeName = filename.replace(/[^a-zA-Z0-9.-]/g, "_");
const tempFilePath = path.join(tempDir, `${timestamp}_${safeName}`);
// Remove data URL prefix if present (data:image/png;base64,...)
const base64Data = data.includes(",") ? data.split(",")[1] : data;
// Write image to temp file
await fs.writeFile(tempFilePath, base64Data, "base64");
console.log("[IPC] Saved image to temp:", tempFilePath);
return { success: true, path: tempFilePath };
} catch (error) {
console.error("[IPC] Failed to save image to temp:", error);
return { success: false, error: error.message };
}
});
// IPC ping for testing communication
ipcMain.handle("ping", () => {
return "pong";
});
// ============================================================================
// Agent IPC Handlers
// ============================================================================
/**
* Start or resume a conversation session
*/
ipcMain.handle("agent:start", async (_, { sessionId, workingDirectory }) => {
try {
return await agentService.startConversation({ sessionId, workingDirectory });
} catch (error) {
console.error("[IPC] agent:start error:", error);
return { success: false, error: error.message };
}
});
/**
* Send a message to the agent - returns immediately, streams via events
*/
ipcMain.handle("agent:send", async (event, { sessionId, message, workingDirectory, imagePaths }) => {
try {
// Create a function to send updates to the renderer
const sendToRenderer = (data) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("agent:stream", {
sessionId,
...data,
});
}
};
// Start processing (runs in background)
agentService
.sendMessage({
sessionId,
message,
workingDirectory,
imagePaths,
sendToRenderer,
})
.catch((error) => {
console.error("[IPC] agent:send background error:", error);
sendToRenderer({
type: "error",
error: error.message,
});
});
// Return immediately
return { success: true };
} catch (error) {
console.error("[IPC] agent:send error:", error);
return { success: false, error: error.message };
}
});
/**
* Get conversation history
*/
ipcMain.handle("agent:getHistory", (_, { sessionId }) => {
try {
return agentService.getHistory(sessionId);
} catch (error) {
console.error("[IPC] agent:getHistory error:", error);
return { success: false, error: error.message };
}
});
/**
* Stop current agent execution
*/
ipcMain.handle("agent:stop", async (_, { sessionId }) => {
try {
return await agentService.stopExecution(sessionId);
} catch (error) {
console.error("[IPC] agent:stop error:", error);
return { success: false, error: error.message };
}
});
/**
* Clear conversation history
*/
ipcMain.handle("agent:clear", async (_, { sessionId }) => {
try {
return await agentService.clearSession(sessionId);
} catch (error) {
console.error("[IPC] agent:clear error:", error);
return { success: false, error: error.message };
}
});
// ============================================================================
// Session Management IPC Handlers
// ============================================================================
/**
* List all sessions
*/
ipcMain.handle("sessions:list", async (_, { includeArchived }) => {
try {
const sessions = await agentService.listSessions({ includeArchived });
return { success: true, sessions };
} catch (error) {
console.error("[IPC] sessions:list error:", error);
return { success: false, error: error.message };
}
});
/**
* Create a new session
*/
ipcMain.handle("sessions:create", async (_, { name, projectPath, workingDirectory }) => {
try {
return await agentService.createSession({ name, projectPath, workingDirectory });
} catch (error) {
console.error("[IPC] sessions:create error:", error);
return { success: false, error: error.message };
}
});
/**
* Update session metadata
*/
ipcMain.handle("sessions:update", async (_, { sessionId, name, tags }) => {
try {
return await agentService.updateSession({ sessionId, name, tags });
} catch (error) {
console.error("[IPC] sessions:update error:", error);
return { success: false, error: error.message };
}
});
/**
* Archive a session
*/
ipcMain.handle("sessions:archive", async (_, { sessionId }) => {
try {
return await agentService.archiveSession(sessionId);
} catch (error) {
console.error("[IPC] sessions:archive error:", error);
return { success: false, error: error.message };
}
});
/**
* Unarchive a session
*/
ipcMain.handle("sessions:unarchive", async (_, { sessionId }) => {
try {
return await agentService.unarchiveSession(sessionId);
} catch (error) {
console.error("[IPC] sessions:unarchive error:", error);
return { success: false, error: error.message };
}
});
/**
* Delete a session permanently
*/
ipcMain.handle("sessions:delete", async (_, { sessionId }) => {
try {
return await agentService.deleteSession(sessionId);
} catch (error) {
console.error("[IPC] sessions:delete error:", error);
return { success: false, error: error.message };
}
});
// ============================================================================
// Auto Mode IPC Handlers
// ============================================================================
/**
* Start auto mode - autonomous feature implementation
*/
ipcMain.handle("auto-mode:start", async (_, { projectPath }) => {
try {
const sendToRenderer = (data) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("auto-mode:event", data);
}
};
return await autoModeService.start({ projectPath, sendToRenderer });
} catch (error) {
console.error("[IPC] auto-mode:start error:", error);
return { success: false, error: error.message };
}
});
/**
* Stop auto mode
*/
ipcMain.handle("auto-mode:stop", async () => {
try {
return await autoModeService.stop();
} catch (error) {
console.error("[IPC] auto-mode:stop error:", error);
return { success: false, error: error.message };
}
});
/**
* Get auto mode status
*/
ipcMain.handle("auto-mode:status", () => {
try {
return { success: true, ...autoModeService.getStatus() };
} catch (error) {
console.error("[IPC] auto-mode:status error:", error);
return { success: false, error: error.message };
}
});
/**
* Run a specific feature
*/
ipcMain.handle("auto-mode:run-feature", async (_, { projectPath, featureId }) => {
try {
const sendToRenderer = (data) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("auto-mode:event", data);
}
};
return await autoModeService.runFeature({ projectPath, featureId, sendToRenderer });
} catch (error) {
console.error("[IPC] auto-mode:run-feature error:", error);
return { success: false, error: error.message };
}
});
/**
* Verify a specific feature by running its tests
*/
ipcMain.handle("auto-mode:verify-feature", async (_, { projectPath, featureId }) => {
console.log("[IPC] auto-mode:verify-feature called with:", { projectPath, featureId });
try {
const sendToRenderer = (data) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("auto-mode:event", data);
}
};
return await autoModeService.verifyFeature({ projectPath, featureId, sendToRenderer });
} catch (error) {
console.error("[IPC] auto-mode:verify-feature error:", error);
return { success: false, error: error.message };
}
});

View File

@@ -21,6 +21,98 @@ contextBridge.exposeInMainWorld("electronAPI", {
// App APIs
getPath: (name) => ipcRenderer.invoke("app:getPath", name),
saveImageToTemp: (data, filename, mimeType) =>
ipcRenderer.invoke("app:saveImageToTemp", { data, filename, mimeType }),
// Agent APIs
agent: {
// Start or resume a conversation
start: (sessionId, workingDirectory) =>
ipcRenderer.invoke("agent:start", { sessionId, workingDirectory }),
// Send a message to the agent
send: (sessionId, message, workingDirectory, imagePaths) =>
ipcRenderer.invoke("agent:send", { sessionId, message, workingDirectory, imagePaths }),
// Get conversation history
getHistory: (sessionId) =>
ipcRenderer.invoke("agent:getHistory", { sessionId }),
// Stop current execution
stop: (sessionId) =>
ipcRenderer.invoke("agent:stop", { sessionId }),
// Clear conversation
clear: (sessionId) =>
ipcRenderer.invoke("agent:clear", { sessionId }),
// Subscribe to streaming events
onStream: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("agent:stream", subscription);
// Return unsubscribe function
return () => ipcRenderer.removeListener("agent:stream", subscription);
},
},
// Session Management APIs
sessions: {
// List all sessions
list: (includeArchived) =>
ipcRenderer.invoke("sessions:list", { includeArchived }),
// Create a new session
create: (name, projectPath, workingDirectory) =>
ipcRenderer.invoke("sessions:create", { name, projectPath, workingDirectory }),
// Update session metadata
update: (sessionId, name, tags) =>
ipcRenderer.invoke("sessions:update", { sessionId, name, tags }),
// Archive a session
archive: (sessionId) =>
ipcRenderer.invoke("sessions:archive", { sessionId }),
// Unarchive a session
unarchive: (sessionId) =>
ipcRenderer.invoke("sessions:unarchive", { sessionId }),
// Delete a session permanently
delete: (sessionId) =>
ipcRenderer.invoke("sessions:delete", { sessionId }),
},
// Auto Mode API
autoMode: {
// Start auto mode
start: (projectPath) =>
ipcRenderer.invoke("auto-mode:start", { projectPath }),
// Stop auto mode
stop: () => ipcRenderer.invoke("auto-mode:stop"),
// Get auto mode status
status: () => ipcRenderer.invoke("auto-mode:status"),
// Run a specific feature
runFeature: (projectPath, featureId) =>
ipcRenderer.invoke("auto-mode:run-feature", { projectPath, featureId }),
// Verify a specific feature by running its tests
verifyFeature: (projectPath, featureId) =>
ipcRenderer.invoke("auto-mode:verify-feature", { projectPath, featureId }),
// Listen for auto mode events
onEvent: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("auto-mode:event", subscription);
// Return unsubscribe function
return () => {
ipcRenderer.removeListener("auto-mode:event", subscription);
};
},
},
});
// Also expose a flag to detect if we're in Electron