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 ( +
+ {/* Drag Handle */} + + + {/* Hotkey indicator */} + {index < 9 && ( + + {index + 1} + + )} + + {/* Project content - clickable area */} +
onSelect(project)} + > + + {project.name} + {currentProjectId === project.id && ( + + )} +
+
+ ); +} + export function Sidebar() { const { projects, @@ -64,11 +153,38 @@ export function Sidebar() { setCurrentView, toggleSidebar, removeProject, + reorderProjects, } = useAppStore(); // State for project picker dropdown const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false); + // Sensors for drag-and-drop + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 5, // Small distance to start drag + }, + }) + ); + + // Handle drag end for reordering projects + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const oldIndex = projects.findIndex((p) => p.id === active.id); + const newIndex = projects.findIndex((p) => p.id === over.id); + + if (oldIndex !== -1 && newIndex !== -1) { + reorderProjects(oldIndex, newIndex); + } + } + }, + [projects, reorderProjects] + ); + /** * Opens the system folder selection dialog and initializes the selected project. * Used by both the 'O' keyboard shortcut and the folder icon button. @@ -312,8 +428,12 @@ export function Sidebar() { onClick={() => setCurrentView("welcome")} data-testid="logo-button" > -
- +
+ Automaker Logo
- {projects.map((project, index) => ( - { - setCurrentProject(project); - setIsProjectPickerOpen(false); - }} - className="flex items-center gap-2 cursor-pointer text-muted-foreground hover:text-foreground hover:bg-accent" - data-testid={`project-option-${project.id}`} + + p.id)} + strategy={verticalListSortingStrategy} > - {index < 9 && ( - - {index + 1} - - )} - - {project.name} - {currentProject?.id === project.id && ( - - )} - - ))} + {projects.map((project, index) => ( + { + setCurrentProject(p); + setIsProjectPickerOpen(false); + }} + /> + ))} + +
diff --git a/app/src/components/ui/description-image-dropzone.tsx b/app/src/components/ui/description-image-dropzone.tsx index 53d92b31..17e73bb8 100644 --- a/app/src/components/ui/description-image-dropzone.tsx +++ b/app/src/components/ui/description-image-dropzone.tsx @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"; import { ImageIcon, X, Loader2 } from "lucide-react"; import { Textarea } from "@/components/ui/textarea"; import { getElectronAPI } from "@/lib/electron"; +import { useAppStore } from "@/store/app-store"; export interface FeatureImagePath { id: string; @@ -51,6 +52,7 @@ export function DescriptionImageDropZone({ new Map() ); const fileInputRef = useRef(null); + const currentProject = useAppStore((state) => state.currentProject); const fileToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { @@ -76,11 +78,14 @@ export function DescriptionImageDropZone({ const api = getElectronAPI(); // Check if saveImageToTemp method exists if (!api.saveImageToTemp) { - // Fallback for mock API - return a mock path + // Fallback for mock API - return a mock path in .automaker/images console.log("[DescriptionImageDropZone] Using mock path for image"); - return `/tmp/automaker-images/${Date.now()}_${filename}`; + return `.automaker/images/${Date.now()}_${filename}`; } - const result = await api.saveImageToTemp(base64Data, filename, mimeType); + + // Get projectPath from the store if available + const projectPath = currentProject?.path; + const result = await api.saveImageToTemp(base64Data, filename, mimeType, projectPath); if (result.success && result.path) { return result.path; } @@ -130,7 +135,7 @@ export function DescriptionImageDropZone({ const tempPath = await saveImageToTemp(base64, file.name, file.type); if (tempPath) { - const imageId = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const imageId = `img-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; const imagePathRef: FeatureImagePath = { id: imageId, path: tempPath, diff --git a/app/src/components/ui/dialog.tsx b/app/src/components/ui/dialog.tsx index d9ccec91..5a5f8b8d 100644 --- a/app/src/components/ui/dialog.tsx +++ b/app/src/components/ui/dialog.tsx @@ -60,7 +60,7 @@ function DialogContent({ - + {!isExpanded && entry.content.slice(0, 80) + (entry.content.length > 80 ? "..." : "")} @@ -153,11 +153,11 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) { className="px-4 pb-3 pt-1" data-testid={`log-entry-content-${entry.id}`} > -
+
{formattedContent.map((part, index) => (
{part.type === "json" ? ( -
+                  
                     {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 (
- + Agent Output
@@ -216,7 +216,7 @@ export function AgentOutputModal({ onClick={() => setViewMode("parsed")} className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${ viewMode === "parsed" - ? "bg-purple-500/20 text-purple-300 shadow-sm" + ? "bg-primary/20 text-primary shadow-sm" : "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50" }`} data-testid="view-mode-parsed" @@ -228,7 +228,7 @@ export function AgentOutputModal({ onClick={() => setViewMode("raw")} className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${ viewMode === "raw" - ? "bg-purple-500/20 text-purple-300 shadow-sm" + ? "bg-primary/20 text-primary shadow-sm" : "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50" }`} data-testid="view-mode-raw" @@ -249,7 +249,7 @@ export function AgentOutputModal({
{isLoading && !output ? (
diff --git a/app/src/components/views/auto-mode-log.tsx b/app/src/components/views/auto-mode-log.tsx deleted file mode 100644 index 3fd59055..00000000 --- a/app/src/components/views/auto-mode-log.tsx +++ /dev/null @@ -1,183 +0,0 @@ -"use client"; - -import { useAppStore, AutoModeActivity } from "@/store/app-store"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { - CheckCircle2, - Loader2, - AlertCircle, - Wrench, - Play, - X, - ClipboardList, - Zap, - ShieldCheck, -} from "lucide-react"; -import { cn } from "@/lib/utils"; - -interface AutoModeLogProps { - onClose?: () => void; -} - -export function AutoModeLog({ onClose }: AutoModeLogProps) { - const { autoModeActivityLog, features, clearAutoModeActivity } = - useAppStore(); - - const getActivityIcon = (type: AutoModeActivity["type"]) => { - switch (type) { - case "start": - return ; - case "progress": - return ; - case "tool": - return ; - case "complete": - return ; - case "error": - return ; - case "planning": - return ( - - ); - case "action": - return ( - - ); - case "verification": - return ( - - ); - } - }; - - const getActivityColor = (type: AutoModeActivity["type"]) => { - switch (type) { - case "start": - return "border-l-blue-500"; - case "progress": - return "border-l-purple-500"; - case "tool": - return "border-l-yellow-500"; - case "complete": - return "border-l-green-500"; - case "error": - return "border-l-red-500"; - case "planning": - return "border-l-cyan-500"; - case "action": - return "border-l-orange-500"; - case "verification": - return "border-l-emerald-500"; - } - }; - - const getFeatureDescription = (featureId: string) => { - const feature = features.find((f) => f.id === featureId); - return feature?.description || "Unknown feature"; - }; - - const formatTime = (date: Date) => { - return new Date(date).toLocaleTimeString("en-US", { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - }; - - return ( - - -
-
- - Auto Mode Activity -
-
- - {onClose && ( - - )} -
-
-
- -
-
- {autoModeActivityLog.length === 0 ? ( -
-

No activity yet

-

- Start auto mode to see activity here -

-
- ) : ( - autoModeActivityLog - .slice() - .reverse() - .map((activity) => ( -
-
-
- {getActivityIcon(activity.type)} -
-
-
- - {formatTime(activity.timestamp)} - - - {getFeatureDescription(activity.featureId)} - -
-

- {activity.message} -

- {activity.tool && ( -
- - - {activity.tool} - -
- )} -
-
-
- )) - )} -
-
-
-
- ); -} diff --git a/app/src/components/views/board-view.tsx b/app/src/components/views/board-view.tsx index bcee08da..fb8febe8 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -23,7 +23,6 @@ import { FeatureImagePath, } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; -import { cn } from "@/lib/utils"; import { Card, CardDescription, @@ -50,7 +49,6 @@ import { } from "@/components/ui/dialog"; import { KanbanColumn } from "./kanban-column"; import { KanbanCard } from "./kanban-card"; -import { AutoModeLog } from "./auto-mode-log"; import { AgentOutputModal } from "./agent-output-modal"; import { Plus, @@ -58,8 +56,6 @@ import { Play, StopCircle, Loader2, - ChevronUp, - ChevronDown, Users, Trash2, FastForward, @@ -114,7 +110,6 @@ export function BoardView() { }); const [isLoading, setIsLoading] = useState(true); const [isMounted, setIsMounted] = useState(false); - const [showActivityLog, setShowActivityLog] = useState(false); const [showOutputModal, setShowOutputModal] = useState(false); const [outputFeature, setOutputFeature] = useState(null); const [featuresWithContext, setFeaturesWithContext] = useState>( @@ -342,12 +337,6 @@ export function BoardView() { } }, [showAddDialog, defaultSkipTests]); - // Auto-show activity log when auto mode starts - useEffect(() => { - if (autoMode.isRunning && !showActivityLog) { - setShowActivityLog(true); - } - }, [autoMode.isRunning, showActivityLog]); // Listen for auto mode feature completion and reload features useEffect(() => { @@ -408,11 +397,12 @@ export function BoardView() { // Check which features have context files useEffect(() => { const checkAllContexts = async () => { - const inProgressFeatures = features.filter( - (f) => f.status === "in_progress" + // Check context for in_progress, waiting_approval, and verified features + const featuresWithPotentialContext = features.filter( + (f) => f.status === "in_progress" || f.status === "waiting_approval" || f.status === "verified" ); const contextChecks = await Promise.all( - inProgressFeatures.map(async (f) => ({ + featuresWithPotentialContext.map(async (f) => ({ id: f.id, hasContext: await checkContextExists(f.id), })) @@ -659,6 +649,34 @@ export function BoardView() { } } + // Delete agent context file if it exists + try { + const api = getElectronAPI(); + const contextPath = `${currentProject.path}/.automaker/agents-context/${featureId}.md`; + await api.deleteFile(contextPath); + console.log(`[Board] Deleted agent context for feature ${featureId}`); + } catch (error) { + // Context file might not exist, which is fine + console.log(`[Board] Context file not found or already deleted for feature ${featureId}`); + } + + // Delete attached images if they exist + if (feature.imagePaths && feature.imagePaths.length > 0) { + try { + const api = getElectronAPI(); + for (const imagePathObj of feature.imagePaths) { + try { + await api.deleteFile(imagePathObj.path); + console.log(`[Board] Deleted image: ${imagePathObj.path}`); + } catch (error) { + console.error(`[Board] Failed to delete image ${imagePathObj.path}:`, error); + } + } + } catch (error) { + console.error(`[Board] Error deleting images for feature ${featureId}:`, error); + } + } + // Remove the feature immediately without confirmation removeFeature(featureId); }; @@ -1175,23 +1193,6 @@ export function BoardView() { )} - {isMounted && autoMode.isRunning && ( - - )} - + + + { + e.stopPropagation(); + handleDeleteClick(e as unknown as React.MouseEvent); + }} + data-testid={`delete-feature-${feature.id}`} + > + + Delete + + + +
+ )}
{isDraggable && (
-
-
-
-
- {Math.round(agentInfo.progressPercentage)}% -
-
- )} - {/* Detailed mode: Show all agent info */} {showAgentInfo && feature.status !== "backlog" && agentInfo && (
@@ -346,39 +344,6 @@ export function KanbanCard({ )}
- {/* Progress Indicator */} - {(isCurrentAutoTask || feature.status === "in_progress") && ( -
-
-
-
-
-
- - - {agentInfo.toolCallCount} tools - - {agentInfo.lastToolUsed && ( - - {agentInfo.lastToolUsed} - - )} -
- {Math.round(agentInfo.progressPercentage)}% -
-
- )} - {/* Task List Progress (if todos found) */} {agentInfo.todos.length > 0 && (
@@ -498,8 +463,8 @@ export function KanbanCard({ }} data-testid={`view-output-${feature.id}`} > - - View Output + + Logs )} {onForceStop && ( @@ -526,7 +491,7 @@ export function KanbanCard({ )} - )} {!isCurrentAutoTask && feature.status === "verified" && ( <> + {/* Logs button if context exists */} + {hasContext && onViewOutput && ( + + )} {/* Move back button for skipTests verified features */} {feature.skipTests && onMoveBackToInProgress && ( - )} {!isCurrentAutoTask && feature.status === "waiting_approval" && ( <> + {/* Logs button if context exists */} + {hasContext && onViewOutput && ( + + )} {/* Follow-up prompt button */} {onFollowUp && ( )} - )} {!isCurrentAutoTask && feature.status === "backlog" && ( <> + {onViewOutput && ( + + )} - )}
diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index f2ab2333..2e45fe3e 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -407,150 +407,162 @@ export function SettingsView() {
- - - - - - - - - - - - +
@@ -573,12 +585,13 @@ export function SettingsView() {
- - - +

Minimal: Shows only title and category diff --git a/app/src/components/views/welcome-view.tsx b/app/src/components/views/welcome-view.tsx index 2cf26ea4..5c690543 100644 --- a/app/src/components/views/welcome-view.tsx +++ b/app/src/components/views/welcome-view.tsx @@ -25,7 +25,6 @@ import { initializeProject } from "@/lib/project-init"; import { FolderOpen, Plus, - Cpu, Folder, Clock, Sparkles, @@ -284,8 +283,12 @@ export function WelcomeView() {

-
- +
+ Automaker Logo

diff --git a/app/src/hooks/use-auto-mode.ts b/app/src/hooks/use-auto-mode.ts index d03b4344..9f2def66 100644 --- a/app/src/hooks/use-auto-mode.ts +++ b/app/src/hooks/use-auto-mode.ts @@ -148,11 +148,11 @@ export function useAutoMode() { throw new Error("Auto mode API not available"); } - const result = await api.autoMode.start(currentProject.path); + const result = await api.autoMode.start(currentProject.path, maxConcurrency); if (result.success) { setAutoModeRunning(true); - console.log("[AutoMode] Started successfully"); + console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`); } else { console.error("[AutoMode] Failed to start:", result.error); throw new Error(result.error || "Failed to start auto mode"); @@ -162,7 +162,7 @@ export function useAutoMode() { setAutoModeRunning(false); throw error; } - }, [currentProject, setAutoModeRunning]); + }, [currentProject, setAutoModeRunning, maxConcurrency]); // Stop auto mode const stop = useCallback(async () => { diff --git a/app/src/hooks/use-electron-agent.ts b/app/src/hooks/use-electron-agent.ts index fafddc4b..f187c37a 100644 --- a/app/src/hooks/use-electron-agent.ts +++ b/app/src/hooks/use-electron-agent.ts @@ -62,7 +62,7 @@ export function useElectronAgent({ imageCount: images?.length || 0 }); - // Save images to temp files and get paths + // Save images to .automaker/images and get paths let imagePaths: string[] | undefined; if (images && images.length > 0) { imagePaths = []; @@ -70,11 +70,12 @@ export function useElectronAgent({ const result = await window.electronAPI.saveImageToTemp( image.data, image.filename, - image.mimeType + image.mimeType, + workingDirectory // Pass workingDirectory as projectPath ); if (result.success && result.path) { imagePaths.push(result.path); - console.log("[useElectronAgent] Saved image to temp:", result.path); + console.log("[useElectronAgent] Saved image to .automaker/images:", result.path); } else { console.error("[useElectronAgent] Failed to save image:", result.error); } @@ -304,7 +305,7 @@ export function useElectronAgent({ imageCount: images?.length || 0 }); - // Save images to temp files and get paths + // Save images to .automaker/images and get paths let imagePaths: string[] | undefined; if (images && images.length > 0) { imagePaths = []; @@ -312,11 +313,12 @@ export function useElectronAgent({ const result = await window.electronAPI.saveImageToTemp( image.data, image.filename, - image.mimeType + image.mimeType, + workingDirectory // Pass workingDirectory as projectPath ); if (result.success && result.path) { imagePaths.push(result.path); - console.log("[useElectronAgent] Saved image to temp:", result.path); + console.log("[useElectronAgent] Saved image to .automaker/images:", result.path); } else { console.error("[useElectronAgent] Failed to save image:", result.error); } diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 91bb6854..8ef9642c 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -58,7 +58,7 @@ export interface AutoModeEvent { } export interface AutoModeAPI { - start: (projectPath: string) => Promise<{ success: boolean; error?: string }>; + start: (projectPath: string, maxConcurrency?: number) => Promise<{ success: boolean; error?: string }>; stop: () => Promise<{ success: boolean; error?: string }>; stopFeature: (featureId: string) => Promise<{ success: boolean; error?: string }>; status: () => Promise<{ success: boolean; isRunning?: boolean; currentFeatureId?: string | null; runningFeatures?: string[]; error?: string }>; @@ -370,12 +370,13 @@ let mockAutoModeTimeouts = new Map(); // Track timeouts function createMockAutoModeAPI(): AutoModeAPI { return { - start: async (projectPath: string) => { + start: async (projectPath: string, maxConcurrency?: number) => { if (mockAutoModeRunning) { return { success: false, error: "Auto mode is already running" }; } mockAutoModeRunning = true; + console.log(`[Mock] Auto mode started with maxConcurrency: ${maxConcurrency || 3}`); const featureId = "auto-mode-0"; mockRunningFeatures.add(featureId); diff --git a/app/src/lib/log-parser.ts b/app/src/lib/log-parser.ts index 76a6ce15..30999752 100644 --- a/app/src/lib/log-parser.ts +++ b/app/src/lib/log-parser.ts @@ -323,11 +323,11 @@ export function getLogTypeColors(type: LogEntryType): { }; case "debug": return { - bg: "bg-purple-500/10", - border: "border-l-purple-500", - text: "text-purple-300", - icon: "text-purple-400", - badge: "bg-purple-500/20 text-purple-300", + bg: "bg-primary/10", + border: "border-l-primary", + text: "text-primary", + icon: "text-primary", + badge: "bg-primary/20 text-primary", }; default: return { diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index abf4117d..d7e983d6 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -155,6 +155,7 @@ export interface AppActions { addProject: (project: Project) => void; removeProject: (projectId: string) => void; setCurrentProject: (project: Project | null) => void; + reorderProjects: (oldIndex: number, newIndex: number) => void; // View actions setCurrentView: (view: ViewMode) => void; @@ -268,6 +269,13 @@ export const useAppStore = create()( set({ projects: get().projects.filter((p) => p.id !== projectId) }); }, + reorderProjects: (oldIndex, newIndex) => { + const projects = [...get().projects]; + const [movedProject] = projects.splice(oldIndex, 1); + projects.splice(newIndex, 0, movedProject); + set({ projects }); + }, + setCurrentProject: (project) => { set({ currentProject: project }); if (project) { diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 2a44841a..7935c869 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -300,7 +300,8 @@ export interface ElectronAPI { saveImageToTemp: ( data: string, filename: string, - mimeType: string + mimeType: string, + projectPath?: string ) => Promise<{ success: boolean; path?: string; diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 00000000..1f7b0c15 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,126 @@ +# Release Command + +This command creates a git tag with a version bump and description of changes. + +## Usage + +``` +/release [major|minor|patch] [description] +``` + +Examples: + +- `/release minor "✨ Added inventory drag and drop functionality"` +- `/release patch "🐛 Fixed bug with item selection"` +- `/release major "💥 Breaking: Refactored API endpoints"` +- `/release minor "Version 0.20.0: Added new features and improvements"` + +## Steps to Execute + +### 1. Parse Version Type and Description + +- Extract the version type from the command: `major`, `minor`, or `patch` +- Extract the description (rest of the command, if provided) +- If no version type provided or invalid, show usage and exit +- Description is optional - if not provided, will auto-generate from commits + +### 2. Generate Changelog from Commits + +- Find the last git tag (version tag): + ```bash + git describe --tags --abbrev=0 + ``` +- If no previous tag exists, use the initial commit or handle gracefully +- Get all commits between the last tag and HEAD: + ```bash + git log ..HEAD --pretty=format:"%h %s" --no-merges + ``` +- Parse commit messages and generate a changelog description: + - Group commits by type (feature, fix, improvement, etc.) based on commit message patterns + - Use emojis to categorize changes (see Emoji Usage section) + - Format as a multi-line changelog with categorized entries + - If user provided a description, prepend it to the auto-generated changelog + - If no commits found, use a default message or prompt user + +### 3. Read Current Version + +- Read `app/package.json` to get the current version (e.g., "0.1.0") +- Parse the version into major, minor, and patch components +- Calculate the new version based on the type: + - **major**: `${major + 1}.0.0` (e.g., 0.1.0 → 1.0.0) + - **minor**: `${major}.${minor + 1}.0` (e.g., 0.1.0 → 0.2.0) + - **patch**: `${major}.${minor}.${patch + 1}` (e.g., 0.1.0 → 0.1.1) + +### 4. Create Git Tag + +- Create an annotated git tag with the new version and description: + ```bash + git tag -a v -m "" + ``` +- Example: `git tag -a v0.2.0 -m "✨ Added inventory drag and drop functionality"` + +### 5. Push Tag to Remote + +- Push the tag to remote: + ```bash + git push origin v + ``` + +## Emoji Usage + +You can use emojis in release notes to categorize changes: + +- ✨ **New features** - New functionality, features, additions +- 🐛 **Bug fixes** - Bug fixes and error corrections +- 🔧 **Improvements** - Refactoring, optimizations, code quality +- ⚡ **Performance** - Performance improvements +- 💥 **Breaking changes** - Breaking API changes, major refactors +- 🎨 **UI/UX** - Visual and user experience updates +- ⚙️ **Configuration** - Config and environment changes +- 📝 **Documentation** - Documentation updates +- 🏗️ **Infrastructure** - Build, deployment, infrastructure +- 🎵 **Audio** - Sound effects, music, audio changes + +## Changelog Generation + +The release command automatically generates a changelog by analyzing commits between the last tag and HEAD: + +1. **Find Last Tag**: Uses `git describe --tags --abbrev=0` to find the most recent version tag +2. **Get Commits**: Retrieves all commits between the last tag and HEAD using `git log ..HEAD` +3. **Parse and Categorize**: Analyzes commit messages to categorize changes: + - Looks for conventional commit patterns (feat:, fix:, refactor:, etc.) + - Detects emoji prefixes in commit messages + - Groups similar changes together +4. **Generate Description**: Creates a formatted changelog with: + - User-provided description (if any) at the top + - Categorized list of changes with appropriate emojis + - Commit hash references for traceability + +### Example Generated Changelog + +``` +✨ Added inventory drag and drop functionality + +Changes since v0.1.0: + +✨ Features: +- Add drag and drop support for inventory items (abc1234) +- Implement new sidebar navigation (def5678) + +🐛 Bug Fixes: +- Fix item selection bug in list view (ghi9012) +- Resolve memory leak in component cleanup (jkl3456) + +🔧 Improvements: +- Refactor API endpoint structure (mno7890) +- Optimize database queries (pqr2345) +``` + +## Notes + +- The tag message should describe what changed in this release +- Use descriptive messages with emojis to categorize changes +- Tags follow semantic versioning (e.g., v0.1.0, v0.2.0, v1.0.0) +- Version is automatically calculated based on the type specified +- If no previous tag exists, all commits from the repository start will be included +- User-provided description (if any) will be prepended to the auto-generated changelog