diff --git a/.automaker/.gitignore b/.automaker/.gitignore index 044777d9..3eb536ef 100644 --- a/.automaker/.gitignore +++ b/.automaker/.gitignore @@ -1,2 +1,8 @@ # Backup files - these are created automatically by the UpdateFeatureStatus tool feature_list.backup.json + +# Agent context files - created during feature execution +agents-context/ + +# Attached images - uploaded by users as feature context +images/ diff --git a/.automaker/categories.json b/.automaker/categories.json index f5b36f96..77daa7b2 100644 --- a/.automaker/categories.json +++ b/.automaker/categories.json @@ -3,5 +3,6 @@ "Core", "Kanban", "Other", - "Settings" + "Settings", + "Uncategorized" ] \ No newline at end of file diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 0637a088..d1c15a56 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -1 +1,106 @@ -[] \ No newline at end of file +[ + { + "id": "feature-1765328064583-6zpz7ddil", + "category": "Kanban", + "description": "remove the auto mode activity panel completley.", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T00:55:21.540Z", + "imagePaths": [ + { + "id": "img-1765328011980-j8d2r6b78", + "path": "/var/folders/yk/56l0_s6978qfh521xf1dtx3r0000gn/T/automaker-images/1765328011979_Screenshot_2025-12-09_at_7.53.30_PM.png", + "filename": "Screenshot 2025-12-09 at 7.53.30 PM.png", + "mimeType": "image/png" + } + ], + "skipTests": true, + "summary": "Removed auto mode activity panel completely. Deleted: auto-mode-log.tsx. Modified: board-view.tsx - removed AutoModeLog import, showActivityLog state, activity toggle button, and activity panel rendering. Also removed unused cn import and ChevronUp/ChevronDown icons." + }, + { + "id": "feature-1765330657132-oapdvbygc", + "category": "Uncategorized", + "description": "these buttons should be refactored to match more with selected theme, make sure they are set to use the button component variant styles", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T01:37:40.700Z", + "imagePaths": [ + { + "id": "img-1765330619380-q9tu8blks", + "path": "/var/folders/yk/56l0_s6978qfh521xf1dtx3r0000gn/T/automaker-images/1765330619376_Screenshot_2025-12-09_at_8.36.56_PM.png", + "filename": "Screenshot 2025-12-09 at 8.36.56 PM.png", + "mimeType": "image/png" + } + ], + "skipTests": true, + "summary": "Refactored theme selector and kanban detail level buttons to use Button component variants. Modified: settings-view.tsx. Changed 12 theme buttons and 3 kanban detail buttons from raw <button> to <Button> with dynamic variant (secondary when selected, outline when unselected) and brand-500 ring highlight for selected state." + }, + { + "id": "feature-1765330774043-35l9kw70q", + "category": "Kanban", + "description": "Increase the width of this modal and reduce font size of log output to make it easier to fit more output in modal", + "steps": [], + "status": "waiting_approval", + "imagePaths": [ + { + "id": "img-1765330741800-jhmtz9ttc", + "path": "/var/folders/yk/56l0_s6978qfh521xf1dtx3r0000gn/T/automaker-images/1765330741799_Screenshot_2025-12-09_at_8.38.59_PM.png", + "filename": "Screenshot 2025-12-09 at 8.38.59 PM.png", + "mimeType": "image/png" + } + ], + "skipTests": true, + "summary": "Increased modal width from max-w-4xl to max-w-6xl and reduced log output font sizes from text-sm to text-xs. Modified: agent-output-modal.tsx (modal width + container font), log-viewer.tsx (log entry content + preview text fonts)." + }, + { + "id": "feature-1765330800921-uwy5iu3lp", + "category": "Uncategorized", + "description": "what color is the screenshot button? don't change code just answer.", + "steps": [], + "status": "waiting_approval", + "imagePaths": [ + { + "id": "img-1765330783407-msplpgmwk", + "path": "/var/folders/yk/56l0_s6978qfh521xf1dtx3r0000gn/T/automaker-images/1765330783407_Screenshot_2025-12-09_at_8.39.40_PM.png", + "filename": "Screenshot 2025-12-09 at 8.39.40 PM.png", + "mimeType": "image/png" + } + ], + "skipTests": true, + "summary": "Answered question about screenshot button color. The image attachment button (Paperclip icon) is blue when active (bg-blue-100/text-blue-600 light, bg-blue-900/text-blue-400 dark) and uses standard outline styling when inactive. No code changes made." + }, + { + "id": "feature-1765331813319-jzlk7eku2", + "category": "Uncategorized", + "description": "describe the attached image do not change code", + "steps": [], + "status": "verified", + "startedAt": "2025-12-10T02:02:54.785Z", + "imagePaths": [ + { + "id": "img-1765331797511-v4ssc1hha", + "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765331797510-ypiiz13rt_Screenshot_2025-12-09_at_8.56.34_PM.png", + "filename": "Screenshot 2025-12-09 at 8.56.34 PM.png", + "mimeType": "image/png" + } + ], + "skipTests": true + }, + { + "id": "feature-1765333165618-qmik9gy7p", + "category": "Uncategorized", + "description": "what is the text in the attache image say?", + "steps": [], + "status": "in_progress", + "startedAt": "2025-12-10T02:19:28.342Z", + "imagePaths": [ + { + "id": "img-1765333155109-on4lk435f", + "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765333155106-czd46vc93_Screenshot_2025-12-09_at_9.19.13_PM.png", + "filename": "Screenshot 2025-12-09 at 9.19.13 PM.png", + "mimeType": "image/png" + } + ], + "skipTests": true + } +] \ No newline at end of file diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 412973d9..10b314fb 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -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", diff --git a/app/electron/main.js b/app/electron/main.js index 75d45d72..1b8f276a 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -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,37 +600,61 @@ 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 }; + } } -}); +); diff --git a/app/electron/preload.js b/app/electron/preload.js index 1b5f49e0..0e47bfee 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -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"), diff --git a/app/electron/services/feature-executor.js b/app/electron/services/feature-executor.js index 239c7518..c626f358 100644 --- a/app/electron/services/feature-executor.js +++ b/app/electron/services/feature-executor.js @@ -75,7 +75,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({ @@ -274,7 +324,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; + } const currentQuery = query({ prompt, options }); execution.query = currentQuery; diff --git a/app/electron/services/prompt-builder.js b/app/electron/services/prompt-builder.js index 44fdfedd..7a4c9f81 100644 --- a/app/electron/services/prompt-builder.js +++ b/app/electron/services/prompt-builder.js @@ -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")} diff --git a/app/package.json b/app/package.json index 4af46e05..b72dc6dc 100644 --- a/app/package.json +++ b/app/package.json @@ -87,7 +87,7 @@ "arch": ["x64", "arm64"] } ], - "icon": "public/icon.png" + "icon": "public/icon_gold.png" }, "win": { "target": [ @@ -96,7 +96,7 @@ "arch": ["x64"] } ], - "icon": "public/icon.png" + "icon": "public/icon_gold.png" }, "linux": { "target": [ @@ -110,7 +110,7 @@ } ], "category": "Development", - "icon": "public/icon.png" + "icon": "public/icon_gold.png" }, "nsis": { "oneClick": false, diff --git a/app/public/icon.png b/app/public/icon.png new file mode 100644 index 00000000..c1f0947f Binary files /dev/null and b/app/public/icon.png differ diff --git a/app/public/icon_gold.png b/app/public/icon_gold.png new file mode 100644 index 00000000..653a6ed8 Binary files /dev/null and b/app/public/icon_gold.png differ diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index b05e93c1..bab8ca63 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -19,10 +19,10 @@ import { PanelLeft, PanelLeftClose, Sparkles, - Cpu, ChevronDown, Check, BookOpen, + GripVertical, } from "lucide-react"; import { DropdownMenu, @@ -37,9 +37,23 @@ import { ACTION_SHORTCUTS, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; -import { getElectronAPI } from "@/lib/electron"; +import { getElectronAPI, Project } from "@/lib/electron"; import { initializeProject } from "@/lib/project-init"; import { toast } from "sonner"; +import { + DndContext, + DragEndEvent, + PointerSensor, + useSensor, + useSensors, + closestCenter, +} from "@dnd-kit/core"; +import { + SortableContext, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; interface NavSection { label?: string; @@ -53,6 +67,81 @@ interface NavItem { shortcut?: string; } +// Sortable Project Item Component +interface SortableProjectItemProps { + project: Project; + index: number; + currentProjectId: string | undefined; + onSelect: (project: Project) => void; +} + +function SortableProjectItem({ + project, + index, + currentProjectId, + onSelect, +}: SortableProjectItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: project.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+
{part.content}
) : (
diff --git a/app/src/components/views/agent-output-modal.tsx b/app/src/components/views/agent-output-modal.tsx
index 14bb1787..307d7437 100644
--- a/app/src/components/views/agent-output-modal.tsx
+++ b/app/src/components/views/agent-output-modal.tsx
@@ -202,13 +202,13 @@ export function AgentOutputModal({
return (