mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
Merge branch 'main' into feat/extend-models-support
This commit is contained in:
@@ -21,6 +21,9 @@ class AutoModeService {
|
||||
this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer }
|
||||
this.autoLoopRunning = false; // Separate flag for the auto loop
|
||||
this.autoLoopAbortController = null;
|
||||
this.autoLoopInterval = null; // Timer for periodic checking
|
||||
this.checkIntervalMs = 5000; // Check every 5 seconds
|
||||
this.maxConcurrency = 3; // Default max concurrency
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,20 +43,20 @@ class AutoModeService {
|
||||
/**
|
||||
* Start auto mode - continuously implement features
|
||||
*/
|
||||
async start({ projectPath, sendToRenderer }) {
|
||||
async start({ projectPath, sendToRenderer, maxConcurrency }) {
|
||||
if (this.autoLoopRunning) {
|
||||
throw new Error("Auto mode loop is already running");
|
||||
}
|
||||
|
||||
this.autoLoopRunning = true;
|
||||
this.maxConcurrency = maxConcurrency || 3;
|
||||
|
||||
console.log("[AutoMode] Starting auto mode for project:", projectPath);
|
||||
console.log(
|
||||
`[AutoMode] Starting auto mode for project: ${projectPath} with max concurrency: ${this.maxConcurrency}`
|
||||
);
|
||||
|
||||
// Run the autonomous loop
|
||||
this.runLoop(projectPath, sendToRenderer).catch((error) => {
|
||||
console.error("[AutoMode] Loop error:", error);
|
||||
this.stop();
|
||||
});
|
||||
// Start the periodic checking loop
|
||||
this.runPeriodicLoop(projectPath, sendToRenderer);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
@@ -66,6 +69,12 @@ class AutoModeService {
|
||||
|
||||
this.autoLoopRunning = false;
|
||||
|
||||
// Clear the interval timer
|
||||
if (this.autoLoopInterval) {
|
||||
clearInterval(this.autoLoopInterval);
|
||||
this.autoLoopInterval = null;
|
||||
}
|
||||
|
||||
// Abort auto loop if running
|
||||
if (this.autoLoopAbortController) {
|
||||
this.autoLoopAbortController.abort();
|
||||
@@ -160,10 +169,7 @@ class AutoModeService {
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file only if verified (not for waiting_approval)
|
||||
if (newStatus === "verified") {
|
||||
await contextManager.deleteContextFile(projectPath, feature.id);
|
||||
}
|
||||
// Keep context file for viewing output later (deleted only when card is removed)
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_complete",
|
||||
@@ -242,10 +248,7 @@ class AutoModeService {
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file if verified
|
||||
if (newStatus === "verified") {
|
||||
await contextManager.deleteContextFile(projectPath, featureId);
|
||||
}
|
||||
// Keep context file for viewing output later (deleted only when card is removed)
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_complete",
|
||||
@@ -385,10 +388,7 @@ class AutoModeService {
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file only if verified (not for waiting_approval)
|
||||
if (newStatus === "verified") {
|
||||
await contextManager.deleteContextFile(projectPath, featureId);
|
||||
}
|
||||
// Keep context file for viewing output later (deleted only when card is removed)
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_complete",
|
||||
@@ -413,114 +413,146 @@ class AutoModeService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Main autonomous loop - picks and implements features
|
||||
* New periodic loop - checks available slots and starts features up to max concurrency
|
||||
* This loop continues running even if there are no backlog items
|
||||
*/
|
||||
async runLoop(projectPath, sendToRenderer) {
|
||||
while (this.autoLoopRunning) {
|
||||
let currentFeatureId = null;
|
||||
try {
|
||||
// Load features from .automaker/feature_list.json
|
||||
const features = await featureLoader.loadFeatures(projectPath);
|
||||
runPeriodicLoop(projectPath, sendToRenderer) {
|
||||
console.log(
|
||||
`[AutoMode] Starting periodic loop with interval: ${this.checkIntervalMs}ms`
|
||||
);
|
||||
|
||||
// Find highest priority incomplete feature
|
||||
const nextFeature = featureLoader.selectNextFeature(features);
|
||||
// Initial check immediately
|
||||
this.checkAndStartFeatures(projectPath, sendToRenderer);
|
||||
|
||||
if (!nextFeature) {
|
||||
console.log("[AutoMode] No more features to implement");
|
||||
sendToRenderer({
|
||||
type: "auto_mode_complete",
|
||||
message: "All features completed!",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
currentFeatureId = nextFeature.id;
|
||||
|
||||
// Skip if this feature is already running (via manual trigger)
|
||||
if (this.runningFeatures.has(currentFeatureId)) {
|
||||
console.log(
|
||||
`[AutoMode] Skipping ${currentFeatureId} - already running`
|
||||
);
|
||||
await this.sleep(3000);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[AutoMode] Selected feature: ${nextFeature.description}`);
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_start",
|
||||
featureId: nextFeature.id,
|
||||
feature: nextFeature,
|
||||
});
|
||||
|
||||
// Register this feature as running
|
||||
const execution = this.createExecutionContext(currentFeatureId);
|
||||
execution.projectPath = projectPath;
|
||||
execution.sendToRenderer = sendToRenderer;
|
||||
this.runningFeatures.set(currentFeatureId, execution);
|
||||
|
||||
// Implement the feature
|
||||
const result = await featureExecutor.implementFeature(
|
||||
nextFeature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
execution
|
||||
);
|
||||
|
||||
// Update feature status based on result
|
||||
// For skipTests features, go to waiting_approval on success instead of verified
|
||||
let newStatus;
|
||||
if (result.passes) {
|
||||
newStatus = nextFeature.skipTests ? "waiting_approval" : "verified";
|
||||
} else {
|
||||
newStatus = "backlog";
|
||||
}
|
||||
await featureLoader.updateFeatureStatus(
|
||||
nextFeature.id,
|
||||
newStatus,
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file only if verified (not for waiting_approval)
|
||||
if (newStatus === "verified") {
|
||||
await contextManager.deleteContextFile(projectPath, nextFeature.id);
|
||||
}
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_complete",
|
||||
featureId: nextFeature.id,
|
||||
passes: result.passes,
|
||||
message: result.message,
|
||||
});
|
||||
|
||||
// Clean up
|
||||
this.runningFeatures.delete(currentFeatureId);
|
||||
|
||||
// Small delay before next feature
|
||||
if (this.autoLoopRunning) {
|
||||
await this.sleep(3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Error in loop iteration:", error);
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_error",
|
||||
error: error.message,
|
||||
featureId: currentFeatureId,
|
||||
});
|
||||
|
||||
// Clean up on error
|
||||
if (currentFeatureId) {
|
||||
this.runningFeatures.delete(currentFeatureId);
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
await this.sleep(5000);
|
||||
// Then check periodically
|
||||
this.autoLoopInterval = setInterval(() => {
|
||||
if (this.autoLoopRunning) {
|
||||
this.checkAndStartFeatures(projectPath, sendToRenderer);
|
||||
}
|
||||
}, this.checkIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check how many features are running and start new ones if under max concurrency
|
||||
*/
|
||||
async checkAndStartFeatures(projectPath, sendToRenderer) {
|
||||
try {
|
||||
// Check how many are currently running
|
||||
const currentRunningCount = this.runningFeatures.size;
|
||||
|
||||
console.log(
|
||||
`[AutoMode] Checking features - Running: ${currentRunningCount}/${this.maxConcurrency}`
|
||||
);
|
||||
|
||||
// Calculate available slots
|
||||
const availableSlots = this.maxConcurrency - currentRunningCount;
|
||||
|
||||
if (availableSlots <= 0) {
|
||||
console.log("[AutoMode] At max concurrency, waiting...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load features from backlog
|
||||
const features = await featureLoader.loadFeatures(projectPath);
|
||||
const backlogFeatures = features.filter((f) => f.status === "backlog");
|
||||
|
||||
if (backlogFeatures.length === 0) {
|
||||
console.log("[AutoMode] No backlog features available, waiting...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Grab up to availableSlots features from backlog
|
||||
const featuresToStart = backlogFeatures.slice(0, availableSlots);
|
||||
|
||||
console.log(
|
||||
`[AutoMode] Starting ${featuresToStart.length} feature(s) from backlog`
|
||||
);
|
||||
|
||||
// Start each feature (don't await - run in parallel like drag operations)
|
||||
for (const feature of featuresToStart) {
|
||||
this.startFeatureAsync(feature, projectPath, sendToRenderer);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Error checking/starting features:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a feature asynchronously (similar to drag operation)
|
||||
*/
|
||||
async startFeatureAsync(feature, projectPath, sendToRenderer) {
|
||||
const featureId = feature.id;
|
||||
|
||||
// Skip if already running
|
||||
if (this.runningFeatures.has(featureId)) {
|
||||
console.log(`[AutoMode] Feature ${featureId} already running, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[AutoMode] Loop ended");
|
||||
this.autoLoopRunning = false;
|
||||
try {
|
||||
console.log(
|
||||
`[AutoMode] Starting feature: ${feature.description.slice(0, 50)}...`
|
||||
);
|
||||
|
||||
// Register this feature as running
|
||||
const execution = this.createExecutionContext(featureId);
|
||||
execution.projectPath = projectPath;
|
||||
execution.sendToRenderer = sendToRenderer;
|
||||
this.runningFeatures.set(featureId, execution);
|
||||
|
||||
// Update status to in_progress with timestamp
|
||||
await featureLoader.updateFeatureStatus(
|
||||
featureId,
|
||||
"in_progress",
|
||||
projectPath
|
||||
);
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_start",
|
||||
featureId: feature.id,
|
||||
feature: feature,
|
||||
});
|
||||
|
||||
// Implement the feature (this runs async in background)
|
||||
const result = await featureExecutor.implementFeature(
|
||||
feature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
execution
|
||||
);
|
||||
|
||||
// Update feature status based on result
|
||||
let newStatus;
|
||||
if (result.passes) {
|
||||
newStatus = feature.skipTests ? "waiting_approval" : "verified";
|
||||
} else {
|
||||
newStatus = "backlog";
|
||||
}
|
||||
await featureLoader.updateFeatureStatus(
|
||||
feature.id,
|
||||
newStatus,
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Keep context file for viewing output later (deleted only when card is removed)
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_complete",
|
||||
featureId: feature.id,
|
||||
passes: result.passes,
|
||||
message: result.message,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[AutoMode] Error running feature ${featureId}:`, error);
|
||||
sendToRenderer({
|
||||
type: "auto_mode_error",
|
||||
error: error.message,
|
||||
featureId: featureId,
|
||||
});
|
||||
} finally {
|
||||
// Clean up this feature's execution
|
||||
this.runningFeatures.delete(featureId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -719,10 +751,7 @@ class AutoModeService {
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file if verified (only for non-skipTests)
|
||||
if (newStatus === "verified") {
|
||||
await contextManager.deleteContextFile(projectPath, feature.id);
|
||||
}
|
||||
// Keep context file for viewing output later (deleted only when card is removed)
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_complete",
|
||||
@@ -778,7 +807,7 @@ class AutoModeService {
|
||||
});
|
||||
|
||||
// Run git commit via the agent
|
||||
const commitResult = await featureExecutor.commitChangesOnly(
|
||||
await featureExecutor.commitChangesOnly(
|
||||
feature,
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
@@ -792,8 +821,7 @@ class AutoModeService {
|
||||
projectPath
|
||||
);
|
||||
|
||||
// Delete context file
|
||||
await contextManager.deleteContextFile(projectPath, featureId);
|
||||
// Keep context file for viewing output later (deleted only when card is removed)
|
||||
|
||||
sendToRenderer({
|
||||
type: "auto_mode_feature_complete",
|
||||
|
||||
@@ -5,18 +5,27 @@ 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;
|
||||
|
||||
// Get icon path - works in both dev and production
|
||||
function getIconPath() {
|
||||
// In dev: __dirname is electron/, so ../public/icon_gold.png
|
||||
// In production: public folder is included in the app bundle
|
||||
return app.isPackaged
|
||||
? path.join(process.resourcesPath, "app", "public", "icon_gold.png")
|
||||
: path.join(__dirname, "../public/icon_gold.png");
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
height: 900,
|
||||
minWidth: 1024,
|
||||
minHeight: 700,
|
||||
icon: getIconPath(),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
@@ -41,6 +50,11 @@ function createWindow() {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Set app icon (dock icon on macOS)
|
||||
if (process.platform === "darwin" && app.dock) {
|
||||
app.dock.setIcon(getIconPath());
|
||||
}
|
||||
|
||||
// Initialize agent service
|
||||
const appDataPath = app.getPath("userData");
|
||||
await agentService.initialize(appDataPath);
|
||||
@@ -160,32 +174,43 @@ 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 });
|
||||
// Save image to .automaker/images directory
|
||||
ipcMain.handle(
|
||||
"app:saveImageToTemp",
|
||||
async (_, { data, filename, mimeType, projectPath }) => {
|
||||
try {
|
||||
// Use .automaker/images directory instead of /tmp
|
||||
// If projectPath is provided, use it; otherwise fall back to app data directory
|
||||
let imagesDir;
|
||||
if (projectPath) {
|
||||
imagesDir = path.join(projectPath, ".automaker", "images");
|
||||
} else {
|
||||
// Fallback for cases where project isn't loaded yet
|
||||
const appDataPath = app.getPath("userData");
|
||||
imagesDir = path.join(appDataPath, "images");
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
await fs.mkdir(imagesDir, { recursive: true });
|
||||
|
||||
// Remove data URL prefix if present (data:image/png;base64,...)
|
||||
const base64Data = data.includes(",") ? data.split(",")[1] : data;
|
||||
// Generate unique filename with unique ID
|
||||
const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
const safeName = filename.replace(/[^a-zA-Z0-9.-]/g, "_");
|
||||
const imageFilePath = path.join(imagesDir, `${uniqueId}_${safeName}`);
|
||||
|
||||
// Write image to temp file
|
||||
await fs.writeFile(tempFilePath, base64Data, "base64");
|
||||
// Remove data URL prefix if present (data:image/png;base64,...)
|
||||
const base64Data = data.includes(",") ? data.split(",")[1] : data;
|
||||
|
||||
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 };
|
||||
// Write image to file
|
||||
await fs.writeFile(imageFilePath, base64Data, "base64");
|
||||
|
||||
console.log("[IPC] Saved image to .automaker/images:", imageFilePath);
|
||||
return { success: true, path: imageFilePath };
|
||||
} catch (error) {
|
||||
console.error("[IPC] Failed to save image:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// IPC ping for testing communication
|
||||
ipcMain.handle("ping", () => {
|
||||
@@ -201,7 +226,10 @@ ipcMain.handle("ping", () => {
|
||||
*/
|
||||
ipcMain.handle("agent:start", async (_, { sessionId, workingDirectory }) => {
|
||||
try {
|
||||
return await agentService.startConversation({ sessionId, workingDirectory });
|
||||
return await agentService.startConversation({
|
||||
sessionId,
|
||||
workingDirectory,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] agent:start error:", error);
|
||||
return { success: false, error: error.message };
|
||||
@@ -211,42 +239,45 @@ ipcMain.handle("agent:start", async (_, { sessionId, workingDirectory }) => {
|
||||
/**
|
||||
* 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", {
|
||||
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,
|
||||
...data,
|
||||
message,
|
||||
workingDirectory,
|
||||
imagePaths,
|
||||
sendToRenderer,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[IPC] agent:send background error:", error);
|
||||
sendToRenderer({
|
||||
type: "error",
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 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 };
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] agent:send error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* Get conversation history
|
||||
@@ -304,14 +335,21 @@ ipcMain.handle("sessions:list", async (_, { includeArchived }) => {
|
||||
/**
|
||||
* 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 };
|
||||
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
|
||||
@@ -368,20 +406,27 @@ ipcMain.handle("sessions:delete", async (_, { sessionId }) => {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
ipcMain.handle(
|
||||
"auto-mode:start",
|
||||
async (_, { projectPath, maxConcurrency }) => {
|
||||
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 };
|
||||
return await autoModeService.start({
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
maxConcurrency,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:start error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* Stop auto mode
|
||||
@@ -410,76 +455,111 @@ ipcMain.handle("auto-mode:status", () => {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
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 };
|
||||
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);
|
||||
}
|
||||
};
|
||||
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 };
|
||||
return await autoModeService.verifyFeature({
|
||||
projectPath,
|
||||
featureId,
|
||||
sendToRenderer,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:verify-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* Resume a specific feature with previous context
|
||||
*/
|
||||
ipcMain.handle("auto-mode:resume-feature", async (_, { projectPath, featureId }) => {
|
||||
console.log("[IPC] auto-mode:resume-feature called with:", { projectPath, featureId });
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
ipcMain.handle(
|
||||
"auto-mode:resume-feature",
|
||||
async (_, { projectPath, featureId }) => {
|
||||
console.log("[IPC] auto-mode:resume-feature called with:", {
|
||||
projectPath,
|
||||
featureId,
|
||||
});
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
return await autoModeService.resumeFeature({ projectPath, featureId, sendToRenderer });
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:resume-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
return await autoModeService.resumeFeature({
|
||||
projectPath,
|
||||
featureId,
|
||||
sendToRenderer,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:resume-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if a context file exists for a feature
|
||||
*/
|
||||
ipcMain.handle("auto-mode:context-exists", async (_, { projectPath, featureId }) => {
|
||||
try {
|
||||
const contextPath = path.join(projectPath, ".automaker", "context", `${featureId}.md`);
|
||||
ipcMain.handle(
|
||||
"auto-mode:context-exists",
|
||||
async (_, { projectPath, featureId }) => {
|
||||
try {
|
||||
await fs.access(contextPath);
|
||||
return { success: true, exists: true };
|
||||
} catch {
|
||||
return { success: true, exists: false };
|
||||
const contextPath = path.join(
|
||||
projectPath,
|
||||
".automaker",
|
||||
"context",
|
||||
`${featureId}.md`
|
||||
);
|
||||
try {
|
||||
await fs.access(contextPath);
|
||||
return { success: true, exists: true };
|
||||
} catch {
|
||||
return { success: true, exists: false };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:context-exists error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:context-exists error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* Analyze a new project - kicks off an agent to analyze the codebase
|
||||
@@ -494,7 +574,10 @@ ipcMain.handle("auto-mode:analyze-project", async (_, { projectPath }) => {
|
||||
}
|
||||
};
|
||||
|
||||
return await autoModeService.analyzeProject({ projectPath, sendToRenderer });
|
||||
return await autoModeService.analyzeProject({
|
||||
projectPath,
|
||||
sendToRenderer,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:analyze-project error:", error);
|
||||
return { success: false, error: error.message };
|
||||
@@ -517,38 +600,62 @@ ipcMain.handle("auto-mode:stop-feature", async (_, { featureId }) => {
|
||||
/**
|
||||
* Follow-up on a feature with additional prompt
|
||||
*/
|
||||
ipcMain.handle("auto-mode:follow-up-feature", async (_, { projectPath, featureId, prompt, imagePaths }) => {
|
||||
console.log("[IPC] auto-mode:follow-up-feature called with:", { projectPath, featureId, prompt, imagePaths });
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
ipcMain.handle(
|
||||
"auto-mode:follow-up-feature",
|
||||
async (_, { projectPath, featureId, prompt, imagePaths }) => {
|
||||
console.log("[IPC] auto-mode:follow-up-feature called with:", {
|
||||
projectPath,
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
});
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
return await autoModeService.followUpFeature({ projectPath, featureId, prompt, imagePaths, sendToRenderer });
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:follow-up-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
return await autoModeService.followUpFeature({
|
||||
projectPath,
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
sendToRenderer,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:follow-up-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* Commit changes for a feature (no further work, just commit)
|
||||
*/
|
||||
ipcMain.handle("auto-mode:commit-feature", async (_, { projectPath, featureId }) => {
|
||||
console.log("[IPC] auto-mode:commit-feature called with:", { projectPath, featureId });
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
ipcMain.handle(
|
||||
"auto-mode:commit-feature",
|
||||
async (_, { projectPath, featureId }) => {
|
||||
console.log("[IPC] auto-mode:commit-feature called with:", {
|
||||
projectPath,
|
||||
featureId,
|
||||
});
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
return await autoModeService.commitFeature({ projectPath, featureId, sendToRenderer });
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:commit-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
return await autoModeService.commitFeature({
|
||||
projectPath,
|
||||
featureId,
|
||||
sendToRenderer,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:commit-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
// Auto Mode API
|
||||
autoMode: {
|
||||
// Start auto mode
|
||||
start: (projectPath) =>
|
||||
ipcRenderer.invoke("auto-mode:start", { projectPath }),
|
||||
start: (projectPath, maxConcurrency) =>
|
||||
ipcRenderer.invoke("auto-mode:start", { projectPath, maxConcurrency }),
|
||||
|
||||
// Stop auto mode
|
||||
stop: () => ipcRenderer.invoke("auto-mode:stop"),
|
||||
|
||||
@@ -275,7 +275,57 @@ class FeatureExecutor {
|
||||
}
|
||||
|
||||
// Build the prompt for this specific feature
|
||||
const prompt = promptBuilder.buildFeaturePrompt(feature);
|
||||
let prompt = promptBuilder.buildFeaturePrompt(feature);
|
||||
|
||||
// Add images to prompt if feature has imagePaths
|
||||
if (feature.imagePaths && feature.imagePaths.length > 0) {
|
||||
const contentBlocks = [];
|
||||
|
||||
// Add text block
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: prompt,
|
||||
});
|
||||
|
||||
// Add image blocks
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
for (const imagePathObj of feature.imagePaths) {
|
||||
try {
|
||||
const imagePath = imagePathObj.path;
|
||||
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] || imagePathObj.mimeType || "image/png";
|
||||
|
||||
contentBlocks.push({
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: mediaType,
|
||||
data: base64Data,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[FeatureExecutor] Added image to prompt: ${imagePath}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[FeatureExecutor] Failed to load image ${imagePathObj.path}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Use content blocks instead of plain text
|
||||
prompt = contentBlocks;
|
||||
}
|
||||
|
||||
// Planning: Analyze the codebase and create implementation plan
|
||||
sendToRenderer({
|
||||
@@ -613,7 +663,58 @@ class FeatureExecutor {
|
||||
}
|
||||
|
||||
// Build prompt with previous context
|
||||
const prompt = promptBuilder.buildResumePrompt(feature, previousContext);
|
||||
let prompt = promptBuilder.buildResumePrompt(feature, previousContext);
|
||||
|
||||
// Add images to prompt if feature has imagePaths or followUpImages
|
||||
const imagePaths = feature.followUpImages || feature.imagePaths;
|
||||
if (imagePaths && imagePaths.length > 0) {
|
||||
const contentBlocks = [];
|
||||
|
||||
// Add text block
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: prompt,
|
||||
});
|
||||
|
||||
// Add image blocks
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
for (const imagePathObj of imagePaths) {
|
||||
try {
|
||||
const imagePath = imagePathObj.path;
|
||||
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] || imagePathObj.mimeType || "image/png";
|
||||
|
||||
contentBlocks.push({
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: mediaType,
|
||||
data: base64Data,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[FeatureExecutor] Added image to resume prompt: ${imagePath}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[FeatureExecutor] Failed to load image ${imagePathObj.path}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Use content blocks instead of plain text
|
||||
prompt = contentBlocks;
|
||||
}
|
||||
|
||||
// Use appropriate provider based on model type
|
||||
let currentQuery;
|
||||
|
||||
@@ -10,6 +10,10 @@ class PromptBuilder {
|
||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
||||
: "";
|
||||
|
||||
const imagesNote = feature.imagePaths && feature.imagePaths.length > 0
|
||||
? `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images will be provided to you visually to help understand the requirements. Review them carefully before implementing.\n`
|
||||
: "";
|
||||
|
||||
return `You are working on a feature implementation task.
|
||||
|
||||
**Current Feature to Implement:**
|
||||
@@ -17,7 +21,7 @@ class PromptBuilder {
|
||||
ID: ${feature.id}
|
||||
Category: ${feature.category}
|
||||
Description: ${feature.description}
|
||||
${skipTestsNote}
|
||||
${skipTestsNote}${imagesNote}
|
||||
**Steps to Complete:**
|
||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||
|
||||
@@ -117,6 +121,10 @@ Begin by reading the project structure and then implementing the feature.`;
|
||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
||||
: "";
|
||||
|
||||
const imagesNote = feature.imagePaths && feature.imagePaths.length > 0
|
||||
? `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images will be provided to you visually to help understand the requirements. Review them carefully before implementing.\n`
|
||||
: "";
|
||||
|
||||
return `You are implementing and verifying a feature until it is complete and working correctly.
|
||||
|
||||
**Feature to Implement/Verify:**
|
||||
@@ -125,7 +133,7 @@ ID: ${feature.id}
|
||||
Category: ${feature.category}
|
||||
Description: ${feature.description}
|
||||
Current Status: ${feature.status}
|
||||
${skipTestsNote}
|
||||
${skipTestsNote}${imagesNote}
|
||||
**Steps that should be implemented:**
|
||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||
|
||||
@@ -216,6 +224,10 @@ Begin by reading the project structure and understanding what needs to be implem
|
||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
||||
: "";
|
||||
|
||||
const imagesNote = feature.imagePaths && feature.imagePaths.length > 0
|
||||
? `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images will be provided to you visually to help understand the requirements. Review them carefully.\n`
|
||||
: "";
|
||||
|
||||
return `You are resuming work on a feature implementation that was previously started.
|
||||
|
||||
**Current Feature:**
|
||||
@@ -223,7 +235,7 @@ Begin by reading the project structure and understanding what needs to be implem
|
||||
ID: ${feature.id}
|
||||
Category: ${feature.category}
|
||||
Description: ${feature.description}
|
||||
${skipTestsNote}
|
||||
${skipTestsNote}${imagesNote}
|
||||
**Steps to Complete:**
|
||||
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user