From 5ec5fe82e6813686422cb6c40c976860287b8076 Mon Sep 17 00:00:00 2001 From: webdevcody Date: Tue, 13 Jan 2026 22:35:45 -0500 Subject: [PATCH] refactor: Enhance project management features and UI components - Updated create-pr.ts to improve commit error handling and logging. - Enhanced project-switcher.tsx with new folder opening functionality and state management for project setup. - Expanded icon-picker.tsx to include a comprehensive list of icons organized by category. - Replaced dialog components with popover components for auto mode and plan settings, improving UI responsiveness. - Refactored board-view components to streamline feature management and enhance user experience. - Removed outdated dialog components and replaced them with popover alternatives for better accessibility. These changes aim to improve the overall usability and functionality of the project management interface. --- .../src/routes/worktree/routes/create-pr.ts | 9 +- .../components/icon-picker.tsx | 423 +++++++++++++++++- .../components/project-context-menu.tsx | 112 +++-- .../project-switcher/project-switcher.tsx | 205 ++++++++- .../sidebar/components/sidebar-header.tsx | 11 +- .../sidebar/components/sidebar-navigation.tsx | 2 +- apps/ui/src/components/views/board-view.tsx | 4 +- .../views/board-view/board-controls.tsx | 49 +- .../views/board-view/board-header.tsx | 145 ++---- .../components/selection-action-bar.tsx | 14 +- .../board-view/components/view-toggle.tsx | 4 +- .../dialogs/auto-mode-settings-dialog.tsx | 68 --- .../dialogs/auto-mode-settings-popover.tsx | 95 ++++ .../dialogs/plan-settings-dialog.tsx | 67 --- .../dialogs/plan-settings-popover.tsx | 61 +++ .../dialogs/worktree-settings-dialog.tsx | 67 --- .../dialogs/worktree-settings-popover.tsx | 61 +++ .../views/board-view/kanban-board.tsx | 45 +- .../src/components/views/graph-view-page.tsx | 1 + .../views/graph-view/graph-canvas.tsx | 12 +- .../views/graph-view/graph-view.tsx | 3 + 21 files changed, 989 insertions(+), 469 deletions(-) delete mode 100644 apps/ui/src/components/views/board-view/dialogs/auto-mode-settings-dialog.tsx create mode 100644 apps/ui/src/components/views/board-view/dialogs/auto-mode-settings-popover.tsx delete mode 100644 apps/ui/src/components/views/board-view/dialogs/plan-settings-dialog.tsx create mode 100644 apps/ui/src/components/views/board-view/dialogs/plan-settings-popover.tsx delete mode 100644 apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx create mode 100644 apps/ui/src/components/views/board-view/dialogs/worktree-settings-popover.tsx diff --git a/apps/server/src/routes/worktree/routes/create-pr.ts b/apps/server/src/routes/worktree/routes/create-pr.ts index ec7ba4dd..1bde9448 100644 --- a/apps/server/src/routes/worktree/routes/create-pr.ts +++ b/apps/server/src/routes/worktree/routes/create-pr.ts @@ -70,9 +70,8 @@ export function createCreatePRHandler() { logger.debug(`Changed files:\n${status}`); } - // If there are changes, commit them + // If there are changes, commit them before creating the PR let commitHash: string | null = null; - let commitError: string | null = null; if (hasChanges) { const message = commitMessage || `Changes from ${branchName}`; logger.debug(`Committing changes with message: ${message}`); @@ -98,14 +97,13 @@ export function createCreatePRHandler() { logger.info(`Commit successful: ${commitHash}`); } catch (commitErr: unknown) { const err = commitErr as { stderr?: string; message?: string }; - commitError = err.stderr || err.message || 'Commit failed'; + const commitError = err.stderr || err.message || 'Commit failed'; logger.error(`Commit failed: ${commitError}`); // Return error immediately - don't proceed with push/PR if commit fails res.status(500).json({ success: false, error: `Failed to commit changes: ${commitError}`, - commitError, }); return; } @@ -381,9 +379,8 @@ export function createCreatePRHandler() { success: true, result: { branch: branchName, - committed: hasChanges && !commitError, + committed: hasChanges, commitHash, - commitError: commitError || undefined, pushed: true, prUrl, prNumber, diff --git a/apps/ui/src/components/layout/project-switcher/components/icon-picker.tsx b/apps/ui/src/components/layout/project-switcher/components/icon-picker.tsx index 10947a51..31ce1d3d 100644 --- a/apps/ui/src/components/layout/project-switcher/components/icon-picker.tsx +++ b/apps/ui/src/components/layout/project-switcher/components/icon-picker.tsx @@ -10,43 +10,434 @@ interface IconPickerProps { onSelectIcon: (icon: string | null) => void; } -// Popular project-related icons +// Comprehensive list of project-related icons from Lucide +// Organized by category for easier browsing const POPULAR_ICONS = [ + // Folders & Files 'Folder', 'FolderOpen', 'FolderCode', 'FolderGit', 'FolderKanban', - 'Package', - 'Box', - 'Boxes', + 'FolderTree', + 'FolderInput', + 'FolderOutput', + 'FolderPlus', + 'File', + 'FileCode', + 'FileText', + 'FileJson', + 'FileImage', + 'FileVideo', + 'FileAudio', + 'FileSpreadsheet', + 'Files', + 'Archive', + + // Code & Development 'Code', 'Code2', 'Braces', - 'FileCode', + 'Brackets', 'Terminal', - 'Globe', - 'Server', - 'Database', + 'TerminalSquare', + 'Command', + 'GitBranch', + 'GitCommit', + 'GitMerge', + 'GitPullRequest', + 'GitCompare', + 'GitFork', + 'GitHub', + 'Gitlab', + 'Bitbucket', + 'Vscode', + + // Packages & Containers + 'Package', + 'PackageSearch', + 'PackageCheck', + 'PackageX', + 'Box', + 'Boxes', + 'Container', + + // UI & Design 'Layout', + 'LayoutGrid', + 'LayoutList', + 'LayoutDashboard', + 'LayoutTemplate', 'Layers', + 'Layers2', + 'Layers3', 'Blocks', 'Component', - 'Puzzle', + 'Palette', + 'Paintbrush', + 'Brush', + 'PenTool', + 'Ruler', + 'Grid', + 'Grid3x3', + 'Square', + 'RectangleHorizontal', + 'RectangleVertical', + 'Circle', + + // Tools & Settings 'Cog', + 'Settings', + 'Settings2', 'Wrench', 'Hammer', + 'Screwdriver', + 'WrenchIcon', + 'Tool', + 'ScrewdriverWrench', + 'Sliders', + 'SlidersHorizontal', + 'Filter', + 'FilterX', + + // Technology & Infrastructure + 'Server', + 'ServerCrash', + 'ServerCog', + 'Database', + 'DatabaseBackup', + 'CloudUpload', + 'CloudDownload', + 'CloudOff', + 'Globe', + 'Globe2', + 'Network', + 'Wifi', + 'WifiOff', + 'Router', + 'Cpu', + 'MemoryStick', + 'HardDrive', + 'HardDriveIcon', + 'CircuitBoard', + 'Microchip', + 'Monitor', + 'MonitorSpeaker', + 'Laptop', + 'Smartphone', + 'Tablet', + 'Mouse', + 'Keyboard', + 'Headphones', + 'Printer', + 'Scanner', + + // Workflow & Process + 'Workflow', 'Zap', 'Rocket', - 'Sparkles', - 'Star', - 'Heart', + 'Flame', + 'Lightning', + 'Bolt', + 'Target', + 'Flag', + 'FlagTriangleRight', + 'CheckCircle', + 'CheckCircle2', + 'XCircle', + 'AlertCircle', + 'Info', + 'HelpCircle', + 'Clock', + 'Timer', + 'Stopwatch', + 'Calendar', + 'CalendarDays', + 'CalendarCheck', + 'CalendarClock', + + // Security & Access 'Shield', + 'ShieldCheck', + 'ShieldAlert', + 'ShieldOff', 'Lock', + 'Unlock', 'Key', - 'Cpu', - 'CircuitBoard', - 'Workflow', + 'KeyRound', + 'Eye', + 'EyeOff', + 'User', + 'Users', + 'UserCheck', + 'UserX', + 'UserPlus', + 'UserCog', + + // Business & Finance + 'Briefcase', + 'Building', + 'Building2', + 'Store', + 'ShoppingCart', + 'ShoppingBag', + 'CreditCard', + 'Wallet', + 'DollarSign', + 'Euro', + 'PoundSterling', + 'Yen', + 'Coins', + 'Receipt', + 'ChartBar', + 'ChartLine', + 'ChartPie', + 'TrendingUp', + 'TrendingDown', + 'Activity', + 'BarChart', + 'LineChart', + 'PieChart', + + // Communication & Media + 'MessageSquare', + 'MessageCircle', + 'Mail', + 'MailOpen', + 'Send', + 'Inbox', + 'Phone', + 'PhoneCall', + 'Video', + 'VideoOff', + 'Camera', + 'CameraOff', + 'Image', + 'ImageIcon', + 'Film', + 'Music', + 'Mic', + 'MicOff', + 'Volume', + 'Volume2', + 'VolumeX', + 'Radio', + 'Podcast', + + // Social & Community + 'Heart', + 'HeartHandshake', + 'Star', + 'StarOff', + 'ThumbsUp', + 'ThumbsDown', + 'Share', + 'Share2', + 'Link', + 'Link2', + 'ExternalLink', + 'AtSign', + 'Hash', + 'Hashtag', + 'Tag', + 'Tags', + + // Navigation & Location + 'Compass', + 'Map', + 'MapPin', + 'Navigation', + 'Navigation2', + 'Route', + 'Plane', + 'Car', + 'Bike', + 'Ship', + 'Train', + 'Bus', + + // Science & Education + 'FlaskConical', + 'FlaskRound', + 'Beaker', + 'TestTube', + 'TestTube2', + 'Microscope', + 'Atom', + 'Brain', + 'GraduationCap', + 'Book', + 'BookOpen', + 'BookMarked', + 'Library', + 'School', + 'University', + + // Food & Health + 'Coffee', + 'Utensils', + 'UtensilsCrossed', + 'Apple', + 'Cherry', + 'Cookie', + 'Cake', + 'Pizza', + 'Beer', + 'Wine', + 'HeartPulse', + 'Dumbbell', + 'Running', + + // Nature & Weather + 'Tree', + 'TreePine', + 'Leaf', + 'Flower', + 'Flower2', + 'Sun', + 'Moon', + 'CloudRain', + 'CloudSnow', + 'CloudLightning', + 'Droplet', + 'Wind', + 'Snowflake', + 'Umbrella', + + // Objects & Symbols + 'Puzzle', + 'PuzzleIcon', + 'Gamepad', + 'Gamepad2', + 'Dice', + 'Dice1', + 'Dice6', + 'Gem', + 'Crown', + 'Trophy', + 'Medal', + 'Award', + 'Gift', + 'GiftIcon', + 'Bell', + 'BellOff', + 'BellRing', + 'Home', + 'House', + 'DoorOpen', + 'DoorClosed', + 'Window', + 'Lightbulb', + 'LightbulbOff', + 'Candle', + 'Flashlight', + 'FlashlightOff', + 'Battery', + 'BatteryFull', + 'BatteryLow', + 'BatteryCharging', + 'Plug', + 'PlugZap', + 'Power', + 'PowerOff', + + // Arrows & Directions + 'ArrowRight', + 'ArrowLeft', + 'ArrowUp', + 'ArrowDown', + 'ArrowUpRight', + 'ArrowDownRight', + 'ArrowDownLeft', + 'ArrowUpLeft', + 'ChevronRight', + 'ChevronLeft', + 'ChevronUp', + 'ChevronDown', + 'Move', + 'MoveUp', + 'MoveDown', + 'MoveLeft', + 'MoveRight', + 'RotateCw', + 'RotateCcw', + 'RefreshCw', + 'RefreshCcw', + + // Shapes & Symbols + 'Diamond', + 'Pentagon', + 'Cross', + 'Plus', + 'Minus', + 'X', + 'Check', + 'Divide', + 'Equal', + 'Infinity', + 'Percent', + + // Miscellaneous + 'Bot', + 'Wand', + 'Wand2', + 'Magic', + 'Stars', + 'Comet', + 'Satellite', + 'SatelliteDish', + 'Radar', + 'RadarIcon', + 'Scan', + 'ScanLine', + 'QrCode', + 'Barcode', + 'ScanSearch', + 'Search', + 'SearchX', + 'ZoomIn', + 'ZoomOut', + 'Maximize', + 'Minimize', + 'Maximize2', + 'Minimize2', + 'Expand', + 'Shrink', + 'Copy', + 'CopyCheck', + 'Clipboard', + 'ClipboardCheck', + 'ClipboardCopy', + 'ClipboardList', + 'ClipboardPaste', + 'Scissors', + 'Cut', + 'FileEdit', + 'Pen', + 'Pencil', + 'Eraser', + 'Trash', + 'Trash2', + 'Delete', + 'ArchiveRestore', + 'Download', + 'Upload', + 'Save', + 'SaveAll', + 'FilePlus', + 'FileMinus', + 'FileX', + 'FileCheck', + 'FileQuestion', + 'FileWarning', + 'FileSearch', + 'FolderSearch', + 'FolderX', + 'FolderCheck', + 'FolderMinus', + 'FolderSync', + 'FolderUp', + 'FolderDown', ]; export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) { @@ -94,7 +485,7 @@ export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) { )} {/* Icons Grid */} - +
{filteredIcons.map((iconName) => { const IconComponent = getIconComponent(iconName); diff --git a/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx b/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx index 84b6ea9a..32a6315d 100644 --- a/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx +++ b/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx @@ -1,7 +1,8 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Edit2, Trash2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useAppStore } from '@/store/app-store'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import type { Project } from '@/lib/electron'; interface ProjectContextMenuProps { @@ -19,6 +20,7 @@ export function ProjectContextMenu({ }: ProjectContextMenuProps) { const menuRef = useRef(null); const { moveProjectToTrash } = useAppStore(); + const [showRemoveDialog, setShowRemoveDialog] = useState(false); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -47,57 +49,73 @@ export function ProjectContextMenu({ }; const handleRemove = () => { - if (confirm(`Remove "${project.name}" from the project list?`)) { - moveProjectToTrash(project.id); - } + setShowRemoveDialog(true); + }; + + const handleConfirmRemove = () => { + moveProjectToTrash(project.id); onClose(); }; return ( -
-
- + <> +
+
+ - + +
-
+ + + ); } diff --git a/apps/ui/src/components/layout/project-switcher/project-switcher.tsx b/apps/ui/src/components/layout/project-switcher/project-switcher.tsx index e6080ab4..442413fd 100644 --- a/apps/ui/src/components/layout/project-switcher/project-switcher.tsx +++ b/apps/ui/src/components/layout/project-switcher/project-switcher.tsx @@ -1,8 +1,8 @@ import { useState, useCallback, useEffect } from 'react'; -import { Plus, Bug } from 'lucide-react'; +import { Plus, Bug, FolderOpen } from 'lucide-react'; import { useNavigate } from '@tanstack/react-router'; import { cn } from '@/lib/utils'; -import { useAppStore } from '@/store/app-store'; +import { useAppStore, type ThemeMode } from '@/store/app-store'; import { useOSDetection } from '@/hooks/use-os-detection'; import { ProjectSwitcherItem } from './components/project-switcher-item'; import { ProjectContextMenu } from './components/project-context-menu'; @@ -12,6 +12,9 @@ import { OnboardingDialog } from '@/components/layout/sidebar/dialogs'; import { useProjectCreation, useProjectTheme } from '@/components/layout/sidebar/hooks'; import type { Project } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron'; +import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init'; +import { toast } from 'sonner'; +import { CreateSpecDialog } from '@/components/views/spec-view/dialogs'; function getOSAbbreviation(os: string): string { switch (os) { @@ -34,6 +37,8 @@ export function ProjectSwitcher() { setCurrentProject, trashedProjects, upsertAndSetCurrentProject, + specCreatingForProject, + setSpecCreatingForProject, } = useAppStore(); const [contextMenuProject, setContextMenuProject] = useState(null); const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>( @@ -41,6 +46,17 @@ export function ProjectSwitcher() { ); const [editDialogProject, setEditDialogProject] = useState(null); + // Setup dialog state for opening existing projects + const [showSetupDialog, setShowSetupDialog] = useState(false); + const [setupProjectPath, setSetupProjectPath] = useState(null); + const [projectOverview, setProjectOverview] = useState(''); + const [generateFeatures, setGenerateFeatures] = useState(true); + const [analyzeProject, setAnalyzeProject] = useState(true); + const [featureCount, setFeatureCount] = useState(5); + + // Derive isCreatingSpec from store state + const isCreatingSpec = specCreatingForProject !== null; + // Version info const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'; const { os } = useOSDetection(); @@ -108,6 +124,109 @@ export function ProjectSwitcher() { api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues'); }, []); + /** + * Opens the system folder selection dialog and initializes the selected project. + */ + const handleOpenFolder = useCallback(async () => { + const api = getElectronAPI(); + const result = await api.openDirectory(); + + if (!result.canceled && result.filePaths[0]) { + const path = result.filePaths[0]; + // 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) + const hadAutomakerDir = await hasAutomakerDir(path); + + // Initialize the .automaker directory structure + const initResult = await initializeProject(path); + + if (!initResult.success) { + toast.error('Failed to initialize project', { + description: initResult.error || 'Unknown error occurred', + }); + return; + } + + // Upsert project and set as current (handles both create and update cases) + // Theme preservation is handled by the store action + const trashedProject = trashedProjects.find((p) => p.path === path); + const effectiveTheme = + (trashedProject?.theme as ThemeMode | undefined) || + (currentProject?.theme as ThemeMode | undefined) || + globalTheme; + upsertAndSetCurrentProject(path, name, effectiveTheme); + + // Check if app_spec.txt exists + const specExists = await hasAppSpec(path); + + if (!hadAutomakerDir && !specExists) { + // This is a brand new project - show setup dialog + setSetupProjectPath(path); + setShowSetupDialog(true); + toast.success('Project opened', { + description: `Opened ${name}. Let's set up your app specification!`, + }); + } else if (initResult.createdFiles && initResult.createdFiles.length > 0) { + toast.success(initResult.isNewProject ? 'Project initialized' : 'Project updated', { + description: `Set up ${initResult.createdFiles.length} file(s) in .automaker`, + }); + } else { + toast.success('Project opened', { + description: `Opened ${name}`, + }); + } + + // Navigate to board view + navigate({ to: '/board' }); + } catch (error) { + console.error('Failed to open project:', error); + toast.error('Failed to open project', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + }, [trashedProjects, upsertAndSetCurrentProject, currentProject, globalTheme, navigate]); + + // Handler for creating initial spec from the setup dialog + const handleCreateInitialSpec = useCallback(async () => { + if (!setupProjectPath) return; + + setSpecCreatingForProject(setupProjectPath); + setShowSetupDialog(false); + + try { + const api = getElectronAPI(); + await api.generateAppSpec({ + projectPath: setupProjectPath, + projectOverview, + generateFeatures, + analyzeProject, + featureCount, + }); + } catch (error) { + console.error('Failed to generate spec:', error); + toast.error('Failed to generate spec', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + setSpecCreatingForProject(null); + } + }, [ + setupProjectPath, + projectOverview, + generateFeatures, + analyzeProject, + featureCount, + setSpecCreatingForProject, + ]); + + const handleSkipSetup = useCallback(() => { + setShowSetupDialog(false); + setSetupProjectPath(null); + }, []); + // Keyboard shortcuts for project switching (1-9, 0) useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -204,7 +323,7 @@ export function ProjectSwitcher() {
{/* Projects List */} -
+
{projects.map((project, index) => ( 0 && ( <> -
+
+ )} {/* Add Project Button - when no projects, show without rule */} {projects.length === 0 && ( - + <> + + + )}
@@ -312,6 +461,26 @@ export function ProjectSwitcher() { onSkip={handleOnboardingSkip} onGenerateSpec={handleOnboardingSkip} /> + + {/* Setup Dialog for Open Project */} + ); } diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx index b4f99513..dcaae0de 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx @@ -2,7 +2,7 @@ import { Folder, LucideIcon } from 'lucide-react'; import * as LucideIcons from 'lucide-react'; import { cn, isMac } from '@/lib/utils'; import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; -import type { Project } from '@/lib/electron'; +import { isElectron, type Project } from '@/lib/electron'; interface SidebarHeaderProps { sidebarOpen: boolean; @@ -25,14 +25,17 @@ export function SidebarHeader({ sidebarOpen, currentProject }: SidebarHeaderProp
{/* Project name and icon display */} {currentProject && (
{/* Project Icon */}
diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx index 6480322c..f1671a78 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx @@ -21,7 +21,7 @@ export function SidebarNavigation({ navigate, }: SidebarNavigationProps) { return ( -
{/* Usage Popover - show if either provider is authenticated, only on desktop */} @@ -148,7 +133,7 @@ export function BoardHeader({ onConcurrencyChange={onConcurrencyChange} isAutoModeRunning={isAutoModeRunning} onAutoModeToggle={onAutoModeToggle} - onOpenAutoModeSettings={() => setShowAutoModeSettings(true)} + onOpenAutoModeSettings={() => {}} onOpenPlanDialog={onOpenPlanDialog} showClaudeUsage={showClaudeUsage} showCodexUsage={showCodexUsage} @@ -160,7 +145,10 @@ export function BoardHeader({ {isMounted && !isMobile && (
-
)} - {/* Worktree Settings Dialog */} - - - {/* Concurrency Control - only show after mount to prevent hydration issues */} - {isMounted && !isMobile && ( - - - - - -
-
-

Max Concurrent Agents

-

- Controls how many AI agents can run simultaneously. Higher values process more - features in parallel but use more API resources. -

-
-
- onConcurrencyChange(value[0])} - min={1} - max={10} - step={1} - className="flex-1" - data-testid="concurrency-slider" - /> - - {maxConcurrency} - -
-
-
-
- )} - {/* Auto Mode Toggle - only show after mount to prevent hydration issues */} {isMounted && !isMobile && (
-
)} - {/* Auto Mode Settings Dialog */} - - {/* Plan Button with Settings - only show on desktop, mobile has it in the menu */} {isMounted && !isMobile && (
@@ -273,24 +200,12 @@ export function BoardHeader({ Plan - +
)} - - {/* Plan Settings Dialog */} -
); diff --git a/apps/ui/src/components/views/board-view/components/selection-action-bar.tsx b/apps/ui/src/components/views/board-view/components/selection-action-bar.tsx index 7938d05e..6982356a 100644 --- a/apps/ui/src/components/views/board-view/components/selection-action-bar.tsx +++ b/apps/ui/src/components/views/board-view/components/selection-action-bar.tsx @@ -30,9 +30,7 @@ export function SelectionActionBar({ }: SelectionActionBarProps) { const [showDeleteDialog, setShowDeleteDialog] = useState(false); - if (selectedCount === 0) return null; - - const allSelected = selectedCount === totalCount; + const allSelected = selectedCount === totalCount && totalCount > 0; const handleDeleteClick = () => { setShowDeleteDialog(true); @@ -55,7 +53,9 @@ export function SelectionActionBar({ data-testid="selection-action-bar" > - {selectedCount} feature{selectedCount !== 1 ? 's' : ''} selected + {selectedCount === 0 + ? 'Select features to edit' + : `${selectedCount} feature${selectedCount !== 1 ? 's' : ''} selected`}
@@ -65,7 +65,8 @@ export function SelectionActionBar({ variant="default" size="sm" onClick={onEdit} - className="h-8 bg-brand-500 hover:bg-brand-600" + disabled={selectedCount === 0} + className="h-8 bg-brand-500 hover:bg-brand-600 disabled:opacity-50" data-testid="selection-edit-button" > @@ -76,7 +77,8 @@ export function SelectionActionBar({ variant="outline" size="sm" onClick={handleDeleteClick} - className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10" + disabled={selectedCount === 0} + className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10 disabled:opacity-50" data-testid="selection-delete-button" > diff --git a/apps/ui/src/components/views/board-view/components/view-toggle.tsx b/apps/ui/src/components/views/board-view/components/view-toggle.tsx index b4d73cbb..3b6a938b 100644 --- a/apps/ui/src/components/views/board-view/components/view-toggle.tsx +++ b/apps/ui/src/components/views/board-view/components/view-toggle.tsx @@ -38,7 +38,7 @@ export function ViewToggle({ viewMode, onViewModeChange, className }: ViewToggle data-testid="view-toggle-kanban" > - Kanban + Kanban
); diff --git a/apps/ui/src/components/views/board-view/dialogs/auto-mode-settings-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/auto-mode-settings-dialog.tsx deleted file mode 100644 index 981cb3ee..00000000 --- a/apps/ui/src/components/views/board-view/dialogs/auto-mode-settings-dialog.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { FastForward, Settings2 } from 'lucide-react'; - -interface AutoModeSettingsDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - skipVerificationInAutoMode: boolean; - onSkipVerificationChange: (value: boolean) => void; -} - -export function AutoModeSettingsDialog({ - open, - onOpenChange, - skipVerificationInAutoMode, - onSkipVerificationChange, -}: AutoModeSettingsDialogProps) { - return ( - - - - - - Auto Mode Settings - - - Configure how auto mode handles feature execution and dependencies. - - - -
- {/* Skip Verification Setting */} -
-
-
- - -
-

- When enabled, auto mode will grab features even if their dependencies are not - verified, as long as they are not currently running. This allows faster pipeline - execution without waiting for manual verification. -

-
-
-
-
-
- ); -} diff --git a/apps/ui/src/components/views/board-view/dialogs/auto-mode-settings-popover.tsx b/apps/ui/src/components/views/board-view/dialogs/auto-mode-settings-popover.tsx new file mode 100644 index 00000000..6928206e --- /dev/null +++ b/apps/ui/src/components/views/board-view/dialogs/auto-mode-settings-popover.tsx @@ -0,0 +1,95 @@ +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Slider } from '@/components/ui/slider'; +import { FastForward, Bot, Settings2 } from 'lucide-react'; + +interface AutoModeSettingsPopoverProps { + skipVerificationInAutoMode: boolean; + onSkipVerificationChange: (value: boolean) => void; + maxConcurrency: number; + runningAgentsCount: number; + onConcurrencyChange: (value: number) => void; +} + +export function AutoModeSettingsPopover({ + skipVerificationInAutoMode, + onSkipVerificationChange, + maxConcurrency, + runningAgentsCount, + onConcurrencyChange, +}: AutoModeSettingsPopoverProps) { + return ( + + + + + +
+
+

Auto Mode Settings

+

+ Configure auto mode execution and agent concurrency. +

+
+ + {/* Max Concurrent Agents */} +
+
+ + + + {runningAgentsCount}/{maxConcurrency} + +
+
+ onConcurrencyChange(value[0])} + min={1} + max={10} + step={1} + className="flex-1" + data-testid="concurrency-slider" + /> + {maxConcurrency} +
+

+ Higher values process more features in parallel but use more API resources. +

+
+ + {/* Skip Verification Setting */} +
+
+ + +
+ +
+ +

+ When enabled, auto mode will grab features even if their dependencies are not verified, + as long as they are not currently running. +

+
+
+
+ ); +} diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-settings-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-settings-dialog.tsx deleted file mode 100644 index e7b58103..00000000 --- a/apps/ui/src/components/views/board-view/dialogs/plan-settings-dialog.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { GitBranch, Settings2 } from 'lucide-react'; - -interface PlanSettingsDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - planUseSelectedWorktreeBranch: boolean; - onPlanUseSelectedWorktreeBranchChange: (value: boolean) => void; -} - -export function PlanSettingsDialog({ - open, - onOpenChange, - planUseSelectedWorktreeBranch, - onPlanUseSelectedWorktreeBranchChange, -}: PlanSettingsDialogProps) { - return ( - - - - - - Plan Settings - - - Configure how the Plan feature creates and organizes new features. - - - -
- {/* Use Selected Worktree Branch Setting */} -
-
-
- - -
-

- Planned features will automatically use isolated worktrees, keeping changes separate - from your main branch until you're ready to merge. -

-
-
-
-
-
- ); -} diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-settings-popover.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-settings-popover.tsx new file mode 100644 index 00000000..00c32d9c --- /dev/null +++ b/apps/ui/src/components/views/board-view/dialogs/plan-settings-popover.tsx @@ -0,0 +1,61 @@ +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { GitBranch, Settings2 } from 'lucide-react'; + +interface PlanSettingsPopoverProps { + planUseSelectedWorktreeBranch: boolean; + onPlanUseSelectedWorktreeBranchChange: (value: boolean) => void; +} + +export function PlanSettingsPopover({ + planUseSelectedWorktreeBranch, + onPlanUseSelectedWorktreeBranchChange, +}: PlanSettingsPopoverProps) { + return ( + + + + + +
+
+

Plan Settings

+

+ Configure how Plan creates and organizes features. +

+
+ +
+
+ + +
+ +
+ +

+ Planned features will automatically use isolated worktrees, keeping changes separate + from your main branch until you're ready to merge. +

+
+
+
+ ); +} diff --git a/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx deleted file mode 100644 index c742e631..00000000 --- a/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { GitBranch, Settings2 } from 'lucide-react'; - -interface WorktreeSettingsDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - addFeatureUseSelectedWorktreeBranch: boolean; - onAddFeatureUseSelectedWorktreeBranchChange: (value: boolean) => void; -} - -export function WorktreeSettingsDialog({ - open, - onOpenChange, - addFeatureUseSelectedWorktreeBranch, - onAddFeatureUseSelectedWorktreeBranchChange, -}: WorktreeSettingsDialogProps) { - return ( - - - - - - Worktree Settings - - - Configure how worktrees affect feature creation and organization. - - - -
- {/* Use Selected Worktree Branch Setting */} -
-
-
- - -
-

- New features will automatically use isolated worktrees, keeping changes separate - from your main branch until you're ready to merge. -

-
-
-
-
-
- ); -} diff --git a/apps/ui/src/components/views/board-view/dialogs/worktree-settings-popover.tsx b/apps/ui/src/components/views/board-view/dialogs/worktree-settings-popover.tsx new file mode 100644 index 00000000..442cdf62 --- /dev/null +++ b/apps/ui/src/components/views/board-view/dialogs/worktree-settings-popover.tsx @@ -0,0 +1,61 @@ +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { GitBranch, Settings2 } from 'lucide-react'; + +interface WorktreeSettingsPopoverProps { + addFeatureUseSelectedWorktreeBranch: boolean; + onAddFeatureUseSelectedWorktreeBranchChange: (value: boolean) => void; +} + +export function WorktreeSettingsPopover({ + addFeatureUseSelectedWorktreeBranch, + onAddFeatureUseSelectedWorktreeBranchChange, +}: WorktreeSettingsPopoverProps) { + return ( + + + + + +
+
+

Worktree Settings

+

+ Configure how worktrees affect feature creation. +

+
+ +
+
+ + +
+ +
+ +

+ New features will automatically use isolated worktrees, keeping changes separate from + your main branch until you're ready to merge. +

+
+
+
+ ); +} diff --git a/apps/ui/src/components/views/board-view/kanban-board.tsx b/apps/ui/src/components/views/board-view/kanban-board.tsx index 54341ad9..c670ab70 100644 --- a/apps/ui/src/components/views/board-view/kanban-board.tsx +++ b/apps/ui/src/components/views/board-view/kanban-board.tsx @@ -44,6 +44,8 @@ interface KanbanBoardProps { runningAutoTasks: string[]; onArchiveAllVerified: () => void; onAddFeature: () => void; + onShowCompletedModal: () => void; + completedCount: number; pipelineConfig: PipelineConfig | null; onOpenPipelineSettings?: () => void; // Selection mode props @@ -88,6 +90,8 @@ export function KanbanBoard({ runningAutoTasks, onArchiveAllVerified, onAddFeature, + onShowCompletedModal, + completedCount, pipelineConfig, onOpenPipelineSettings, isSelectionMode = false, @@ -140,17 +144,36 @@ export function KanbanBoard({ showBorder={backgroundSettings.columnBorderEnabled} hideScrollbar={backgroundSettings.hideScrollbar} headerAction={ - column.id === 'verified' && columnFeatures.length > 0 ? ( - + column.id === 'verified' ? ( +
+ {columnFeatures.length > 0 && ( + + )} + +
) : column.id === 'backlog' ? (
+ + {/* Empty state when all nodes are filtered out */} {filterResult.hasActiveFilter && filterResult.matchedNodeIds.size === 0 && ( diff --git a/apps/ui/src/components/views/graph-view/graph-view.tsx b/apps/ui/src/components/views/graph-view/graph-view.tsx index 34959f91..998c91e2 100644 --- a/apps/ui/src/components/views/graph-view/graph-view.tsx +++ b/apps/ui/src/components/views/graph-view/graph-view.tsx @@ -22,6 +22,7 @@ interface GraphViewProps { onUpdateFeature?: (featureId: string, updates: Partial) => void; onSpawnTask?: (feature: Feature) => void; onDeleteTask?: (feature: Feature) => void; + onAddFeature?: () => void; } export function GraphView({ @@ -40,6 +41,7 @@ export function GraphView({ onUpdateFeature, onSpawnTask, onDeleteTask, + onAddFeature, }: GraphViewProps) { const { currentProject } = useAppStore(); @@ -212,6 +214,7 @@ export function GraphView({ onNodeDoubleClick={handleNodeDoubleClick} nodeActionCallbacks={nodeActionCallbacks} onCreateDependency={handleCreateDependency} + onAddFeature={onAddFeature} backgroundStyle={backgroundImageStyle} backgroundSettings={backgroundSettings} className="h-full"