From 15981c8e1b7c892d8e793c0e58a3b4b22cc24b44 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 10 Dec 2025 19:11:36 -0500 Subject: [PATCH] feat: restructure feature management and update project files - Introduced a new `package-lock.json` to manage dependencies. - Removed obsolete `.automaker/feature_list.json` and replaced it with a new structure under `.automaker/features/{id}/feature.json` for better organization. - Updated various components to utilize the new features API for managing features, including creation, updates, and deletions. - Enhanced the UI to reflect changes in feature management, including updates to the sidebar and board view. - Improved documentation and comments throughout the codebase to clarify the new feature management process. --- .automaker/.gitignore | 13 - .../feature-1765333578668-qbzk7xihs.md | 8 - .../feature-1765334118538-zc6n2ngu8.md | 4 - .automaker/app_spec.txt | 8 +- .automaker/categories.json | 3 +- .automaker/feature_list.json | 395 ----------- ...h4_Screenshot_2025-12-10_at_5.33.08_PM.png | Bin 0 -> 38219 bytes ...60_Screenshot_2025-12-10_at_6.09.19_PM.png | Bin 0 -> 37452 bytes ...8-implement-profile-view-and-in-the-sideba | 1 - ...9-so-we-added-ai-profiles-add-a-default-op | 1 - app/electron/agent-service.js | 27 +- app/electron/auto-mode-service.js | 2 +- app/electron/main.js | 194 +++++- app/electron/preload.js | 119 +++- app/electron/services/context-manager.js | 144 ++-- app/electron/services/feature-loader.js | 543 ++++++++++----- app/electron/services/mcp-server-factory.js | 6 +- app/electron/services/mcp-server-stdio.js | 2 +- app/electron/services/prompt-builder.js | 105 +-- .../services/spec-regeneration-service.js | 72 +- app/electron/services/worktree-manager.js | 11 +- app/package-lock.json | 73 ++ app/package.json | 2 + app/public/sounds/ding.mp3 | Bin 0 -> 22320 bytes app/src/app/page.tsx | 52 +- app/src/components/layout/sidebar.tsx | 2 +- .../components/ui/category-autocomplete.tsx | 277 ++------ app/src/components/ui/command.tsx | 184 +++++ app/src/components/ui/popover.tsx | 48 ++ .../components/views/agent-output-modal.tsx | 35 +- app/src/components/views/agent-tools-view.tsx | 2 +- app/src/components/views/agent-view.tsx | 8 +- app/src/components/views/analysis-view.tsx | 27 +- app/src/components/views/board-view.tsx | 321 ++++++--- .../views/feature-suggestions-dialog.tsx | 11 +- app/src/components/views/interview-view.tsx | 42 +- app/src/components/views/kanban-card.tsx | 82 ++- app/src/components/views/settings-view.tsx | 193 ++++-- app/src/components/views/spec-view.tsx | 2 +- app/src/lib/electron.ts | 640 ++++++++++++++---- app/src/lib/project-init.ts | 32 +- app/src/store/app-store.ts | 14 +- app/tests/utils.ts | 419 +++++++----- package-lock.json | 6 + 44 files changed, 2486 insertions(+), 1644 deletions(-) delete mode 100644 .automaker/.gitignore delete mode 100644 .automaker/agents-context/feature-1765333578668-qbzk7xihs.md delete mode 100644 .automaker/agents-context/feature-1765334118538-zc6n2ngu8.md delete mode 100644 .automaker/feature_list.json create mode 100644 .automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png create mode 100644 .automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png delete mode 160000 .automaker/worktrees/176536627888-implement-profile-view-and-in-the-sideba delete mode 160000 .automaker/worktrees/176536775869-so-we-added-ai-profiles-add-a-default-op create mode 100644 app/public/sounds/ding.mp3 create mode 100644 app/src/components/ui/command.tsx create mode 100644 app/src/components/ui/popover.tsx create mode 100644 package-lock.json diff --git a/.automaker/.gitignore b/.automaker/.gitignore deleted file mode 100644 index c0ba5858..00000000 --- a/.automaker/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# 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/ - -# Launch script - local development script -launch.sh - -.cursor \ No newline at end of file diff --git a/.automaker/agents-context/feature-1765333578668-qbzk7xihs.md b/.automaker/agents-context/feature-1765333578668-qbzk7xihs.md deleted file mode 100644 index 72c3c1eb..00000000 --- a/.automaker/agents-context/feature-1765333578668-qbzk7xihs.md +++ /dev/null @@ -1,8 +0,0 @@ -📋 Planning implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task -⚡ Executing implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task - -❌ Error: Reconnecting... 1/5 -📋 Planning implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task -⚡ Executing implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task -📋 Planning implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task -⚡ Executing implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task diff --git a/.automaker/agents-context/feature-1765334118538-zc6n2ngu8.md b/.automaker/agents-context/feature-1765334118538-zc6n2ngu8.md deleted file mode 100644 index e0d3335d..00000000 --- a/.automaker/agents-context/feature-1765334118538-zc6n2ngu8.md +++ /dev/null @@ -1,4 +0,0 @@ -📋 Planning implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task -⚡ Executing implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task -📋 Planning implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task -⚡ Executing implementation for: For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task diff --git a/.automaker/app_spec.txt b/.automaker/app_spec.txt index e9b2c014..56f3c7ab 100644 --- a/.automaker/app_spec.txt +++ b/.automaker/app_spec.txt @@ -42,14 +42,14 @@ - "Project Ingestion": Analyzes existing codebases to understand structure - Auto-generation of `.automaker/app_spec.txt` based on codebase analysis - - Auto-generation of `.automaker/feature_list.json`: + - Auto-generation of features in `.automaker/features/{id}/feature.json`: - Scans code for implemented features - Creates test cases for existing features - Marks existing features as "passes": true automatically - - Visual representation of `.automaker/feature_list.json` + - Visual representation of features from `.automaker/features/` folder - Columns: Backlog, Planned, In Progress, Review, Verified (Passed), Failed - Drag-and-drop interface to reprioritize tasks - direct editing of feature details (steps, description) from the card @@ -90,7 +90,7 @@ - Sidebar: Project List, Settings, Logs, Plugins - Main Content: - **Spec View**: Split editor for `.automaker/app_spec.txt` - - **Board View**: Kanban board for `.automaker/feature_list.json` + - **Board View**: Kanban board for `.automaker/features/` folder - **Code View**: Read-only Monaco editor to see what the agent is writing - **Agent View**: Chat-like interface showing agent thought process and tool usage. Also used for the "New Project Interview". @@ -123,7 +123,7 @@ - Build Kanban board with drag-and-drop - - Connect Kanban state to `.automaker/feature_list.json` filesystem + - Connect Kanban state to `.automaker/features/` filesystem - Implement "Run Feature" capability - Integrate standard prompts library diff --git a/.automaker/categories.json b/.automaker/categories.json index 77daa7b2..14e8f098 100644 --- a/.automaker/categories.json +++ b/.automaker/categories.json @@ -4,5 +4,6 @@ "Kanban", "Other", "Settings", - "Uncategorized" + "Uncategorized", + "ka" ] \ No newline at end of file diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json deleted file mode 100644 index 03770f5d..00000000 --- a/.automaker/feature_list.json +++ /dev/null @@ -1,395 +0,0 @@ -[ - { - "id": "feature-1765387670653-bl83444lj", - "category": "Kanban", - "description": "In the output logs of the proc agent output in the file diffs Can you add a scroll bar so it actually scroll to see all these new styles right now it seems like I can't scroll", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T17:42:09.158Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed scrolling for file diffs in agent output modal. Changed approach: parent container (agent-output-modal.tsx) now handles scrolling with overflow-y-auto, while GitDiffPanel uses natural height without flex-based scrolling. Modified: agent-output-modal.tsx (line 304), git-diff-panel.tsx (lines 461, 500, 525, 614).", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765387746902-na752mp1y", - "category": "Kanban", - "description": "When the add feature modal pops up, make sure that the description is always the main focus. When it first loads up. Do not focus the prompt tab, which is currently doing this.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T17:29:13.854Z", - "imagePaths": [], - "skipTests": true, - "summary": "Added autoFocus prop to DescriptionImageDropZone component. Modified: description-image-dropzone.tsx (added autoFocus prop support), board-view.tsx (enabled autoFocus on add feature modal). Now the description textarea receives focus when the modal opens instead of the prompt tab.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765388139100-ln31jgp5n", - "category": "Uncategorized", - "description": "Can you add a disclaimer .md file to this project saying that this uses a bunch of AI related tooling which could have access to your operating system and change and delete files and so use at your own risk. We tried to check it for security of vulnerability to make sure it's good. But you assume the risk and you should be reviewing the code yourself before you try to run it. And also sandboxing this so it doesn't have access to your whole operating system like using Docker to sandbox before you run it or use a virtual machine to sandbox it. and that we do not recommend running locally on your computer due to the risk of it having access to everything on your computer.\n\nUpdate or read me with a short paragraph overview/description at the top followed by a disclaimer section in red that points to the disclaimer file with the same disclaimer information.\n\nThen a section that lists out all the features of cool emojis.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T17:35:40.700Z", - "imagePaths": [], - "skipTests": true, - "summary": "Created DISCLAIMER.md with comprehensive security warnings about AI tooling risks and sandboxing recommendations. Updated README.md with project overview, red caution disclaimer section linking to DISCLAIMER.md, and features list with emojis covering all major functionality (Kanban, AI agents, multi-model support, etc.).", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765388388144-oa1dewze9", - "category": "Uncategorized", - "description": "Please fix the styling of the hotkeys to be more using the theme colors. Notice that they're kind of gray. I would rather than have some type of like light green if they're not active and then the brighter green if they are active and also the add feature but in the top right it's not very legible. So fix the accessibility of the hotkey but also keep it within the theme. You might just have to change the text inside of it to be bright green.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T17:40:02.745Z", - "imagePaths": [ - { - "id": "img-1765388352835-dgx4ishp0", - "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388352832-6jnbgw8kg_Screenshot_2025-12-10_at_12.39.10_PM.png", - "filename": "Screenshot 2025-12-10 at 12.39.10 PM.png", - "mimeType": "image/png" - }, - { - "id": "img-1765388356955-a0gdovp5b", - "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388356954-d59a65nf9_Screenshot_2025-12-10_at_12.39.15_PM.png", - "filename": "Screenshot 2025-12-10 at 12.39.15 PM.png", - "mimeType": "image/png" - } - ], - "skipTests": true, - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765388402095-x66aduwg3", - "category": "Uncategorized", - "description": "Can you please add some spacing and fix the styling of the hotkey with the command enter and make it so they're both vertically aligned for those icons?", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T17:44:08.667Z", - "imagePaths": [ - { - "id": "img-1765388390408-eefybe95t", - "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388390408-nn320yoyc_Screenshot_2025-12-10_at_12.39.47_PM.png", - "filename": "Screenshot 2025-12-10 at 12.39.47 PM.png", - "mimeType": "image/png" - } - ], - "skipTests": true, - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765388662444-as3hqn7be", - "category": "Uncategorized", - "description": "Fix the styling on all the buttons when I hover over them with my mouse they never change to a click mouse cursor. In order they seem to show any type of like hover state changes, if they do, at least for the certain game I'm using, it's not very obvious that you're hovering over the button.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T17:45:59.666Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed hover cursor styling on all interactive elements. Modified: button.tsx (added cursor-pointer to base styles), dropdown-menu.tsx (added cursor-pointer to all menu items), checkbox.tsx (added cursor-pointer), tabs.tsx (added cursor-pointer to triggers), dialog.tsx (added cursor-pointer to close button), slider.tsx (added cursor-grab to thumb, cursor-pointer to track), globals.css (added global CSS rules for clickable elements to ensure consistent cursor behavior).", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765388693856-yx1dk1acj", - "category": "Kanban", - "description": "The tabs in the add new feature modal for the prompt model and testing tabs. They don't seem to look like tabs when I'm on a certain theme. Can you verify that those are hooked into the theme? And make sure that the active one is colored differently than the unactive ones. Keep the primary colors when doing this.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T17:46:00.019Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed tabs component theme integration. Modified: tabs.tsx. Changes: (1) Added visible border to TabsList container using theme's border color, (2) Changed inactive tab text to foreground/70 for better contrast, (3) Enhanced active tab with shadow-md and semi-transparent primary border, (4) Improved hover state with full accent background. Active tabs now properly use bg-primary/text-primary-foreground which adapts to each theme.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765388754462-bek0flvkj", - "category": "Uncategorized", - "description": "There's a strange issue when I when when these agents are like doing things it seems like it completely refreshes the whole Kanban board and there's like a black flash. Can you verify that the data loading does not cause the entire component to refresh? Maybe there's an issue with the react effect or how the component is rendered maybe we need some used memos or something but it shouldn't refresh the whole page it should just like update the individual cards when they change.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T17:47:20.170Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed Kanban board flash/refresh issue. Changes: (1) board-view.tsx - Added isInitialLoadRef to only show loading spinner on initial load, not on feature reloads; memoized column features with useMemo to prevent recalculation on every render. (2) kanban-card.tsx - Wrapped with React.memo to prevent unnecessary re-renders. (3) kanban-column.tsx - Wrapped with React.memo for performance. The flash was caused by loadFeatures setting isLoading=true on every reload, which caused the entire board to unmount and show a loading spinner.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765388793845-yhluf0sry", - "category": "Uncategorized", - "description": "Add in the ability so that every project can have its own selected theme. This will allow me to have different projects have different themes so I can easily differentiate when I have one project selected or not.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T18:00:33.814Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed per-project theme support. Modified: settings-view.tsx (now saves theme to project when project is selected, shows label indicating scope), page.tsx (computes effectiveTheme from currentProject?.theme || theme), app-store.ts (added setProjectTheme action, theme property on Project interface). When a project is selected, changing theme in Settings saves to that project only.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765389333728-y74hmz2yp", - "category": "Agent Runner", - "description": "On the Agent Runner, I took a screenshot and dropped it into the text area and after a certain amount of time, it's like the image preview just completely went away. Can you debug and fix this on the Agent Runner?", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T18:11:17.561Z", - "imagePaths": [], - "skipTests": true, - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765389352488-j9bez5ztx", - "category": "Kanban", - "description": "It seems like the category typehead is no longer working. Can you double check that code didn't break? It should have kept track of categories inside of the categories.json file inside the .automaker folder when adding new features modal", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T18:17:22.274Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed category typeahead dropdown being clipped by overflow containers. Modified: category-autocomplete.tsx. Changed dropdown to use React Portal (createPortal) to render to document.body with fixed positioning and z-index 9999. Added scroll/resize position tracking to keep dropdown aligned with input.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765389420151-jzdsjzn9u", - "category": "Kanban", - "description": "Add in the ability to just click and drag a card from the waiting approval directly into the verify column as I can usually just commit it manually if I want to.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T18:05:08.252Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed drag-and-drop from waiting_approval to verified column. The issue was condition ordering in handleDragEnd - the skipTests check was intercepting waiting_approval features before they could be handled. Moved waiting_approval status check before skipTests check in board-view.tsx:731-752. Also updated agent memory with this lesson.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765389468077-9x3vt1yjq", - "category": "Uncategorized", - "description": "The commit functionality on the waiting approval cards doesn't seem to work. It just committed everything in my working copy for git. I think I should be a little bit more intelligent and figure out what files it changed for that AI session and then only try to git add those individual files and commit those. Right now it just basically did a git add all and committed those. Re-factor the prompting or figure out a way to make it so it's more specific on what it's going to commit with the future change.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T18:17:22.580Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed commit functionality to only commit files changed during the AI session, not all working directory changes. Added git state tracking in context-manager.js (saveInitialGitState, getFilesChangedDuringSession methods) and updated commit prompt in feature-executor.js to use specific file lists instead of 'git add .'", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765389502705-6deep7mvi", - "category": "Uncategorized", - "description": "I'm noticing that a lot of buttons in the UI, especially the ones that are submitting, are either missing the submit hotkey or they're not styled properly. Look at the add feature submit button that's on the add feature modal and abstract away a submit button so that on every single page that needs to submit something I can reuse this type of hotkey functionality. In fact, every single button should be abstracted enough where I can provide a hotkey and it will automatically listen if I press that hotkey when it's in view.", - "steps": [], - "status": "waiting_approval", - "startedAt": "2025-12-10T19:03:41.338Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed duplicate hotkey listener issue. When HotkeyButton was used with simple keys (N, F, G) that were already handled by useKeyboardShortcuts, it created duplicate listeners. Added hotkeyActive={false} to HotkeyButton instances in board-view.tsx (Add Feature, Start Next), context-view.tsx (Add File), profiles-view.tsx (New Profile), and session-manager.tsx (New) where useKeyboardShortcuts already handles the hotkey. Also updated memory.md with this lesson learned.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765389772166-an3yk3kpo", - "category": "Uncategorized", - "description": "Can you add some more padding to the bottom of the settings panel? Notice that I can't scroll down all the way. And that doesn't highlight the left sub navigation to highlight it pink when I'm on that section. I should be able to scroll a bit further and just have like blank space at the bottom. So I can eventually get to that actual section.", - "steps": [], - "status": "verified", - "imagePaths": [ - { - "id": "img-1765389750685-jhq6rcidc", - "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765389750683-mqb0j7a3z_Screenshot_2025-12-10_at_1.02.26_PM.png", - "filename": "Screenshot 2025-12-10 at 1.02.26 PM.png", - "mimeType": "image/png" - } - ], - "skipTests": true, - "summary": "Added bottom padding (pb-96) to settings panel content area to allow scrolling past last section. Improved scroll detection to highlight the last navigation item when scrolled to bottom. Modified: settings-view.tsx", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765389829239-bbk596u6z", - "category": "Uncategorized", - "description": "Add some type of XML highlighting to the spec editor view. Right now it's just all grayscale and it's kind of ugly to look at. And try to make the syntax highlighting match the current selected theme.", - "steps": [], - "status": "verified", - "imagePaths": [], - "skipTests": true, - "summary": "Added XML syntax highlighting to spec editor view. Created: xml-syntax-editor.tsx component with custom XML tokenizer and theme-aware syntax highlighting. Modified: spec-view.tsx to use new editor, globals.css with 500+ lines of theme-specific syntax highlighting colors for all 12 themes (light, dark, retro, dracula, nord, monokai, tokyonight, solarized, gruvbox, catppuccin, onedark, synthwave). Features: highlights tag brackets, tag names, attribute names, attribute values, comments, CDATA, DOCTYPE. Tab key indentation supported.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765389859334-si9ivtehw", - "category": "Uncategorized", - "description": "Add a search bar to the top of the Kanban column that allows me to search the filter down just to show the cards I'm interested in by keyword.", - "steps": [], - "status": "verified", - "startedAt": "2025-12-10T18:09:26.193Z", - "imagePaths": [], - "skipTests": true, - "summary": "Added forward slash (/) keyboard shortcut to focus search input. Modified: board-view.tsx - added searchInputRef, registered '/' shortcut in boardShortcuts, updated placeholder to show hint '(Press / to focus)'", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765390022638-nalulsdxv", - "category": "Uncategorized", - "description": "In the project select can you actually remove the whole like 1 2 3 4 5 hotkeys instead? Just make it be a type ahead so when I open the panel I just should be able to type in the first letter or two of the project that I want and press enter and that should Just select it for me", - "steps": [], - "status": "verified", - "imagePaths": [], - "skipTests": true, - "summary": "Replaced hotkey-based project selection (1-9) with type-ahead search. Modified: sidebar.tsx. Added search input with filtering, arrow key navigation (↑↓), Enter to select, and visual highlighting. Auto-focuses search when dropdown opens.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765390055621-ewc4w7k5h", - "category": "Uncategorized", - "description": "In the add new feature prompt, instead of disabling the add feature button until we type into the description, keep it enabled. But if you click it, make sure you just show the client side validation and turn the description box in any other required field as red so that the user knows they have to fill it in.", - "steps": [], - "status": "verified", - "imagePaths": [], - "skipTests": true, - "summary": "Added client-side validation for the Add Feature dialog. The Add Feature button is now always enabled. When clicked without a description, it shows a red border around the description field using aria-invalid styling. Modified: board-view.tsx (added descriptionError state, validation in handleAddFeature, error prop passing), description-image-dropzone.tsx (added error prop that sets aria-invalid on textarea).", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765390131625-ymqxr5gln", - "category": "Uncategorized", - "description": "Can you please in the top right of the Kanban board use the show three icons for the Kanban card display formatting. You can look at the settings page to see that there's three different settings that we use for displaying the Kanban card information. But I also just want this to be really quickly accessible at the top right of the Kanban that they can switch between those three toggles. Keep them simple only just icons you don't need to put words in them. Make sure they do have harbor states though, or tooltips I mean.", - "steps": [], - "status": "waiting_approval", - "startedAt": "2025-12-10T18:58:21.431Z", - "imagePaths": [], - "skipTests": true, - "summary": "Moved Kanban card detail toggle icons to search bar row. Modified: board-view.tsx. Three icons (Minimize2, Square, Maximize2) now appear on the right side of the search bar with tooltips and hover states.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765390359456-n0vvdurjb", - "category": "Kanban", - "description": "When the item is in the backlog, do not show the logs. There's no reason for a user to look at the logs if it's in the backlog. So remove the logs button from the card, the Kanban card, if it's in the backlog.", - "steps": [], - "status": "verified", - "imagePaths": [], - "skipTests": true, - "summary": "Removed logs button from Kanban cards when feature is in backlog. Modified: kanban-card.tsx - removed the dedicated logs button section for backlog items (lines 743-761) and added condition to hide logs option in dropdown menu for backlog items.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765390428237-4ekiscpsf", - "category": "Uncategorized", - "description": "On the Kanban search bar, instead of press slash to focus, just use the normal shortcut display button that we've been using everywhere else in the application. Can you keep it consistent, please?", - "steps": [], - "status": "verified", - "imagePaths": [ - { - "id": "img-1765390414226-66vm6cly4", - "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765390414225-7o9wizw90_Screenshot_2025-12-10_at_1.13.32_PM.png", - "filename": "Screenshot 2025-12-10 at 1.13.32 PM.png", - "mimeType": "image/png" - } - ], - "skipTests": true, - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765390699789-uaxtse6hn", - "category": "Uncategorized", - "description": "Please fix the styling of this on the Agent Runner, make it match the theme of the project.", - "steps": [], - "status": "verified", - "imagePaths": [ - { - "id": "img-1765390692809-0hahbe30j", - "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765390692808-u8dgwxx9n_Screenshot_2025-12-10_at_1.18.07_PM.png", - "filename": "Screenshot 2025-12-10 at 1.18.07 PM.png", - "mimeType": "image/png" - } - ], - "skipTests": true, - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765393057026-cjgr70d97", - "category": "Kanban", - "description": "there is a major bug: stopping auto mode should not cancel all running tasks, it should just turn off the auto toggle.", - "steps": [], - "status": "waiting_approval", - "startedAt": "2025-12-10T18:57:56.137Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed auto mode stop to only turn off the toggle, not cancel running tasks. Modified: auto-mode-service.js (removed abort/clear logic from stop()), use-auto-mode.ts (removed clearRunningTasks from stop callback). Running features now complete naturally.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765393405243-xe047s4h5", - "category": "Uncategorized", - "description": "fix the style of the input on the kanban route to add a border around the entire input", - "steps": [], - "status": "waiting_approval", - "startedAt": "2025-12-10T19:08:38.024Z", - "imagePaths": [ - { - "id": "img-1765393386453-nd1qucdne", - "path": "/Users/webdevcody/Workspace/automaker/.automaker/images/1765393386452-bz4q5pbkw_Screenshot_2025-12-10_at_2.03.04_PM.png", - "filename": "Screenshot 2025-12-10 at 2.03.04 PM.png", - "mimeType": "image/png" - } - ], - "skipTests": true, - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765395197833-hkxty2nb9", - "category": "Uncategorized", - "description": "on the agent runner chat window, style the text to match the theme primary or secondary, restyle it to match the selected theme", - "steps": [], - "status": "waiting_approval", - "startedAt": "2025-12-10T19:33:18.742Z", - "imagePaths": [ - { - "id": "img-1765395175327-vdj77vwtb", - "path": "/Users/webdevcody/Workspace/automaker/.automaker/images/1765395175325-plp2txel3_Screenshot_2025-12-10_at_2.32.52_PM.png", - "filename": "Screenshot 2025-12-10 at 2.32.52 PM.png", - "mimeType": "image/png" - } - ], - "skipTests": true, - "summary": "Styled agent chat messages to use theme primary colors. Modified: agent-view.tsx, interview-view.tsx. Assistant messages now have border-l-4 border-primary, text-primary for content and timestamps. Loading/thinking indicators also styled with theme colors.", - "model": "opus", - "thinkingLevel": "none" - }, - { - "id": "feature-1765395217816-22cwdpnu9", - "category": "Uncategorized", - "description": "the add feature shortcut on the add new feature modal does not work anymore, please fix it", - "steps": [], - "status": "waiting_approval", - "startedAt": "2025-12-10T19:35:54.453Z", - "imagePaths": [], - "skipTests": true, - "summary": "Fixed Cmd+Enter shortcut not working in Add Feature modal when input is focused. Modified: hotkey-button.tsx - Changed logic to allow cmdCtrl modifier shortcuts even when typing in input fields, since they are intentional submit actions.", - "model": "opus", - "thinkingLevel": "none" - } -] \ No newline at end of file diff --git a/.automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png b/.automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png new file mode 100644 index 0000000000000000000000000000000000000000..c383cdfe6ae9a8adc576ac712423efe72661e53c GIT binary patch literal 38219 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 H>BPSQ^G4nZ literal 0 HcmV?d00001 diff --git a/.automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png b/.automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png new file mode 100644 index 0000000000000000000000000000000000000000..77e03afafe1559899cbf49c04129da23d61178a1 GIT binary patch literal 37452 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 literal 0 HcmV?d00001 diff --git a/.automaker/worktrees/176536627888-implement-profile-view-and-in-the-sideba b/.automaker/worktrees/176536627888-implement-profile-view-and-in-the-sideba deleted file mode 160000 index a78b6763..00000000 --- a/.automaker/worktrees/176536627888-implement-profile-view-and-in-the-sideba +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a78b6763de82102803fe731662483ade55dd738d diff --git a/.automaker/worktrees/176536775869-so-we-added-ai-profiles-add-a-default-op b/.automaker/worktrees/176536775869-so-we-added-ai-profiles-add-a-default-op deleted file mode 160000 index a78b6763..00000000 --- a/.automaker/worktrees/176536775869-so-we-added-ai-profiles-add-a-default-op +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a78b6763de82102803fe731662483ade55dd738d diff --git a/app/electron/agent-service.js b/app/electron/agent-service.js index 65f1fd33..82d446f5 100644 --- a/app/electron/agent-service.js +++ b/app/electron/agent-service.js @@ -441,20 +441,9 @@ class AgentService { return `You are an AI assistant helping users build software. You are part of the Automaker application, which is designed to help developers plan, design, and implement software projects autonomously. -**🚨 CRITICAL FILE PROTECTION 🚨** - -THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION: -- .automaker/feature_list.json - -**YOU MUST NEVER:** -- Use the Write tool on .automaker/feature_list.json -- Use the Edit tool on .automaker/feature_list.json -- Use any Bash command that writes to .automaker/feature_list.json -- Attempt to read and rewrite .automaker/feature_list.json - -**CATASTROPHIC CONSEQUENCES:** -Directly modifying .automaker/feature_list.json can erase all project features permanently. -This file is managed by specialized tools only. NEVER touch it directly. +**Feature Storage:** +Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. +Use the UpdateFeatureStatus tool to manage features, not direct file edits. Your role is to: - Help users define their project requirements and specifications @@ -462,7 +451,7 @@ Your role is to: - Suggest technical approaches and architectures - Guide them through the development process - Be conversational and helpful -- Write, edit, and modify code files as requested (EXCEPT .automaker/feature_list.json) +- Write, edit, and modify code files as requested - Execute commands and tests - Search and analyze the codebase @@ -474,10 +463,10 @@ When discussing projects, help users think through: - Testing strategies You have full access to the codebase and can: -- Read files to understand existing code (including .automaker/feature_list.json for viewing only) -- Write new files (NEVER .automaker/feature_list.json) -- Edit existing files (NEVER .automaker/feature_list.json) -- Run bash commands (but never commands that modify .automaker/feature_list.json) +- Read files to understand existing code +- Write new files +- Edit existing files +- Run bash commands - Search for code patterns - Execute tests and builds diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 2407dedb..fbc2f232 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -90,7 +90,7 @@ class AutoModeService { content: `Working in isolated branch: ${result.branchName}\n`, }); - // Update feature with worktree info in feature_list.json + // Update feature with worktree info await featureLoader.updateFeatureWorktree( feature.id, projectPath, diff --git a/app/electron/main.js b/app/electron/main.js index 43118263..7f5c9c63 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -819,7 +819,10 @@ ipcMain.handle("claude:check-cli", async () => { try { const claudeCliDetector = require("./services/claude-cli-detector"); const path = require("path"); - const credentialsPath = path.join(app.getPath("userData"), "credentials.json"); + const credentialsPath = path.join( + app.getPath("userData"), + "credentials.json" + ); const fullStatus = claudeCliDetector.getFullStatus(credentialsPath); // Return in format expected by settings view (status: "installed" | "not_installed") @@ -833,7 +836,9 @@ ipcMain.handle("claude:check-cli", async () => { recommendation: fullStatus.installed ? null : "Install Claude Code CLI for optimal performance with ultrathink.", - installCommands: fullStatus.installed ? null : claudeCliDetector.getInstallCommands(), + installCommands: fullStatus.installed + ? null + : claudeCliDetector.getInstallCommands(), }; } catch (error) { console.error("[IPC] claude:check-cli error:", error); @@ -1389,7 +1394,10 @@ ipcMain.handle("git:get-file-diff", async (_, { projectPath, filePath }) => { ipcMain.handle("setup:claude-status", async () => { try { const claudeCliDetector = require("./services/claude-cli-detector"); - const credentialsPath = path.join(app.getPath("userData"), "credentials.json"); + const credentialsPath = path.join( + app.getPath("userData"), + "credentials.json" + ); const result = claudeCliDetector.getFullStatus(credentialsPath); console.log("[IPC] setup:claude-status result:", result); return result; @@ -1424,7 +1432,7 @@ ipcMain.handle("setup:install-claude", async (event) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("setup:install-progress", { cli: "claude", - ...progress + ...progress, }); } }; @@ -1448,7 +1456,7 @@ ipcMain.handle("setup:install-codex", async (event) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("setup:install-progress", { cli: "codex", - ...progress + ...progress, }); } }; @@ -1472,7 +1480,7 @@ ipcMain.handle("setup:auth-claude", async (event) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("setup:auth-progress", { cli: "claude", - ...progress + ...progress, }); } }; @@ -1496,7 +1504,7 @@ ipcMain.handle("setup:auth-codex", async (event, { apiKey }) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("setup:auth-progress", { cli: "codex", - ...progress + ...progress, }); } }; @@ -1532,7 +1540,11 @@ ipcMain.handle("setup:store-api-key", async (_, { provider, apiKey }) => { credentials[provider] = apiKey; // Write back - await fs.writeFile(configPath, JSON.stringify(credentials, null, 2), "utf-8"); + await fs.writeFile( + configPath, + JSON.stringify(credentials, null, 2), + "utf-8" + ); console.log("[IPC] setup:store-api-key stored successfully for:", provider); return { success: true }; @@ -1559,7 +1571,7 @@ ipcMain.handle("setup:get-api-keys", async () => { hasAnthropicKey: !!credentials.anthropic, hasAnthropicOAuthToken: !!credentials.anthropic_oauth_token, hasOpenAIKey: !!credentials.openai, - hasGoogleKey: !!credentials.google + hasGoogleKey: !!credentials.google, }; } catch (e) { return { @@ -1567,7 +1579,7 @@ ipcMain.handle("setup:get-api-keys", async () => { hasAnthropicKey: false, hasAnthropicOAuthToken: false, hasOpenAIKey: false, - hasGoogleKey: false + hasGoogleKey: false, }; } } catch (error) { @@ -1582,9 +1594,16 @@ ipcMain.handle("setup:get-api-keys", async () => { ipcMain.handle("setup:configure-codex-mcp", async (_, { projectPath }) => { try { const codexConfigManager = require("./services/codex-config-manager"); - const mcpServerPath = path.join(__dirname, "services", "mcp-server-factory.js"); + const mcpServerPath = path.join( + __dirname, + "services", + "mcp-server-factory.js" + ); - const configPath = await codexConfigManager.configureMcpServer(projectPath, mcpServerPath); + const configPath = await codexConfigManager.configureMcpServer( + projectPath, + mcpServerPath + ); return { success: true, configPath }; } catch (error) { @@ -1605,6 +1624,155 @@ ipcMain.handle("setup:get-platform", async () => { homeDir: os.homedir(), isWindows: process.platform === "win32", isMac: process.platform === "darwin", - isLinux: process.platform === "linux" + isLinux: process.platform === "linux", }; }); + +// ============================================================================ +// Features IPC Handlers +// ============================================================================ + +/** + * Get all features for a project + */ +ipcMain.handle("features:getAll", async (_, { projectPath }) => { + try { + // Security check + if (!isPathAllowed(projectPath)) { + return { + success: false, + error: "Access denied: Path is outside allowed project directories", + }; + } + + const featureLoader = require("./services/feature-loader"); + const features = await featureLoader.getAll(projectPath); + return { success: true, features }; + } catch (error) { + console.error("[IPC] features:getAll error:", error); + return { success: false, error: error.message }; + } +}); + +/** + * Get a single feature by ID + */ +ipcMain.handle("features:get", async (_, { projectPath, featureId }) => { + try { + // Security check + if (!isPathAllowed(projectPath)) { + return { + success: false, + error: "Access denied: Path is outside allowed project directories", + }; + } + + const featureLoader = require("./services/feature-loader"); + const feature = await featureLoader.get(projectPath, featureId); + if (!feature) { + return { success: false, error: "Feature not found" }; + } + return { success: true, feature }; + } catch (error) { + console.error("[IPC] features:get error:", error); + return { success: false, error: error.message }; + } +}); + +/** + * Create a new feature + */ +ipcMain.handle("features:create", async (_, { projectPath, feature }) => { + try { + // Security check + if (!isPathAllowed(projectPath)) { + return { + success: false, + error: "Access denied: Path is outside allowed project directories", + }; + } + + const featureLoader = require("./services/feature-loader"); + const createdFeature = await featureLoader.create(projectPath, feature); + return { success: true, feature: createdFeature }; + } catch (error) { + console.error("[IPC] features:create error:", error); + return { success: false, error: error.message }; + } +}); + +/** + * Update a feature (partial updates supported) + */ +ipcMain.handle( + "features:update", + async (_, { projectPath, featureId, updates }) => { + try { + // Security check + if (!isPathAllowed(projectPath)) { + return { + success: false, + error: "Access denied: Path is outside allowed project directories", + }; + } + + const featureLoader = require("./services/feature-loader"); + const updatedFeature = await featureLoader.update( + projectPath, + featureId, + updates + ); + return { success: true, feature: updatedFeature }; + } catch (error) { + console.error("[IPC] features:update error:", error); + return { success: false, error: error.message }; + } + } +); + +/** + * Delete a feature and its folder + */ +ipcMain.handle("features:delete", async (_, { projectPath, featureId }) => { + try { + // Security check + if (!isPathAllowed(projectPath)) { + return { + success: false, + error: "Access denied: Path is outside allowed project directories", + }; + } + + const featureLoader = require("./services/feature-loader"); + await featureLoader.delete(projectPath, featureId); + return { success: true }; + } catch (error) { + console.error("[IPC] features:delete error:", error); + return { success: false, error: error.message }; + } +}); + +/** + * Get agent output for a feature + */ +ipcMain.handle( + "features:getAgentOutput", + async (_, { projectPath, featureId }) => { + try { + // Security check + if (!isPathAllowed(projectPath)) { + return { + success: false, + error: "Access denied: Path is outside allowed project directories", + }; + } + + const featureLoader = require("./services/feature-loader"); + const content = await featureLoader.getAgentOutput(projectPath, featureId); + return { success: true, content }; + } catch (error) { + console.error("[IPC] features:getAgentOutput error:", error); + return { success: false, error: error.message }; + } + } +); diff --git a/app/electron/preload.js b/app/electron/preload.js index 3e2b94ed..37230315 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -24,7 +24,12 @@ contextBridge.exposeInMainWorld("electronAPI", { // App APIs getPath: (name) => ipcRenderer.invoke("app:getPath", name), saveImageToTemp: (data, filename, mimeType, projectPath) => - ipcRenderer.invoke("app:saveImageToTemp", { data, filename, mimeType, projectPath }), + ipcRenderer.invoke("app:saveImageToTemp", { + data, + filename, + mimeType, + projectPath, + }), // Agent APIs agent: { @@ -34,19 +39,22 @@ contextBridge.exposeInMainWorld("electronAPI", { // Send a message to the agent send: (sessionId, message, workingDirectory, imagePaths) => - ipcRenderer.invoke("agent:send", { sessionId, message, workingDirectory, imagePaths }), + ipcRenderer.invoke("agent:send", { + sessionId, + message, + workingDirectory, + imagePaths, + }), // Get conversation history getHistory: (sessionId) => ipcRenderer.invoke("agent:getHistory", { sessionId }), // Stop current execution - stop: (sessionId) => - ipcRenderer.invoke("agent:stop", { sessionId }), + stop: (sessionId) => ipcRenderer.invoke("agent:stop", { sessionId }), // Clear conversation - clear: (sessionId) => - ipcRenderer.invoke("agent:clear", { sessionId }), + clear: (sessionId) => ipcRenderer.invoke("agent:clear", { sessionId }), // Subscribe to streaming events onStream: (callback) => { @@ -65,7 +73,11 @@ contextBridge.exposeInMainWorld("electronAPI", { // Create a new session create: (name, projectPath, workingDirectory) => - ipcRenderer.invoke("sessions:create", { name, projectPath, workingDirectory }), + ipcRenderer.invoke("sessions:create", { + name, + projectPath, + workingDirectory, + }), // Update session metadata update: (sessionId, name, tags) => @@ -80,8 +92,7 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.invoke("sessions:unarchive", { sessionId }), // Delete a session permanently - delete: (sessionId) => - ipcRenderer.invoke("sessions:delete", { sessionId }), + delete: (sessionId) => ipcRenderer.invoke("sessions:delete", { sessionId }), }, // Auto Mode API @@ -98,19 +109,32 @@ contextBridge.exposeInMainWorld("electronAPI", { // Run a specific feature runFeature: (projectPath, featureId, useWorktrees) => - ipcRenderer.invoke("auto-mode:run-feature", { projectPath, featureId, useWorktrees }), + ipcRenderer.invoke("auto-mode:run-feature", { + projectPath, + featureId, + useWorktrees, + }), // Verify a specific feature by running its tests verifyFeature: (projectPath, featureId) => - ipcRenderer.invoke("auto-mode:verify-feature", { projectPath, featureId }), + ipcRenderer.invoke("auto-mode:verify-feature", { + projectPath, + featureId, + }), // Resume a specific feature with previous context resumeFeature: (projectPath, featureId) => - ipcRenderer.invoke("auto-mode:resume-feature", { projectPath, featureId }), + ipcRenderer.invoke("auto-mode:resume-feature", { + projectPath, + featureId, + }), // Check if context file exists for a feature contextExists: (projectPath, featureId) => - ipcRenderer.invoke("auto-mode:context-exists", { projectPath, featureId }), + ipcRenderer.invoke("auto-mode:context-exists", { + projectPath, + featureId, + }), // Analyze a new project - kicks off an agent to analyze codebase analyzeProject: (projectPath) => @@ -122,11 +146,19 @@ contextBridge.exposeInMainWorld("electronAPI", { // Follow-up on a feature with additional prompt followUpFeature: (projectPath, featureId, prompt, imagePaths) => - ipcRenderer.invoke("auto-mode:follow-up-feature", { projectPath, featureId, prompt, imagePaths }), + ipcRenderer.invoke("auto-mode:follow-up-feature", { + projectPath, + featureId, + prompt, + imagePaths, + }), // Commit changes for a feature commitFeature: (projectPath, featureId) => - ipcRenderer.invoke("auto-mode:commit-feature", { projectPath, featureId }), + ipcRenderer.invoke("auto-mode:commit-feature", { + projectPath, + featureId, + }), // Listen for auto mode events onEvent: (callback) => { @@ -167,7 +199,11 @@ contextBridge.exposeInMainWorld("electronAPI", { // Merge feature worktree changes back to main branch mergeFeature: (projectPath, featureId, options) => - ipcRenderer.invoke("worktree:merge-feature", { projectPath, featureId, options }), + ipcRenderer.invoke("worktree:merge-feature", { + projectPath, + featureId, + options, + }), // Get worktree info for a feature getInfo: (projectPath, featureId) => @@ -178,8 +214,7 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.invoke("worktree:get-status", { projectPath, featureId }), // List all feature worktrees - list: (projectPath) => - ipcRenderer.invoke("worktree:list", { projectPath }), + list: (projectPath) => ipcRenderer.invoke("worktree:list", { projectPath }), // Get file diffs for a feature worktree getDiffs: (projectPath, featureId) => @@ -187,7 +222,11 @@ contextBridge.exposeInMainWorld("electronAPI", { // Get diff for a specific file in a worktree getFileDiff: (projectPath, featureId, filePath) => - ipcRenderer.invoke("worktree:get-file-diff", { projectPath, featureId, filePath }), + ipcRenderer.invoke("worktree:get-file-diff", { + projectPath, + featureId, + filePath, + }), }, // Git Operations APIs (for non-worktree operations) @@ -229,11 +268,18 @@ contextBridge.exposeInMainWorld("electronAPI", { specRegeneration: { // Create initial app spec for a new project create: (projectPath, projectOverview, generateFeatures = true) => - ipcRenderer.invoke("spec-regeneration:create", { projectPath, projectOverview, generateFeatures }), + ipcRenderer.invoke("spec-regeneration:create", { + projectPath, + projectOverview, + generateFeatures, + }), // Regenerate the app spec generate: (projectPath, projectDefinition) => - ipcRenderer.invoke("spec-regeneration:generate", { projectPath, projectDefinition }), + ipcRenderer.invoke("spec-regeneration:generate", { + projectPath, + projectDefinition, + }), // Stop regenerating spec stop: () => ipcRenderer.invoke("spec-regeneration:stop"), @@ -305,6 +351,37 @@ contextBridge.exposeInMainWorld("electronAPI", { }; }, }, + + // Features API + features: { + // Get all features for a project + getAll: (projectPath) => + ipcRenderer.invoke("features:getAll", { projectPath }), + + // Get a single feature by ID + get: (projectPath, featureId) => + ipcRenderer.invoke("features:get", { projectPath, featureId }), + + // Create a new feature + create: (projectPath, feature) => + ipcRenderer.invoke("features:create", { projectPath, feature }), + + // Update a feature (partial updates supported) + update: (projectPath, featureId, updates) => + ipcRenderer.invoke("features:update", { + projectPath, + featureId, + updates, + }), + + // Delete a feature and its folder + delete: (projectPath, featureId) => + ipcRenderer.invoke("features:delete", { projectPath, featureId }), + + // Get agent output for a feature + getAgentOutput: (projectPath, featureId) => + ipcRenderer.invoke("features:getAgentOutput", { projectPath, featureId }), + }, }); // Also expose a flag to detect if we're in Electron diff --git a/app/electron/services/context-manager.js b/app/electron/services/context-manager.js index 607c806b..c9f08ab4 100644 --- a/app/electron/services/context-manager.js +++ b/app/electron/services/context-manager.js @@ -12,16 +12,21 @@ class ContextManager { if (!projectPath) return; try { - const contextDir = path.join(projectPath, ".automaker", "agents-context"); + const featureDir = path.join( + projectPath, + ".automaker", + "features", + featureId + ); - // Ensure directory exists + // Ensure feature directory exists try { - await fs.access(contextDir); + await fs.access(featureDir); } catch { - await fs.mkdir(contextDir, { recursive: true }); + await fs.mkdir(featureDir, { recursive: true }); } - const filePath = path.join(contextDir, `${featureId}.md`); + const filePath = path.join(featureDir, "agent-output.md"); // Append to existing file or create new one try { @@ -43,8 +48,9 @@ class ContextManager { const contextPath = path.join( projectPath, ".automaker", - "agents-context", - `${featureId}.md` + "features", + featureId, + "agent-output.md" ); const content = await fs.readFile(contextPath, "utf-8"); return content; @@ -64,8 +70,9 @@ class ContextManager { const contextPath = path.join( projectPath, ".automaker", - "agents-context", - `${featureId}.md` + "features", + featureId, + "agent-output.md" ); await fs.unlink(contextPath); console.log( @@ -213,13 +220,18 @@ This helps future agent runs avoid the same pitfalls. try { const { execSync } = require("child_process"); - const contextDir = path.join(projectPath, ".automaker", "agents-context"); + const featureDir = path.join( + projectPath, + ".automaker", + "features", + featureId + ); - // Ensure directory exists + // Ensure feature directory exists try { - await fs.access(contextDir); + await fs.access(featureDir); } catch { - await fs.mkdir(contextDir, { recursive: true }); + await fs.mkdir(featureDir, { recursive: true }); } // Get list of modified files (both staged and unstaged) @@ -233,25 +245,34 @@ This helps future agent runs avoid the same pitfalls. modifiedFiles = modifiedOutput.split("\n").filter(Boolean); } } catch (error) { - console.log("[ContextManager] No modified files or git error:", error.message); + console.log( + "[ContextManager] No modified files or git error:", + error.message + ); } // Get list of untracked files let untrackedFiles = []; try { - const untrackedOutput = execSync("git ls-files --others --exclude-standard", { - cwd: projectPath, - encoding: "utf-8", - }).trim(); + const untrackedOutput = execSync( + "git ls-files --others --exclude-standard", + { + cwd: projectPath, + encoding: "utf-8", + } + ).trim(); if (untrackedOutput) { untrackedFiles = untrackedOutput.split("\n").filter(Boolean); } } catch (error) { - console.log("[ContextManager] Error getting untracked files:", error.message); + console.log( + "[ContextManager] Error getting untracked files:", + error.message + ); } // Save the initial state to a JSON file - const stateFile = path.join(contextDir, `${featureId}-git-state.json`); + const stateFile = path.join(featureDir, "git-state.json"); const state = { timestamp: new Date().toISOString(), modifiedFiles, @@ -259,14 +280,20 @@ This helps future agent runs avoid the same pitfalls. }; await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf-8"); - console.log(`[ContextManager] Saved initial git state for ${featureId}:`, { - modifiedCount: modifiedFiles.length, - untrackedCount: untrackedFiles.length, - }); + console.log( + `[ContextManager] Saved initial git state for ${featureId}:`, + { + modifiedCount: modifiedFiles.length, + untrackedCount: untrackedFiles.length, + } + ); return state; } catch (error) { - console.error("[ContextManager] Failed to save initial git state:", error); + console.error( + "[ContextManager] Failed to save initial git state:", + error + ); return { modifiedFiles: [], untrackedFiles: [] }; } } @@ -284,13 +311,16 @@ This helps future agent runs avoid the same pitfalls. const stateFile = path.join( projectPath, ".automaker", - "agents-context", - `${featureId}-git-state.json` + "features", + featureId, + "git-state.json" ); const content = await fs.readFile(stateFile, "utf-8"); return JSON.parse(content); } catch (error) { - console.log(`[ContextManager] No initial git state found for ${featureId}`); + console.log( + `[ContextManager] No initial git state found for ${featureId}` + ); return null; } } @@ -307,15 +337,19 @@ This helps future agent runs avoid the same pitfalls. const stateFile = path.join( projectPath, ".automaker", - "agents-context", - `${featureId}-git-state.json` + "features", + featureId, + "git-state.json" ); await fs.unlink(stateFile); console.log(`[ContextManager] Deleted git state file for ${featureId}`); } catch (error) { // File might not exist, which is fine if (error.code !== "ENOENT") { - console.error("[ContextManager] Failed to delete git state file:", error); + console.error( + "[ContextManager] Failed to delete git state file:", + error + ); } } } @@ -334,7 +368,10 @@ This helps future agent runs avoid the same pitfalls. const { execSync } = require("child_process"); // Get initial state - const initialState = await this.getInitialGitState(projectPath, featureId); + const initialState = await this.getInitialGitState( + projectPath, + featureId + ); // Get current state let currentModified = []; @@ -352,10 +389,13 @@ This helps future agent runs avoid the same pitfalls. let currentUntracked = []; try { - const untrackedOutput = execSync("git ls-files --others --exclude-standard", { - cwd: projectPath, - encoding: "utf-8", - }).trim(); + const untrackedOutput = execSync( + "git ls-files --others --exclude-standard", + { + cwd: projectPath, + encoding: "utf-8", + } + ).trim(); if (untrackedOutput) { currentUntracked = untrackedOutput.split("\n").filter(Boolean); } @@ -365,7 +405,9 @@ This helps future agent runs avoid the same pitfalls. if (!initialState) { // No initial state - all current changes are considered from this session - console.log("[ContextManager] No initial state found, returning all current changes"); + console.log( + "[ContextManager] No initial state found, returning all current changes" + ); return { newFiles: currentUntracked, modifiedFiles: currentModified, @@ -377,21 +419,31 @@ This helps future agent runs avoid the same pitfalls. const initialUntrackedSet = new Set(initialState.untrackedFiles || []); // New files = current untracked - initial untracked - const newFiles = currentUntracked.filter(f => !initialUntrackedSet.has(f)); + const newFiles = currentUntracked.filter( + (f) => !initialUntrackedSet.has(f) + ); // Modified files = current modified - initial modified - const modifiedFiles = currentModified.filter(f => !initialModifiedSet.has(f)); + const modifiedFiles = currentModified.filter( + (f) => !initialModifiedSet.has(f) + ); - console.log(`[ContextManager] Files changed during session for ${featureId}:`, { - newFilesCount: newFiles.length, - modifiedFilesCount: modifiedFiles.length, - newFiles, - modifiedFiles, - }); + console.log( + `[ContextManager] Files changed during session for ${featureId}:`, + { + newFilesCount: newFiles.length, + modifiedFilesCount: modifiedFiles.length, + newFiles, + modifiedFiles, + } + ); return { newFiles, modifiedFiles }; } catch (error) { - console.error("[ContextManager] Failed to calculate changed files:", error); + console.error( + "[ContextManager] Failed to calculate changed files:", + error + ); return { newFiles: [], modifiedFiles: [] }; } } diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index c5239fe3..1ff3a7c0 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -2,36 +2,343 @@ const path = require("path"); const fs = require("fs/promises"); /** - * Feature Loader - Handles loading and selecting features from feature_list.json + * Feature Loader - Handles loading and managing features from individual feature folders + * Each feature is stored in .automaker/features/{featureId}/feature.json */ class FeatureLoader { /** - * Load features from .automaker/feature_list.json + * Get the features directory path */ - async loadFeatures(projectPath) { - const featuresPath = path.join( - projectPath, - ".automaker", - "feature_list.json" + getFeaturesDir(projectPath) { + return path.join(projectPath, ".automaker", "features"); + } + + /** + * Get the path to a specific feature folder + */ + getFeatureDir(projectPath, featureId) { + return path.join(this.getFeaturesDir(projectPath), featureId); + } + + /** + * Get the path to a feature's feature.json file + */ + getFeatureJsonPath(projectPath, featureId) { + return path.join( + this.getFeatureDir(projectPath, featureId), + "feature.json" ); + } + /** + * Get the path to a feature's agent-output.md file + */ + getAgentOutputPath(projectPath, featureId) { + return path.join( + this.getFeatureDir(projectPath, featureId), + "agent-output.md" + ); + } + + /** + * Generate a new feature ID + */ + generateFeatureId() { + return `feature-${Date.now()}-${Math.random() + .toString(36) + .substring(2, 11)}`; + } + + /** + * Ensure all image paths for a feature are stored within the feature directory + */ + async ensureFeatureImages(projectPath, featureId, feature) { + if ( + !feature || + !Array.isArray(feature.imagePaths) || + feature.imagePaths.length === 0 + ) { + return; + } + + const featureDir = this.getFeatureDir(projectPath, featureId); + const featureImagesDir = path.join(featureDir, "images"); + await fs.mkdir(featureImagesDir, { recursive: true }); + + const updatedImagePaths = []; + + for (const entry of feature.imagePaths) { + const isStringEntry = typeof entry === "string"; + const currentPathValue = isStringEntry ? entry : entry.path; + + if (!currentPathValue) { + updatedImagePaths.push(entry); + continue; + } + + let resolvedCurrentPath = currentPathValue; + if (!path.isAbsolute(resolvedCurrentPath)) { + resolvedCurrentPath = path.join(projectPath, resolvedCurrentPath); + } + resolvedCurrentPath = path.normalize(resolvedCurrentPath); + + // Skip if file doesn't exist + try { + await fs.access(resolvedCurrentPath); + } catch { + console.warn( + `[FeatureLoader] Image file missing for ${featureId}: ${resolvedCurrentPath}` + ); + updatedImagePaths.push(entry); + continue; + } + + const relativeToFeatureImages = path.relative( + featureImagesDir, + resolvedCurrentPath + ); + const alreadyInFeatureDir = + relativeToFeatureImages === "" || + (!relativeToFeatureImages.startsWith("..") && + !path.isAbsolute(relativeToFeatureImages)); + + let finalPath = resolvedCurrentPath; + + if (!alreadyInFeatureDir) { + const originalName = path.basename(resolvedCurrentPath); + let targetPath = path.join(featureImagesDir, originalName); + + // Avoid overwriting files by appending a counter if needed + let counter = 1; + while (true) { + try { + await fs.access(targetPath); + const parsed = path.parse(originalName); + targetPath = path.join( + featureImagesDir, + `${parsed.name}-${counter}${parsed.ext}` + ); + counter += 1; + } catch { + break; + } + } + + try { + await fs.rename(resolvedCurrentPath, targetPath); + finalPath = targetPath; + } catch (error) { + console.warn( + `[FeatureLoader] Failed to move image ${resolvedCurrentPath}: ${error.message}` + ); + updatedImagePaths.push(entry); + continue; + } + } + + updatedImagePaths.push( + isStringEntry ? finalPath : { ...entry, path: finalPath } + ); + } + + feature.imagePaths = updatedImagePaths; + } + + /** + * Get all features for a project + */ + async getAll(projectPath) { try { - const content = await fs.readFile(featuresPath, "utf-8"); - const features = JSON.parse(content); + const featuresDir = this.getFeaturesDir(projectPath); - // Ensure each feature has an ID - return features.map((f, index) => ({ - ...f, - id: f.id || `feature-${index}-${Date.now()}`, - })); + // Check if features directory exists + try { + await fs.access(featuresDir); + } catch { + // Directory doesn't exist, return empty array + return []; + } + + // Read all feature directories + const entries = await fs.readdir(featuresDir, { withFileTypes: true }); + const featureDirs = entries.filter((entry) => entry.isDirectory()); + + // Load each feature + const features = []; + for (const dir of featureDirs) { + const featureId = dir.name; + const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); + + try { + const content = await fs.readFile(featureJsonPath, "utf-8"); + const feature = JSON.parse(content); + features.push(feature); + } catch (error) { + console.error( + `[FeatureLoader] Failed to load feature ${featureId}:`, + error + ); + // Continue loading other features + } + } + + // Sort by creation order (feature IDs contain timestamp) + features.sort((a, b) => { + const aTime = a.id ? parseInt(a.id.split("-")[1] || "0") : 0; + const bTime = b.id ? parseInt(b.id.split("-")[1] || "0") : 0; + return aTime - bTime; + }); + + return features; } catch (error) { - console.error("[FeatureLoader] Failed to load features:", error); + console.error("[FeatureLoader] Failed to get all features:", error); return []; } } /** - * Update feature status in .automaker/feature_list.json + * Get a single feature by ID + */ + async get(projectPath, featureId) { + try { + const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); + const content = await fs.readFile(featureJsonPath, "utf-8"); + return JSON.parse(content); + } catch (error) { + if (error.code === "ENOENT") { + return null; + } + console.error( + `[FeatureLoader] Failed to get feature ${featureId}:`, + error + ); + throw error; + } + } + + /** + * Create a new feature + */ + async create(projectPath, featureData) { + const featureId = featureData.id || this.generateFeatureId(); + const featureDir = this.getFeatureDir(projectPath, featureId); + const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); + + // Ensure features directory exists + const featuresDir = this.getFeaturesDir(projectPath); + await fs.mkdir(featuresDir, { recursive: true }); + + // Create feature directory + await fs.mkdir(featureDir, { recursive: true }); + + // Ensure feature has an ID + const feature = { ...featureData, id: featureId }; + + // Move any uploaded images into the feature directory + await this.ensureFeatureImages(projectPath, featureId, feature); + + // Write feature.json + await fs.writeFile( + featureJsonPath, + JSON.stringify(feature, null, 2), + "utf-8" + ); + + console.log(`[FeatureLoader] Created feature ${featureId}`); + return feature; + } + + /** + * Update a feature (partial updates supported) + */ + async update(projectPath, featureId, updates) { + try { + const feature = await this.get(projectPath, featureId); + if (!feature) { + throw new Error(`Feature ${featureId} not found`); + } + + // Merge updates + const updatedFeature = { ...feature, ...updates }; + + // Move any new images into the feature directory + await this.ensureFeatureImages(projectPath, featureId, updatedFeature); + + // Write back to file + const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); + await fs.writeFile( + featureJsonPath, + JSON.stringify(updatedFeature, null, 2), + "utf-8" + ); + + console.log(`[FeatureLoader] Updated feature ${featureId}`); + return updatedFeature; + } catch (error) { + console.error( + `[FeatureLoader] Failed to update feature ${featureId}:`, + error + ); + throw error; + } + } + + /** + * Delete a feature and its entire folder + */ + async delete(projectPath, featureId) { + try { + const featureDir = this.getFeatureDir(projectPath, featureId); + await fs.rm(featureDir, { recursive: true, force: true }); + console.log(`[FeatureLoader] Deleted feature ${featureId}`); + } catch (error) { + if (error.code === "ENOENT") { + // Feature doesn't exist, that's fine + return; + } + console.error( + `[FeatureLoader] Failed to delete feature ${featureId}:`, + error + ); + throw error; + } + } + + /** + * Get agent output for a feature + */ + async getAgentOutput(projectPath, featureId) { + try { + const agentOutputPath = this.getAgentOutputPath(projectPath, featureId); + const content = await fs.readFile(agentOutputPath, "utf-8"); + return content; + } catch (error) { + if (error.code === "ENOENT") { + return null; + } + console.error( + `[FeatureLoader] Failed to get agent output for ${featureId}:`, + error + ); + return null; + } + } + + // ============================================================================ + // Legacy methods for backward compatibility (used by backend services) + // ============================================================================ + + /** + * Load all features for a project (legacy API) + * Features are stored in .automaker/features/{id}/feature.json + */ + async loadFeatures(projectPath) { + return await this.getAll(projectPath); + } + + /** + * Update feature status (legacy API) + * Features are stored in .automaker/features/{id}/feature.json * @param {string} featureId - The ID of the feature to update * @param {string} status - The new status * @param {string} projectPath - Path to the project @@ -39,126 +346,26 @@ class FeatureLoader { * @param {string} [error] - Optional error message if feature errored */ async updateFeatureStatus(featureId, status, projectPath, summary, error) { - const featuresPath = path.join( - projectPath, - ".automaker", - "feature_list.json" - ); - - // 🛡️ SAFETY: Create backup before any modification - const backupPath = path.join( - projectPath, - ".automaker", - "feature_list.backup.json" - ); - - try { - const originalContent = await fs.readFile(featuresPath, "utf-8"); - await fs.writeFile(backupPath, originalContent, "utf-8"); - console.log(`[FeatureLoader] Created backup at ${backupPath}`); - } catch (error) { - console.warn(`[FeatureLoader] Could not create backup: ${error.message}`); + const updates = { status }; + if (summary !== undefined) { + updates.summary = summary; } - - const features = await this.loadFeatures(projectPath); - - // 🛡️ VALIDATION: Ensure we loaded features successfully - if (!Array.isArray(features)) { - throw new Error("CRITICAL: features is not an array - aborting to prevent data loss"); - } - - if (features.length === 0) { - console.warn(`[FeatureLoader] WARNING: Feature list is empty. This may indicate corruption.`); - // Try to restore from backup - try { - const backupContent = await fs.readFile(backupPath, "utf-8"); - const backupFeatures = JSON.parse(backupContent); - if (Array.isArray(backupFeatures) && backupFeatures.length > 0) { - console.log(`[FeatureLoader] Restored ${backupFeatures.length} features from backup`); - // Use backup features instead - features.length = 0; - features.push(...backupFeatures); - } - } catch (backupError) { - console.error(`[FeatureLoader] Could not restore from backup: ${backupError.message}`); - } - } - - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - console.error(`[FeatureLoader] Feature ${featureId} not found`); - return; - } - - // Update the status field - feature.status = status; - - // Update the summary field if provided - if (summary) { - feature.summary = summary; - } - - // Update the error field (set or clear) - if (error) { - feature.error = error; + if (error !== undefined) { + updates.error = error; } else { - // Clear any previous error when status changes without error - delete feature.error; + // Clear error if not provided + const feature = await this.get(projectPath, featureId); + if (feature && feature.error) { + updates.error = undefined; + } } - // Save back to file - const toSave = features.map((f) => { - const featureData = { - id: f.id, - category: f.category, - description: f.description, - steps: f.steps, - status: f.status, - }; - // Preserve optional fields if they exist - if (f.skipTests !== undefined) { - featureData.skipTests = f.skipTests; - } - if (f.images !== undefined) { - featureData.images = f.images; - } - if (f.imagePaths !== undefined) { - featureData.imagePaths = f.imagePaths; - } - if (f.startedAt !== undefined) { - featureData.startedAt = f.startedAt; - } - if (f.summary !== undefined) { - featureData.summary = f.summary; - } - if (f.model !== undefined) { - featureData.model = f.model; - } - if (f.thinkingLevel !== undefined) { - featureData.thinkingLevel = f.thinkingLevel; - } - if (f.error !== undefined) { - featureData.error = f.error; - } - // Preserve worktree info - if (f.worktreePath !== undefined) { - featureData.worktreePath = f.worktreePath; - } - if (f.branchName !== undefined) { - featureData.branchName = f.branchName; - } - return featureData; - }); - - // 🛡️ FINAL VALIDATION: Ensure we're not writing an empty array - if (!Array.isArray(toSave) || toSave.length === 0) { - throw new Error("CRITICAL: Attempted to save empty feature list - aborting to prevent data loss"); - } - - await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8"); - console.log(`[FeatureLoader] Updated feature ${featureId}: status=${status}${summary ? `, summary="${summary}"` : ""}`); - console.log(`[FeatureLoader] Successfully saved ${toSave.length} features to feature_list.json`); + await this.update(projectPath, featureId, updates); + console.log( + `[FeatureLoader] Updated feature ${featureId}: status=${status}${ + summary ? `, summary="${summary}"` : "" + }` + ); } /** @@ -168,70 +375,38 @@ class FeatureLoader { selectNextFeature(features) { // Find first feature that is in backlog or in_progress status // Skip verified and waiting_approval (which needs user input) - return features.find((f) => f.status !== "verified" && f.status !== "waiting_approval"); + return features.find( + (f) => f.status !== "verified" && f.status !== "waiting_approval" + ); } /** - * Update worktree info for a feature + * Update worktree info for a feature (legacy API) + * Features are stored in .automaker/features/{id}/feature.json * @param {string} featureId - The ID of the feature to update * @param {string} projectPath - Path to the project * @param {string|null} worktreePath - Path to the worktree (null to clear) * @param {string|null} branchName - Name of the feature branch (null to clear) */ - async updateFeatureWorktree(featureId, projectPath, worktreePath, branchName) { - const featuresPath = path.join( - projectPath, - ".automaker", - "feature_list.json" - ); - - const features = await this.loadFeatures(projectPath); - - if (!Array.isArray(features) || features.length === 0) { - console.error("[FeatureLoader] Cannot update worktree: feature list is empty"); - return; - } - - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - console.error(`[FeatureLoader] Feature ${featureId} not found`); - return; - } - - // Update or clear worktree info + async updateFeatureWorktree( + featureId, + projectPath, + worktreePath, + branchName + ) { + const updates = {}; if (worktreePath) { - feature.worktreePath = worktreePath; - feature.branchName = branchName; + updates.worktreePath = worktreePath; + updates.branchName = branchName; } else { - delete feature.worktreePath; - delete feature.branchName; + updates.worktreePath = null; + updates.branchName = null; } - // Save back to file (reuse the same mapping logic) - const toSave = features.map((f) => { - const featureData = { - id: f.id, - category: f.category, - description: f.description, - steps: f.steps, - status: f.status, - }; - if (f.skipTests !== undefined) featureData.skipTests = f.skipTests; - if (f.images !== undefined) featureData.images = f.images; - if (f.imagePaths !== undefined) featureData.imagePaths = f.imagePaths; - if (f.startedAt !== undefined) featureData.startedAt = f.startedAt; - if (f.summary !== undefined) featureData.summary = f.summary; - if (f.model !== undefined) featureData.model = f.model; - if (f.thinkingLevel !== undefined) featureData.thinkingLevel = f.thinkingLevel; - if (f.error !== undefined) featureData.error = f.error; - if (f.worktreePath !== undefined) featureData.worktreePath = f.worktreePath; - if (f.branchName !== undefined) featureData.branchName = f.branchName; - return featureData; - }); - - await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8"); - console.log(`[FeatureLoader] Updated feature ${featureId}: worktreePath=${worktreePath}, branchName=${branchName}`); + await this.update(projectPath, featureId, updates); + console.log( + `[FeatureLoader] Updated feature ${featureId}: worktreePath=${worktreePath}, branchName=${branchName}` + ); } } diff --git a/app/electron/services/mcp-server-factory.js b/app/electron/services/mcp-server-factory.js index bbb57d5e..b890dfbe 100644 --- a/app/electron/services/mcp-server-factory.js +++ b/app/electron/services/mcp-server-factory.js @@ -9,8 +9,8 @@ class McpServerFactory { /** * Create a custom MCP server with the UpdateFeatureStatus tool * This tool allows Claude Code to safely update feature status without - * directly modifying the feature_list.json file, preventing race conditions - * and accidental state restoration. + * directly modifying feature files, preventing race conditions + * and accidental state corruption. */ createFeatureToolsServer(updateFeatureStatusCallback, projectPath) { return createSdkMcpServer({ @@ -19,7 +19,7 @@ class McpServerFactory { tools: [ tool( "UpdateFeatureStatus", - "Update the status of a feature in the feature list. Use this tool instead of directly modifying feature_list.json to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.", + "Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.", { featureId: z.string().describe("The ID of the feature to update"), status: z.enum(["backlog", "in_progress", "verified"]).describe("The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically."), diff --git a/app/electron/services/mcp-server-stdio.js b/app/electron/services/mcp-server-stdio.js index b7f5c1db..c0da07d7 100644 --- a/app/electron/services/mcp-server-stdio.js +++ b/app/electron/services/mcp-server-stdio.js @@ -144,7 +144,7 @@ async function handleToolsList(params, id) { tools: [ { name: 'UpdateFeatureStatus', - description: 'Update the status of a feature in the feature list. Use this tool instead of directly modifying feature_list.json to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.', + description: 'Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.', inputSchema: { type: 'object', properties: { diff --git a/app/electron/services/prompt-builder.js b/app/electron/services/prompt-builder.js index d03284a1..2c793403 100644 --- a/app/electron/services/prompt-builder.js +++ b/app/electron/services/prompt-builder.js @@ -69,7 +69,7 @@ ${ } ${ feature.skipTests ? "4" : "6" -}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json +}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** ${ feature.skipTests ? "5. **DO NOT commit changes** - the user will review and commit manually" @@ -83,7 +83,7 @@ When you have completed the feature${ }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: - Call the tool with: featureId="${feature.id}" and status="verified" - **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" -- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions +- **DO NOT manually edit feature files** - this can cause race conditions - The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data - **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior @@ -113,7 +113,7 @@ ${ ? "- Skip automated testing (skipTests=true) - user will manually verify" : "- Write comprehensive Playwright tests\n- Ensure all existing tests still pass\n- Mark the feature as passing only when all tests are green\n- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle" } -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** +- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly** - **CRITICAL: Always include a summary when marking feature as verified** ${ feature.skipTests @@ -223,7 +223,7 @@ ${ } ${ feature.skipTests ? "4" : "8" -}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json +}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** ${ feature.skipTests ? "5. **DO NOT commit changes** - the user will review and commit manually" @@ -237,7 +237,7 @@ When you have completed the feature${ }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: - Call the tool with: featureId="${feature.id}" and status="verified" - **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" -- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions +- **DO NOT manually edit feature files** - this can cause race conditions - The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data - **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior @@ -275,7 +275,7 @@ ${ ? "- Skip automated testing (skipTests=true) - user will manually verify\n- **DO NOT commit changes** - user will review and commit manually" : "- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure\n- Only mark as verified if Playwright tests pass\n- **CRITICAL: Delete test files after they pass** - tests should not accumulate\n- Update test utilities if functionality changed\n- Make a git commit when the feature is complete\n- Be thorough and persistent in fixing issues" } -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** +- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly** - **CRITICAL: Always include a summary when marking feature as verified** Begin by reading the project structure and understanding what needs to be implemented or fixed.`; @@ -358,7 +358,7 @@ ${ } ${ feature.skipTests ? "4" : "6" -}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json +}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** ${ feature.skipTests ? "5. **DO NOT commit changes** - the user will review and commit manually" @@ -372,7 +372,7 @@ When you have completed the feature${ }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: - Call the tool with: featureId="${feature.id}" and status="verified" - **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" -- **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions +- **DO NOT manually edit feature files** - this can cause race conditions - The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data - **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior @@ -402,7 +402,7 @@ ${ ? "- Skip automated testing (skipTests=true) - user will manually verify" : "- Write comprehensive Playwright tests if not already done\n- Ensure all tests pass before marking as verified\n- **CRITICAL: Delete test files after verification**" } -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** +- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly** - **CRITICAL: Always include a summary when marking feature as verified** ${ feature.skipTests @@ -491,38 +491,16 @@ Analyze this project's codebase and update the .automaker/app_spec.txt file with \`\`\` -4. **IMPORTANT - Generate Feature List:** - After writing the app_spec.txt, you MUST update .automaker/feature_list.json with features from the implementation_roadmap section: - - Read the app_spec.txt you just created - - For EVERY feature in each phase of the implementation_roadmap, create an entry - - Write ALL features to .automaker/feature_list.json +4. Ensure .automaker/context/ directory exists - The feature_list.json format should be: - \`\`\`json - [ - { - "id": "feature--", - "category": "", - "description": "", - "status": "backlog", - "steps": ["Step 1", "Step 2", "..."], - "skipTests": true - } - ] - \`\`\` - - Generate unique IDs using the current timestamp and index (e.g., "feature-1234567890-0", "feature-1234567890-1", etc.) - -5. Ensure .automaker/context/ directory exists - -6. Ensure .automaker/agents-context/ directory exists +5. Ensure .automaker/features/ directory exists **Important:** - Be concise but accurate - Only include information you can verify from the codebase - If unsure about something, note it as "to be determined" - Don't make up features that don't exist -- Include EVERY feature from the roadmap in feature_list.json - do not skip any +- Features are stored in .automaker/features/{id}/feature.json - each feature gets its own folder Begin by exploring the project structure.`; } @@ -563,27 +541,12 @@ You are implementing features for manual user review. This means: ${modeHeader} ${memoryContent} -**🚨 CRITICAL FILE PROTECTION - READ THIS FIRST 🚨** - -THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION: -- .automaker/feature_list.json - -**YOU MUST NEVER:** -- Use the Write tool on feature_list.json -- Use the Edit tool on feature_list.json -- Use any Bash command that writes to feature_list.json (echo, sed, awk, etc.) -- Attempt to read and rewrite feature_list.json -- UNDER ANY CIRCUMSTANCES touch this file directly - -**CATASTROPHIC CONSEQUENCES:** -Directly modifying feature_list.json can: -- Erase all project features permanently -- Corrupt the project state beyond recovery -- Destroy hours/days of planning work -- This is a FIREABLE OFFENSE - you will be terminated if you do this +**Feature Storage:** +Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. **THE ONLY WAY to update features:** Use the mcp__automaker-tools__UpdateFeatureStatus tool with featureId, status, and summary parameters. +Do NOT manually edit feature.json files directly. ${contextFilesPreview} @@ -594,7 +557,7 @@ Your role is to: - Create comprehensive Playwright tests using testing utilities (only if skipTests is false) - Ensure all tests pass before marking features complete (only if skipTests is false) - **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false) -- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature_list.json +- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature files - **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done - Commit working code to git (only if skipTests is false - skipTests features require manual review) - Be thorough and detail-oriented @@ -609,7 +572,7 @@ If a feature has skipTests=true: **IMPORTANT - UpdateFeatureStatus Tool:** You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status: - Call with featureId, status="verified", and summary="Description of what was done" -- **DO NOT manually edit .automaker/feature_list.json** - this can cause race conditions and restore old state +- **DO NOT manually edit feature files** - this can cause race conditions and restore old state - The tool safely updates the status without corrupting other feature data - **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct @@ -704,27 +667,12 @@ You are completing features for manual user review. This means: ${modeHeader} ${memoryContent} -**🚨 CRITICAL FILE PROTECTION - READ THIS FIRST 🚨** - -THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION: -- .automaker/feature_list.json - -**YOU MUST NEVER:** -- Use the Write tool on feature_list.json -- Use the Edit tool on feature_list.json -- Use any Bash command that writes to feature_list.json (echo, sed, awk, etc.) -- Attempt to read and rewrite feature_list.json -- UNDER ANY CIRCUMSTANCES touch this file directly - -**CATASTROPHIC CONSEQUENCES:** -Directly modifying feature_list.json can: -- Erase all project features permanently -- Corrupt the project state beyond recovery -- Destroy hours/days of planning work -- This is a FIREABLE OFFENSE - you will be terminated if you do this +**Feature Storage:** +Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. **THE ONLY WAY to update features:** Use the mcp__automaker-tools__UpdateFeatureStatus tool with featureId, status, and summary parameters. +Do NOT manually edit feature.json files directly. ${contextFilesPreview} @@ -737,7 +685,7 @@ Your role is to: - If other tests fail, verify if those tests are still accurate or should be updated or deleted (only if skipTests is false) - Continue rerunning tests and fixing issues until ALL tests pass (only if skipTests is false) - **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false) -- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature_list.json +- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature files - **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done - **Update test utilities (tests/utils.ts) if functionality changed** - keep helpers in sync with code (only if skipTests is false) - Commit working code to git (only if skipTests is false - skipTests features require manual review) @@ -752,7 +700,7 @@ If a feature has skipTests=true: **IMPORTANT - UpdateFeatureStatus Tool:** You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status: - Call with featureId, status="verified", and summary="Description of what was done" -- **DO NOT manually edit .automaker/feature_list.json** - this can cause race conditions and restore old state +- **DO NOT manually edit feature files** - this can cause race conditions and restore old state - The tool safely updates the status without corrupting other feature data - **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct @@ -820,7 +768,6 @@ Your goal is to: - Identify programming languages, frameworks, and libraries - Detect existing features and capabilities - Update the .automaker/app_spec.txt with accurate information -- Generate a feature list in .automaker/feature_list.json based on the implementation roadmap - Ensure all required .automaker files and directories exist Be efficient - don't read every file, focus on: @@ -829,11 +776,9 @@ Be efficient - don't read every file, focus on: - Directory structure - README and documentation -**CRITICAL - Feature List Generation:** -After creating/updating the app_spec.txt, you MUST also update .automaker/feature_list.json: -1. Read the app_spec.txt you just wrote -2. Extract all features from the implementation_roadmap section -3. Write them to .automaker/feature_list.json in the correct format +**Feature Storage:** +Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. +Use the UpdateFeatureStatus tool to manage features, not direct file edits. You have access to Read, Write, Edit, Glob, Grep, and Bash tools. Use them to explore the structure and write the necessary files.`; } diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js index f0aa116b..99cad5f1 100644 --- a/app/electron/services/spec-regeneration-service.js +++ b/app/electron/services/spec-regeneration-service.js @@ -92,7 +92,7 @@ class SpecRegenerationService { * @param {string} projectOverview - User's project description * @param {Function} sendToRenderer - Function to send events to renderer * @param {Object} execution - Execution context with abort controller - * @param {boolean} generateFeatures - Whether to generate feature_list.json entries + * @param {boolean} generateFeatures - Whether to generate feature entries in features folder */ async createInitialSpec(projectPath, projectOverview, sendToRenderer, execution, generateFeatures = true) { console.log(`[SpecRegeneration] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`); @@ -187,43 +187,6 @@ class SpecRegenerationService { * @param {boolean} generateFeatures - Whether features should be generated */ getInitialCreationSystemPrompt(generateFeatures = true) { - const featureListInstructions = generateFeatures - ? ` -**FEATURE LIST GENERATION** - -After creating the app_spec.txt, you MUST also update the .automaker/feature_list.json file with all features from the implementation_roadmap section. - -For EACH feature in each phase of the implementation_roadmap: -1. Read the app_spec.txt you just created -2. Extract every single feature from each phase (phase_1, phase_2, phase_3, phase_4, etc.) -3. Write ALL features to .automaker/feature_list.json in order - -The feature_list.json format should be: -\`\`\`json -[ - { - "id": "feature--", - "category": "", - "description": "", - "status": "backlog", - "steps": ["Step 1", "Step 2", "..."], - "skipTests": true - } -] -\`\`\` - -IMPORTANT: Include EVERY feature from the implementation_roadmap. Do not skip any.` - : ` -**CRITICAL FILE PROTECTION** - -THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION: -- .automaker/feature_list.json - -**YOU MUST NEVER:** -- Use the Write tool on .automaker/feature_list.json -- Use the Edit tool on .automaker/feature_list.json -- Use any Bash command that writes to .automaker/feature_list.json`; - return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project overview. You should: @@ -241,10 +204,13 @@ When analyzing, look at: - Framework-specific patterns (Next.js, React, Django, etc.) - Database configurations and schemas - API structures and patterns -${featureListInstructions} + +**Feature Storage:** +Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. +Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features. You CAN and SHOULD modify: -- .automaker/app_spec.txt (this is your primary target)${generateFeatures ? '\n- .automaker/feature_list.json (to populate features from implementation_roadmap)' : ''} +- .automaker/app_spec.txt (this is your primary target) You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`; } @@ -252,20 +218,9 @@ You have access to file reading, writing, and search tools. Use them to understa /** * Build the prompt for initial spec creation * @param {string} projectOverview - User's project description - * @param {boolean} generateFeatures - Whether to generate feature_list.json entries + * @param {boolean} generateFeatures - Whether to generate feature entries in features folder */ buildInitialCreationPrompt(projectOverview, generateFeatures = true) { - const featureGenerationStep = generateFeatures - ? ` -5. **IMPORTANT - GENERATE FEATURE LIST**: After writing the app_spec.txt: - - Read back the app_spec.txt file you just created - - Look at the implementation_roadmap section - - For EVERY feature listed in each phase (phase_1, phase_2, phase_3, phase_4, etc.), create an entry - - Write ALL these features to \`.automaker/feature_list.json\` in the order they appear - - Each feature should have: id (feature-timestamp-index), category (phase name), description, status: "backlog", steps array, and skipTests: true - - Do NOT skip any features - include every single one from the roadmap` - : ''; - return `I need you to create an initial application specification for my project. I haven't set up an app_spec.txt yet, so this will be the first one. **My Project Overview:** @@ -295,7 +250,6 @@ ${APP_SPEC_XML_TEMPLATE} - **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built 4. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\` -${featureGenerationStep} **Guidelines:** - Be comprehensive! Include ALL features needed for a complete application @@ -420,15 +374,9 @@ When analyzing, look at: - Database configurations and schemas - API structures and patterns -**CRITICAL FILE PROTECTION** - -THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION: -- .automaker/feature_list.json - -**YOU MUST NEVER:** -- Use the Write tool on .automaker/feature_list.json -- Use the Edit tool on .automaker/feature_list.json -- Use any Bash command that writes to .automaker/feature_list.json +**Feature Storage:** +Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. +Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features. You CAN and SHOULD modify: - .automaker/app_spec.txt (this is your primary target) diff --git a/app/electron/services/worktree-manager.js b/app/electron/services/worktree-manager.js index 1f7fef3c..0e1cfc45 100644 --- a/app/electron/services/worktree-manager.js +++ b/app/electron/services/worktree-manager.js @@ -176,15 +176,8 @@ class WorktreeManager { try { await fs.mkdir(automakerDst, { recursive: true }); - // Copy feature_list.json - const featureListSrc = path.join(automakerSrc, "feature_list.json"); - const featureListDst = path.join(automakerDst, "feature_list.json"); - try { - const content = await fs.readFile(featureListSrc, "utf-8"); - await fs.writeFile(featureListDst, content, "utf-8"); - } catch { - // Feature list might not exist yet - } + // Note: Features are stored in .automaker/features/{id}/feature.json + // These are managed by the main project, not copied to worktrees // Copy app_spec.txt if it exists const appSpecSrc = path.join(automakerSrc, "app_spec.txt"); diff --git a/app/package-lock.json b/app/package-lock.json index b45f727a..a56a7a6a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", @@ -24,6 +25,7 @@ "@tanstack/react-query": "^5.90.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "dotenv": "^17.2.3", "lucide-react": "^0.556.0", "next": "16.0.7", @@ -2955,6 +2957,61 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -5737,6 +5794,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/app/package.json b/app/package.json index 6d3515f3..d94302df 100644 --- a/app/package.json +++ b/app/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", @@ -31,6 +32,7 @@ "@tanstack/react-query": "^5.90.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "dotenv": "^17.2.3", "lucide-react": "^0.556.0", "next": "16.0.7", diff --git a/app/public/sounds/ding.mp3 b/app/public/sounds/ding.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0b2b0445b4c029cbc2f4ee0f406d3d7f327b50fd GIT binary patch literal 22320 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 Jq-Fiz_dnVG>Ouej literal 0 HcmV?d00001 diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 9870d1fd..e320c2fd 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { Sidebar } from "@/components/layout/sidebar"; import { WelcomeView } from "@/components/views/welcome-view"; import { BoardView } from "@/components/views/board-view"; @@ -20,6 +20,45 @@ export default function Home() { const { currentView, setCurrentView, setIpcConnected, theme, currentProject } = useAppStore(); const { isFirstRun, setupComplete } = useSetupStore(); const [isMounted, setIsMounted] = useState(false); + const [streamerPanelOpen, setStreamerPanelOpen] = useState(false); + + // Hidden streamer panel - opens with "\" key + const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => { + // Don't trigger when typing in inputs + const activeElement = document.activeElement; + if (activeElement) { + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === "input" || tagName === "textarea" || tagName === "select") { + return; + } + if (activeElement.getAttribute("contenteditable") === "true") { + return; + } + const role = activeElement.getAttribute("role"); + if (role === "textbox" || role === "searchbox" || role === "combobox") { + return; + } + } + + // Don't trigger with modifier keys + if (event.ctrlKey || event.altKey || event.metaKey) { + return; + } + + // Check for "\" key (backslash) + if (event.key === "\\") { + event.preventDefault(); + setStreamerPanelOpen((prev) => !prev); + } + }, []); + + // Register the "\" shortcut for streamer panel + useEffect(() => { + window.addEventListener("keydown", handleStreamerPanelShortcut); + return () => { + window.removeEventListener("keydown", handleStreamerPanelShortcut); + }; + }, [handleStreamerPanelShortcut]); // Compute the effective theme: project theme takes priority over global theme // This is reactive because it depends on currentProject and theme from the store @@ -162,7 +201,9 @@ export default function Home() { return (

-
{renderView()}
+
+ {renderView()} +
{/* Environment indicator - only show after mount to prevent hydration issues */} {isMounted && !isElectron() && ( @@ -170,6 +211,13 @@ export default function Home() { Web Mode (Mock IPC) )} + + {/* Hidden streamer panel - opens with "\" key, pushes content */} +
); } diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index e40c72dd..bd4f35a8 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -1271,7 +1271,7 @@ export function Sidebar() { Generate feature list

- Automatically populate feature_list.json with all features + Automatically create features in the features folder from the implementation roadmap after the spec is generated.

diff --git a/app/src/components/ui/category-autocomplete.tsx b/app/src/components/ui/category-autocomplete.tsx index c0742a07..7addab59 100644 --- a/app/src/components/ui/category-autocomplete.tsx +++ b/app/src/components/ui/category-autocomplete.tsx @@ -1,11 +1,23 @@ "use client"; import * as React from "react"; -import { useState, useRef, useEffect, useCallback } from "react"; -import { createPortal } from "react-dom"; +import { Check, ChevronsUpDown } from "lucide-react"; + import { cn } from "@/lib/utils"; -import { Input } from "./input"; -import { Check, ChevronDown } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; interface CategoryAutocompleteProps { value: string; @@ -26,225 +38,54 @@ export function CategoryAutocomplete({ disabled = false, "data-testid": testId, }: CategoryAutocompleteProps) { - const [isOpen, setIsOpen] = useState(false); - const [inputValue, setInputValue] = useState(value); - const [filteredSuggestions, setFilteredSuggestions] = useState([]); - const [highlightedIndex, setHighlightedIndex] = useState(-1); - const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }); - const containerRef = useRef(null); - const inputRef = useRef(null); - const listRef = useRef(null); - - // Update internal state when value prop changes - useEffect(() => { - setInputValue(value); - }, [value]); - - // Filter suggestions based on input - useEffect(() => { - const searchTerm = inputValue.toLowerCase().trim(); - if (searchTerm === "") { - setFilteredSuggestions(suggestions); - } else { - const filtered = suggestions.filter((s) => - s.toLowerCase().includes(searchTerm) - ); - setFilteredSuggestions(filtered); - } - setHighlightedIndex(-1); - }, [inputValue, suggestions]); - - // Update dropdown position when open and handle scroll/resize - useEffect(() => { - const updatePosition = () => { - if (isOpen && containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - setDropdownPosition({ - top: rect.bottom + window.scrollY, - left: rect.left + window.scrollX, - width: rect.width, - }); - } - }; - - updatePosition(); - - if (isOpen) { - window.addEventListener("scroll", updatePosition, true); - window.addEventListener("resize", updatePosition); - return () => { - window.removeEventListener("scroll", updatePosition, true); - window.removeEventListener("resize", updatePosition); - }; - } - }, [isOpen]); - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - containerRef.current && - !containerRef.current.contains(event.target as Node) && - listRef.current && - !listRef.current.contains(event.target as Node) - ) { - setIsOpen(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - // Scroll highlighted item into view - useEffect(() => { - if (highlightedIndex >= 0 && listRef.current) { - const items = listRef.current.querySelectorAll("li"); - const highlightedItem = items[highlightedIndex]; - if (highlightedItem) { - highlightedItem.scrollIntoView({ block: "nearest" }); - } - } - }, [highlightedIndex]); - - const handleInputChange = useCallback( - (e: React.ChangeEvent) => { - const newValue = e.target.value; - setInputValue(newValue); - onChange(newValue); - setIsOpen(true); - }, - [onChange] - ); - - const handleSelect = useCallback( - (suggestion: string) => { - setInputValue(suggestion); - onChange(suggestion); - setIsOpen(false); - inputRef.current?.focus(); - }, - [onChange] - ); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (!isOpen) { - if (e.key === "ArrowDown" || e.key === "Enter") { - e.preventDefault(); - setIsOpen(true); - } - return; - } - - switch (e.key) { - case "ArrowDown": - e.preventDefault(); - setHighlightedIndex((prev) => - prev < filteredSuggestions.length - 1 ? prev + 1 : prev - ); - break; - case "ArrowUp": - e.preventDefault(); - setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1)); - break; - case "Enter": - e.preventDefault(); - if (highlightedIndex >= 0 && filteredSuggestions[highlightedIndex]) { - handleSelect(filteredSuggestions[highlightedIndex]); - } else { - setIsOpen(false); - } - break; - case "Escape": - e.preventDefault(); - setIsOpen(false); - break; - case "Tab": - setIsOpen(false); - break; - } - }, - [isOpen, highlightedIndex, filteredSuggestions, handleSelect] - ); - - const handleFocus = useCallback(() => { - setIsOpen(true); - }, []); + const [open, setOpen] = React.useState(false); return ( -
-
- + + -
- - {isOpen && filteredSuggestions.length > 0 && typeof document !== "undefined" && - createPortal( -
    - {filteredSuggestions.map((suggestion, index) => ( -
  • { - e.preventDefault(); - handleSelect(suggestion); - }} - onMouseEnter={() => setHighlightedIndex(index)} - data-testid={`category-option-${suggestion.toLowerCase().replace(/\s+/g, "-")}`} - > - {inputValue === suggestion && ( - - )} - + {value + ? suggestions.find((s) => s === value) ?? value + : placeholder} + + + + + + + + No category found. + + {suggestions.map((suggestion) => ( + { + onChange(currentValue === value ? "" : currentValue); + setOpen(false); + }} + data-testid={`category-option-${suggestion.toLowerCase().replace(/\s+/g, "-")}`} + > {suggestion} - -
  • - ))} -
, - document.body - )} -
+ + + ))} + + + + + ); } diff --git a/app/src/components/ui/command.tsx b/app/src/components/ui/command.tsx new file mode 100644 index 00000000..8cb4ca7a --- /dev/null +++ b/app/src/components/ui/command.tsx @@ -0,0 +1,184 @@ +"use client" + +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps & { + title?: string + description?: string + className?: string + showCloseButton?: boolean +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/app/src/components/ui/popover.tsx b/app/src/components/ui/popover.tsx new file mode 100644 index 00000000..01e468b6 --- /dev/null +++ b/app/src/components/ui/popover.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/app/src/components/views/agent-output-modal.tsx b/app/src/components/views/agent-output-modal.tsx index b3590805..0dd6429c 100644 --- a/app/src/components/views/agent-output-modal.tsx +++ b/app/src/components/views/agent-output-modal.tsx @@ -20,6 +20,8 @@ interface AgentOutputModalProps { onClose: () => void; featureDescription: string; featureId: string; + /** The status of the feature - used to determine if spinner should be shown */ + featureStatus?: string; /** Called when a number key (0-9) is pressed while the modal is open */ onNumberKeyPress?: (key: string) => void; } @@ -31,6 +33,7 @@ export function AgentOutputModal({ onClose, featureDescription, featureId, + featureStatus, onNumberKeyPress, }: AgentOutputModalProps) { const [output, setOutput] = useState(""); @@ -70,16 +73,18 @@ export function AgentOutputModal({ projectPathRef.current = currentProject.path; setProjectPath(currentProject.path); - // Ensure context directory exists - const contextDir = `${currentProject.path}/.automaker/agents-context`; - await api.mkdir(contextDir); + // Use features API to get agent output + if (api.features) { + const result = await api.features.getAgentOutput( + currentProject.path, + featureId + ); - // Try to read existing output file - const outputPath = `${contextDir}/${featureId}.md`; - const result = await api.readFile(outputPath); - - if (result.success && result.content) { - setOutput(result.content); + if (result.success) { + setOutput(result.content || ""); + } else { + setOutput(""); + } } else { setOutput(""); } @@ -102,9 +107,10 @@ export function AgentOutputModal({ if (!api) return; try { - const contextDir = `${projectPathRef.current}/.automaker/agents-context`; - const outputPath = `${contextDir}/${featureId}.md`; - + // Use features API - agent output is stored in features/{id}/agent-output.md + // We need to write it directly since there's no updateAgentOutput method + // The context-manager handles this on the backend, but for frontend edits we write directly + const outputPath = `${projectPathRef.current}/.automaker/features/${featureId}/agent-output.md`; await api.writeFile(outputPath, newContent); } catch (error) { console.error("Failed to save output:", error); @@ -250,7 +256,10 @@ export function AgentOutputModal({
- + {featureStatus !== "verified" && + featureStatus !== "waiting_approval" && ( + + )} Agent Output
diff --git a/app/src/components/views/agent-tools-view.tsx b/app/src/components/views/agent-tools-view.tsx index e08eb762..b9a4fada 100644 --- a/app/src/components/views/agent-tools-view.tsx +++ b/app/src/components/views/agent-tools-view.tsx @@ -155,7 +155,7 @@ export function AgentToolsView() { // In mock mode, simulate terminal output // In real Electron mode, this would use child_process const mockOutputs: Record = { - ls: "app_spec.txt\nfeature_list.json\nnode_modules\npackage.json\nsrc\ntests\ntsconfig.json", + ls: "app_spec.txt\nfeatures\nnode_modules\npackage.json\nsrc\ntests\ntsconfig.json", pwd: currentProject?.path || "/Users/demo/project", "echo hello": "hello", whoami: "automaker-agent", diff --git a/app/src/components/views/agent-view.tsx b/app/src/components/views/agent-view.tsx index c99bf916..6436915a 100644 --- a/app/src/components/views/agent-view.tsx +++ b/app/src/components/views/agent-view.tsx @@ -594,11 +594,11 @@ export function AgentView() { className={cn( "max-w-[80%]", message.role === "user" - ? "bg-primary text-primary-foreground" + ? "bg-transparent border border-primary text-foreground" : "border-l-4 border-primary bg-card" )} > - + {message.role === "assistant" ? ( {message.content} @@ -610,9 +610,9 @@ export function AgentView() { )}

diff --git a/app/src/components/views/analysis-view.tsx b/app/src/components/views/analysis-view.tsx index 48f4e07d..2860cb79 100644 --- a/app/src/components/views/analysis-view.tsx +++ b/app/src/components/views/analysis-view.tsx @@ -409,7 +409,7 @@ ${Object.entries(projectAnalysis.filesByExtension) } }, [currentProject, projectAnalysis]); - // Generate .automaker/feature_list.json from analysis + // Generate features from analysis and save to .automaker/features folder const generateFeatureList = useCallback(async () => { if (!currentProject || !projectAnalysis) return; @@ -755,23 +755,12 @@ ${Object.entries(projectAnalysis.filesByExtension) }); } - // Generate the feature list content - const featureListContent = JSON.stringify(detectedFeatures, null, 2); - - // Write the feature list file - const featureListPath = `${currentProject.path}/feature_list.json`; - const writeResult = await api.writeFile( - featureListPath, - featureListContent - ); - - if (writeResult.success) { - setFeatureListGenerated(true); - } else { - setFeatureListError( - writeResult.error || "Failed to write feature list file" - ); + // Create each feature using the features API + for (const feature of detectedFeatures) { + await api.features.create(currentProject.path, feature); } + + setFeatureListGenerated(true); } catch (error) { console.error("Failed to generate feature list:", error); setFeatureListError( @@ -1041,7 +1030,7 @@ ${Object.entries(projectAnalysis.filesByExtension) Generate Feature List - Create .automaker/feature_list.json from analysis + Create features from analysis @@ -1074,7 +1063,7 @@ ${Object.entries(projectAnalysis.filesByExtension) data-testid="feature-list-generated-success" > - feature_list.json created successfully! + Features created successfully!

)} {featureListError && ( diff --git a/app/src/components/views/board-view.tsx b/app/src/components/views/board-view.tsx index 3389ee27..faf7f1dc 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -85,6 +85,7 @@ import { Minimize2, Square, Maximize2, + Shuffle, } from "lucide-react"; import { toast } from "sonner"; import { Slider } from "@/components/ui/slider"; @@ -242,6 +243,8 @@ export function BoardView() { const [followUpPreviewMap, setFollowUpPreviewMap] = useState( () => new Map() ); + const [editFeaturePreviewMap, setEditFeaturePreviewMap] = + useState(() => new Map()); // Local state to temporarily show advanced options when profiles-only mode is enabled const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false); @@ -390,7 +393,7 @@ export function BoardView() { return rectIntersection(args); }, []); - // Load features from file + // Load features using features API const loadFeatures = useCallback(async () => { if (!currentProject) return; @@ -419,21 +422,25 @@ export function BoardView() { try { const api = getElectronAPI(); - const result = await api.readFile( - `${currentProject.path}/.automaker/feature_list.json` - ); + if (!api.features) { + console.error("[BoardView] Features API not available"); + return; + } - if (result.success && result.content) { - const parsed = JSON.parse(result.content); - const featuresWithIds = parsed.map((f: any, index: number) => ({ - ...f, - id: f.id || `feature-${index}-${Date.now()}`, - status: f.status || "backlog", - startedAt: f.startedAt, // Preserve startedAt timestamp - // Ensure model and thinkingLevel are set for backward compatibility - model: f.model || "opus", - thinkingLevel: f.thinkingLevel || "none", - })); + const result = await api.features.getAll(currentProject.path); + + if (result.success && result.features) { + const featuresWithIds = result.features.map( + (f: any, index: number) => ({ + ...f, + id: f.id || `feature-${index}-${Date.now()}`, + status: f.status || "backlog", + startedAt: f.startedAt, // Preserve startedAt timestamp + // Ensure model and thinkingLevel are set for backward compatibility + model: f.model || "opus", + thinkingLevel: f.thinkingLevel || "none", + }) + ); setFeatures(featuresWithIds); } } catch (error) { @@ -529,6 +536,9 @@ export function BoardView() { // Reload features when a feature is completed console.log("[Board] Feature completed, reloading features..."); loadFeatures(); + // Play ding sound when feature is done + const audio = new Audio("/sounds/ding.mp3"); + audio.play().catch((err) => console.warn("Could not play ding sound:", err)); } else if (event.type === "auto_mode_error") { // Reload features when an error occurs (feature moved to waiting_approval) console.log( @@ -627,41 +637,75 @@ export function BoardView() { } }, [features, isLoading]); - // Save features to file - const saveFeatures = useCallback(async () => { - if (!currentProject) return; + // Persist feature update to API (replaces saveFeatures) + const persistFeatureUpdate = useCallback( + async (featureId: string, updates: Partial) => { + if (!currentProject) return; - try { - const api = getElectronAPI(); - const toSave = features.map((f) => ({ - id: f.id, - category: f.category, - description: f.description, - steps: f.steps, - status: f.status, - startedAt: f.startedAt, - imagePaths: f.imagePaths, - skipTests: f.skipTests, - summary: f.summary, - model: f.model, - thinkingLevel: f.thinkingLevel, - error: f.error, - })); - await api.writeFile( - `${currentProject.path}/.automaker/feature_list.json`, - JSON.stringify(toSave, null, 2) - ); - } catch (error) { - console.error("Failed to save features:", error); - } - }, [currentProject, features]); + try { + const api = getElectronAPI(); + if (!api.features) { + console.error("[BoardView] Features API not available"); + return; + } - // Save when features change (after initial load is complete) - useEffect(() => { - if (!isLoading && !isSwitchingProjectRef.current) { - saveFeatures(); - } - }, [features, saveFeatures, isLoading]); + const result = await api.features.update( + currentProject.path, + featureId, + updates + ); + if (result.success && result.feature) { + updateFeature(result.feature.id, result.feature); + } + } catch (error) { + console.error("Failed to persist feature update:", error); + } + }, + [currentProject, updateFeature] + ); + + // Persist feature creation to API + const persistFeatureCreate = useCallback( + async (feature: Feature) => { + if (!currentProject) return; + + try { + const api = getElectronAPI(); + if (!api.features) { + console.error("[BoardView] Features API not available"); + return; + } + + const result = await api.features.create(currentProject.path, feature); + if (result.success && result.feature) { + updateFeature(result.feature.id, result.feature); + } + } catch (error) { + console.error("Failed to persist feature creation:", error); + } + }, + [currentProject, updateFeature] + ); + + // Persist feature deletion to API + const persistFeatureDelete = useCallback( + async (featureId: string) => { + if (!currentProject) return; + + try { + const api = getElectronAPI(); + if (!api.features) { + console.error("[BoardView] Features API not available"); + return; + } + + await api.features.delete(currentProject.path, featureId); + } catch (error) { + console.error("Failed to persist feature deletion:", error); + } + }, + [currentProject] + ); const handleDragStart = (event: DragStartEvent) => { const { active } = event; @@ -690,13 +734,15 @@ export function BoardView() { // Determine if dragging is allowed based on status and skipTests // - Backlog items can always be dragged // - waiting_approval items can always be dragged (to allow manual verification via drag) + // - verified items can always be dragged (to allow moving back to waiting_approval) // - skipTests (non-TDD) items can be dragged between in_progress and verified - // - Non-skipTests (TDD) items that are in progress or verified cannot be dragged + // - Non-skipTests (TDD) items that are in progress cannot be dragged (they are running) if ( draggedFeature.status !== "backlog" && - draggedFeature.status !== "waiting_approval" + draggedFeature.status !== "waiting_approval" && + draggedFeature.status !== "verified" ) { - // Only allow dragging in_progress/verified if it's a skipTests feature and not currently running + // Only allow dragging in_progress if it's a skipTests feature and not currently running if (!draggedFeature.skipTests || isRunningTask) { console.log( "[Board] Cannot drag feature - TDD feature or currently running" @@ -744,14 +790,17 @@ export function BoardView() { // From backlog if (targetStatus === "in_progress") { // Update with startedAt timestamp - updateFeature(featureId, { + const updates = { status: targetStatus, startedAt: new Date().toISOString(), - }); + }; + updateFeature(featureId, updates); + persistFeatureUpdate(featureId, updates); console.log("[Board] Feature moved to in_progress, starting agent..."); await handleRunFeature(draggedFeature); } else { moveFeature(featureId, targetStatus); + persistFeatureUpdate(featureId, { status: targetStatus }); } } else if (draggedFeature.status === "waiting_approval") { // waiting_approval features can be dragged to verified for manual verification @@ -759,6 +808,7 @@ export function BoardView() { // features often have skipTests=true, and we want status-based handling first if (targetStatus === "verified") { moveFeature(featureId, "verified"); + persistFeatureUpdate(featureId, { status: "verified" }); toast.success("Feature verified", { description: `Manually verified: ${draggedFeature.description.slice( 0, @@ -768,6 +818,7 @@ export function BoardView() { } else if (targetStatus === "backlog") { // Allow moving waiting_approval cards back to backlog moveFeature(featureId, "backlog"); + persistFeatureUpdate(featureId, { status: "backlog" }); toast.info("Feature moved to backlog", { description: `Moved to Backlog: ${draggedFeature.description.slice( 0, @@ -783,6 +834,7 @@ export function BoardView() { ) { // Manual verify via drag moveFeature(featureId, "verified"); + persistFeatureUpdate(featureId, { status: "verified" }); toast.success("Feature verified", { description: `Marked as verified: ${draggedFeature.description.slice( 0, @@ -790,16 +842,14 @@ export function BoardView() { )}${draggedFeature.description.length > 50 ? "..." : ""}`, }); } else if ( - targetStatus === "in_progress" && + targetStatus === "waiting_approval" && draggedFeature.status === "verified" ) { - // Move back to in_progress - updateFeature(featureId, { - status: "in_progress", - startedAt: new Date().toISOString(), - }); + // Move verified feature back to waiting_approval + moveFeature(featureId, "waiting_approval"); + persistFeatureUpdate(featureId, { status: "waiting_approval" }); toast.info("Feature moved back", { - description: `Moved back to In Progress: ${draggedFeature.description.slice( + description: `Moved back to Waiting Approval: ${draggedFeature.description.slice( 0, 50 )}${draggedFeature.description.length > 50 ? "..." : ""}`, @@ -807,6 +857,30 @@ export function BoardView() { } else if (targetStatus === "backlog") { // Allow moving skipTests cards back to backlog moveFeature(featureId, "backlog"); + persistFeatureUpdate(featureId, { status: "backlog" }); + toast.info("Feature moved to backlog", { + description: `Moved to Backlog: ${draggedFeature.description.slice( + 0, + 50 + )}${draggedFeature.description.length > 50 ? "..." : ""}`, + }); + } + } else if (draggedFeature.status === "verified") { + // Handle verified TDD (non-skipTests) features being moved back + if (targetStatus === "waiting_approval") { + // Move verified feature back to waiting_approval + moveFeature(featureId, "waiting_approval"); + persistFeatureUpdate(featureId, { status: "waiting_approval" }); + toast.info("Feature moved back", { + description: `Moved back to Waiting Approval: ${draggedFeature.description.slice( + 0, + 50 + )}${draggedFeature.description.length > 50 ? "..." : ""}`, + }); + } else if (targetStatus === "backlog") { + // Allow moving verified cards back to backlog + moveFeature(featureId, "backlog"); + persistFeatureUpdate(featureId, { status: "backlog" }); toast.info("Feature moved to backlog", { description: `Moved to Backlog: ${draggedFeature.description.slice( 0, @@ -828,17 +902,19 @@ export function BoardView() { const normalizedThinking = modelSupportsThinking(selectedModel) ? newFeature.thinkingLevel : "none"; - addFeature({ + const newFeatureData = { category, description: newFeature.description, steps: newFeature.steps.filter((s) => s.trim()), - status: "backlog", + status: "backlog" as const, images: newFeature.images, imagePaths: newFeature.imagePaths, skipTests: newFeature.skipTests, model: selectedModel, thinkingLevel: normalizedThinking, - }); + }; + const createdFeature = addFeature(newFeatureData); + persistFeatureCreate(createdFeature); // Persist the category saveCategory(category); setNewFeature({ @@ -864,14 +940,19 @@ export function BoardView() { ? editingFeature.thinkingLevel : "none"; - updateFeature(editingFeature.id, { + const updates = { category: editingFeature.category, description: editingFeature.description, steps: editingFeature.steps, skipTests: editingFeature.skipTests, model: selectedModel, thinkingLevel: normalizedThinking, - }); + imagePaths: editingFeature.imagePaths, + }; + updateFeature(editingFeature.id, updates); + persistFeatureUpdate(editingFeature.id, updates); + // Clear the preview map after saving + setEditFeaturePreviewMap(new Map()); // Persist the category if it's new if (editingFeature.category) { saveCategory(editingFeature.category); @@ -904,13 +985,14 @@ export function BoardView() { } } - // Delete agent context file if it exists + // Note: Agent context file will be deleted automatically when feature folder is deleted + // via persistFeatureDelete, so no manual deletion needed if (currentProject) { 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}`); + // Feature folder deletion handles agent-output.md automatically + console.log( + `[Board] Feature ${featureId} will be deleted (including agent context)` + ); } catch (error) { // Context file might not exist, which is fine console.log( @@ -944,6 +1026,7 @@ export function BoardView() { // Remove the feature immediately without confirmation removeFeature(featureId); + persistFeatureDelete(featureId); }; const handleRunFeature = async (feature: Feature) => { @@ -1056,6 +1139,7 @@ export function BoardView() { description: feature.description, }); moveFeature(feature.id, "verified"); + persistFeatureUpdate(feature.id, { status: "verified" }); toast.success("Feature verified", { description: `Marked as verified: ${feature.description.slice(0, 50)}${ feature.description.length > 50 ? "..." : "" @@ -1069,10 +1153,12 @@ export function BoardView() { id: feature.id, description: feature.description, }); - updateFeature(feature.id, { - status: "in_progress", + const updates = { + status: "in_progress" as const, startedAt: new Date().toISOString(), - }); + }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); toast.info("Feature moved back", { description: `Moved back to In Progress: ${feature.description.slice( 0, @@ -1119,10 +1205,12 @@ export function BoardView() { } // Move feature back to in_progress before sending follow-up - updateFeature(featureId, { - status: "in_progress", + const updates = { + status: "in_progress" as const, startedAt: new Date().toISOString(), - }); + }; + updateFeature(featureId, updates); + persistFeatureUpdate(featureId, updates); // Reset follow-up state immediately (close dialog, clear form) setShowFollowUpDialog(false); @@ -1181,6 +1269,7 @@ export function BoardView() { console.log("[Board] Feature committed successfully"); // Move to verified status moveFeature(feature.id, "verified"); + persistFeatureUpdate(feature.id, { status: "verified" }); toast.success("Feature committed", { description: `Committed and verified: ${feature.description.slice( 0, @@ -1210,7 +1299,9 @@ export function BoardView() { id: feature.id, description: feature.description, }); - updateFeature(feature.id, { status: "waiting_approval" }); + const updates = { status: "waiting_approval" as const }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); toast.info("Feature ready for review", { description: `Ready for approval: ${feature.description.slice(0, 50)}${ feature.description.length > 50 ? "..." : "" @@ -1426,6 +1517,7 @@ export function BoardView() { if (targetStatus !== feature.status) { moveFeature(feature.id, targetStatus); + persistFeatureUpdate(feature.id, { status: targetStatus }); } toast.success("Agent stopped", { @@ -1473,10 +1565,12 @@ export function BoardView() { for (const feature of featuresToStart) { // Update the feature status with startedAt timestamp - updateFeature(feature.id, { - status: "in_progress", + const updates = { + status: "in_progress" as const, startedAt: new Date().toISOString(), - }); + }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); // Start the agent for this feature await handleRunFeature(feature); } @@ -1885,7 +1979,24 @@ export function BoardView() { } }} > - + { + // Prevent dialog from closing when clicking on category autocomplete dropdown + const target = e.target as HTMLElement; + if (target.closest('[data-testid="category-autocomplete-list"]')) { + e.preventDefault(); + } + }} + onInteractOutside={(e) => { + // Prevent dialog from closing when clicking on category autocomplete dropdown + const target = e.target as HTMLElement; + if (target.closest('[data-testid="category-autocomplete-list"]')) { + e.preventDefault(); + } + }} + > Add New Feature @@ -2276,10 +2387,28 @@ export function BoardView() { if (!open) { setEditingFeature(null); setShowEditAdvancedOptions(false); + setEditFeaturePreviewMap(new Map()); } }} > - + { + // Prevent dialog from closing when clicking on category autocomplete dropdown + const target = e.target as HTMLElement; + if (target.closest('[data-testid="category-autocomplete-list"]')) { + e.preventDefault(); + } + }} + onInteractOutside={(e) => { + // Prevent dialog from closing when clicking on category autocomplete dropdown + const target = e.target as HTMLElement; + if (target.closest('[data-testid="category-autocomplete-list"]')) { + e.preventDefault(); + } + }} + > Edit Feature Modify the feature details. @@ -2308,16 +2437,24 @@ export function BoardView() {
-