From 0bfe77f9f19bbc316ec1db219ada569141588a97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:18:37 +0000 Subject: [PATCH 1/6] Initial plan From 7cf9a9f11a84d9526536b6816a02a97383de923b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:29:56 +0000 Subject: [PATCH 2/6] Add markdown rendering to interview-view using Markdown component from ui/markdown Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com> --- app/src/components/views/interview-view.tsx | 27 +++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/components/views/interview-view.tsx b/app/src/components/views/interview-view.tsx index 941d0ec8..c1a9a5e1 100644 --- a/app/src/components/views/interview-view.tsx +++ b/app/src/components/views/interview-view.tsx @@ -17,6 +17,7 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; import { getElectronAPI } from "@/lib/electron"; +import { Markdown } from "@/components/ui/markdown"; interface InterviewMessage { id: string; @@ -100,10 +101,15 @@ export function InterviewView() { // Auto-scroll to bottom when messages change useEffect(() => { if (messagesContainerRef.current) { - messagesContainerRef.current.scrollTo({ - top: messagesContainerRef.current.scrollHeight, - behavior: "smooth", - }); + // Use a small delay to ensure DOM is updated + setTimeout(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTo({ + top: messagesContainerRef.current.scrollHeight, + behavior: "smooth", + }); + } + }, 100); } }, [messages]); @@ -437,10 +443,15 @@ export function InterviewView() { )} > -

{message.content}

+ {message.role === "assistant" ? ( + + {message.content} + + ) : ( +

+ {message.content} +

+ )}

Date: Wed, 10 Dec 2025 23:09:27 +0100 Subject: [PATCH 3/6] fix: scrolling issue --- app/src/components/views/interview-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/views/interview-view.tsx b/app/src/components/views/interview-view.tsx index c1a9a5e1..dc0a2883 100644 --- a/app/src/components/views/interview-view.tsx +++ b/app/src/components/views/interview-view.tsx @@ -359,7 +359,7 @@ export function InterviewView() { return (

{/* Header */} From a4dc21fd8448195006fda93a568ef24187a9ea3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 22:42:17 +0000 Subject: [PATCH 4/6] Add cleanup function to setTimeout in useEffect to prevent memory leaks Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com> --- app/src/components/views/interview-view.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/components/views/interview-view.tsx b/app/src/components/views/interview-view.tsx index dc0a2883..01cefa21 100644 --- a/app/src/components/views/interview-view.tsx +++ b/app/src/components/views/interview-view.tsx @@ -100,9 +100,10 @@ export function InterviewView() { // Auto-scroll to bottom when messages change useEffect(() => { + let timeoutId: NodeJS.Timeout | undefined; if (messagesContainerRef.current) { // Use a small delay to ensure DOM is updated - setTimeout(() => { + timeoutId = setTimeout(() => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTo({ top: messagesContainerRef.current.scrollHeight, @@ -111,6 +112,11 @@ export function InterviewView() { } }, 100); } + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; }, [messages]); // Auto-focus input From 995456358115027234000e76a8b80ea6f0a38db7 Mon Sep 17 00:00:00 2001 From: trueheads Date: Wed, 10 Dec 2025 17:23:56 -0600 Subject: [PATCH 5/6] UI fixes for path issues in project name, adjusted some UI styling inconsistencies as well. --- .automaker/app_spec.txt | 210 +++++++++++--------- .automaker/feature_list.json | 225 ++++++++++++++++++++++ app/src/components/layout/sidebar.tsx | 3 +- app/src/components/views/welcome-view.tsx | 19 +- app/src/lib/electron.ts | 1 + 5 files changed, 353 insertions(+), 105 deletions(-) diff --git a/.automaker/app_spec.txt b/.automaker/app_spec.txt index e9b2c014..c97a3214 100644 --- a/.automaker/app_spec.txt +++ b/.automaker/app_spec.txt @@ -1,136 +1,156 @@ Automaker - Autonomous AI Development Studio - + - Automaker is a native desktop application that empowers developers to build software autonomously. It acts as an intelligent orchestrator, managing the entire development lifecycle from specification to implementation. Built with Electron and Next.js, it provides a seamless GUI for configuring projects, defining requirements (.automaker/app_spec.txt), and tracking progress via an interactive Kanban board. It leverages a dual-model architecture: Claude 3.5 Opus for complex logic/architecture and Gemini 3 Pro for UI/UX design. + Automaker is a sophisticated desktop application that empowers developers to build software autonomously through AI-powered agents. Built with Electron and Next.js, it provides an intelligent GUI for project management, feature tracking via Kanban boards, and autonomous code generation. The application leverages multiple AI models (Claude, GPT) and supports complex workflows including git worktree isolation, testing automation, and multi-model agent execution. It acts as a complete development orchestrator, managing the entire lifecycle from specification to verified implementation. - Next.js (App Router) - shadcn/ui - Tailwind CSS - Zustand / TanStack Query - dnd-kit (for Kanban) + Next.js 16.0.7 (App Router) + shadcn/ui with Radix UI primitives + Tailwind CSS 4.0 + Zustand with persistence + @dnd-kit for Kanban board Lucide React + TanStack Query for server state - Electron - TypeScript - Electron IPC (tRPC or raw IPC) - Node.js fs/promises + Electron 39.2.6 + TypeScript 5.x + Electron IPC with security sandboxing + Node.js fs/promises with path validation - Claude 3.5 Opus (via Anthropic SDK) - Gemini 3 Pro (via Google Generative AI SDK) - LangChain or Custom Agent Loop + Claude 3.5 (Opus, Sonnet, Haiku) via Anthropic Claude Agent SDK + GPT-5.1 Codex family via OpenAI CLI + Custom Agent Service with streaming responses + Dynamic model provider system with CLI detection - Playwright (for E2E testing of Automaker itself) - Vitest + Playwright for E2E testing + Jest/Vitest compatible + Agent-driven test execution and verification + + Git with worktree isolation support + Feature branch management + Automated commit and merge capabilities + - - Open existing local projects - - Create new projects from scratch (Wizard/Interview Mode) - - Project configuration (name, path, ignore patterns) - - Visual file explorer + - Open and manage multiple local projects + - Project-specific themes and configurations + - Session management with project context + - Recently used project cycling (Q/E shortcuts) + - Project search and type-ahead selection + - Trash and restore functionality for projects - + - - "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`: - - Scans code for implemented features - - Creates test cases for existing features - - Marks existing features as "passes": true automatically + - Automatic codebase analysis and understanding + - Auto-generation and updating of app_spec.txt + - Feature extraction from existing codebases + - Technology stack detection and documentation + - Project structure analysis with file tree visualization - - Visual representation of `.automaker/feature_list.json` - - 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 - - "Play" button on cards to trigger the agent for that specific feature + - Visual Kanban board with drag-and-drop functionality + - Multiple status columns: Backlog, In Progress, Waiting Approval, Verified + - Feature cards with detailed information display (3 detail levels) + - Real-time status updates during agent execution + - Search and filtering capabilities + - Category management and autocomplete + - Image attachment support for feature descriptions - - **The Architect (Claude 3.5 Opus)**: - - Reads spec and feature list - - Plans implementation steps - - Writes functional code (backend, logic, state) - - Writes tests - - Uses standard prompts (e.g. `.automaker/coding_prompt.md`) to ensure quality and consistency. - - **The Designer (Gemini 3 Pro)**: - - Receives UI requirements - - Generates Tailwind classes and React components - - Ensures visual consistency and aesthetics - - **The Interviewer**: - - Interactive chat mode to gather requirements for new projects. - - Asks clarifying questions to define the `.automaker/app_spec.txt`. - - Suggests tech stacks and features based on user intent. - - **The QA Bot**: - - Runs local tests (Playwright/Jest) in the target project - - Reports results back to the Kanban board - - Updates "passes" status automatically + - Multi-model agent system with profile-based execution + - Streaming agent output with real-time logs + - Git worktree isolation for safe feature development + - Automatic testing and verification workflows + - Context-aware prompt generation + - Agent memory and learning capabilities + - Concurrent feature processing with configurable limits + - Follow-up and resume capabilities + + - Git worktree management for isolated development + - Feature-specific branching and merging + - Automated commit generation with file tracking + - Test-driven development support + - Code review and approval workflows + - Revert and rollback capabilities + + + + - Dark/Light theme support with 12 custom themes + - Per-project theme configurations + - Comprehensive keyboard shortcut system + - Sidebar navigation with project switching + - Multi-view architecture (Board, Spec, Agent, Context, Settings) + - Setup wizard for first-time configuration + - CLI integration status monitoring + + - - Workflow Editor: Configure the agent loop (e.g., Plan -> Code -> Test -> Review) - - Prompt Manager: Edit system prompts for Architect and Designer. Defaults to using `.automaker/coding_prompt.md` as the base instruction set. - - Model Registry: Add/Configure different models (OpenAI, Groq, local LLMs) - - Plugin System: Hooks for pre/post generation steps + - AI Profile system for model/thinking level presets + - Keyboard shortcut customization + - Model provider plugin architecture + - Context file management for agent guidance + - Feature suggestion generation + - Spec regeneration workflows - - - - 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` - - **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". - - - - Dark/Light mode support (system sync) - - "Hacker" aesthetic option (terminal-like) - - Professional/Clean default - - - - - - - "Browser Mode": Run the Next.js frontend in a standard browser with mocked Electron IPC for rapid UI iteration. - - "Electron Mode": Full desktop app testing. - - Hot Reloading for both Main and Renderer processes. - - + + - Complete Kanban board with drag-and-drop functionality + - Multi-model AI agent execution (Claude + GPT/Codex) + - Git worktree isolation for features + - Real-time agent output streaming and logging + - Project management with session persistence + - Theme system with 12 themes + per-project themes + - Comprehensive settings panel with all configurations + - Feature image attachment and context system + - Agent profiles with model/thinking level presets + - Keyboard shortcut system with customization + - CLI integration detection (Claude Code + Codex CLI) + - Auto mode for autonomous feature processing + - Feature suggestions generation + - Spec regeneration and project analysis + - Context file management + - Chat history and session management + - File diff viewing and git integration + - Search and filtering across all features + - Category management and autocomplete + - Test automation and verification workflows + - - Setup Next.js + Electron boilerplate - - Implement IPC bridge - - Create Project Management UI (Open/Create) + - Enhanced error handling and recovery mechanisms + - Performance optimization for large projects + - Improved memory management for long-running sessions + - Advanced logging and debugging capabilities - - Port python agent logic to TypeScript - - Implement "Project Ingestion" (Spec/Feature List generation) - - Integrate Claude 3.5 Opus and Gemini 3 Pro - - Implement "New Project Interview" workflow + - Plugin system for custom model providers + - Advanced workflow customization engine + - Team collaboration features + - Cloud synchronization capabilities + - Advanced project templates and scaffolding - - - Build Kanban board with drag-and-drop - - Connect Kanban state to `.automaker/feature_list.json` filesystem - - Implement "Run Feature" capability - - Integrate standard prompts library - - - - Advanced terminal integration - - Settings & Extensibility - - UI refinement - + + - Enhanced accessibility features + - Advanced theme customization + - Performance monitoring and analytics + - Documentation generation automation + - Integration with external development tools + - Advanced security auditing and sandboxing + \ No newline at end of file diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 03770f5d..39309333 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -391,5 +391,230 @@ "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" + }, + { + "id": "feature-1734710400000-0", + "category": "Phase 1: Foundation", + "description": "Enhanced error handling and recovery mechanisms", + "steps": [ + "Add comprehensive error boundary components", + "Implement agent execution error recovery", + "Add retry mechanisms for failed operations", + "Create detailed error logging and reporting" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-1", + "category": "Phase 1: Foundation", + "description": "Performance optimization for large projects", + "steps": [ + "Implement file tree virtualization for large directories", + "Add lazy loading for Kanban cards", + "Optimize agent output streaming for large outputs", + "Implement background processing for project analysis" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-2", + "category": "Phase 1: Foundation", + "description": "Improved memory management for long-running sessions", + "steps": [ + "Add memory usage monitoring", + "Implement automatic cleanup of old sessions", + "Add garbage collection for unused resources", + "Optimize state management for large feature lists" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-3", + "category": "Phase 1: Foundation", + "description": "Advanced logging and debugging capabilities", + "steps": [ + "Implement detailed agent execution tracing", + "Add debug mode with verbose logging", + "Create log export and analysis tools", + "Add performance profiling capabilities" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-4", + "category": "Phase 2: Core Logic", + "description": "Plugin system for custom model providers", + "steps": [ + "Design plugin API interface", + "Implement plugin loading and management", + "Add support for custom model configurations", + "Create plugin marketplace or registry" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-5", + "category": "Phase 2: Core Logic", + "description": "Advanced workflow customization engine", + "steps": [ + "Create visual workflow editor", + "Add custom prompt templates", + "Implement conditional workflow logic", + "Add workflow sharing and importing" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-6", + "category": "Phase 2: Core Logic", + "description": "Team collaboration features", + "steps": [ + "Add user authentication and authorization", + "Implement real-time collaboration on features", + "Add commenting and discussion features", + "Create team project sharing capabilities" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-7", + "category": "Phase 2: Core Logic", + "description": "Cloud synchronization capabilities", + "steps": [ + "Implement cloud storage integration", + "Add automatic project backup and sync", + "Create cross-device project access", + "Add conflict resolution for concurrent edits" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-8", + "category": "Phase 2: Core Logic", + "description": "Advanced project templates and scaffolding", + "steps": [ + "Create project template system", + "Add framework-specific scaffolding", + "Implement custom template creation", + "Add template marketplace or sharing" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-9", + "category": "Phase 3: Polish", + "description": "Enhanced accessibility features", + "steps": [ + "Add full keyboard navigation support", + "Implement screen reader compatibility", + "Add high contrast and accessibility themes", + "Create accessibility testing automation" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-10", + "category": "Phase 3: Polish", + "description": "Advanced theme customization", + "steps": [ + "Add custom color picker for themes", + "Implement CSS variable overrides", + "Create theme sharing and importing", + "Add animation and transition customization" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-11", + "category": "Phase 3: Polish", + "description": "Performance monitoring and analytics", + "steps": [ + "Add application performance monitoring", + "Implement usage analytics and insights", + "Create performance dashboard", + "Add optimization recommendations" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-12", + "category": "Phase 3: Polish", + "description": "Documentation generation automation", + "steps": [ + "Add automatic README generation", + "Implement API documentation extraction", + "Create feature documentation templates", + "Add documentation consistency checking" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-13", + "category": "Phase 3: Polish", + "description": "Integration with external development tools", + "steps": [ + "Add IDE plugin/extension support", + "Implement CI/CD pipeline integration", + "Add issue tracking system integration", + "Create webhook and API endpoints" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1734710400000-14", + "category": "Phase 3: Polish", + "description": "Advanced security auditing and sandboxing", + "steps": [ + "Implement code security scanning", + "Add enhanced sandboxing capabilities", + "Create security audit reports", + "Add permission management system" + ], + "status": "backlog", + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" } ] \ No newline at end of file diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index e40c72dd..bb19ea9a 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -393,7 +393,8 @@ export function Sidebar() { if (!result.canceled && result.filePaths[0]) { const path = result.filePaths[0]; - const name = path.split("/").pop() || "Untitled Project"; + // Extract folder name from path (works on both Windows and Mac/Linux) + const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project"; try { // Check if this is a brand new project (no .automaker directory) diff --git a/app/src/components/views/welcome-view.tsx b/app/src/components/views/welcome-view.tsx index d17236ca..6b6e4a4e 100644 --- a/app/src/components/views/welcome-view.tsx +++ b/app/src/components/views/welcome-view.tsx @@ -157,7 +157,8 @@ export function WelcomeView() { if (!result.canceled && result.filePaths[0]) { const path = result.filePaths[0]; - const name = path.split("/").pop() || "Untitled Project"; + // Extract folder name from path (works on both Windows and Mac/Linux) + const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project"; await initializeAndOpenProject(path, name); } }, [initializeAndOpenProject]); @@ -309,9 +310,9 @@ export function WelcomeView() { data-testid="new-project-card" >
-
-
-
+
+
+
@@ -327,7 +328,7 @@ export function WelcomeView() {
)} + + {/* 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() {
-