mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
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:
680
app/electron/agent-service.js
Normal file
680
app/electron/agent-service.js
Normal 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();
|
||||
808
app/electron/auto-mode-service.js
Normal file
808
app/electron/auto-mode-service.js
Normal 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();
|
||||
@@ -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 };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user