Merge main into feat/extend-models-support

Resolved conflicts:
- feature_list.json: Merged all features from both branches
- feature-loader.js: Included both model/thinkingLevel and error fields
- board-view.tsx: Merged model/thinkingLevel and error fields, kept currentProject check
- settings-view.tsx: Merged CLI status checks with navigation/scroll code
- app-store.ts: Included both model/thinkingLevel and error fields in Feature interface

Fixed linting errors in settings-view.tsx
This commit is contained in:
Kacper
2025-12-10 10:25:13 +01:00
19 changed files with 856 additions and 395 deletions

View File

@@ -1,117 +0,0 @@
📋 Planning implementation for: switch the order inside the add new feature panel so descriptino comes first followed by an optional category, also update the edit feature panel.
⚡ Executing implementation for: switch the order inside the add new feature panel so descriptino comes first followed by an optional category, also update the edit feature panel.
Starting code implementation...
🔧 Tool: Task
🔧 Tool: Bash
🔧 Tool: Glob
🔧 Tool: Glob
🔧 Tool: Grep
🔧 Tool: Grep
🔧 Tool: Glob
🔧 Tool: Glob
🔧 Tool: Bash
🔧 Tool: Read
🔧 Tool: Bash
🔧 Tool: Bash
🔧 Tool: Bash
🔧 Tool: Bash
🔧 Tool: Read
🔧 Tool: Read
🔧 Tool: Grep
🔧 Tool: Read
🔧 Tool: Grep
🔧 Tool: Read
🔧 Tool: Bash
🔧 Tool: Read
🔧 Tool: Bash
🔧 Tool: Read
🔧 Tool: Read
🔧 Tool: Read
🔧 Tool: Bash
🔧 Tool: Bash
🔧 Tool: Read
🔧 Tool: Bash
🔧 Tool: Bash
🔧 Tool: Read
🔧 Tool: Bash
🔧 Tool: Read
Now I have a good understanding of the codebase. Let me read the specific files to see the exact structure of the add and edit feature dialogs:
🔧 Tool: Read
Now I have a clear understanding of both dialogs. I need to:
1. In the **Add Feature Dialog** (lines 1342-1419): Move the Description field before the Category field
2. In the **Edit Feature Dialog** (lines 1452-1536): Move the Description field before the Category field
Let me make these edits:
🔧 Tool: Edit
Now let me update the Edit Feature Dialog similarly:
🔧 Tool: Edit
Both dialogs have been updated. Now let me verify the changes are correct by reading the updated sections:
🔧 Tool: Read
Now let me verify the edit dialog changes:
🔧 Tool: Read
Both dialogs have been successfully updated. The changes are:
1. **Add New Feature Dialog**:
- Description field now comes first
- Category field now comes second with label "Category (optional)"
2. **Edit Feature Dialog**:
- Description field now comes first
- Category field now comes second with label "Category (optional)"
Now let me mark this feature as verified using the UpdateFeatureStatus tool:
🔧 Tool: mcp__automaker-tools__UpdateFeatureStatus
I've completed the feature implementation. Here's a summary of what was done:
## Changes Made
**File Modified:** `app/src/components/views/board-view.tsx`
### Add New Feature Dialog (lines 1342-1368)
- Moved **Description** field to the first position (was second)
- Moved **Category** field to the second position (was first)
- Updated Category label from "Category" to "Category (optional)"
### Edit Feature Dialog (lines 1452-1482)
- Moved **Description** field to the first position (was second)
- Moved **Category** field to the second position (was first)
- Updated Category label from "Category" to "Category (optional)"
The feature has been marked as `waiting_approval` (automatically converted from `verified` because `skipTests=true`). Please manually verify the changes and commit when ready.✅ Verifying implementation for: switch the order inside the add new feature panel so descriptino comes first followed by an optional category, also update the edit feature panel.
Verifying implementation and checking test results...
✓ Verification successful: All tests passed

View File

@@ -1,139 +0,0 @@
📋 Planning implementation for: remember the users choice for skip testing and add a setting toggle in settings page to let them change their default when making new tasks. if it's enabled, show the steps to allow user to add manual testing steps. change label from steps to Verification Steps
⚡ Executing implementation for: remember the users choice for skip testing and add a setting toggle in settings page to let them change their default when making new tasks. if it's enabled, show the steps to allow user to add manual testing steps. change label from steps to Verification Steps
Starting code implementation...
🔧 Tool: Glob
Let me look at the project structure more specifically:
🔧 Tool: Glob
🔧 Tool: Read
🔧 Tool: Read
🔧 Tool: Read
Now I have a good understanding of the codebase. Let me implement the feature:
1. Add a `defaultSkipTests` setting to the app store (persisted)
2. Add a toggle in the settings page
3. Update the "Add Feature" dialog to:
- Default skipTests based on user preference
- Show "Verification Steps" label instead of "Steps" when skipTests is enabled
4. Also update the edit dialog label
🔧 Tool: TodoWrite
🔧 Tool: Edit
Input: {
"file_path": "/Users/webdevcody/Workspace/automaker/app/src/store/app-store.ts",
"old_string": " // Kanban Card Display Settings\n kanbanCardDetailLevel: KanbanCardDetailLevel; // Level of detail shown on kanban cards\n}",
"new_string": " // Kanban Card Display Settings\n kanbanCardDetailLevel: KanbanCardDetailLevel; // Level of detail shown on kanban cards\n\n // Feature Default Settings\n defaultSkipTests: boolean; // Default value for skip tests when creating new features\n}"
}
🔧 Tool: Edit
🔧 Tool: Edit
Input: {
"file_path": "/Users/webdevcody/Workspace/automaker/app/src/store/app-store.ts",
"old_string": " maxConcurrency: 3, // Default to 3 concurrent agents\n kanbanCardDetailLevel: \"standard\", // Default to standard detail level\n};",
"new_string": " maxConcurrency: 3, // Default to 3 concurrent agents\n kanbanCardDetailLevel: \"standard\", // Default to standard detail level\n defaultSkipTests: false, // Default to TDD mode (tests enabled)\n};"
}
🔧 Tool: Edit
Now I need to add `defaultSkipTests` to the persisted state:
🔧 Tool: Edit
🔧 Tool: TodoWrite
Now let me add the toggle in the settings page. I'll add a new section for Feature Defaults:
🔧 Tool: Read
🔧 Tool: Edit
🔧 Tool: Edit
Now let me find where I can add the new section. Let me read more of the settings view to find where to add the feature defaults section:
🔧 Tool: Read
Now let me add the Feature Defaults section after the Kanban Card Display section:
🔧 Tool: Edit
🔧 Tool: TodoWrite
Now let me update the board-view.tsx to:
1. Use `defaultSkipTests` as the default value for new features
2. Change the label from "Steps" to "Verification Steps" when skipTests is enabled
🔧 Tool: Read
🔧 Tool: Edit
Now I need to update the newFeature state initialization to use defaultSkipTests. Since it's initialized with a static value, I need to use an effect to sync it when the dialog opens or when defaultSkipTests changes:
🔧 Tool: Read
I need to update the handleAddFeature to reset skipTests to the default value:
🔧 Tool: Edit
Now I also need to make sure the initial state and when dialog opens uses the default. Let me add an effect that resets the form when the dialog opens:
🔧 Tool: Read
I need to add an effect to reset the skipTests when the dialog opens. Let me find a good place to add it:
🔧 Tool: Edit
Now let me update the label from "Steps" to "Verification Steps" when skipTests is enabled. Let me find the Add Feature dialog:
🔧 Tool: Read
I'll now update the label to change dynamically based on skipTests. I also need to:
1. Change the "Steps" label to "Verification Steps" when skipTests is enabled
2. Only show the steps section when skipTests is enabled (based on the requirement "if it's enabled, show the steps to allow user to add manual testing steps")
Let me update the add feature dialog:
🔧 Tool: Edit
🔧 Tool: TodoWrite
Now let me find and update the Edit Feature dialog to also show "Verification Steps" when skipTests is enabled:
🔧 Tool: Read
🔧 Tool: Edit
🔧 Tool: TodoWrite
Now let me verify the implementation is correct by checking that everything compiles. Let me run a quick type check:
🔧 Tool: Bash
The errors are from an example file and API routes, not from the files I modified. The core application code I changed compiles correctly. Let me verify specifically that the files I modified don't have errors:
🔧 Tool: Bash
The errors are in a different file (analysis-view.tsx) that imports from the store but has missing types - not related to my changes. The store, board-view, and settings-view themselves have no type errors.
Let me now mark the feature as verified using the UpdateFeatureStatus tool:
🔧 Tool: mcp__automaker-tools__UpdateFeatureStatus
Let me check if the feature exists in the feature list:
🔧 Tool: Bash
🔧 Tool: Read
The feature list is empty. This could be because the feature is being tracked elsewhere or the file was cleared. Let me clear my todo list and provide a summary of what was implemented:
🔧 Tool: TodoWrite
## Feature Implementation Complete
I have successfully implemented the feature to remember the user's choice for skip testing and added a settings toggle. Here's a summary of the changes made:
### Files Modified:
1. **`app/src/store/app-store.ts`**
- Added `defaultSkipTests: boolean` to the AppState interface
- Added `setDefaultSkipTests: (skip: boolean) => void` action
- Set initial state to `false` (TDD mode by default)
- Added `defaultSkipTests` to the persisted state so it's remembered across sessions
2. **`app/src/components/views/settings-view.tsx`**
- Added `FlaskConical` icon import and `Checkbox` component import
- Added `defaultSkipTests` and `setDefaultSkipTests` from the store
- Added a new "Feature Defaults" section with a toggle for "Skip automated testing by default"
- The toggle is displayed with helpful description text
3. **`app/src/components/views/board-view.tsx`**
- Added `defaultSkipTests` from the store
- Added an effect to sync `skipTests` default when the Add Feature dialog opens
- **Add Feature Dialog**:
- Moved the skip tests checkbox above the steps section
- Steps section now only shows when `skipTests` is enabled
- Changed label from "Steps" to "Verification Steps"
- Changed button from "Add Step" to "Add Verification Step"
- Changed placeholder from "Step X" to "Verification step X"
- **Edit Feature Dialog**:
- Applied the same changes - steps only show when `skipTests` is enabled
- Changed label to "Verification Steps" and button to "Add Verification Step"
- Reset form uses `defaultSkipTests` value after adding a feature
### Behavior:
- Users can set their default preference in Settings → Feature Defaults
- When creating a new feature, the "Skip automated testing" checkbox will be pre-checked based on their preference
- When skip tests is enabled, the "Verification Steps" input section appears, allowing users to add manual testing/verification steps
- The setting persists across browser sessions via localStorage
**Note:** The feature could not be marked as verified via the UpdateFeatureStatus tool because the feature list appears to be empty. Since this is a `skipTests` feature, the user should manually review and commit the changes.✅ Verifying implementation for: remember the users choice for skip testing and add a setting toggle in settings page to let them change their default when making new tasks. if it's enabled, show the steps to allow user to add manual testing steps. change label from steps to Verification Steps
Verifying implementation and checking test results...
✗ Verification: Tests need attention

View File

@@ -187,6 +187,24 @@
"model": "opus",
"thinkingLevel": "none"
},
{
"id": "feature-1765335919754-r010d1fw5",
"category": "Uncategorized",
"description": "what does the text in the button say?\n",
"steps": [],
"status": "waiting_approval",
"startedAt": "2025-12-10T03:05:34.894Z",
"imagePaths": [
{
"id": "img-1765335919132-0x3t37l1r",
"path": "/Users/webdevcody/Library/Application Support/automaker/images/1765335919131-g4qvs053g_Screenshot_2025-12-09_at_10.05.17_PM.png",
"filename": "Screenshot 2025-12-09 at 10.05.17 PM.png",
"mimeType": "image/png"
}
],
"skipTests": true,
"summary": "Investigated button text in the app. Main buttons found in welcome-view.tsx: \"Create Project\" (primary action), \"Browse Folder\" (secondary action), \"Browse\" (directory selector), \"Cancel\", \"Get Started\". No code changes made - this was an investigative question."
},
{
"id": "feature-1765336835703-4xona4xy9",
"category": "Uncategorized",

View File

@@ -181,6 +181,31 @@ class AutoModeService {
return { success: true, passes: result.passes };
} catch (error) {
console.error("[AutoMode] Error running feature:", error);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
sendToRenderer({
type: "auto_mode_error",
error: error.message,
@@ -260,6 +285,31 @@ class AutoModeService {
return { success: true, passes: result.passes };
} catch (error) {
console.error("[AutoMode] Error verifying feature:", error);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
sendToRenderer({
type: "auto_mode_error",
error: error.message,
@@ -400,6 +450,31 @@ class AutoModeService {
return { success: true, passes: finalResult.passes };
} catch (error) {
console.error("[AutoMode] Error resuming feature:", error);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
sendToRenderer({
type: "auto_mode_error",
error: error.message,
@@ -544,6 +619,31 @@ class AutoModeService {
});
} catch (error) {
console.error(`[AutoMode] Error running feature ${featureId}:`, error);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
sendToRenderer({
type: "auto_mode_error",
error: error.message,
@@ -761,6 +861,31 @@ class AutoModeService {
});
} catch (error) {
console.error("[AutoMode] Error in follow-up:", error);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
sendToRenderer({
type: "auto_mode_error",
error: error.message,

View File

@@ -3,7 +3,7 @@ const path = require("path");
// Load environment variables from .env file
require("dotenv").config({ path: path.join(__dirname, "../.env") });
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
const fs = require("fs/promises");
const agentService = require("./agent-service");
const autoModeService = require("./auto-mode-service");
@@ -12,11 +12,11 @@ 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 dev: __dirname is electron/, so ../public/logo.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");
? path.join(process.resourcesPath, "app", "public", "logo.png")
: path.join(__dirname, "../public/logo.png");
}
function createWindow() {
@@ -169,6 +169,15 @@ ipcMain.handle("fs:deleteFile", async (_, filePath) => {
}
});
ipcMain.handle("fs:trashItem", async (_, targetPath) => {
try {
await shell.trashItem(targetPath);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
// App data path
ipcMain.handle("app:getPath", (_, name) => {
return app.getPath(name);
@@ -193,7 +202,9 @@ ipcMain.handle(
await fs.mkdir(imagesDir, { recursive: true });
// Generate unique filename with unique ID
const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
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}`);

View File

@@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
exists: (filePath) => ipcRenderer.invoke("fs:exists", filePath),
stat: (filePath) => ipcRenderer.invoke("fs:stat", filePath),
deleteFile: (filePath) => ipcRenderer.invoke("fs:deleteFile", filePath),
trashItem: (filePath) => ipcRenderer.invoke("fs:trashItem", filePath),
// App APIs
getPath: (name) => ipcRenderer.invoke("app:getPath", name),

View File

@@ -699,7 +699,8 @@ class FeatureExecutor {
const path = require("path");
for (const imagePathObj of imagePaths) {
try {
const imagePath = imagePathObj.path;
// Handle both string paths and FeatureImagePath objects
const imagePath = typeof imagePathObj === 'string' ? imagePathObj : imagePathObj.path;
const imageBuffer = fs.readFileSync(imagePath);
const base64Data = imageBuffer.toString("base64");
const ext = path.extname(imagePath).toLowerCase();
@@ -710,7 +711,9 @@ class FeatureExecutor {
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || imagePathObj.mimeType || "image/png";
const mediaType = typeof imagePathObj === 'string'
? (mimeTypeMap[ext] || "image/png")
: (mimeTypeMap[ext] || imagePathObj.mimeType || "image/png");
contentBlocks.push({
type: "image",
@@ -723,8 +726,9 @@ class FeatureExecutor {
console.log(`[FeatureExecutor] Added image to resume prompt: ${imagePath}`);
} catch (error) {
const errorPath = typeof imagePathObj === 'string' ? imagePathObj : imagePathObj.path;
console.error(
`[FeatureExecutor] Failed to load image ${imagePathObj.path}:`,
`[FeatureExecutor] Failed to load image ${errorPath}:`,
error
);
}

View File

@@ -36,8 +36,9 @@ class FeatureLoader {
* @param {string} status - The new status
* @param {string} projectPath - Path to the project
* @param {string} [summary] - Optional summary of what was done
* @param {string} [error] - Optional error message if feature errored
*/
async updateFeatureStatus(featureId, status, projectPath, summary) {
async updateFeatureStatus(featureId, status, projectPath, summary, error) {
const featuresPath = path.join(
projectPath,
".automaker",
@@ -98,6 +99,14 @@ class FeatureLoader {
feature.summary = summary;
}
// Update the error field (set or clear)
if (error) {
feature.error = error;
} else {
// Clear any previous error when status changes without error
delete feature.error;
}
// Save back to file
const toSave = features.map((f) => {
const featureData = {
@@ -129,6 +138,9 @@ class FeatureLoader {
if (f.thinkingLevel !== undefined) {
featureData.thinkingLevel = f.thinkingLevel;
}
if (f.error !== undefined) {
featureData.error = f.error;
}
return featureData;
});

View File

@@ -10,9 +10,18 @@ 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`
: "";
let imagesNote = "";
if (feature.imagePaths && feature.imagePaths.length > 0) {
const imagesList = feature.imagePaths.map((img, idx) =>
` ${idx + 1}. ${img.filename} (${img.mimeType})\n Path: ${img.path}`
).join("\n");
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
${imagesList}
You can use the Read tool to view these images at any time during implementation. Review them carefully before implementing.\n`;
}
return `You are working on a feature implementation task.
@@ -121,9 +130,18 @@ 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`
: "";
let imagesNote = "";
if (feature.imagePaths && feature.imagePaths.length > 0) {
const imagesList = feature.imagePaths.map((img, idx) =>
` ${idx + 1}. ${img.filename} (${img.mimeType})\n Path: ${img.path}`
).join("\n");
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
${imagesList}
You can use the Read tool to view these images at any time during implementation. Review them carefully before implementing.\n`;
}
return `You are implementing and verifying a feature until it is complete and working correctly.
@@ -224,9 +242,24 @@ 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`
: "";
// For resume, check both followUpImages and imagePaths
const imagePaths = feature.followUpImages || feature.imagePaths;
let imagesNote = "";
if (imagePaths && imagePaths.length > 0) {
const imagesList = imagePaths.map((img, idx) => {
// Handle both FeatureImagePath objects and simple path strings
const path = typeof img === 'string' ? img : img.path;
const filename = typeof img === 'string' ? path.split('/').pop() : img.filename;
const mimeType = typeof img === 'string' ? 'image/*' : img.mimeType;
return ` ${idx + 1}. ${filename} (${mimeType})\n Path: ${path}`;
}).join("\n");
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
${imagesList}
You can use the Read tool to view these images at any time. Review them carefully.\n`;
}
return `You are resuming work on a feature implementation that was previously started.

View File

@@ -87,7 +87,7 @@
"arch": ["x64", "arm64"]
}
],
"icon": "public/icon_gold.png"
"icon": "public/logo.png"
},
"win": {
"target": [
@@ -96,7 +96,7 @@
"arch": ["x64"]
}
],
"icon": "public/icon_gold.png"
"icon": "public/logo.png"
},
"linux": {
"target": [
@@ -110,7 +110,7 @@
}
],
"category": "Development",
"icon": "public/icon_gold.png"
"icon": "public/logo.png"
},
"nsis": {
"oneClick": false,

BIN
app/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -3,7 +3,6 @@
import { useState, useMemo, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import { useAppStore } from "@/store/app-store";
import Link from "next/link";
import {
FolderOpen,
Plus,
@@ -11,25 +10,32 @@ import {
FileText,
LayoutGrid,
Bot,
ChevronLeft,
ChevronRight,
Folder,
X,
Wrench,
PanelLeft,
PanelLeftClose,
Sparkles,
ChevronDown,
Check,
BookOpen,
GripVertical,
Trash2,
Undo2,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
useKeyboardShortcuts,
NAV_SHORTCUTS,
@@ -37,7 +43,7 @@ import {
ACTION_SHORTCUTS,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import { getElectronAPI, Project } from "@/lib/electron";
import { getElectronAPI, Project, TrashedProject } from "@/lib/electron";
import { initializeProject } from "@/lib/project-init";
import { toast } from "sonner";
import {
@@ -145,6 +151,7 @@ function SortableProjectItem({
export function Sidebar() {
const {
projects,
trashedProjects,
currentProject,
currentView,
sidebarOpen,
@@ -152,12 +159,17 @@ export function Sidebar() {
setCurrentProject,
setCurrentView,
toggleSidebar,
removeProject,
restoreTrashedProject,
deleteTrashedProject,
emptyTrash,
reorderProjects,
} = useAppStore();
// State for project picker dropdown
const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);
const [showTrashDialog, setShowTrashDialog] = useState(false);
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
// Sensors for drag-and-drop
const sensors = useSensors(
@@ -239,6 +251,73 @@ export function Sidebar() {
}
}, [addProject, setCurrentProject]);
const handleRestoreProject = useCallback(
(projectId: string) => {
restoreTrashedProject(projectId);
toast.success("Project restored", {
description: "Added back to your project list.",
});
setShowTrashDialog(false);
},
[restoreTrashedProject]
);
const handleDeleteProjectFromDisk = useCallback(
async (trashedProject: TrashedProject) => {
const confirmed = window.confirm(
`Delete "${trashedProject.name}" from disk?\nThis sends the folder to your system Trash.`
);
if (!confirmed) return;
setActiveTrashId(trashedProject.id);
try {
const api = getElectronAPI();
if (!api.trashItem) {
throw new Error("System Trash is not available in this build.");
}
const result = await api.trashItem(trashedProject.path);
if (!result.success) {
throw new Error(result.error || "Failed to delete project folder");
}
deleteTrashedProject(trashedProject.id);
toast.success("Project folder sent to system Trash", {
description: trashedProject.path,
});
} catch (error) {
console.error("[Sidebar] Failed to delete project from disk:", error);
toast.error("Failed to delete project folder", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setActiveTrashId(null);
}
},
[deleteTrashedProject]
);
const handleEmptyTrash = useCallback(() => {
if (trashedProjects.length === 0) {
setShowTrashDialog(false);
return;
}
const confirmed = window.confirm(
"Clear all trashed projects from Automaker? This does not delete folders from disk."
);
if (!confirmed) return;
setIsEmptyingTrash(true);
try {
emptyTrash();
toast.success("Trash cleared");
setShowTrashDialog(false);
} finally {
setIsEmptyingTrash(false);
}
}, [emptyTrash, trashedProjects.length]);
const navSections: NavSection[] = [
{
label: "Project",
@@ -428,16 +507,16 @@ export function Sidebar() {
onClick={() => setCurrentView("welcome")}
data-testid="logo-button"
>
<div className="relative flex items-center justify-center w-8 h-8 rounded-lg group">
<div className="relative flex items-center justify-center rounded-lg group">
<img
src="/icon_gold.png"
src="/logo.png"
alt="Automaker Logo"
className="w-8 h-8 group-hover:rotate-12 transition-transform"
className="size-8 group-hover:rotate-12 transition-transform"
/>
</div>
<span
className={cn(
"ml-3 font-bold text-sidebar-foreground text-base tracking-tight",
"ml-1 font-bold text-sidebar-foreground text-base tracking-tight",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
@@ -455,7 +534,7 @@ export function Sidebar() {
title="New Project"
data-testid="new-project-button"
>
<Plus className="w-4 h-4 flex-shrink-0" />
<Plus className="w-4 h-4 shrink-0" />
<span className="ml-2 text-sm font-medium hidden lg:block whitespace-nowrap">
New
</span>
@@ -467,13 +546,23 @@ export function Sidebar() {
data-testid="open-project-button"
>
<FolderOpen className="w-4 h-4 shrink-0" />
<span className="ml-2 text-sm font-medium hidden lg:block whitespace-nowrap">
Open
</span>
<span className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500 ml-2">
{ACTION_SHORTCUTS.openProject}
</span>
</button>
<button
onClick={() => setShowTrashDialog(true)}
className="group flex items-center justify-center px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-primary hover:bg-destructive/10 border border-sidebar-border"
title="Trash"
data-testid="trash-button"
>
<Trash2 className="size-4 shrink-0" />
{trashedProjects.length > 0 && (
<span className="absolute -top-[2px] -right-[2px] flex items-center justify-center w-5 h-5 text-[10px] font-medium rounded-full text-brand-500">
{trashedProjects.length > 9 ? "9+" : trashedProjects.length}
</span>
)}
</button>
</div>
)}
@@ -581,7 +670,7 @@ export function Sidebar() {
isActive
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
!sidebarOpen && "justify-center"
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? item.label : undefined}
data-testid={`nav-${item.id}`}
@@ -599,7 +688,7 @@ export function Sidebar() {
/>
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1",
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
@@ -665,7 +754,7 @@ export function Sidebar() {
/>
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1",
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
@@ -691,6 +780,91 @@ export function Sidebar() {
</button>
</div>
</div>
<Dialog open={showTrashDialog} onOpenChange={setShowTrashDialog}>
<DialogContent className="bg-popover border-border max-w-2xl">
<DialogHeader>
<DialogTitle>Trash</DialogTitle>
<DialogDescription className="text-muted-foreground">
Restore projects to the sidebar or delete their folders using your
system Trash.
</DialogDescription>
</DialogHeader>
{trashedProjects.length === 0 ? (
<p className="text-sm text-muted-foreground">Trash is empty.</p>
) : (
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1">
{trashedProjects.map((project) => (
<div
key={project.id}
className="flex items-start justify-between gap-3 rounded-md border border-sidebar-border bg-sidebar-accent/20 p-3"
>
<div className="space-y-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{project.name}
</p>
<p className="text-xs text-muted-foreground break-all">
{project.path}
</p>
<p className="text-[11px] text-muted-foreground/80">
Trashed {new Date(project.trashedAt).toLocaleString()}
</p>
</div>
<div className="flex flex-col gap-2 shrink-0">
<Button
size="sm"
variant="secondary"
onClick={() => handleRestoreProject(project.id)}
data-testid={`restore-project-${project.id}`}
>
<Undo2 className="h-3.5 w-3.5 mr-1.5" />
Restore
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDeleteProjectFromDisk(project)}
disabled={activeTrashId === project.id}
data-testid={`delete-project-disk-${project.id}`}
>
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
{activeTrashId === project.id
? "Deleting..."
: "Delete from disk"}
</Button>
<Button
size="sm"
variant="ghost"
className="text-muted-foreground hover:text-foreground"
onClick={() => deleteTrashedProject(project.id)}
data-testid={`remove-project-${project.id}`}
>
<X className="h-3.5 w-3.5 mr-1.5" />
Remove from list
</Button>
</div>
</div>
))}
</div>
)}
<DialogFooter className="flex justify-between">
<Button variant="ghost" onClick={() => setShowTrashDialog(false)}>
Close
</Button>
{trashedProjects.length > 0 && (
<Button
variant="outline"
onClick={handleEmptyTrash}
disabled={isEmptyingTrash}
data-testid="empty-trash"
>
{isEmptyingTrash ? "Clearing..." : "Empty Trash"}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</aside>
);
}

View File

@@ -237,7 +237,7 @@ export function AgentOutputModal({
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent
className="w-[90vw] max-w-[90vw] max-h-[80vh] flex flex-col"
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
data-testid="agent-output-modal"
>
<DialogHeader className="flex-shrink-0">

View File

@@ -416,17 +416,35 @@ export function BoardView() {
}
}, [showAddDialog, defaultSkipTests]);
// Listen for auto mode feature completion and reload features
// Listen for auto mode feature completion and errors to reload features
useEffect(() => {
const api = getElectronAPI();
if (!api?.autoMode) return;
const { removeRunningTask } = useAppStore.getState();
const unsubscribe = api.autoMode.onEvent((event) => {
if (event.type === "auto_mode_feature_complete") {
// Reload features when a feature is completed
console.log("[Board] Feature completed, reloading features...");
loadFeatures();
} else if (event.type === "auto_mode_error") {
// Reload features when an error occurs (feature moved to waiting_approval)
console.log(
"[Board] Feature error, reloading features...",
event.error
);
// Remove from running tasks so it moves to the correct column
if (event.featureId) {
removeRunningTask(event.featureId);
}
loadFeatures();
// Show error toast
toast.error("Agent encountered an error", {
description: event.error || "Check the logs for details",
});
}
});
@@ -478,7 +496,10 @@ export function BoardView() {
const checkAllContexts = async () => {
// 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"
(f) =>
f.status === "in_progress" ||
f.status === "waiting_approval" ||
f.status === "verified"
);
const contextChecks = await Promise.all(
featuresWithPotentialContext.map(async (f) => ({
@@ -520,6 +541,7 @@ export function BoardView() {
summary: f.summary,
model: f.model,
thinkingLevel: f.thinkingLevel,
error: f.error,
}));
await api.writeFile(
`${currentProject.path}/.automaker/feature_list.json`,
@@ -754,7 +776,9 @@ export function BoardView() {
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}`);
console.log(
`[Board] Context file not found or already deleted for feature ${featureId}`
);
}
}
@@ -767,11 +791,17 @@ export function BoardView() {
await api.deleteFile(imagePathObj.path);
console.log(`[Board] Deleted image: ${imagePathObj.path}`);
} catch (error) {
console.error(`[Board] Failed to delete image ${imagePathObj.path}:`, error);
console.error(
`[Board] Failed to delete image ${imagePathObj.path}:`,
error
);
}
}
} catch (error) {
console.error(`[Board] Error deleting images for feature ${featureId}:`, error);
console.error(
`[Board] Error deleting images for feature ${featureId}:`,
error
);
}
}
@@ -2009,10 +2039,15 @@ export function BoardView() {
try {
const contextPath = `${currentProject.path}/.automaker/agents-context/${feature.id}.md`;
await api.deleteFile(contextPath);
console.log(`[Board] Deleted agent context for feature ${feature.id}`);
console.log(
`[Board] Deleted agent context for feature ${feature.id}`
);
} catch (error) {
// Context file might not exist, which is fine
console.debug("[Board] No context file to delete for feature:", feature.id);
console.debug(
"[Board] No context file to delete for feature:",
feature.id
);
}
// Remove the feature

View File

@@ -39,7 +39,6 @@ import {
RotateCcw,
StopCircle,
Hand,
ArrowLeft,
MessageSquare,
GitCommit,
Cpu,
@@ -49,6 +48,7 @@ import {
Expand,
FileText,
MoreVertical,
AlertCircle,
} from "lucide-react";
import { CountUpTimer } from "@/components/ui/count-up-timer";
import { getElectronAPI } from "@/lib/electron";
@@ -199,7 +199,10 @@ export function KanbanCard({
"cursor-grab active:cursor-grabbing transition-all backdrop-blur-sm border-border relative kanban-card-content",
isDragging && "opacity-50 scale-105 shadow-lg",
isCurrentAutoTask &&
"border-running-indicator border-2 shadow-running-indicator/50 shadow-lg animate-pulse"
"border-running-indicator border-2 shadow-running-indicator/50 shadow-lg animate-pulse",
feature.error &&
!isCurrentAutoTask &&
"border-red-500 border-2 shadow-red-500/30 shadow-lg"
)}
data-testid={`kanban-card-${feature.id}`}
{...attributes}
@@ -214,7 +217,7 @@ export function KanbanCard({
</div>
)}
{/* Skip Tests indicator badge */}
{feature.skipTests && (
{feature.skipTests && !feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
@@ -228,6 +231,21 @@ export function KanbanCard({
<span>Manual</span>
</div>
)}
{/* Error indicator badge */}
{feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
shortcutKey ? "top-2 left-10" : "top-2 left-2",
"bg-red-500/20 border border-red-500/50 text-red-400"
)}
data-testid={`error-badge-${feature.id}`}
title={feature.error}
>
<AlertCircle className="w-3 h-3" />
<span>Errored</span>
</div>
)}
<CardHeader className="p-3 pb-2">
{isCurrentAutoTask && (
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
@@ -255,6 +273,28 @@ export function KanbanCard({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
data-testid={`edit-feature-${feature.id}`}
>
<Edit className="w-3 h-3 mr-2" />
Edit
</DropdownMenuItem>
{onViewOutput && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
data-testid={`view-logs-${feature.id}`}
>
<FileText className="w-3 h-3 mr-2" />
Logs
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
@@ -565,55 +605,10 @@ export function KanbanCard({
Logs
</Button>
)}
{/* Move back button for skipTests verified features */}
{feature.skipTests && onMoveBackToInProgress && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-yellow-500 hover:text-yellow-500 hover:bg-yellow-500/10"
onClick={(e) => {
e.stopPropagation();
onMoveBackToInProgress();
}}
data-testid={`move-back-${feature.id}`}
>
<ArrowLeft className="w-3 h-3 mr-1" />
Back
</Button>
)}
<Button
variant="ghost"
size="sm"
className="flex-1 h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
data-testid={`edit-feature-${feature.id}`}
>
<Edit className="w-3 h-3 mr-1" />
Edit
</Button>
</>
)}
{!isCurrentAutoTask && feature.status === "waiting_approval" && (
<>
{/* Logs button if context exists */}
{hasContext && onViewOutput && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
data-testid={`view-output-waiting-${feature.id}`}
>
<FileText className="w-3 h-3 mr-1" />
Logs
</Button>
)}
{/* Follow-up prompt button */}
{onFollowUp && (
<Button
@@ -665,19 +660,6 @@ export function KanbanCard({
Logs
</Button>
)}
<Button
variant="ghost"
size="sm"
className="flex-1 h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
data-testid={`edit-feature-${feature.id}`}
>
<Edit className="w-3 h-3 mr-1" />
Edit
</Button>
</>
)}
</div>

View File

@@ -1,10 +1,11 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import { useAppStore } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import {
Settings,
Key,
@@ -32,9 +33,28 @@ import {
Square,
Maximize2,
FlaskConical,
Trash2,
Folder,
} from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
// Navigation items for the side panel
const NAV_ITEMS = [
{ id: "api-keys", label: "API Keys", icon: Key },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
{ id: "danger", label: "Danger Zone", icon: Trash2 },
];
export function SettingsView() {
const {
@@ -47,6 +67,8 @@ export function SettingsView() {
setKanbanCardDetailLevel,
defaultSkipTests,
setDefaultSkipTests,
currentProject,
moveProjectToTrash,
} = useAppStore();
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
const [googleKey, setGoogleKey] = useState(apiKeys.google);
@@ -101,6 +123,9 @@ export function SettingsView() {
success: boolean;
message: string;
} | null>(null);
const [activeSection, setActiveSection] = useState("api-keys");
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setAnthropicKey(apiKeys.anthropic);
@@ -131,6 +156,52 @@ export function SettingsView() {
checkCliStatus();
}, []);
// Track scroll position to highlight active nav item
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
const handleScroll = () => {
const sections = NAV_ITEMS.map((item) => ({
id: item.id,
element: document.getElementById(item.id),
})).filter((s) => s.element);
const containerRect = container.getBoundingClientRect();
const scrollTop = container.scrollTop;
for (let i = sections.length - 1; i >= 0; i--) {
const section = sections[i];
if (section.element) {
const rect = section.element.getBoundingClientRect();
const relativeTop = rect.top - containerRect.top + scrollTop;
if (scrollTop >= relativeTop - 100) {
setActiveSection(section.id);
break;
}
}
}
};
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, []);
const scrollToSection = useCallback((sectionId: string) => {
const element = document.getElementById(sectionId);
if (element && scrollContainerRef.current) {
const container = scrollContainerRef.current;
const containerRect = container.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const relativeTop = elementRect.top - containerRect.top + container.scrollTop;
container.scrollTo({
top: relativeTop - 24,
behavior: "smooth",
});
}
}, []);
const handleTestConnection = async () => {
setTestingConnection(true);
setTestResult(null);
@@ -157,7 +228,7 @@ export function SettingsView() {
message: data.error || "Failed to connect to Claude API.",
});
}
} catch (error) {
} catch {
setTestResult({
success: false,
message: "Network error. Please check your connection.",
@@ -193,7 +264,7 @@ export function SettingsView() {
message: data.error || "Failed to connect to Gemini API.",
});
}
} catch (error) {
} catch {
setGeminiTestResult({
success: false,
message: "Network error. Please check your connection.",
@@ -246,7 +317,7 @@ export function SettingsView() {
});
}
}
} catch (error) {
} catch {
setOpenaiTestResult({
success: false,
message: "Network error. Please check your connection.",
@@ -288,23 +359,60 @@ export function SettingsView() {
</div>
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* API Keys Section */}
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Key className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
API Keys
</h2>
{/* Content Area with Sidebar */}
<div className="flex-1 flex overflow-hidden">
{/* Sticky Side Navigation */}
<nav className="hidden lg:block w-48 shrink-0 border-r border-border bg-card/50 backdrop-blur-sm">
<div className="sticky top-0 p-4 space-y-1">
{NAV_ITEMS.filter((item) => item.id !== "danger" || currentProject).map(
(item) => {
const Icon = item.icon;
const isActive = activeSection === item.id;
return (
<button
key={item.id}
onClick={() => scrollToSection(item.id)}
className={cn(
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all text-left",
isActive
? "bg-brand-500/10 text-brand-500 border border-brand-500/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
>
<Icon
className={cn(
"w-4 h-4 shrink-0",
isActive ? "text-brand-500" : ""
)}
/>
<span className="truncate">{item.label}</span>
</button>
);
}
)}
</div>
</nav>
{/* Scrollable Content */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* API Keys Section */}
<div
id="api-keys"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Key className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
API Keys
</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure your AI provider API keys. Keys are stored locally in
your browser.
</p>
</div>
<p className="text-sm text-muted-foreground">
Configure your AI provider API keys. Keys are stored locally in
your browser.
</p>
</div>
<div className="p-6 space-y-6">
{/* Claude/Anthropic API Key */}
<div className="space-y-3">
@@ -592,7 +700,7 @@ export function SettingsView() {
<div className="text-sm">
<p className="font-medium text-yellow-500">Security Notice</p>
<p className="text-yellow-500/80 text-xs mt-1">
API keys are stored in your browser's local storage. Never
API keys are stored in your browser&apos;s local storage. Never
share your API keys or commit them to version control.
</p>
</div>
@@ -780,7 +888,10 @@ export function SettingsView() {
)}
{/* Appearance Section */}
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div
id="appearance"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Palette className="w-5 h-5 text-brand-500" />
@@ -958,7 +1069,10 @@ export function SettingsView() {
</div>
{/* Kanban Card Display Section */}
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div
id="kanban"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<LayoutGrid className="w-5 h-5 text-brand-500" />
@@ -1036,7 +1150,10 @@ export function SettingsView() {
</div>
{/* Feature Defaults Section */}
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div
id="defaults"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<FlaskConical className="w-5 h-5 text-brand-500" />
@@ -1078,6 +1195,51 @@ export function SettingsView() {
</div>
</div>
{/* Delete Project Section - Only show when a project is selected */}
{currentProject && (
<div
id="danger"
className="rounded-xl border border-destructive/30 bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-destructive/30">
<div className="flex items-center gap-2 mb-2">
<Trash2 className="w-5 h-5 text-destructive" />
<h2 className="text-lg font-semibold text-foreground">
Danger Zone
</h2>
</div>
<p className="text-sm text-muted-foreground">
Permanently remove this project from Automaker.
</p>
</div>
<div className="p-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{currentProject.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{currentProject.path}
</p>
</div>
</div>
<Button
variant="destructive"
onClick={() => setShowDeleteDialog(true)}
data-testid="delete-project-button"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete Project
</Button>
</div>
</div>
</div>
)}
{/* Save Button */}
<div className="flex items-center gap-4">
<Button
@@ -1105,6 +1267,64 @@ export function SettingsView() {
</div>
</div>
</div>
</div>
{/* Delete Project Confirmation Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent className="bg-popover border-border max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="w-5 h-5 text-destructive" />
Delete Project
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Are you sure you want to move this project to Trash?
</DialogDescription>
</DialogHeader>
{currentProject && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{currentProject.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{currentProject.path}
</p>
</div>
</div>
)}
<p className="text-sm text-muted-foreground">
The folder will remain on disk until you permanently delete it from Trash.
</p>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="ghost"
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => {
if (currentProject) {
moveProjectToTrash(currentProject.id);
setShowDeleteDialog(false);
}
}}
data-testid="confirm-delete-project"
>
<Trash2 className="w-4 h-4 mr-2" />
Move to Trash
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -284,11 +284,7 @@ export function WelcomeView() {
<div className="px-8 py-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl flex items-center justify-center">
<img
src="/icon_gold.png"
alt="Automaker Logo"
className="w-10 h-10"
/>
<img src="/logo.png" alt="Automaker Logo" className="w-10 h-10" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">

View File

@@ -76,6 +76,7 @@ export interface ElectronAPI {
exists: (filePath: string) => Promise<boolean>;
stat: (filePath: string) => Promise<StatResult>;
deleteFile: (filePath: string) => Promise<WriteResult>;
trashItem?: (filePath: string) => Promise<WriteResult>;
getPath: (name: string) => Promise<string>;
saveImageToTemp?: (data: string, filename: string, mimeType: string) => Promise<SaveImageResult>;
autoMode?: AutoModeAPI;
@@ -150,6 +151,7 @@ const mockFeatures = [
const STORAGE_KEYS = {
PROJECTS: "automaker_projects",
CURRENT_PROJECT: "automaker_current_project",
TRASHED_PROJECTS: "automaker_trashed_projects",
} as const;
// Mock file system using localStorage
@@ -370,6 +372,10 @@ export const getElectronAPI = (): ElectronAPI => {
return { success: true };
},
trashItem: async () => {
return { success: true };
},
getPath: async (name: string) => {
if (name === "userData") {
return "/mock/userData";
@@ -828,6 +834,11 @@ export interface Project {
lastOpened?: string;
}
export interface TrashedProject extends Project {
trashedAt: string;
deletedFromDisk?: boolean;
}
export const getStoredProjects = (): Project[] => {
if (typeof window === "undefined") return [];
const stored = localStorage.getItem(STORAGE_KEYS.PROJECTS);
@@ -869,3 +880,14 @@ export const removeProject = (projectId: string): void => {
const projects = getStoredProjects().filter((p) => p.id !== projectId);
saveProjects(projects);
};
export const getStoredTrashedProjects = (): TrashedProject[] => {
if (typeof window === "undefined") return [];
const stored = localStorage.getItem(STORAGE_KEYS.TRASHED_PROJECTS);
return stored ? JSON.parse(stored) : [];
};
export const saveTrashedProjects = (projects: TrashedProject[]): void => {
if (typeof window === "undefined") return;
localStorage.setItem(STORAGE_KEYS.TRASHED_PROJECTS, JSON.stringify(projects));
};

View File

@@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { Project } from "@/lib/electron";
import type { Project, TrashedProject } from "@/lib/electron";
export type ViewMode =
| "welcome"
@@ -107,12 +107,14 @@ export interface Feature {
summary?: string; // Summary of what was done/modified by the agent
model?: AgentModel; // Model to use for this feature (defaults to opus)
thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none)
error?: string; // Error message if the agent errored during processing
}
export interface AppState {
// Project state
projects: Project[];
currentProject: Project | null;
trashedProjects: TrashedProject[];
// View state
currentView: ViewMode;
@@ -175,6 +177,10 @@ export interface AppActions {
setProjects: (projects: Project[]) => void;
addProject: (project: Project) => void;
removeProject: (projectId: string) => void;
moveProjectToTrash: (projectId: string) => void;
restoreTrashedProject: (projectId: string) => void;
deleteTrashedProject: (projectId: string) => void;
emptyTrash: () => void;
setCurrentProject: (project: Project | null) => void;
reorderProjects: (oldIndex: number, newIndex: number) => void;
@@ -237,6 +243,7 @@ export interface AppActions {
const initialState: AppState = {
projects: [],
currentProject: null,
trashedProjects: [],
currentView: "welcome",
sidebarOpen: true,
theme: "dark",
@@ -291,6 +298,82 @@ export const useAppStore = create<AppState & AppActions>()(
set({ projects: get().projects.filter((p) => p.id !== projectId) });
},
moveProjectToTrash: (projectId) => {
const project = get().projects.find((p) => p.id === projectId);
if (!project) return;
const remainingProjects = get().projects.filter(
(p) => p.id !== projectId
);
const existingTrash = get().trashedProjects.filter(
(p) => p.id !== projectId
);
const trashedProject: TrashedProject = {
...project,
trashedAt: new Date().toISOString(),
deletedFromDisk: false,
};
const isCurrent = get().currentProject?.id === projectId;
set({
projects: remainingProjects,
trashedProjects: [trashedProject, ...existingTrash],
currentProject: isCurrent ? null : get().currentProject,
currentView: isCurrent ? "welcome" : get().currentView,
});
},
restoreTrashedProject: (projectId) => {
const trashed = get().trashedProjects.find((p) => p.id === projectId);
if (!trashed) return;
const remainingTrash = get().trashedProjects.filter(
(p) => p.id !== projectId
);
const existingProjects = get().projects;
const samePathProject = existingProjects.find(
(p) => p.path === trashed.path
);
const projectsWithoutId = existingProjects.filter(
(p) => p.id !== projectId
);
// If a project with the same path already exists, keep it and just remove from trash
if (samePathProject) {
set({
trashedProjects: remainingTrash,
currentProject: samePathProject,
currentView: "board",
});
return;
}
const restoredProject: Project = {
id: trashed.id,
name: trashed.name,
path: trashed.path,
lastOpened: new Date().toISOString(),
};
set({
trashedProjects: remainingTrash,
projects: [...projectsWithoutId, restoredProject],
currentProject: restoredProject,
currentView: "board",
});
},
deleteTrashedProject: (projectId) => {
set({
trashedProjects: get().trashedProjects.filter(
(p) => p.id !== projectId
),
});
},
emptyTrash: () => set({ trashedProjects: [] }),
reorderProjects: (oldIndex, newIndex) => {
const projects = [...get().projects];
const [movedProject] = projects.splice(oldIndex, 1);
@@ -517,6 +600,7 @@ export const useAppStore = create<AppState & AppActions>()(
partialize: (state) => ({
projects: state.projects,
currentProject: state.currentProject,
trashedProjects: state.trashedProjects,
currentView: state.currentView,
theme: state.theme,
sidebarOpen: state.sidebarOpen,