mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
- Extract board-view into organized subfolders following new pattern: - components/: kanban-card, kanban-column - dialogs/: all dialog and modal components (8 files) - hooks/: all board-specific hooks (10 files) - shared/: reusable components between dialogs (model-selector, etc.) - Rename all files to kebab-case convention - Add barrel exports (index.ts) for clean imports - Add docs/folder-pattern.md documenting the folder structure - Reduce board-view.tsx from ~3600 lines to ~490 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
499 lines
15 KiB
TypeScript
499 lines
15 KiB
TypeScript
"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 { 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,
|
|
} = useAppStore();
|
|
const shortcuts = useKeyboardShortcutsConfig();
|
|
const {
|
|
features: hookFeatures,
|
|
isLoading,
|
|
persistedCategories,
|
|
loadFeatures,
|
|
saveCategory,
|
|
} = useBoardFeatures({ currentProject });
|
|
const [editingFeature, setEditingFeature] = useState<Feature | null>(null);
|
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
const [showOutputModal, setShowOutputModal] = useState(false);
|
|
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
|
|
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(
|
|
new Set()
|
|
);
|
|
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] =
|
|
useState(false);
|
|
const [showBoardBackgroundModal, setShowBoardBackgroundModal] =
|
|
useState(false);
|
|
const [showCompletedModal, setShowCompletedModal] = useState(false);
|
|
const [deleteCompletedFeature, setDeleteCompletedFeature] =
|
|
useState<Feature | null>(null);
|
|
|
|
// 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<boolean> => {
|
|
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
|
|
const { activeFeature, handleDragStart, handleDragEnd } = useBoardDragDrop({
|
|
features: hookFeatures,
|
|
currentProject,
|
|
runningAutoTasks,
|
|
persistFeatureUpdate,
|
|
handleStartImplementation,
|
|
});
|
|
|
|
// Use column features hook
|
|
const { getColumnFeatures, completedFeatures } = useBoardColumnFeatures({
|
|
features: hookFeatures,
|
|
runningAutoTasks,
|
|
searchQuery,
|
|
});
|
|
|
|
// Use background hook
|
|
const { backgroundSettings, backgroundImageStyle } = useBoardBackground({
|
|
currentProject,
|
|
});
|
|
|
|
if (!currentProject) {
|
|
return (
|
|
<div
|
|
className="flex-1 flex items-center justify-center"
|
|
data-testid="board-view-no-project"
|
|
>
|
|
<p className="text-muted-foreground">No project selected</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div
|
|
className="flex-1 flex items-center justify-center"
|
|
data-testid="board-view-loading"
|
|
>
|
|
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="flex-1 flex flex-col overflow-hidden content-bg relative"
|
|
data-testid="board-view"
|
|
>
|
|
{/* Header */}
|
|
<BoardHeader
|
|
projectName={currentProject.name}
|
|
maxConcurrency={maxConcurrency}
|
|
onConcurrencyChange={setMaxConcurrency}
|
|
isAutoModeRunning={autoMode.isRunning}
|
|
onStartAutoMode={() => autoMode.start()}
|
|
onStopAutoMode={() => autoMode.stop()}
|
|
onAddFeature={() => setShowAddDialog(true)}
|
|
addFeatureShortcut={{
|
|
key: shortcuts.addFeature,
|
|
action: () => setShowAddDialog(true),
|
|
description: "Add new feature",
|
|
}}
|
|
isMounted={isMounted}
|
|
/>
|
|
|
|
{/* Main Content Area */}
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{/* Search Bar Row */}
|
|
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
|
<BoardSearchBar
|
|
searchQuery={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
isCreatingSpec={isCreatingSpec}
|
|
creatingSpecProjectPath={creatingSpecProjectPath ?? undefined}
|
|
currentProjectPath={currentProject?.path}
|
|
/>
|
|
|
|
{/* Board Background & Detail Level Controls */}
|
|
<BoardControls
|
|
isMounted={isMounted}
|
|
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
|
|
onShowCompletedModal={() => setShowCompletedModal(true)}
|
|
completedCount={completedFeatures.length}
|
|
kanbanCardDetailLevel={kanbanCardDetailLevel}
|
|
onDetailLevelChange={setKanbanCardDetailLevel}
|
|
/>
|
|
</div>
|
|
{/* Kanban Columns */}
|
|
<KanbanBoard
|
|
sensors={sensors}
|
|
collisionDetectionStrategy={collisionDetectionStrategy}
|
|
onDragStart={handleDragStart}
|
|
onDragEnd={handleDragEnd}
|
|
activeFeature={activeFeature}
|
|
getColumnFeatures={getColumnFeatures}
|
|
backgroundImageStyle={backgroundImageStyle}
|
|
backgroundSettings={backgroundSettings}
|
|
onEdit={(feature) => 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)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Board Background Modal */}
|
|
<BoardBackgroundModal
|
|
open={showBoardBackgroundModal}
|
|
onOpenChange={setShowBoardBackgroundModal}
|
|
/>
|
|
|
|
{/* Completed Features Modal */}
|
|
<CompletedFeaturesModal
|
|
open={showCompletedModal}
|
|
onOpenChange={setShowCompletedModal}
|
|
completedFeatures={completedFeatures}
|
|
onUnarchive={handleUnarchiveFeature}
|
|
onDelete={(feature) => setDeleteCompletedFeature(feature)}
|
|
/>
|
|
|
|
{/* Delete Completed Feature Confirmation Dialog */}
|
|
<DeleteCompletedFeatureDialog
|
|
feature={deleteCompletedFeature}
|
|
onClose={() => setDeleteCompletedFeature(null)}
|
|
onConfirm={async () => {
|
|
if (deleteCompletedFeature) {
|
|
await handleDeleteFeature(deleteCompletedFeature.id);
|
|
setDeleteCompletedFeature(null);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{/* Add Feature Dialog */}
|
|
<AddFeatureDialog
|
|
open={showAddDialog}
|
|
onOpenChange={setShowAddDialog}
|
|
onAdd={handleAddFeature}
|
|
categorySuggestions={categorySuggestions}
|
|
defaultSkipTests={defaultSkipTests}
|
|
isMaximized={isMaximized}
|
|
showProfilesOnly={showProfilesOnly}
|
|
aiProfiles={aiProfiles}
|
|
/>
|
|
|
|
{/* Edit Feature Dialog */}
|
|
<EditFeatureDialog
|
|
feature={editingFeature}
|
|
onClose={() => setEditingFeature(null)}
|
|
onUpdate={handleUpdateFeature}
|
|
categorySuggestions={categorySuggestions}
|
|
isMaximized={isMaximized}
|
|
showProfilesOnly={showProfilesOnly}
|
|
aiProfiles={aiProfiles}
|
|
/>
|
|
|
|
{/* Agent Output Modal */}
|
|
<AgentOutputModal
|
|
open={showOutputModal}
|
|
onClose={() => setShowOutputModal(false)}
|
|
featureDescription={outputFeature?.description || ""}
|
|
featureId={outputFeature?.id || ""}
|
|
featureStatus={outputFeature?.status}
|
|
onNumberKeyPress={handleOutputModalNumberKeyPress}
|
|
/>
|
|
|
|
{/* Delete All Verified Dialog */}
|
|
<DeleteAllVerifiedDialog
|
|
open={showDeleteAllVerifiedDialog}
|
|
onOpenChange={setShowDeleteAllVerifiedDialog}
|
|
verifiedCount={getColumnFeatures("verified").length}
|
|
onConfirm={async () => {
|
|
await handleDeleteAllVerified();
|
|
setShowDeleteAllVerifiedDialog(false);
|
|
}}
|
|
/>
|
|
|
|
{/* Follow-Up Prompt Dialog */}
|
|
<FollowUpDialog
|
|
open={showFollowUpDialog}
|
|
onOpenChange={handleFollowUpDialogChange}
|
|
feature={followUpFeature}
|
|
prompt={followUpPrompt}
|
|
imagePaths={followUpImagePaths}
|
|
previewMap={followUpPreviewMap}
|
|
onPromptChange={setFollowUpPrompt}
|
|
onImagePathsChange={setFollowUpImagePaths}
|
|
onPreviewMapChange={setFollowUpPreviewMap}
|
|
onSend={handleSendFollowUp}
|
|
isMaximized={isMaximized}
|
|
/>
|
|
|
|
{/* Feature Suggestions Dialog */}
|
|
<FeatureSuggestionsDialog
|
|
open={showSuggestionsDialog}
|
|
onClose={closeSuggestionsDialog}
|
|
projectPath={currentProject.path}
|
|
suggestions={featureSuggestions}
|
|
setSuggestions={updateSuggestions}
|
|
isGenerating={isGeneratingSuggestions}
|
|
setIsGenerating={setIsGeneratingSuggestions}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|