From 06c02de1cbcb18d2f033bdb8f68fa094c93ab96f Mon Sep 17 00:00:00 2001 From: Kacper Date: Sun, 4 Jan 2026 22:24:03 +0100 Subject: [PATCH 1/4] feat: add mass edit feature for backlog kanban cards Add ability to select multiple backlog features and edit their configuration in bulk. Selection is limited to backlog column features in the current branch/worktree only. Changes: - Add selection mode toggle in board controls - Add checkbox selection on kanban cards (backlog only) - Disable drag and drop during selection mode - Hide action buttons during selection mode - Add floating selection action bar with Edit/Clear/Select All - Add mass edit dialog with all configuration options in single scroll view - Add server endpoint for bulk feature updates --- apps/server/src/routes/features/index.ts | 6 + .../src/routes/features/routes/bulk-update.ts | 75 ++++ apps/ui/src/components/views/board-view.tsx | 108 +++++ .../views/board-view/board-controls.tsx | 33 +- .../views/board-view/components/index.ts | 1 + .../components/kanban-card/card-actions.tsx | 7 + .../components/kanban-card/card-header.tsx | 7 +- .../components/kanban-card/kanban-card.tsx | 59 ++- .../components/selection-action-bar.tsx | 78 ++++ .../views/board-view/dialogs/index.ts | 1 + .../board-view/dialogs/mass-edit-dialog.tsx | 370 ++++++++++++++++++ .../views/board-view/hooks/index.ts | 1 + .../board-view/hooks/use-selection-mode.ts | 82 ++++ .../views/board-view/kanban-board.tsx | 10 + apps/ui/src/lib/http-api-client.ts | 17 +- 15 files changed, 840 insertions(+), 15 deletions(-) create mode 100644 apps/server/src/routes/features/routes/bulk-update.ts create mode 100644 apps/ui/src/components/views/board-view/components/selection-action-bar.tsx create mode 100644 apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx create mode 100644 apps/ui/src/components/views/board-view/hooks/use-selection-mode.ts diff --git a/apps/server/src/routes/features/index.ts b/apps/server/src/routes/features/index.ts index 8cb287d1..4f62ee17 100644 --- a/apps/server/src/routes/features/index.ts +++ b/apps/server/src/routes/features/index.ts @@ -9,6 +9,7 @@ import { createListHandler } from './routes/list.js'; import { createGetHandler } from './routes/get.js'; import { createCreateHandler } from './routes/create.js'; import { createUpdateHandler } from './routes/update.js'; +import { createBulkUpdateHandler } from './routes/bulk-update.js'; import { createDeleteHandler } from './routes/delete.js'; import { createAgentOutputHandler, createRawOutputHandler } from './routes/agent-output.js'; import { createGenerateTitleHandler } from './routes/generate-title.js'; @@ -20,6 +21,11 @@ export function createFeaturesRoutes(featureLoader: FeatureLoader): Router { router.post('/get', validatePathParams('projectPath'), createGetHandler(featureLoader)); router.post('/create', validatePathParams('projectPath'), createCreateHandler(featureLoader)); router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader)); + router.post( + '/bulk-update', + validatePathParams('projectPath'), + createBulkUpdateHandler(featureLoader) + ); router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader)); router.post('/agent-output', createAgentOutputHandler(featureLoader)); router.post('/raw-output', createRawOutputHandler(featureLoader)); diff --git a/apps/server/src/routes/features/routes/bulk-update.ts b/apps/server/src/routes/features/routes/bulk-update.ts new file mode 100644 index 00000000..a1c97e72 --- /dev/null +++ b/apps/server/src/routes/features/routes/bulk-update.ts @@ -0,0 +1,75 @@ +/** + * POST /bulk-update endpoint - Update multiple features at once + */ + +import type { Request, Response } from 'express'; +import { FeatureLoader } from '../../../services/feature-loader.js'; +import type { Feature } from '@automaker/types'; +import { getErrorMessage, logError } from '../common.js'; + +interface BulkUpdateRequest { + projectPath: string; + featureIds: string[]; + updates: Partial; +} + +interface BulkUpdateResult { + featureId: string; + success: boolean; + error?: string; +} + +export function createBulkUpdateHandler(featureLoader: FeatureLoader) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, featureIds, updates } = req.body as BulkUpdateRequest; + + if (!projectPath || !featureIds || !Array.isArray(featureIds) || featureIds.length === 0) { + res.status(400).json({ + success: false, + error: 'projectPath and featureIds (non-empty array) are required', + }); + return; + } + + if (!updates || Object.keys(updates).length === 0) { + res.status(400).json({ + success: false, + error: 'updates object with at least one field is required', + }); + return; + } + + const results: BulkUpdateResult[] = []; + const updatedFeatures: Feature[] = []; + + for (const featureId of featureIds) { + try { + const updated = await featureLoader.update(projectPath, featureId, updates); + results.push({ featureId, success: true }); + updatedFeatures.push(updated); + } catch (error) { + results.push({ + featureId, + success: false, + error: getErrorMessage(error), + }); + } + } + + const successCount = results.filter((r) => r.success).length; + const failureCount = results.filter((r) => !r.success).length; + + res.json({ + success: failureCount === 0, + updatedCount: successCount, + failedCount: failureCount, + results, + features: updatedFeatures, + }); + } catch (error) { + logError(error, 'Bulk update features failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index f59ccbe6..1dc99b05 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -56,7 +56,10 @@ import { useBoardBackground, useBoardPersistence, useFollowUpState, + useSelectionMode, } from './board-view/hooks'; +import { SelectionActionBar } from './board-view/components'; +import { MassEditDialog } from './board-view/dialogs'; // Stable empty array to avoid infinite loop in selector const EMPTY_WORKTREES: ReturnType['getWorktrees']> = []; @@ -154,6 +157,19 @@ export function BoardView() { handleFollowUpDialogChange, } = useFollowUpState(); + // Selection mode hook for mass editing + const { + isSelectionMode, + selectedFeatureIds, + selectedCount, + toggleSelectionMode, + toggleFeatureSelection, + selectAll, + clearSelection, + exitSelectionMode, + } = useSelectionMode(); + const [showMassEditDialog, setShowMassEditDialog] = useState(false); + // Search filter for Kanban cards const [searchQuery, setSearchQuery] = useState(''); // Plan approval loading state @@ -447,6 +463,72 @@ export function BoardView() { currentWorktreeBranch, }); + // Handler for bulk updating multiple features + const handleBulkUpdate = useCallback( + async (updates: Partial) => { + if (!currentProject || selectedFeatureIds.size === 0) return; + + try { + const api = getHttpApiClient(); + const featureIds = Array.from(selectedFeatureIds); + const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates); + + if (result.success) { + // Update local state + featureIds.forEach((featureId) => { + updateFeature(featureId, updates); + }); + toast.success(`Updated ${result.updatedCount} features`); + exitSelectionMode(); + } else { + toast.error('Failed to update some features', { + description: `${result.failedCount} features failed to update`, + }); + } + } catch (error) { + logger.error('Bulk update failed:', error); + toast.error('Failed to update features'); + } + }, + [currentProject, selectedFeatureIds, updateFeature, exitSelectionMode] + ); + + // Get selected features for mass edit dialog + const selectedFeatures = useMemo(() => { + return hookFeatures.filter((f) => selectedFeatureIds.has(f.id)); + }, [hookFeatures, selectedFeatureIds]); + + // Get backlog feature IDs in current branch for "Select All" + const allSelectableFeatureIds = useMemo(() => { + return hookFeatures + .filter((f) => { + // Only backlog features + if (f.status !== 'backlog') return false; + + // Filter by current worktree branch + const featureBranch = f.branchName; + if (!featureBranch) { + // No branch assigned - only selectable on primary worktree + return currentWorktreePath === null; + } + if (currentWorktreeBranch === null) { + // Viewing main but branch hasn't been initialized + return currentProject?.path + ? isPrimaryWorktreeBranch(currentProject.path, featureBranch) + : false; + } + // Match by branch name + return featureBranch === currentWorktreeBranch; + }) + .map((f) => f.id); + }, [ + hookFeatures, + currentWorktreePath, + currentWorktreeBranch, + currentProject?.path, + isPrimaryWorktreeBranch, + ]); + // Handler for addressing PR comments - creates a feature and starts it automatically const handleAddressPRComments = useCallback( async (worktree: WorktreeInfo, prInfo: PRInfo) => { @@ -1069,6 +1151,8 @@ export function BoardView() { onDetailLevelChange={setKanbanCardDetailLevel} boardViewMode={boardViewMode} onBoardViewModeChange={setBoardViewMode} + isSelectionMode={isSelectionMode} + onToggleSelectionMode={toggleSelectionMode} /> {/* View Content - Kanban or Graph */} @@ -1109,6 +1193,9 @@ export function BoardView() { currentProject?.path ? pipelineConfigByProject[currentProject.path] || null : null } onOpenPipelineSettings={() => setShowPipelineSettings(true)} + isSelectionMode={isSelectionMode} + selectedFeatureIds={selectedFeatureIds} + onToggleFeatureSelection={toggleFeatureSelection} /> ) : ( + {/* Selection Action Bar */} + {isSelectionMode && ( + setShowMassEditDialog(true)} + onClear={clearSelection} + onSelectAll={() => selectAll(allSelectableFeatureIds)} + /> + )} + + {/* Mass Edit Dialog */} + setShowMassEditDialog(false)} + selectedFeatures={selectedFeatures} + onApply={handleBulkUpdate} + showProfilesOnly={showProfilesOnly} + aiProfiles={aiProfiles} + /> + {/* Board Background Modal */} void; boardViewMode: BoardViewMode; onBoardViewModeChange: (mode: BoardViewMode) => void; + isSelectionMode?: boolean; + onToggleSelectionMode?: () => void; } export function BoardControls({ @@ -24,6 +35,8 @@ export function BoardControls({ onDetailLevelChange, boardViewMode, onBoardViewModeChange, + isSelectionMode = false, + onToggleSelectionMode, }: BoardControlsProps) { if (!isMounted) return null; @@ -75,6 +88,24 @@ export function BoardControls({ + {/* Selection Mode Toggle */} + + + + + +

{isSelectionMode ? 'Exit Select Mode' : 'Select Mode'}

+
+
+ {/* Board Background Button */} diff --git a/apps/ui/src/components/views/board-view/components/index.ts b/apps/ui/src/components/views/board-view/components/index.ts index c8760520..514e407d 100644 --- a/apps/ui/src/components/views/board-view/components/index.ts +++ b/apps/ui/src/components/views/board-view/components/index.ts @@ -1,2 +1,3 @@ export { KanbanCard } from './kanban-card/kanban-card'; export { KanbanColumn } from './kanban-column'; +export { SelectionActionBar } from './selection-action-bar'; diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx index b791216b..f9ac0d65 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx @@ -17,6 +17,7 @@ interface CardActionsProps { isCurrentAutoTask: boolean; hasContext?: boolean; shortcutKey?: string; + isSelectionMode?: boolean; onEdit: () => void; onViewOutput?: () => void; onVerify?: () => void; @@ -35,6 +36,7 @@ export function CardActions({ isCurrentAutoTask, hasContext, shortcutKey, + isSelectionMode = false, onEdit, onViewOutput, onVerify, @@ -47,6 +49,11 @@ export function CardActions({ onViewPlan, onApprovePlan, }: CardActionsProps) { + // Hide all actions when in selection mode + if (isSelectionMode) { + return null; + } + return (
{isCurrentAutoTask && ( diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/card-header.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/card-header.tsx index 6f486caa..b48f78a3 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/card-header.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/card-header.tsx @@ -29,6 +29,7 @@ interface CardHeaderProps { feature: Feature; isDraggable: boolean; isCurrentAutoTask: boolean; + isSelectionMode?: boolean; onEdit: () => void; onDelete: () => void; onViewOutput?: () => void; @@ -39,6 +40,7 @@ export function CardHeaderSection({ feature, isDraggable, isCurrentAutoTask, + isSelectionMode = false, onEdit, onDelete, onViewOutput, @@ -59,7 +61,7 @@ export function CardHeaderSection({ return ( {/* Running task header */} - {isCurrentAutoTask && ( + {isCurrentAutoTask && !isSelectionMode && (
@@ -119,7 +121,7 @@ export function CardHeaderSection({ )} {/* Backlog header */} - {!isCurrentAutoTask && feature.status === 'backlog' && ( + {!isCurrentAutoTask && !isSelectionMode && feature.status === 'backlog' && (
+ + {!allSelected && ( + + )} + + +
+
+ ); +} diff --git a/apps/ui/src/components/views/board-view/dialogs/index.ts b/apps/ui/src/components/views/board-view/dialogs/index.ts index 6979f9d4..b8d5aa30 100644 --- a/apps/ui/src/components/views/board-view/dialogs/index.ts +++ b/apps/ui/src/components/views/board-view/dialogs/index.ts @@ -7,3 +7,4 @@ export { DeleteCompletedFeatureDialog } from './delete-completed-feature-dialog' export { EditFeatureDialog } from './edit-feature-dialog'; export { FollowUpDialog } from './follow-up-dialog'; export { PlanApprovalDialog } from './plan-approval-dialog'; +export { MassEditDialog } from './mass-edit-dialog'; diff --git a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx new file mode 100644 index 00000000..1b4b1467 --- /dev/null +++ b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx @@ -0,0 +1,370 @@ +import { useState, useEffect, useMemo } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Settings2, AlertCircle } from 'lucide-react'; +import { modelSupportsThinking } from '@/lib/utils'; +import { Feature, ModelAlias, ThinkingLevel, AIProfile, PlanningMode } from '@/store/app-store'; +import { + ModelSelector, + ThinkingLevelSelector, + ProfileQuickSelect, + TestingTabContent, + PrioritySelector, + PlanningModeSelector, +} from '../shared'; +import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types'; +import { cn } from '@/lib/utils'; + +interface MassEditDialogProps { + open: boolean; + onClose: () => void; + selectedFeatures: Feature[]; + onApply: (updates: Partial) => Promise; + showProfilesOnly: boolean; + aiProfiles: AIProfile[]; +} + +interface ApplyState { + model: boolean; + thinkingLevel: boolean; + planningMode: boolean; + requirePlanApproval: boolean; + priority: boolean; + skipTests: boolean; +} + +function getMixedValues(features: Feature[]): Record { + if (features.length === 0) return {}; + const first = features[0]; + return { + model: !features.every((f) => f.model === first.model), + thinkingLevel: !features.every((f) => f.thinkingLevel === first.thinkingLevel), + planningMode: !features.every((f) => f.planningMode === first.planningMode), + requirePlanApproval: !features.every( + (f) => f.requirePlanApproval === first.requirePlanApproval + ), + priority: !features.every((f) => f.priority === first.priority), + skipTests: !features.every((f) => f.skipTests === first.skipTests), + }; +} + +function getInitialValue(features: Feature[], key: keyof Feature, defaultValue: T): T { + if (features.length === 0) return defaultValue; + return (features[0][key] as T) ?? defaultValue; +} + +interface FieldWrapperProps { + label: string; + isMixed: boolean; + willApply: boolean; + onApplyChange: (apply: boolean) => void; + children: React.ReactNode; +} + +function FieldWrapper({ label, isMixed, willApply, onApplyChange, children }: FieldWrapperProps) { + return ( +
+
+
+ onApplyChange(!!checked)} + className="data-[state=checked]:bg-brand-500 data-[state=checked]:border-brand-500" + /> + +
+ {isMixed && ( + + + Mixed values + + )} +
+
{children}
+
+ ); +} + +export function MassEditDialog({ + open, + onClose, + selectedFeatures, + onApply, + showProfilesOnly, + aiProfiles, +}: MassEditDialogProps) { + const [isApplying, setIsApplying] = useState(false); + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + // Track which fields to apply + const [applyState, setApplyState] = useState({ + model: false, + thinkingLevel: false, + planningMode: false, + requirePlanApproval: false, + priority: false, + skipTests: false, + }); + + // Field values + const [model, setModel] = useState('sonnet'); + const [thinkingLevel, setThinkingLevel] = useState('none'); + const [planningMode, setPlanningMode] = useState('skip'); + const [requirePlanApproval, setRequirePlanApproval] = useState(false); + const [priority, setPriority] = useState(2); + const [skipTests, setSkipTests] = useState(false); + + // Calculate mixed values + const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]); + + // Reset state when dialog opens with new features + useEffect(() => { + if (open && selectedFeatures.length > 0) { + setApplyState({ + model: false, + thinkingLevel: false, + planningMode: false, + requirePlanApproval: false, + priority: false, + skipTests: false, + }); + setModel(getInitialValue(selectedFeatures, 'model', 'sonnet') as ModelAlias); + setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel); + setPlanningMode(getInitialValue(selectedFeatures, 'planningMode', 'skip') as PlanningMode); + setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false)); + setPriority(getInitialValue(selectedFeatures, 'priority', 2)); + setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false)); + setShowAdvancedOptions(false); + } + }, [open, selectedFeatures]); + + const handleModelSelect = (newModel: string) => { + const isCursor = isCursorModel(newModel); + setModel(newModel as ModelAlias); + if (isCursor || !modelSupportsThinking(newModel)) { + setThinkingLevel('none'); + } + }; + + const handleProfileSelect = (profile: AIProfile) => { + if (profile.provider === 'cursor') { + const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`; + setModel(cursorModel as ModelAlias); + setThinkingLevel('none'); + } else { + setModel((profile.model || 'sonnet') as ModelAlias); + setThinkingLevel(profile.thinkingLevel || 'none'); + } + setApplyState((prev) => ({ ...prev, model: true, thinkingLevel: true })); + }; + + const handleApply = async () => { + const updates: Partial = {}; + + if (applyState.model) updates.model = model; + if (applyState.thinkingLevel) updates.thinkingLevel = thinkingLevel; + if (applyState.planningMode) updates.planningMode = planningMode; + if (applyState.requirePlanApproval) updates.requirePlanApproval = requirePlanApproval; + if (applyState.priority) updates.priority = priority; + if (applyState.skipTests) updates.skipTests = skipTests; + + if (Object.keys(updates).length === 0) { + onClose(); + return; + } + + setIsApplying(true); + try { + await onApply(updates); + onClose(); + } finally { + setIsApplying(false); + } + }; + + const hasAnyApply = Object.values(applyState).some(Boolean); + const isCurrentModelCursor = isCursorModel(model); + const modelAllowsThinking = !isCurrentModelCursor && modelSupportsThinking(model); + + return ( + !open && onClose()}> + + + Edit {selectedFeatures.length} Features + + Select which settings to apply to all selected features. + + + +
+ {/* Show Advanced Options Toggle */} + {showProfilesOnly && ( +
+
+

Simple Mode Active

+

+ Only showing AI profiles. Advanced model tweaking is hidden. +

+
+ +
+ )} + + {/* Quick Select Profile Section */} + {aiProfiles.length > 0 && ( +
+ +

+ Selecting a profile will automatically enable model settings +

+ +
+ )} + + {/* Separator */} + {aiProfiles.length > 0 && (!showProfilesOnly || showAdvancedOptions) && ( +
+ )} + + {/* Model Selection */} + {(!showProfilesOnly || showAdvancedOptions) && ( + <> + setApplyState((prev) => ({ ...prev, model: apply }))} + > + + + + {modelAllowsThinking && ( + + setApplyState((prev) => ({ ...prev, thinkingLevel: apply })) + } + > + + + )} + + )} + + {/* Separator before options */} + {(!showProfilesOnly || showAdvancedOptions) &&
} + + {/* Planning Mode */} + + setApplyState((prev) => ({ + ...prev, + planningMode: apply, + requirePlanApproval: apply, + })) + } + > + + + + {/* Priority */} + setApplyState((prev) => ({ ...prev, priority: apply }))} + > + + + + {/* Testing */} + setApplyState((prev) => ({ ...prev, skipTests: apply }))} + > + + +
+ + + + + + +
+ ); +} diff --git a/apps/ui/src/components/views/board-view/hooks/index.ts b/apps/ui/src/components/views/board-view/hooks/index.ts index 9b855b06..272937f4 100644 --- a/apps/ui/src/components/views/board-view/hooks/index.ts +++ b/apps/ui/src/components/views/board-view/hooks/index.ts @@ -7,3 +7,4 @@ export { useBoardEffects } from './use-board-effects'; export { useBoardBackground } from './use-board-background'; export { useBoardPersistence } from './use-board-persistence'; export { useFollowUpState } from './use-follow-up-state'; +export { useSelectionMode } from './use-selection-mode'; diff --git a/apps/ui/src/components/views/board-view/hooks/use-selection-mode.ts b/apps/ui/src/components/views/board-view/hooks/use-selection-mode.ts new file mode 100644 index 00000000..1470f447 --- /dev/null +++ b/apps/ui/src/components/views/board-view/hooks/use-selection-mode.ts @@ -0,0 +1,82 @@ +import { useState, useCallback, useEffect } from 'react'; + +interface UseSelectionModeReturn { + isSelectionMode: boolean; + selectedFeatureIds: Set; + selectedCount: number; + toggleSelectionMode: () => void; + toggleFeatureSelection: (featureId: string) => void; + selectAll: (featureIds: string[]) => void; + clearSelection: () => void; + isFeatureSelected: (featureId: string) => boolean; + exitSelectionMode: () => void; +} + +export function useSelectionMode(): UseSelectionModeReturn { + const [isSelectionMode, setIsSelectionMode] = useState(false); + const [selectedFeatureIds, setSelectedFeatureIds] = useState>(new Set()); + + const toggleSelectionMode = useCallback(() => { + setIsSelectionMode((prev) => { + if (prev) { + // Exiting selection mode - clear selection + setSelectedFeatureIds(new Set()); + } + return !prev; + }); + }, []); + + const exitSelectionMode = useCallback(() => { + setIsSelectionMode(false); + setSelectedFeatureIds(new Set()); + }, []); + + const toggleFeatureSelection = useCallback((featureId: string) => { + setSelectedFeatureIds((prev) => { + const next = new Set(prev); + if (next.has(featureId)) { + next.delete(featureId); + } else { + next.add(featureId); + } + return next; + }); + }, []); + + const selectAll = useCallback((featureIds: string[]) => { + setSelectedFeatureIds(new Set(featureIds)); + }, []); + + const clearSelection = useCallback(() => { + setSelectedFeatureIds(new Set()); + }, []); + + const isFeatureSelected = useCallback( + (featureId: string) => selectedFeatureIds.has(featureId), + [selectedFeatureIds] + ); + + // Handle Escape key to exit selection mode + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isSelectionMode) { + exitSelectionMode(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isSelectionMode, exitSelectionMode]); + + return { + isSelectionMode, + selectedFeatureIds, + selectedCount: selectedFeatureIds.size, + toggleSelectionMode, + toggleFeatureSelection, + selectAll, + clearSelection, + isFeatureSelected, + exitSelectionMode, + }; +} 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 c21711b9..5cef1aa9 100644 --- a/apps/ui/src/components/views/board-view/kanban-board.tsx +++ b/apps/ui/src/components/views/board-view/kanban-board.tsx @@ -50,6 +50,10 @@ interface KanbanBoardProps { onArchiveAllVerified: () => void; pipelineConfig: PipelineConfig | null; onOpenPipelineSettings?: () => void; + // Selection mode props + isSelectionMode?: boolean; + selectedFeatureIds?: Set; + onToggleFeatureSelection?: (featureId: string) => void; } export function KanbanBoard({ @@ -83,6 +87,9 @@ export function KanbanBoard({ onArchiveAllVerified, pipelineConfig, onOpenPipelineSettings, + isSelectionMode = false, + selectedFeatureIds = new Set(), + onToggleFeatureSelection, }: KanbanBoardProps) { // Generate columns including pipeline steps const columns = useMemo(() => getColumnsWithPipeline(pipelineConfig), [pipelineConfig]); @@ -200,6 +207,9 @@ export function KanbanBoard({ glassmorphism={backgroundSettings.cardGlassmorphism} cardBorderEnabled={backgroundSettings.cardBorderEnabled} cardBorderOpacity={backgroundSettings.cardBorderOpacity} + isSelectionMode={isSelectionMode} + isSelected={selectedFeatureIds.has(feature.id)} + onToggleSelect={() => onToggleFeatureSelection?.(feature.id)} /> ); })} diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 75c1e6c5..2fad2389 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -1147,7 +1147,20 @@ export class HttpApiClient implements ElectronAPI { }; // Features API - features: FeaturesAPI = { + features: FeaturesAPI & { + bulkUpdate: ( + projectPath: string, + featureIds: string[], + updates: Partial + ) => Promise<{ + success: boolean; + updatedCount?: number; + failedCount?: number; + results?: Array<{ featureId: string; success: boolean; error?: string }>; + features?: Feature[]; + error?: string; + }>; + } = { getAll: (projectPath: string) => this.post('/api/features/list', { projectPath }), get: (projectPath: string, featureId: string) => this.post('/api/features/get', { projectPath, featureId }), @@ -1161,6 +1174,8 @@ export class HttpApiClient implements ElectronAPI { this.post('/api/features/agent-output', { projectPath, featureId }), generateTitle: (description: string) => this.post('/api/features/generate-title', { description }), + bulkUpdate: (projectPath: string, featureIds: string[], updates: Partial) => + this.post('/api/features/bulk-update', { projectPath, featureIds, updates }), }; // Auto Mode API From 1117afc37a9ed5639601fd2ffd7e8e34235c15c3 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sun, 4 Jan 2026 23:24:24 +0100 Subject: [PATCH 2/4] refactor: update mass edit dialog and introduce new select components - Removed advanced options toggle and related state from the mass edit dialog for a cleaner UI. - Replaced ProfileQuickSelect with ProfileSelect for better profile management. - Introduced new PlanningModeSelect and PrioritySelect components for streamlined selection of planning modes and priorities. - Updated imports in shared index to include new select components. - Enhanced the mass edit dialog to utilize the new components, improving user experience during bulk edits. --- .../board-view/dialogs/mass-edit-dialog.tsx | 117 ++++-------- .../views/board-view/shared/index.ts | 3 + .../shared/planning-mode-select.tsx | 148 +++++++++++++++ .../board-view/shared/priority-select.tsx | 112 +++++++++++ .../board-view/shared/profile-select.tsx | 175 ++++++++++++++++++ package-lock.json | 100 ++++------ 6 files changed, 507 insertions(+), 148 deletions(-) create mode 100644 apps/ui/src/components/views/board-view/shared/planning-mode-select.tsx create mode 100644 apps/ui/src/components/views/board-view/shared/priority-select.tsx create mode 100644 apps/ui/src/components/views/board-view/shared/profile-select.tsx diff --git a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx index 1b4b1467..6e198e63 100644 --- a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx @@ -10,18 +10,12 @@ import { import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; -import { Settings2, AlertCircle } from 'lucide-react'; +import { AlertCircle } from 'lucide-react'; import { modelSupportsThinking } from '@/lib/utils'; import { Feature, ModelAlias, ThinkingLevel, AIProfile, PlanningMode } from '@/store/app-store'; -import { - ModelSelector, - ThinkingLevelSelector, - ProfileQuickSelect, - TestingTabContent, - PrioritySelector, - PlanningModeSelector, -} from '../shared'; -import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types'; +import { ProfileSelect, TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared'; +import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; +import { isCursorModel, PROVIDER_PREFIXES, type PhaseModelEntry } from '@automaker/types'; import { cn } from '@/lib/utils'; interface MassEditDialogProps { @@ -113,7 +107,6 @@ export function MassEditDialog({ aiProfiles, }: MassEditDialogProps) { const [isApplying, setIsApplying] = useState(false); - const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); // Track which fields to apply const [applyState, setApplyState] = useState({ @@ -153,7 +146,6 @@ export function MassEditDialog({ setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false)); setPriority(getInitialValue(selectedFeatures, 'priority', 2)); setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false)); - setShowAdvancedOptions(false); } }, [open, selectedFeatures]); @@ -216,27 +208,6 @@ export function MassEditDialog({
- {/* Show Advanced Options Toggle */} - {showProfilesOnly && ( -
-
-

Simple Mode Active

-

- Only showing AI profiles. Advanced model tweaking is hidden. -

-
- -
- )} - {/* Quick Select Profile Section */} {aiProfiles.length > 0 && (
@@ -244,7 +215,7 @@ export function MassEditDialog({

Selecting a profile will automatically enable model settings

- )} + {/* Model Selector */} +
+ +

+ Or select a specific model configuration +

+ { + setModel(entry.model as ModelAlias); + setThinkingLevel(entry.thinkingLevel || 'none'); + // Auto-enable model and thinking level for apply state + setApplyState((prev) => ({ + ...prev, + model: true, + thinkingLevel: true, + })); + }} + compact + /> +
+ {/* Separator */} - {aiProfiles.length > 0 && (!showProfilesOnly || showAdvancedOptions) && ( -
- )} - - {/* Model Selection */} - {(!showProfilesOnly || showAdvancedOptions) && ( - <> - setApplyState((prev) => ({ ...prev, model: apply }))} - > - - - - {modelAllowsThinking && ( - - setApplyState((prev) => ({ ...prev, thinkingLevel: apply })) - } - > - - - )} - - )} - - {/* Separator before options */} - {(!showProfilesOnly || showAdvancedOptions) &&
} +
{/* Planning Mode */} - { + setPlanningMode(newMode); + // Auto-suggest approval based on mode, but user can override + setRequirePlanApproval(newMode === 'spec' || newMode === 'full'); + }} requireApproval={requirePlanApproval} onRequireApprovalChange={setRequirePlanApproval} - featureDescription="" - testIdPrefix="mass-edit" - compact + testIdPrefix="mass-edit-planning" /> @@ -329,7 +284,7 @@ export function MassEditDialog({ willApply={applyState.priority} onApplyChange={(apply) => setApplyState((prev) => ({ ...prev, priority: apply }))} > - void; + requireApproval?: boolean; + onRequireApprovalChange?: (require: boolean) => void; + testIdPrefix?: string; + className?: string; + disabled?: boolean; +} + +const modes = [ + { + value: 'skip' as const, + label: 'Skip', + description: 'Direct implementation, no upfront planning', + icon: Zap, + color: 'text-emerald-500', + }, + { + value: 'lite' as const, + label: 'Lite', + description: 'Think through approach, create task list', + icon: ClipboardList, + color: 'text-blue-500', + }, + { + value: 'spec' as const, + label: 'Spec', + description: 'Generate spec with acceptance criteria', + icon: FileText, + color: 'text-purple-500', + }, + { + value: 'full' as const, + label: 'Full', + description: 'Comprehensive spec with phased plan', + icon: ScrollText, + color: 'text-amber-500', + }, +]; + +/** + * PlanningModeSelect - Compact dropdown selector for planning modes + * + * A lightweight alternative to PlanningModeSelector for contexts where + * spec management UI is not needed (e.g., mass edit, bulk operations). + * + * Shows icon + label in dropdown, with description text below. + * Does not include spec generation, approval, or require-approval checkbox. + * + * @example + * ```tsx + * { + * setPlanningMode(mode); + * setRequireApproval(mode === 'spec' || mode === 'full'); + * }} + * testIdPrefix="mass-edit-planning" + * /> + * ``` + */ +export function PlanningModeSelect({ + mode, + onModeChange, + requireApproval, + onRequireApprovalChange, + testIdPrefix = 'planning-mode', + className, + disabled = false, +}: PlanningModeSelectProps) { + const selectedMode = modes.find((m) => m.value === mode); + + // Disable approval checkbox for skip/lite modes since they don't use planning + const isApprovalDisabled = disabled || mode === 'skip' || mode === 'lite'; + + return ( +
+ + {selectedMode &&

{selectedMode.description}

} + {onRequireApprovalChange && ( +
+ onRequireApprovalChange(!!checked)} + disabled={isApprovalDisabled} + data-testid={`${testIdPrefix}-require-approval-checkbox`} + /> + +
+ )} +
+ ); +} diff --git a/apps/ui/src/components/views/board-view/shared/priority-select.tsx b/apps/ui/src/components/views/board-view/shared/priority-select.tsx new file mode 100644 index 00000000..834dacab --- /dev/null +++ b/apps/ui/src/components/views/board-view/shared/priority-select.tsx @@ -0,0 +1,112 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { AlertCircle, ChevronDown, ChevronUp } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface PrioritySelectProps { + selectedPriority: number; + onPrioritySelect: (priority: number) => void; + testIdPrefix?: string; + className?: string; + disabled?: boolean; +} + +const priorities = [ + { + value: 1, + label: 'High', + description: 'Urgent, needs immediate attention', + icon: ChevronUp, + color: 'text-red-500', + bgColor: 'bg-red-500/10', + }, + { + value: 2, + label: 'Medium', + description: 'Normal priority, standard workflow', + icon: AlertCircle, + color: 'text-yellow-500', + bgColor: 'bg-yellow-500/10', + }, + { + value: 3, + label: 'Low', + description: 'Can wait, not time-sensitive', + icon: ChevronDown, + color: 'text-blue-500', + bgColor: 'bg-blue-500/10', + }, +]; + +/** + * PrioritySelect - Compact dropdown selector for feature priority + * + * A lightweight alternative to PrioritySelector for contexts where + * space is limited (e.g., mass edit, bulk operations). + * + * Shows icon + priority level in dropdown, with description below. + * + * @example + * ```tsx + * + * ``` + */ +export function PrioritySelect({ + selectedPriority, + onPrioritySelect, + testIdPrefix = 'priority', + className, + disabled = false, +}: PrioritySelectProps) { + const selectedPriorityObj = priorities.find((p) => p.value === selectedPriority); + + return ( +
+ + {selectedPriorityObj && ( +

{selectedPriorityObj.description}

+ )} +
+ ); +} diff --git a/apps/ui/src/components/views/board-view/shared/profile-select.tsx b/apps/ui/src/components/views/board-view/shared/profile-select.tsx new file mode 100644 index 00000000..d586ff6c --- /dev/null +++ b/apps/ui/src/components/views/board-view/shared/profile-select.tsx @@ -0,0 +1,175 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Brain, Terminal } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types'; +import { CURSOR_MODEL_MAP, profileHasThinking, PROVIDER_PREFIXES } from '@automaker/types'; +import { PROFILE_ICONS } from './model-constants'; + +/** + * Get display string for a profile's model configuration + */ +function getProfileModelDisplay(profile: AIProfile): string { + if (profile.provider === 'cursor') { + const cursorModel = profile.cursorModel || 'auto'; + const modelConfig = CURSOR_MODEL_MAP[cursorModel]; + return modelConfig?.label || cursorModel; + } + // Claude + return profile.model || 'sonnet'; +} + +/** + * Get display string for a profile's thinking configuration + */ +function getProfileThinkingDisplay(profile: AIProfile): string | null { + if (profile.provider === 'cursor') { + // For Cursor, thinking is embedded in the model + return profileHasThinking(profile) ? 'thinking' : null; + } + // Claude + return profile.thinkingLevel && profile.thinkingLevel !== 'none' ? profile.thinkingLevel : null; +} + +interface ProfileSelectProps { + profiles: AIProfile[]; + selectedModel: ModelAlias | CursorModelId; + selectedThinkingLevel: ThinkingLevel; + selectedCursorModel?: string; // For detecting cursor profile selection + onSelect: (profile: AIProfile) => void; + testIdPrefix?: string; + className?: string; + disabled?: boolean; +} + +/** + * ProfileSelect - Compact dropdown selector for AI profiles + * + * A lightweight alternative to ProfileQuickSelect for contexts where + * space is limited (e.g., mass edit, bulk operations). + * + * Shows icon + profile name in dropdown, with model details below. + * + * @example + * ```tsx + * + * ``` + */ +export function ProfileSelect({ + profiles, + selectedModel, + selectedThinkingLevel, + selectedCursorModel, + onSelect, + testIdPrefix = 'profile-select', + className, + disabled = false, +}: ProfileSelectProps) { + if (profiles.length === 0) { + return null; + } + + // Check if a profile is selected + const isProfileSelected = (profile: AIProfile): boolean => { + if (profile.provider === 'cursor') { + // For cursor profiles, check if cursor model matches + const profileCursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`; + return selectedCursorModel === profileCursorModel; + } + // For Claude profiles + return selectedModel === profile.model && selectedThinkingLevel === profile.thinkingLevel; + }; + + const selectedProfile = profiles.find(isProfileSelected); + + return ( +
+ + {selectedProfile && ( +

+ {getProfileModelDisplay(selectedProfile)} + {getProfileThinkingDisplay(selectedProfile) && + ` + ${getProfileThinkingDisplay(selectedProfile)}`} +

+ )} +
+ ); +} diff --git a/package-lock.json b/package-lock.json index 98ca8545..c6e8e649 100644 --- a/package-lock.json +++ b/package-lock.json @@ -675,6 +675,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1258,6 +1259,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz", "integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1300,6 +1302,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2120,7 +2123,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -2142,7 +2144,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2159,7 +2160,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -2174,7 +2174,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2942,7 +2941,6 @@ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -3067,7 +3065,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -3084,7 +3081,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -3101,7 +3097,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -3210,7 +3205,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3233,7 +3227,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3256,7 +3249,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3342,7 +3334,6 @@ ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.7.0" }, @@ -3365,7 +3356,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3385,7 +3375,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3785,8 +3774,7 @@ "version": "16.0.10", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { "version": "16.0.10", @@ -3800,7 +3788,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3817,7 +3804,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3834,7 +3820,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3851,7 +3836,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3868,7 +3852,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3885,7 +3868,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3902,7 +3884,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3919,7 +3900,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -4010,6 +3990,7 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -5450,7 +5431,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -5784,6 +5764,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.6.tgz", "integrity": "sha512-qWFxi2D6eGc1L03RzUuhyEOplZ7Q6q62YOl7Of9Y0q4YjwQwxRm4zxwDVtvUIoy4RLVCpqp5UoE+Nxv2PY9trg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-store": "^0.8.0", @@ -6210,6 +6191,7 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -6352,6 +6334,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6362,6 +6345,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6467,6 +6451,7 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -6960,7 +6945,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@xyflow/react": { "version": "12.10.0", @@ -7058,6 +7044,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7118,6 +7105,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7716,6 +7704,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8247,8 +8236,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", @@ -8553,8 +8541,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-env": { "version": "10.1.0", @@ -8651,6 +8638,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -8952,6 +8940,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -9278,7 +9267,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -9299,7 +9287,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -9550,6 +9537,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9864,6 +9852,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11531,7 +11520,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11553,7 +11541,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11575,7 +11562,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11597,7 +11583,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11619,7 +11604,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11641,7 +11625,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11663,7 +11646,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11685,7 +11667,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11707,7 +11688,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11729,7 +11709,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11751,7 +11730,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -14039,7 +14017,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -14056,7 +14033,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -14074,7 +14050,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -14263,6 +14238,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14272,6 +14248,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14630,7 +14607,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14819,6 +14795,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz", "integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -14867,7 +14844,6 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -14918,7 +14894,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14941,7 +14916,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14964,7 +14938,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14981,7 +14954,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14998,7 +14970,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15015,7 +14986,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15032,7 +15002,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15049,7 +15018,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15066,7 +15034,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -15083,7 +15050,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15106,7 +15072,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15129,7 +15094,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15152,7 +15116,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15175,7 +15138,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15198,7 +15160,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15667,7 +15628,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -15837,7 +15797,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -15901,7 +15860,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -15999,6 +15957,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16203,6 +16162,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16574,6 +16534,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -16663,7 +16624,8 @@ "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", @@ -16689,6 +16651,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16731,6 +16694,7 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -16988,6 +16952,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -17056,6 +17021,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From a4290b5863287bdd8e57c84604dd9325b08a9041 Mon Sep 17 00:00:00 2001 From: webdevcody Date: Tue, 6 Jan 2026 16:11:29 -0500 Subject: [PATCH 3/4] feat: enhance development environment with Docker support and UI improvements - Introduced a new `docker-compose.dev.yml` for development mode, enabling live reload and improved container management. - Updated `dev.mjs` to utilize `launchDockerDevContainers` for starting development containers with live reload capabilities. - Refactored `printModeMenu` to differentiate between development and production Docker options. - Enhanced the `BoardView` and `KanbanBoard` components by streamlining props and improving UI interactions. - Removed the `start.mjs` script, consolidating production launch logic into `dev.mjs` for a more unified approach. --- Dockerfile | 8 + Dockerfile.dev | 80 +++++ README.md | 19 +- apps/ui/src/components/views/board-view.tsx | 6 +- .../views/board-view/board-controls.tsx | 33 +- .../views/board-view/kanban-board.tsx | 48 +-- dev.mjs | 6 +- docker-compose.dev.yml | 142 ++++++++ package-lock.json | 107 ++++-- package.json | 3 +- scripts/launcher-utils.mjs | 308 ++++++++++++++---- start.mjs | 247 -------------- 12 files changed, 575 insertions(+), 432 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 docker-compose.dev.yml delete mode 100755 start.mjs diff --git a/Dockerfile b/Dockerfile index e45ddf24..c32b1764 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,10 @@ RUN npm run build:packages && npm run build --workspace=apps/server # ============================================================================= FROM node:22-slim AS server +# Build argument for tracking which commit this image was built from +ARG GIT_COMMIT_SHA=unknown +LABEL automaker.git.commit.sha="${GIT_COMMIT_SHA}" + # Install git, curl, bash (for terminal), gosu (for user switching), and GitHub CLI (pinned version, multi-arch) RUN apt-get update && apt-get install -y --no-install-recommends \ git curl bash gosu ca-certificates openssh-client \ @@ -184,6 +188,10 @@ RUN npm run build:packages && npm run build --workspace=apps/ui # ============================================================================= FROM nginx:alpine AS ui +# Build argument for tracking which commit this image was built from +ARG GIT_COMMIT_SHA=unknown +LABEL automaker.git.commit.sha="${GIT_COMMIT_SHA}" + # Copy built files COPY --from=ui-builder /app/apps/ui/dist /usr/share/nginx/html diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000..87ac6bf6 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,80 @@ +# Automaker Development Dockerfile +# For development with live reload via volume mounting +# Source code is NOT copied - it's mounted as a volume +# +# Usage: +# docker compose -f docker-compose.dev.yml up + +FROM node:22-slim + +# Install build dependencies for native modules (node-pty) and runtime tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 make g++ \ + git curl bash gosu ca-certificates openssh-client \ + && GH_VERSION="2.63.2" \ + && ARCH=$(uname -m) \ + && case "$ARCH" in \ + x86_64) GH_ARCH="amd64" ;; \ + aarch64|arm64) GH_ARCH="arm64" ;; \ + *) echo "Unsupported architecture: $ARCH" && exit 1 ;; \ + esac \ + && curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" -o gh.tar.gz \ + && tar -xzf gh.tar.gz \ + && mv gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh /usr/local/bin/gh \ + && rm -rf gh.tar.gz gh_${GH_VERSION}_linux_${GH_ARCH} \ + && rm -rf /var/lib/apt/lists/* + +# Install Claude CLI globally +RUN npm install -g @anthropic-ai/claude-code + +# Create non-root user +RUN groupadd -g 1001 automaker && \ + useradd -u 1001 -g automaker -m -d /home/automaker -s /bin/bash automaker && \ + mkdir -p /home/automaker/.local/bin && \ + mkdir -p /home/automaker/.cursor && \ + chown -R automaker:automaker /home/automaker && \ + chmod 700 /home/automaker/.cursor + +# Install Cursor CLI as automaker user +USER automaker +ENV HOME=/home/automaker +RUN curl https://cursor.com/install -fsS | bash || true +USER root + +# Add PATH to profile for Cursor CLI +RUN mkdir -p /etc/profile.d && \ + echo 'export PATH="/home/automaker/.local/bin:$PATH"' > /etc/profile.d/cursor-cli.sh && \ + chmod +x /etc/profile.d/cursor-cli.sh + +# Add to user bashrc files +RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /home/automaker/.bashrc && \ + chown automaker:automaker /home/automaker/.bashrc +RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /root/.bashrc + +WORKDIR /app + +# Create directories with proper permissions +RUN mkdir -p /data /projects && chown automaker:automaker /data /projects + +# Configure git for mounted volumes +RUN git config --system --add safe.directory '*' && \ + git config --system credential.helper '!gh auth git-credential' + +# Copy entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Environment variables +ENV PORT=3008 +ENV DATA_DIR=/data +ENV HOME=/home/automaker +ENV PATH="/home/automaker/.local/bin:${PATH}" + +# Expose both dev ports +EXPOSE 3007 3008 + +# Use entrypoint for permission handling +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] + +# Default command - will be overridden by docker-compose +CMD ["npm", "run", "dev:web"] diff --git a/README.md b/README.md index 9ca0f368..8bfd2a0a 100644 --- a/README.md +++ b/README.md @@ -117,24 +117,16 @@ cd automaker # 2. Install dependencies npm install -# 3. Build shared packages (Now can be skipped npm install / run dev does it automaticly) +# 3. Build shared packages (can be skipped - npm run dev does it automatically) npm run build:packages -# 4. Start Automaker (production mode) -npm run start +# 4. Start Automaker +npm run dev # Choose between: # 1. Web Application (browser at localhost:3007) # 2. Desktop Application (Electron - recommended) ``` -**Note:** The `npm run start` command will: - -- Check for dependencies and install if needed -- Build the application if needed -- Kill any processes on ports 3007/3008 -- Present an interactive menu to choose your run mode -- Run in production mode (no hot reload) - **Authentication Setup:** On first run, Automaker will automatically show a setup wizard where you can configure authentication. You can choose to: - Use **Claude Code CLI** (recommended) - Automaker will detect your CLI credentials automatically @@ -150,7 +142,7 @@ export ANTHROPIC_API_KEY="sk-ant-..." echo "ANTHROPIC_API_KEY=sk-ant-..." > .env ``` -**For Development:** If you want to develop on Automaker with Vite live reload and hot module replacement, use `npm run dev` instead. This will start the development server with fast refresh and instant updates as you make changes. +**For Development:** `npm run dev` starts the development server with Vite live reload and hot module replacement for fast refresh and instant updates as you make changes. ## How to Run @@ -194,9 +186,6 @@ npm run dev:web ```bash # Build for web deployment (uses Vite) npm run build - -# Run production build -npm run start ``` #### Desktop Application diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 1dc99b05..2c82261b 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -1151,8 +1151,6 @@ export function BoardView() { onDetailLevelChange={setKanbanCardDetailLevel} boardViewMode={boardViewMode} onBoardViewModeChange={setBoardViewMode} - isSelectionMode={isSelectionMode} - onToggleSelectionMode={toggleSelectionMode} />
{/* View Content - Kanban or Graph */} @@ -1175,7 +1173,6 @@ export function BoardView() { onManualVerify={handleManualVerify} onMoveBackToInProgress={handleMoveBackToInProgress} onFollowUp={handleOpenFollowUp} - onCommit={handleCommitFeature} onComplete={handleCompleteFeature} onImplement={handleStartImplementation} onViewPlan={(feature) => setViewPlanFeature(feature)} @@ -1186,8 +1183,6 @@ export function BoardView() { }} featuresWithContext={featuresWithContext} runningAutoTasks={runningAutoTasks} - shortcuts={shortcuts} - onStartNextFeatures={handleStartNextFeatures} onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)} pipelineConfig={ currentProject?.path ? pipelineConfigByProject[currentProject.path] || null : null @@ -1196,6 +1191,7 @@ export function BoardView() { isSelectionMode={isSelectionMode} selectedFeatureIds={selectedFeatureIds} onToggleFeatureSelection={toggleFeatureSelection} + onToggleSelectionMode={toggleSelectionMode} /> ) : ( void; boardViewMode: BoardViewMode; onBoardViewModeChange: (mode: BoardViewMode) => void; - isSelectionMode?: boolean; - onToggleSelectionMode?: () => void; } export function BoardControls({ @@ -35,8 +24,6 @@ export function BoardControls({ onDetailLevelChange, boardViewMode, onBoardViewModeChange, - isSelectionMode = false, - onToggleSelectionMode, }: BoardControlsProps) { if (!isMounted) return null; @@ -88,24 +75,6 @@ export function BoardControls({
- {/* Selection Mode Toggle */} - - - - - -

{isSelectionMode ? 'Exit Select Mode' : 'Select Mode'}

-
-
- {/* Board Background Button */} 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 5cef1aa9..2962852d 100644 --- a/apps/ui/src/components/views/board-view/kanban-board.tsx +++ b/apps/ui/src/components/views/board-view/kanban-board.tsx @@ -2,13 +2,11 @@ import { useMemo } from 'react'; import { DndContext, DragOverlay } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { Button } from '@/components/ui/button'; -import { HotkeyButton } from '@/components/ui/hotkey-button'; import { KanbanColumn, KanbanCard } from './components'; import { Feature } from '@/store/app-store'; -import { FastForward, Archive, Plus, Settings2 } from 'lucide-react'; -import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; +import { Archive, Settings2, CheckSquare, GripVertical } from 'lucide-react'; import { useResponsiveKanban } from '@/hooks/use-responsive-kanban'; -import { getColumnsWithPipeline, type Column, type ColumnId } from './constants'; +import { getColumnsWithPipeline, type ColumnId } from './constants'; import type { PipelineConfig } from '@automaker/types'; interface KanbanBoardProps { @@ -37,7 +35,6 @@ interface KanbanBoardProps { onManualVerify: (feature: Feature) => void; onMoveBackToInProgress: (feature: Feature) => void; onFollowUp: (feature: Feature) => void; - onCommit: (feature: Feature) => void; onComplete: (feature: Feature) => void; onImplement: (feature: Feature) => void; onViewPlan: (feature: Feature) => void; @@ -45,8 +42,6 @@ interface KanbanBoardProps { onSpawnTask?: (feature: Feature) => void; featuresWithContext: Set; runningAutoTasks: string[]; - shortcuts: ReturnType; - onStartNextFeatures: () => void; onArchiveAllVerified: () => void; pipelineConfig: PipelineConfig | null; onOpenPipelineSettings?: () => void; @@ -54,6 +49,7 @@ interface KanbanBoardProps { isSelectionMode?: boolean; selectedFeatureIds?: Set; onToggleFeatureSelection?: (featureId: string) => void; + onToggleSelectionMode?: () => void; } export function KanbanBoard({ @@ -74,7 +70,6 @@ export function KanbanBoard({ onManualVerify, onMoveBackToInProgress, onFollowUp, - onCommit, onComplete, onImplement, onViewPlan, @@ -82,14 +77,13 @@ export function KanbanBoard({ onSpawnTask, featuresWithContext, runningAutoTasks, - shortcuts, - onStartNextFeatures, onArchiveAllVerified, pipelineConfig, onOpenPipelineSettings, isSelectionMode = false, selectedFeatureIds = new Set(), onToggleFeatureSelection, + onToggleSelectionMode, }: KanbanBoardProps) { // Generate columns including pipeline steps const columns = useMemo(() => getColumnsWithPipeline(pipelineConfig), [pipelineConfig]); @@ -133,20 +127,26 @@ export function KanbanBoard({ Complete All ) : column.id === 'backlog' ? ( - columnFeatures.length > 0 && ( - - - Make - - ) + ) : column.id === 'in_progress' ? (