"use client"; import { useEffect, useState, useCallback, useMemo } from "react"; import { PointerSensor, useSensor, useSensors, rectIntersection, pointerWithin, } from "@dnd-kit/core"; import { useAppStore, Feature } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { BoardBackgroundModal } from "@/components/dialogs/board-background-modal"; import { RefreshCw } from "lucide-react"; import { useAutoMode } from "@/hooks/use-auto-mode"; import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts"; import { useWindowState } from "@/hooks/use-window-state"; // Board-view specific imports import { BoardHeader } from "./board-view/board-header"; import { BoardSearchBar } from "./board-view/board-search-bar"; import { BoardControls } from "./board-view/board-controls"; import { KanbanBoard } from "./board-view/kanban-board"; import { AddFeatureDialog, AgentOutputModal, CompletedFeaturesModal, DeleteAllVerifiedDialog, DeleteCompletedFeatureDialog, EditFeatureDialog, FeatureSuggestionsDialog, FollowUpDialog, } from "./board-view/dialogs"; import { CreateWorktreeDialog } from "./board-view/dialogs/create-worktree-dialog"; import { DeleteWorktreeDialog } from "./board-view/dialogs/delete-worktree-dialog"; import { CommitWorktreeDialog } from "./board-view/dialogs/commit-worktree-dialog"; import { CreatePRDialog } from "./board-view/dialogs/create-pr-dialog"; import { CreateBranchDialog } from "./board-view/dialogs/create-branch-dialog"; import { WorktreeSelector } from "./board-view/components"; import { COLUMNS } from "./board-view/constants"; import { useBoardFeatures, useBoardDragDrop, useBoardActions, useBoardKeyboardShortcuts, useBoardColumnFeatures, useBoardEffects, useBoardBackground, useBoardPersistence, useFollowUpState, useSuggestionsState, } from "./board-view/hooks"; export function BoardView() { const { currentProject, maxConcurrency, setMaxConcurrency, defaultSkipTests, showProfilesOnly, aiProfiles, kanbanCardDetailLevel, setKanbanCardDetailLevel, specCreatingForProject, setSpecCreatingForProject, getCurrentWorktree, } = useAppStore(); const shortcuts = useKeyboardShortcutsConfig(); const { features: hookFeatures, isLoading, persistedCategories, loadFeatures, saveCategory, } = useBoardFeatures({ currentProject }); const [editingFeature, setEditingFeature] = useState(null); const [showAddDialog, setShowAddDialog] = useState(false); const [isMounted, setIsMounted] = useState(false); const [showOutputModal, setShowOutputModal] = useState(false); const [outputFeature, setOutputFeature] = useState(null); const [featuresWithContext, setFeaturesWithContext] = useState>( new Set() ); const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] = useState(false); const [showBoardBackgroundModal, setShowBoardBackgroundModal] = useState(false); const [showCompletedModal, setShowCompletedModal] = useState(false); const [deleteCompletedFeature, setDeleteCompletedFeature] = useState(null); // Worktree dialog states const [showCreateWorktreeDialog, setShowCreateWorktreeDialog] = useState(false); const [showDeleteWorktreeDialog, setShowDeleteWorktreeDialog] = useState(false); const [showCommitWorktreeDialog, setShowCommitWorktreeDialog] = useState(false); const [showCreatePRDialog, setShowCreatePRDialog] = useState(false); const [showCreateBranchDialog, setShowCreateBranchDialog] = useState(false); const [selectedWorktreeForAction, setSelectedWorktreeForAction] = useState<{ path: string; branch: string; isMain: boolean; hasChanges?: boolean; changedFilesCount?: number; } | null>(null); const [worktreeRefreshKey, setWorktreeRefreshKey] = useState(0); // Follow-up state hook const { showFollowUpDialog, followUpFeature, followUpPrompt, followUpImagePaths, followUpPreviewMap, setShowFollowUpDialog, setFollowUpFeature, setFollowUpPrompt, setFollowUpImagePaths, setFollowUpPreviewMap, handleFollowUpDialogChange, } = useFollowUpState(); // Suggestions state hook const { showSuggestionsDialog, suggestionsCount, featureSuggestions, isGeneratingSuggestions, setShowSuggestionsDialog, setSuggestionsCount, setFeatureSuggestions, setIsGeneratingSuggestions, updateSuggestions, closeSuggestionsDialog, } = useSuggestionsState(); // Search filter for Kanban cards const [searchQuery, setSearchQuery] = useState(""); // Derive spec creation state from store - check if current project is the one being created const isCreatingSpec = specCreatingForProject === currentProject?.path; const creatingSpecProjectPath = specCreatingForProject ?? undefined; const checkContextExists = useCallback( async (featureId: string): Promise => { if (!currentProject) return false; try { const api = getElectronAPI(); if (!api?.autoMode?.contextExists) { return false; } const result = await api.autoMode.contextExists( currentProject.path, featureId ); return result.success && result.exists === true; } catch (error) { console.error("[Board] Error checking context:", error); return false; } }, [currentProject] ); // Use board effects hook useBoardEffects({ currentProject, specCreatingForProject, setSpecCreatingForProject, setSuggestionsCount, setFeatureSuggestions, setIsGeneratingSuggestions, checkContextExists, features: hookFeatures, isLoading, setFeaturesWithContext, }); // Auto mode hook const autoMode = useAutoMode(); // Get runningTasks from the hook (scoped to current project) const runningAutoTasks = autoMode.runningTasks; // Window state hook for compact dialog mode const { isMaximized } = useWindowState(); // Keyboard shortcuts hook will be initialized after actions hook // Prevent hydration issues useEffect(() => { setIsMounted(true); }, []); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ); // Get unique categories from existing features AND persisted categories for autocomplete suggestions const categorySuggestions = useMemo(() => { const featureCategories = hookFeatures .map((f) => f.category) .filter(Boolean); // Merge feature categories with persisted categories const allCategories = [...featureCategories, ...persistedCategories]; return [...new Set(allCategories)].sort(); }, [hookFeatures, persistedCategories]); // Custom collision detection that prioritizes columns over cards const collisionDetectionStrategy = useCallback( (args: any) => { // First, check if pointer is within a column const pointerCollisions = pointerWithin(args); const columnCollisions = pointerCollisions.filter((collision: any) => COLUMNS.some((col) => col.id === collision.id) ); // If we found a column collision, use that if (columnCollisions.length > 0) { return columnCollisions; } // Otherwise, use rectangle intersection for cards return rectIntersection(args); }, [] ); // Use persistence hook const { persistFeatureCreate, persistFeatureUpdate, persistFeatureDelete, } = useBoardPersistence({ currentProject }); // Get in-progress features for keyboard shortcuts (needed before actions hook) const inProgressFeaturesForShortcuts = useMemo(() => { return hookFeatures.filter((f) => { const isRunning = runningAutoTasks.includes(f.id); return isRunning || f.status === "in_progress"; }); }, [hookFeatures, runningAutoTasks]); // Extract all action handlers into a hook const { handleAddFeature, handleUpdateFeature, handleDeleteFeature, handleStartImplementation, handleVerifyFeature, handleResumeFeature, handleManualVerify, handleMoveBackToInProgress, handleOpenFollowUp, handleSendFollowUp, handleCommitFeature, handleRevertFeature, handleMergeFeature, handleCompleteFeature, handleUnarchiveFeature, handleViewOutput, handleOutputModalNumberKeyPress, handleForceStopFeature, handleStartNextFeatures, handleDeleteAllVerified, } = useBoardActions({ currentProject, features: hookFeatures, runningAutoTasks, loadFeatures, persistFeatureCreate, persistFeatureUpdate, persistFeatureDelete, saveCategory, setEditingFeature, setShowOutputModal, setOutputFeature, followUpFeature, followUpPrompt, followUpImagePaths, setFollowUpFeature, setFollowUpPrompt, setFollowUpImagePaths, setFollowUpPreviewMap, setShowFollowUpDialog, inProgressFeaturesForShortcuts, outputFeature, }); // Use keyboard shortcuts hook (after actions hook) useBoardKeyboardShortcuts({ features: hookFeatures, runningAutoTasks, onAddFeature: () => setShowAddDialog(true), onStartNextFeatures: handleStartNextFeatures, onViewOutput: handleViewOutput, }); // Use drag and drop hook // Get current worktree path for filtering features and assigning to cards const currentWorktreePath = currentProject ? getCurrentWorktree(currentProject.path) : null; const { activeFeature, handleDragStart, handleDragEnd } = useBoardDragDrop({ features: hookFeatures, currentProject, runningAutoTasks, persistFeatureUpdate, handleStartImplementation, currentWorktreePath, projectPath: currentProject?.path || null, }); // Use column features hook const { getColumnFeatures, completedFeatures } = useBoardColumnFeatures({ features: hookFeatures, runningAutoTasks, searchQuery, currentWorktreePath, projectPath: currentProject?.path || null, }); // Use background hook const { backgroundSettings, backgroundImageStyle } = useBoardBackground({ currentProject, }); if (!currentProject) { return (

No project selected

); } if (isLoading) { return (
); } return (
{/* Header */} autoMode.start()} onStopAutoMode={() => autoMode.stop()} onAddFeature={() => setShowAddDialog(true)} addFeatureShortcut={{ key: shortcuts.addFeature, action: () => setShowAddDialog(true), description: "Add new feature", }} isMounted={isMounted} /> {/* Worktree Selector */} setShowCreateWorktreeDialog(true)} onDeleteWorktree={(worktree) => { setSelectedWorktreeForAction(worktree); setShowDeleteWorktreeDialog(true); }} onCommit={(worktree) => { setSelectedWorktreeForAction(worktree); setShowCommitWorktreeDialog(true); }} onCreatePR={(worktree) => { setSelectedWorktreeForAction(worktree); setShowCreatePRDialog(true); }} onCreateBranch={(worktree) => { setSelectedWorktreeForAction(worktree); setShowCreateBranchDialog(true); }} /> {/* Main Content Area */}
{/* Search Bar Row */}
{/* Board Background & Detail Level Controls */} setShowBoardBackgroundModal(true)} onShowCompletedModal={() => setShowCompletedModal(true)} completedCount={completedFeatures.length} kanbanCardDetailLevel={kanbanCardDetailLevel} onDetailLevelChange={setKanbanCardDetailLevel} />
{/* Kanban Columns */} setEditingFeature(feature)} onDelete={(featureId) => handleDeleteFeature(featureId)} onViewOutput={handleViewOutput} onVerify={handleVerifyFeature} onResume={handleResumeFeature} onForceStop={handleForceStopFeature} onManualVerify={handleManualVerify} onMoveBackToInProgress={handleMoveBackToInProgress} onFollowUp={handleOpenFollowUp} onCommit={handleCommitFeature} onRevert={handleRevertFeature} onMerge={handleMergeFeature} onComplete={handleCompleteFeature} onImplement={handleStartImplementation} featuresWithContext={featuresWithContext} runningAutoTasks={runningAutoTasks} shortcuts={shortcuts} onStartNextFeatures={handleStartNextFeatures} onShowSuggestions={() => setShowSuggestionsDialog(true)} suggestionsCount={suggestionsCount} onDeleteAllVerified={() => setShowDeleteAllVerifiedDialog(true)} />
{/* Board Background Modal */} {/* Completed Features Modal */} setDeleteCompletedFeature(feature)} /> {/* Delete Completed Feature Confirmation Dialog */} setDeleteCompletedFeature(null)} onConfirm={async () => { if (deleteCompletedFeature) { await handleDeleteFeature(deleteCompletedFeature.id); setDeleteCompletedFeature(null); } }} /> {/* Add Feature Dialog */} {/* Edit Feature Dialog */} setEditingFeature(null)} onUpdate={handleUpdateFeature} categorySuggestions={categorySuggestions} isMaximized={isMaximized} showProfilesOnly={showProfilesOnly} aiProfiles={aiProfiles} /> {/* Agent Output Modal */} setShowOutputModal(false)} featureDescription={outputFeature?.description || ""} featureId={outputFeature?.id || ""} featureStatus={outputFeature?.status} onNumberKeyPress={handleOutputModalNumberKeyPress} /> {/* Delete All Verified Dialog */} { await handleDeleteAllVerified(); setShowDeleteAllVerifiedDialog(false); }} /> {/* Follow-Up Prompt Dialog */} {/* Feature Suggestions Dialog */} {/* Create Worktree Dialog */} setWorktreeRefreshKey((k) => k + 1)} /> {/* Delete Worktree Dialog */} { setWorktreeRefreshKey((k) => k + 1); setSelectedWorktreeForAction(null); }} /> {/* Commit Worktree Dialog */} { setWorktreeRefreshKey((k) => k + 1); setSelectedWorktreeForAction(null); }} /> {/* Create PR Dialog */} { setWorktreeRefreshKey((k) => k + 1); setSelectedWorktreeForAction(null); }} /> {/* Create Branch Dialog */} { setWorktreeRefreshKey((k) => k + 1); setSelectedWorktreeForAction(null); }} />
); }