diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index 4588cfe7..14c56341 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -62,6 +62,7 @@ import { AgentOutputModal } from "./agent-output-modal"; import { FeatureSuggestionsDialog } from "./feature-suggestions-dialog"; import { BoardBackgroundModal } from "@/components/dialogs/board-background-modal"; import { AddFeatureDialog } from "./board-view/AddFeatureDialog"; +import { EditFeatureDialog } from "./board-view/EditFeatureDialog"; import { Plus, RefreshCw, @@ -132,50 +133,7 @@ const COLUMNS: { id: ColumnId; title: string; colorClass: string }[] = [ }, ]; -type ModelOption = { - id: AgentModel; - label: string; - description: string; - badge?: string; - provider: "claude"; -}; -const CLAUDE_MODELS: ModelOption[] = [ - { - id: "haiku", - label: "Claude Haiku", - description: "Fast and efficient for simple tasks.", - badge: "Speed", - provider: "claude", - }, - { - id: "sonnet", - label: "Claude Sonnet", - description: "Balanced performance with strong reasoning.", - badge: "Balanced", - provider: "claude", - }, - { - id: "opus", - label: "Claude Opus", - description: "Most capable model for complex work.", - badge: "Premium", - provider: "claude", - }, -]; - -// Profile icon mapping -const PROFILE_ICONS: Record< - string, - React.ComponentType<{ className?: string }> -> = { - Brain, - Zap, - Scale, - Cpu, - Rocket, - Sparkles, -}; export function BoardView() { const { @@ -227,10 +185,6 @@ export function BoardView() { const [followUpPreviewMap, setFollowUpPreviewMap] = useState( () => new Map() ); - const [editFeaturePreviewMap, setEditFeaturePreviewMap] = - useState(() => new Map()); - // Local state to temporarily show advanced options when profiles-only mode is enabled - const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false); const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false); const [suggestionsCount, setSuggestionsCount] = useState(0); const [featureSuggestions, setFeatureSuggestions] = useState< @@ -989,30 +943,23 @@ export function BoardView() { saveCategory(featureData.category); }; - const handleUpdateFeature = () => { - if (!editingFeature) return; - - const selectedModel = (editingFeature.model ?? "opus") as AgentModel; - const normalizedThinking = modelSupportsThinking(selectedModel) - ? editingFeature.thinkingLevel - : "none"; - - const updates = { - category: editingFeature.category, - description: editingFeature.description, - steps: editingFeature.steps, - skipTests: editingFeature.skipTests, - model: selectedModel, - thinkingLevel: normalizedThinking, - imagePaths: editingFeature.imagePaths, - }; - updateFeature(editingFeature.id, updates); - persistFeatureUpdate(editingFeature.id, updates); - // Clear the preview map after saving - setEditFeaturePreviewMap(new Map()); + const handleUpdateFeature = ( + featureId: string, + updates: { + category: string; + description: string; + steps: string[]; + skipTests: boolean; + model: AgentModel; + thinkingLevel: ThinkingLevel; + imagePaths: DescriptionImagePath[]; + } + ) => { + updateFeature(featureId, updates); + persistFeatureUpdate(featureId, updates); // Persist the category if it's new - if (editingFeature.category) { - saveCategory(editingFeature.category); + if (updates.category) { + saveCategory(updates.category); } setEditingFeature(null); }; @@ -1741,39 +1688,7 @@ export function BoardView() { startNextFeaturesRef.current = handleStartNextFeatures; }, [handleStartNextFeatures]); - const renderModelOptions = ( - options: ModelOption[], - selectedModel: AgentModel, - onSelect: (model: AgentModel) => void, - testIdPrefix = "model-select" - ) => ( -
- {options.map((option) => { - const isSelected = selectedModel === option.id; - // Shorter display names for compact view - const shortName = option.label.replace("Claude ", ""); - return ( - - ); - })} -
- ); - const editModelAllowsThinking = modelSupportsThinking(editingFeature?.model); if (!currentProject) { return ( @@ -2379,363 +2294,15 @@ export function BoardView() { /> {/* Edit Feature Dialog */} - { - if (!open) { - setEditingFeature(null); - setShowEditAdvancedOptions(false); - setEditFeaturePreviewMap(new Map()); - } - }} - > - { - // Prevent dialog from closing when clicking on category autocomplete dropdown - const target = e.target as HTMLElement; - if (target.closest('[data-testid="category-autocomplete-list"]')) { - e.preventDefault(); - } - }} - onInteractOutside={(e) => { - // Prevent dialog from closing when clicking on category autocomplete dropdown - const target = e.target as HTMLElement; - if (target.closest('[data-testid="category-autocomplete-list"]')) { - e.preventDefault(); - } - }} - > - - Edit Feature - Modify the feature details. - - {editingFeature && ( - - - - - Prompt - - - - Model - - - - Testing - - - - {/* Prompt Tab */} - -
- - - setEditingFeature({ - ...editingFeature, - description: value, - }) - } - images={editingFeature.imagePaths ?? []} - onImagesChange={(images) => - setEditingFeature({ - ...editingFeature, - imagePaths: images, - }) - } - placeholder="Describe the feature..." - previewMap={editFeaturePreviewMap} - onPreviewMapChange={setEditFeaturePreviewMap} - data-testid="edit-feature-description" - /> -
-
- - - setEditingFeature({ - ...editingFeature, - category: value, - }) - } - suggestions={categorySuggestions} - placeholder="e.g., Core, UI, API" - data-testid="edit-feature-category" - /> -
-
- - {/* Model Tab */} - - {/* Show Advanced Options Toggle - only when profiles-only mode is enabled */} - {showProfilesOnly && ( -
-
-

- Simple Mode Active -

-

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

-
- -
- )} - - {/* Quick Select Profile Section */} - {aiProfiles.length > 0 && ( -
-
- - - Presets - -
-
- {aiProfiles.slice(0, 6).map((profile) => { - const IconComponent = profile.icon - ? PROFILE_ICONS[profile.icon] - : Brain; - const isSelected = - editingFeature.model === profile.model && - editingFeature.thinkingLevel === - profile.thinkingLevel; - return ( - - ); - })} -
-

- Or customize below. -

-
- )} - - {/* Separator */} - {aiProfiles.length > 0 && - (!showProfilesOnly || showEditAdvancedOptions) && ( -
- )} - - {/* Claude Models Section - Hidden when showProfilesOnly is true and showEditAdvancedOptions is false */} - {(!showProfilesOnly || showEditAdvancedOptions) && ( -
-
- - - Native - -
- {renderModelOptions( - CLAUDE_MODELS, - (editingFeature.model ?? "opus") as AgentModel, - (model) => - setEditingFeature({ - ...editingFeature, - model, - thinkingLevel: modelSupportsThinking(model) - ? editingFeature.thinkingLevel - : "none", - }), - "edit-model-select" - )} - - {/* Thinking Level - Only shown when Claude model is selected */} - {editModelAllowsThinking && ( -
- -
- {( - [ - "none", - "low", - "medium", - "high", - "ultrathink", - ] as ThinkingLevel[] - ).map((level) => ( - - ))} -
-

- Higher levels give more time to reason through complex - problems. -

-
- )} -
- )} - - - {/* Testing Tab */} - -
- - setEditingFeature({ - ...editingFeature, - skipTests: checked !== true, - }) - } - data-testid="edit-skip-tests-checkbox" - /> -
- - -
-
-

- When enabled, this feature will use automated TDD. When - disabled, it will require manual verification. -

- - {/* Verification Steps - Only shown when skipTests is enabled */} - {editingFeature.skipTests && ( -
- -

- Add manual steps to verify this feature works correctly. -

- {editingFeature.steps.map((step, index) => ( - { - const steps = [...editingFeature.steps]; - steps[index] = e.target.value; - setEditingFeature({ ...editingFeature, steps }); - }} - data-testid={`edit-feature-step-${index}`} - /> - ))} - -
- )} -
- - )} - - - - Save Changes - - - -
+ setEditingFeature(null)} + onUpdate={handleUpdateFeature} + categorySuggestions={categorySuggestions} + isMaximized={isMaximized} + showProfilesOnly={showProfilesOnly} + aiProfiles={aiProfiles} + /> {/* Agent Output Modal */} +> = { + Brain, + Zap, + Scale, + Cpu, + Rocket, + Sparkles, +}; + +interface EditFeatureDialogProps { + feature: Feature | null; + onClose: () => void; + onUpdate: (featureId: string, updates: { + category: string; + description: string; + steps: string[]; + skipTests: boolean; + model: AgentModel; + thinkingLevel: ThinkingLevel; + imagePaths: DescriptionImagePath[]; + }) => void; + categorySuggestions: string[]; + isMaximized: boolean; + showProfilesOnly: boolean; + aiProfiles: AIProfile[]; +} + +export function EditFeatureDialog({ + feature, + onClose, + onUpdate, + categorySuggestions, + isMaximized, + showProfilesOnly, + aiProfiles, +}: EditFeatureDialogProps) { + const [editingFeature, setEditingFeature] = useState(feature); + const [editFeaturePreviewMap, setEditFeaturePreviewMap] = + useState(() => new Map()); + const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false); + + // Update local state when feature prop changes + useEffect(() => { + setEditingFeature(feature); + if (!feature) { + setEditFeaturePreviewMap(new Map()); + setShowEditAdvancedOptions(false); + } + }, [feature]); + + const handleUpdate = () => { + if (!editingFeature) return; + + const selectedModel = (editingFeature.model ?? "opus") as AgentModel; + const normalizedThinking = modelSupportsThinking(selectedModel) + ? editingFeature.thinkingLevel + : "none"; + + const updates = { + category: editingFeature.category, + description: editingFeature.description, + steps: editingFeature.steps, + skipTests: editingFeature.skipTests, + model: selectedModel, + thinkingLevel: normalizedThinking, + imagePaths: editingFeature.imagePaths ?? [], + }; + + onUpdate(editingFeature.id, updates); + setEditFeaturePreviewMap(new Map()); + setShowEditAdvancedOptions(false); + onClose(); + }; + + const handleDialogClose = (open: boolean) => { + if (!open) { + onClose(); + } + }; + + const renderModelOptions = ( + options: ModelOption[], + selectedModel: AgentModel, + onSelect: (model: AgentModel) => void, + testIdPrefix = "model-select" + ) => ( +
+ {options.map((option) => { + const isSelected = selectedModel === option.id; + // Shorter display names for compact view + const shortName = option.label.replace("Claude ", ""); + return ( + + ); + })} +
+ ); + + const editModelAllowsThinking = modelSupportsThinking(editingFeature?.model); + + if (!editingFeature) { + return null; + } + + return ( + + { + // Prevent dialog from closing when clicking on category autocomplete dropdown + const target = e.target as HTMLElement; + if (target.closest('[data-testid="category-autocomplete-list"]')) { + e.preventDefault(); + } + }} + onInteractOutside={(e) => { + // Prevent dialog from closing when clicking on category autocomplete dropdown + const target = e.target as HTMLElement; + if (target.closest('[data-testid="category-autocomplete-list"]')) { + e.preventDefault(); + } + }} + > + + Edit Feature + Modify the feature details. + + + + + + Prompt + + + + Model + + + + Testing + + + + {/* Prompt Tab */} + +
+ + + setEditingFeature({ + ...editingFeature, + description: value, + }) + } + images={editingFeature.imagePaths ?? []} + onImagesChange={(images) => + setEditingFeature({ + ...editingFeature, + imagePaths: images, + }) + } + placeholder="Describe the feature..." + previewMap={editFeaturePreviewMap} + onPreviewMapChange={setEditFeaturePreviewMap} + data-testid="edit-feature-description" + /> +
+
+ + + setEditingFeature({ + ...editingFeature, + category: value, + }) + } + suggestions={categorySuggestions} + placeholder="e.g., Core, UI, API" + data-testid="edit-feature-category" + /> +
+
+ + {/* Model Tab */} + + {/* Show Advanced Options Toggle - only when profiles-only mode is enabled */} + {showProfilesOnly && ( +
+
+

+ Simple Mode Active +

+

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

+
+ +
+ )} + + {/* Quick Select Profile Section */} + {aiProfiles.length > 0 && ( +
+
+ + + Presets + +
+
+ {aiProfiles.slice(0, 6).map((profile) => { + const IconComponent = profile.icon + ? PROFILE_ICONS[profile.icon] + : Brain; + const isSelected = + editingFeature.model === profile.model && + editingFeature.thinkingLevel === profile.thinkingLevel; + return ( + + ); + })} +
+

+ Or customize below. +

+
+ )} + + {/* Separator */} + {aiProfiles.length > 0 && + (!showProfilesOnly || showEditAdvancedOptions) && ( +
+ )} + + {/* Claude Models Section - Hidden when showProfilesOnly is true and showEditAdvancedOptions is false */} + {(!showProfilesOnly || showEditAdvancedOptions) && ( +
+
+ + + Native + +
+ {renderModelOptions( + CLAUDE_MODELS, + (editingFeature.model ?? "opus") as AgentModel, + (model) => + setEditingFeature({ + ...editingFeature, + model, + thinkingLevel: modelSupportsThinking(model) + ? editingFeature.thinkingLevel + : "none", + }), + "edit-model-select" + )} + + {/* Thinking Level - Only shown when Claude model is selected */} + {editModelAllowsThinking && ( +
+ +
+ {( + [ + "none", + "low", + "medium", + "high", + "ultrathink", + ] as ThinkingLevel[] + ).map((level) => ( + + ))} +
+

+ Higher levels give more time to reason through complex + problems. +

+
+ )} +
+ )} + + + {/* Testing Tab */} + +
+ + setEditingFeature({ + ...editingFeature, + skipTests: checked !== true, + }) + } + data-testid="edit-skip-tests-checkbox" + /> +
+ + +
+
+

+ When enabled, this feature will use automated TDD. When disabled, + it will require manual verification. +

+ + {/* Verification Steps - Only shown when skipTests is enabled */} + {editingFeature.skipTests && ( +
+ +

+ Add manual steps to verify this feature works correctly. +

+ {editingFeature.steps.map((step, index) => ( + { + const steps = [...editingFeature.steps]; + steps[index] = e.target.value; + setEditingFeature({ ...editingFeature, steps }); + }} + data-testid={`edit-feature-step-${index}`} + /> + ))} + +
+ )} +
+ + + + + Save Changes + + + +
+ ); +}