mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
refactor(board-view): extract AddFeatureDialog component
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dir \"C:\\Users\\Ben\\Desktop\\appdev\\git\\automaker\\apps\\app\\public\")"
|
||||
"Bash(dir \"C:\\Users\\Ben\\Desktop\\appdev\\git\\automaker\\apps\\app\\public\")",
|
||||
"Bash(find:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
310
REFACTORING_CANDIDATES.md
Normal file
310
REFACTORING_CANDIDATES.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Large Files - Refactoring Candidates
|
||||
|
||||
This document tracks files in the AutoMaker codebase that exceed 3000 lines or are significantly large (1000+ lines) and should be considered for refactoring into smaller, more maintainable components.
|
||||
|
||||
**Last Updated:** 2025-12-15
|
||||
**Total Large Files:** 8
|
||||
**Combined Size:** 15,027 lines
|
||||
|
||||
---
|
||||
|
||||
## 🔴 CRITICAL - Over 3000 Lines
|
||||
|
||||
### 1. board-view.tsx - 3,325 lines
|
||||
**Path:** `apps/app/src/components/views/board-view.tsx`
|
||||
**Type:** React Component (TSX)
|
||||
**Priority:** VERY HIGH
|
||||
|
||||
**Description:**
|
||||
Main Kanban board view component that serves as the centerpiece of the application.
|
||||
|
||||
**Current Responsibilities:**
|
||||
- Feature/task card management and drag-and-drop operations using @dnd-kit
|
||||
- Adding, editing, and deleting features
|
||||
- Running autonomous agents to implement features
|
||||
- Displaying feature status across multiple columns (Backlog, In Progress, Waiting Approval, Verified)
|
||||
- Model/AI profile selection for feature implementation
|
||||
- Advanced options configuration (thinking level, model selection, skip tests)
|
||||
- Search/filtering functionality for cards
|
||||
- Output modal for viewing agent results
|
||||
- Feature suggestions dialog
|
||||
- Board background customization
|
||||
- Integration with Electron APIs for IPC communication
|
||||
- Keyboard shortcuts support
|
||||
- 40+ state variables for managing UI state
|
||||
|
||||
**Refactoring Recommendations:**
|
||||
Extract into smaller components:
|
||||
- `AddFeatureDialog.tsx` - Feature creation dialog with image upload
|
||||
- `EditFeatureDialog.tsx` - Feature editing dialog
|
||||
- `AgentOutputModal.tsx` - Already exists, verify separation
|
||||
- `FeatureSuggestionsDialog.tsx` - Already exists, verify separation
|
||||
- `BoardHeader.tsx` - Header with controls and search
|
||||
- `BoardSearchBar.tsx` - Search and filter functionality
|
||||
- `ConcurrencyControl.tsx` - Concurrency slider component
|
||||
- `BoardActions.tsx` - Action buttons (add feature, auto mode, etc.)
|
||||
- `DragDropContext.tsx` - Wrap drag-and-drop logic
|
||||
- Custom hooks:
|
||||
- `useBoardFeatures.ts` - Feature loading and management
|
||||
- `useBoardDragDrop.ts` - Drag and drop handlers
|
||||
- `useBoardActions.ts` - Feature action handlers (run, verify, delete, etc.)
|
||||
- `useBoardKeyboardShortcuts.ts` - Keyboard shortcut logic
|
||||
|
||||
---
|
||||
|
||||
## 🟡 HIGH PRIORITY - 2000+ Lines
|
||||
|
||||
### 2. sidebar.tsx - 2,396 lines
|
||||
**Path:** `apps/app/src/components/layout/sidebar.tsx`
|
||||
**Type:** React Component (TSX)
|
||||
**Priority:** HIGH
|
||||
|
||||
**Description:**
|
||||
Main navigation sidebar with comprehensive project management.
|
||||
|
||||
**Current Responsibilities:**
|
||||
- Project folder navigation and selection
|
||||
- View mode switching (Board, Agent, Settings, etc.)
|
||||
- Project operations (create, delete, rename)
|
||||
- Theme and appearance controls
|
||||
- Terminal, Wiki, and other view launchers
|
||||
- Drag-and-drop project reordering
|
||||
- Settings and configuration access
|
||||
|
||||
**Refactoring Recommendations:**
|
||||
Split into focused components:
|
||||
- `ProjectSelector.tsx` - Project list and selection
|
||||
- `NavigationTabs.tsx` - View mode tabs
|
||||
- `ProjectActions.tsx` - Create, delete, rename operations
|
||||
- `SettingsMenu.tsx` - Settings dropdown
|
||||
- `ThemeSelector.tsx` - Theme controls
|
||||
- `ViewLaunchers.tsx` - Terminal, Wiki launchers
|
||||
- Custom hooks:
|
||||
- `useProjectManagement.ts` - Project CRUD operations
|
||||
- `useSidebarState.ts` - Sidebar state management
|
||||
|
||||
---
|
||||
|
||||
### 3. electron.ts - 2,356 lines
|
||||
**Path:** `apps/app/src/lib/electron.ts`
|
||||
**Type:** TypeScript Utility/API Bridge
|
||||
**Priority:** HIGH
|
||||
|
||||
**Description:**
|
||||
Electron IPC bridge and type definitions for frontend-backend communication.
|
||||
|
||||
**Current Responsibilities:**
|
||||
- File system operations (read, write, directory listing)
|
||||
- Project management APIs
|
||||
- Feature management APIs
|
||||
- Terminal/shell execution
|
||||
- Auto mode and agent execution APIs
|
||||
- Worktree management
|
||||
- Provider status APIs
|
||||
- Event handling and subscriptions
|
||||
|
||||
**Refactoring Recommendations:**
|
||||
Modularize into domain-specific API modules:
|
||||
- `api/file-system-api.ts` - File operations
|
||||
- `api/project-api.ts` - Project CRUD
|
||||
- `api/feature-api.ts` - Feature management
|
||||
- `api/execution-api.ts` - Auto mode and agent execution
|
||||
- `api/provider-api.ts` - Provider status and management
|
||||
- `api/worktree-api.ts` - Git worktree operations
|
||||
- `api/terminal-api.ts` - Terminal/shell APIs
|
||||
- `types/electron-types.ts` - Shared type definitions
|
||||
- `electron.ts` - Main export aggregator
|
||||
|
||||
---
|
||||
|
||||
### 4. app-store.ts - 2,174 lines
|
||||
**Path:** `apps/app/src/store/app-store.ts`
|
||||
**Type:** TypeScript State Management (Zustand Store)
|
||||
**Priority:** HIGH
|
||||
|
||||
**Description:**
|
||||
Centralized application state store using Zustand.
|
||||
|
||||
**Current Responsibilities:**
|
||||
- Global app state types and interfaces
|
||||
- Project and feature management state
|
||||
- Theme and appearance settings
|
||||
- API keys configuration
|
||||
- Keyboard shortcuts configuration
|
||||
- Terminal themes configuration
|
||||
- Auto mode settings
|
||||
- All store mutations and selectors
|
||||
|
||||
**Refactoring Recommendations:**
|
||||
Split into domain-specific stores:
|
||||
- `stores/projects-store.ts` - Project state and actions
|
||||
- `stores/features-store.ts` - Feature state and actions
|
||||
- `stores/ui-store.ts` - UI state (theme, sidebar, modals)
|
||||
- `stores/settings-store.ts` - User settings and preferences
|
||||
- `stores/execution-store.ts` - Auto mode and running tasks
|
||||
- `stores/provider-store.ts` - Provider configuration
|
||||
- `types/store-types.ts` - Shared type definitions
|
||||
- `app-store.ts` - Main store aggregator with combined selectors
|
||||
|
||||
---
|
||||
|
||||
## 🟢 MEDIUM PRIORITY - 1000-2000 Lines
|
||||
|
||||
### 5. auto-mode-service.ts - 1,232 lines
|
||||
**Path:** `apps/server/src/services/auto-mode-service.ts`
|
||||
**Type:** TypeScript Service (Backend)
|
||||
**Priority:** MEDIUM-HIGH
|
||||
|
||||
**Description:**
|
||||
Core autonomous feature implementation service.
|
||||
|
||||
**Current Responsibilities:**
|
||||
- Worktree creation and management
|
||||
- Feature execution with Claude Agent SDK
|
||||
- Concurrent execution with concurrency limits
|
||||
- Progress streaming via events
|
||||
- Verification and merge workflows
|
||||
- Provider management
|
||||
- Error handling and classification
|
||||
|
||||
**Refactoring Recommendations:**
|
||||
Extract into service modules:
|
||||
- `services/worktree-manager.ts` - Worktree operations
|
||||
- `services/feature-executor.ts` - Feature execution logic
|
||||
- `services/concurrency-manager.ts` - Concurrency control
|
||||
- `services/verification-service.ts` - Verification workflows
|
||||
- `utils/error-classifier.ts` - Error handling utilities
|
||||
|
||||
---
|
||||
|
||||
### 6. spec-view.tsx - 1,230 lines
|
||||
**Path:** `apps/app/src/components/views/spec-view.tsx`
|
||||
**Type:** React Component (TSX)
|
||||
**Priority:** MEDIUM
|
||||
|
||||
**Description:**
|
||||
Specification editor view component for feature specification management.
|
||||
|
||||
**Refactoring Recommendations:**
|
||||
Extract editor components and hooks:
|
||||
- `SpecEditor.tsx` - Main editor component
|
||||
- `SpecToolbar.tsx` - Editor toolbar
|
||||
- `SpecSidebar.tsx` - Spec navigation sidebar
|
||||
- `useSpecEditor.ts` - Editor state management
|
||||
|
||||
---
|
||||
|
||||
### 7. kanban-card.tsx - 1,180 lines
|
||||
**Path:** `apps/app/src/components/views/kanban-card.tsx`
|
||||
**Type:** React Component (TSX)
|
||||
**Priority:** MEDIUM
|
||||
|
||||
**Description:**
|
||||
Individual Kanban card component with rich feature display and interaction.
|
||||
|
||||
**Refactoring Recommendations:**
|
||||
Split into smaller card components:
|
||||
- `KanbanCardHeader.tsx` - Card title and metadata
|
||||
- `KanbanCardBody.tsx` - Card content
|
||||
- `KanbanCardActions.tsx` - Action buttons
|
||||
- `KanbanCardStatus.tsx` - Status indicators
|
||||
- `useKanbanCard.ts` - Card interaction logic
|
||||
|
||||
---
|
||||
|
||||
### 8. analysis-view.tsx - 1,134 lines
|
||||
**Path:** `apps/app/src/components/views/analysis-view.tsx`
|
||||
**Type:** React Component (TSX)
|
||||
**Priority:** MEDIUM
|
||||
|
||||
**Description:**
|
||||
Analysis view component for displaying and managing feature analysis data.
|
||||
|
||||
**Refactoring Recommendations:**
|
||||
Extract visualization and data components:
|
||||
- `AnalysisChart.tsx` - Chart/graph components
|
||||
- `AnalysisTable.tsx` - Data table
|
||||
- `AnalysisFilters.tsx` - Filter controls
|
||||
- `useAnalysisData.ts` - Data fetching and processing
|
||||
|
||||
---
|
||||
|
||||
## Refactoring Strategy
|
||||
|
||||
### Phase 1: Critical (Immediate)
|
||||
1. **board-view.tsx** - Break into dialogs, header, and custom hooks
|
||||
- Extract all dialogs first (AddFeature, EditFeature)
|
||||
- Move to custom hooks for business logic
|
||||
- Split remaining UI into smaller components
|
||||
|
||||
### Phase 2: High Priority (Next Sprint)
|
||||
2. **sidebar.tsx** - Componentize navigation and project management
|
||||
3. **electron.ts** - Modularize into API domains
|
||||
4. **app-store.ts** - Split into domain stores
|
||||
|
||||
### Phase 3: Medium Priority (Future)
|
||||
5. **auto-mode-service.ts** - Extract service modules
|
||||
6. **spec-view.tsx** - Break into editor components
|
||||
7. **kanban-card.tsx** - Split card into sub-components
|
||||
8. **analysis-view.tsx** - Extract visualization components
|
||||
|
||||
---
|
||||
|
||||
## General Refactoring Guidelines
|
||||
|
||||
### When Refactoring Large Components:
|
||||
|
||||
1. **Extract Dialogs/Modals First**
|
||||
- Move dialog components to separate files
|
||||
- Keep dialog state management in parent initially
|
||||
- Later extract to custom hooks if complex
|
||||
|
||||
2. **Create Custom Hooks for Business Logic**
|
||||
- Move data fetching to `useFetch*` hooks
|
||||
- Move complex state logic to `use*State` hooks
|
||||
- Move side effects to `use*Effect` hooks
|
||||
|
||||
3. **Split UI into Presentational Components**
|
||||
- Header/toolbar components
|
||||
- Content area components
|
||||
- Footer/action components
|
||||
|
||||
4. **Move Utils and Helpers**
|
||||
- Extract pure functions to utility files
|
||||
- Move constants to separate constant files
|
||||
- Create type files for shared interfaces
|
||||
|
||||
### When Refactoring Large Files:
|
||||
|
||||
1. **Identify Domains/Concerns**
|
||||
- Group related functionality
|
||||
- Find natural boundaries
|
||||
|
||||
2. **Extract Gradually**
|
||||
- Start with least coupled code
|
||||
- Work towards core functionality
|
||||
- Test after each extraction
|
||||
|
||||
3. **Maintain Type Safety**
|
||||
- Export types from extracted modules
|
||||
- Use shared type files for common interfaces
|
||||
- Ensure no type errors after refactoring
|
||||
|
||||
---
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
- [ ] board-view.tsx (3,325 lines)
|
||||
- [ ] sidebar.tsx (2,396 lines)
|
||||
- [ ] electron.ts (2,356 lines)
|
||||
- [ ] app-store.ts (2,174 lines)
|
||||
- [ ] auto-mode-service.ts (1,232 lines)
|
||||
- [ ] spec-view.tsx (1,230 lines)
|
||||
- [ ] kanban-card.tsx (1,180 lines)
|
||||
- [ ] analysis-view.tsx (1,134 lines)
|
||||
|
||||
**Target:** All files under 500 lines, most under 300 lines
|
||||
|
||||
---
|
||||
|
||||
*Generated: 2025-12-15*
|
||||
@@ -61,6 +61,7 @@ import { KanbanCard } from "./kanban-card";
|
||||
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 {
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -201,16 +202,6 @@ export function BoardView() {
|
||||
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(null);
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [newFeature, setNewFeature] = useState({
|
||||
category: "",
|
||||
description: "",
|
||||
steps: [""],
|
||||
images: [] as FeatureImage[],
|
||||
imagePaths: [] as DescriptionImagePath[],
|
||||
skipTests: false,
|
||||
model: "opus" as AgentModel,
|
||||
thinkingLevel: "none" as ThinkingLevel,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [showOutputModal, setShowOutputModal] = useState(false);
|
||||
@@ -233,15 +224,12 @@ export function BoardView() {
|
||||
DescriptionImagePath[]
|
||||
>([]);
|
||||
// Preview maps to persist image previews across tab switches
|
||||
const [newFeaturePreviewMap, setNewFeaturePreviewMap] =
|
||||
useState<ImagePreviewMap>(() => new Map());
|
||||
const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(
|
||||
() => new Map()
|
||||
);
|
||||
const [editFeaturePreviewMap, setEditFeaturePreviewMap] =
|
||||
useState<ImagePreviewMap>(() => new Map());
|
||||
// Local state to temporarily show advanced options when profiles-only mode is enabled
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false);
|
||||
const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false);
|
||||
const [suggestionsCount, setSuggestionsCount] = useState(0);
|
||||
@@ -251,8 +239,6 @@ export function BoardView() {
|
||||
const [isGeneratingSuggestions, setIsGeneratingSuggestions] = useState(false);
|
||||
// Search filter for Kanban cards
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
// Validation state for add feature form
|
||||
const [descriptionError, setDescriptionError] = useState(false);
|
||||
// Derive spec creation state from store - check if current project is the one being created
|
||||
const isCreatingSpec = specCreatingForProject === currentProject?.path;
|
||||
const creatingSpecProjectPath = specCreatingForProject;
|
||||
@@ -589,15 +575,6 @@ export function BoardView() {
|
||||
[currentProject, persistedCategories]
|
||||
);
|
||||
|
||||
// Sync skipTests default when dialog opens
|
||||
useEffect(() => {
|
||||
if (showAddDialog) {
|
||||
setNewFeature((prev) => ({
|
||||
...prev,
|
||||
skipTests: defaultSkipTests,
|
||||
}));
|
||||
}
|
||||
}, [showAddDialog, defaultSkipTests]);
|
||||
|
||||
// Listen for auto mode feature completion and errors to reload features
|
||||
useEffect(() => {
|
||||
@@ -992,45 +969,24 @@ export function BoardView() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFeature = () => {
|
||||
// Validate description is required
|
||||
if (!newFeature.description.trim()) {
|
||||
setDescriptionError(true);
|
||||
return;
|
||||
}
|
||||
const category = newFeature.category || "Uncategorized";
|
||||
const selectedModel = newFeature.model;
|
||||
const normalizedThinking = modelSupportsThinking(selectedModel)
|
||||
? newFeature.thinkingLevel
|
||||
: "none";
|
||||
const handleAddFeature = (featureData: {
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
images: FeatureImage[];
|
||||
imagePaths: DescriptionImagePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
}) => {
|
||||
const newFeatureData = {
|
||||
category,
|
||||
description: newFeature.description,
|
||||
steps: newFeature.steps.filter((s) => s.trim()),
|
||||
...featureData,
|
||||
status: "backlog" as const,
|
||||
images: newFeature.images,
|
||||
imagePaths: newFeature.imagePaths,
|
||||
skipTests: newFeature.skipTests,
|
||||
model: selectedModel,
|
||||
thinkingLevel: normalizedThinking,
|
||||
};
|
||||
const createdFeature = addFeature(newFeatureData);
|
||||
persistFeatureCreate(createdFeature);
|
||||
// Persist the category
|
||||
saveCategory(category);
|
||||
setNewFeature({
|
||||
category: "",
|
||||
description: "",
|
||||
steps: [""],
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: "opus",
|
||||
thinkingLevel: "none",
|
||||
});
|
||||
// Clear the preview map when the feature is added
|
||||
setNewFeaturePreviewMap(new Map());
|
||||
setShowAddDialog(false);
|
||||
saveCategory(featureData.category);
|
||||
};
|
||||
|
||||
const handleUpdateFeature = () => {
|
||||
@@ -1817,7 +1773,6 @@ export function BoardView() {
|
||||
</div>
|
||||
);
|
||||
|
||||
const newModelAllowsThinking = modelSupportsThinking(newFeature.model);
|
||||
const editModelAllowsThinking = modelSupportsThinking(editingFeature?.model);
|
||||
|
||||
if (!currentProject) {
|
||||
@@ -2412,363 +2367,16 @@ export function BoardView() {
|
||||
</Dialog>
|
||||
|
||||
{/* Add Feature Dialog */}
|
||||
<Dialog
|
||||
<AddFeatureDialog
|
||||
open={showAddDialog}
|
||||
onOpenChange={(open) => {
|
||||
setShowAddDialog(open);
|
||||
// Clear preview map, validation error, and reset advanced options when dialog closes
|
||||
if (!open) {
|
||||
setNewFeaturePreviewMap(new Map());
|
||||
setShowAdvancedOptions(false);
|
||||
setDescriptionError(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
compact={!isMaximized}
|
||||
data-testid="add-feature-dialog"
|
||||
onPointerDownOutside={(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();
|
||||
}
|
||||
}}
|
||||
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();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Feature</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new feature card for the Kanban board.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs
|
||||
defaultValue="prompt"
|
||||
className="py-4 flex-1 min-h-0 flex flex-col"
|
||||
>
|
||||
<TabsList className="w-full grid grid-cols-3 mb-4">
|
||||
<TabsTrigger value="prompt" data-testid="tab-prompt">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Prompt
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="model" data-testid="tab-model">
|
||||
<Settings2 className="w-4 h-4 mr-2" />
|
||||
Model
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="testing" data-testid="tab-testing">
|
||||
<FlaskConical className="w-4 h-4 mr-2" />
|
||||
Testing
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Prompt Tab */}
|
||||
<TabsContent value="prompt" className="space-y-4 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<DescriptionImageDropZone
|
||||
value={newFeature.description}
|
||||
onChange={(value) => {
|
||||
setNewFeature({ ...newFeature, description: value });
|
||||
if (value.trim()) {
|
||||
setDescriptionError(false);
|
||||
}
|
||||
}}
|
||||
images={newFeature.imagePaths}
|
||||
onImagesChange={(images) =>
|
||||
setNewFeature({ ...newFeature, imagePaths: images })
|
||||
}
|
||||
placeholder="Describe the feature..."
|
||||
previewMap={newFeaturePreviewMap}
|
||||
onPreviewMapChange={setNewFeaturePreviewMap}
|
||||
autoFocus
|
||||
error={descriptionError}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category (optional)</Label>
|
||||
<CategoryAutocomplete
|
||||
value={newFeature.category}
|
||||
onChange={(value) =>
|
||||
setNewFeature({ ...newFeature, category: value })
|
||||
}
|
||||
suggestions={categorySuggestions}
|
||||
placeholder="e.g., Core, UI, API"
|
||||
data-testid="feature-category-input"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Model Tab */}
|
||||
<TabsContent value="model" className="space-y-4 overflow-y-auto">
|
||||
{/* Show Advanced Options Toggle - only when profiles-only mode is enabled */}
|
||||
{showProfilesOnly && (
|
||||
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Simple Mode Active
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only showing AI profiles. Advanced model tweaking is
|
||||
hidden.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
data-testid="show-advanced-options-toggle"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 mr-2" />
|
||||
{showAdvancedOptions ? "Hide" : "Show"} Advanced
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Select Profile Section */}
|
||||
{aiProfiles.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-2">
|
||||
<UserCircle className="w-4 h-4 text-brand-500" />
|
||||
Quick Select Profile
|
||||
</Label>
|
||||
<span className="text-[11px] px-2 py-0.5 rounded-full border border-brand-500/40 text-brand-500">
|
||||
Presets
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{aiProfiles.slice(0, 6).map((profile) => {
|
||||
const IconComponent = profile.icon
|
||||
? PROFILE_ICONS[profile.icon]
|
||||
: Brain;
|
||||
const isSelected =
|
||||
newFeature.model === profile.model &&
|
||||
newFeature.thinkingLevel === profile.thinkingLevel;
|
||||
return (
|
||||
<button
|
||||
key={profile.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: profile.model,
|
||||
thinkingLevel: profile.thinkingLevel,
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2 rounded-lg border text-left transition-all",
|
||||
isSelected
|
||||
? "bg-brand-500/10 border-brand-500 text-foreground"
|
||||
: "bg-background hover:bg-accent border-input"
|
||||
)}
|
||||
data-testid={`profile-quick-select-${profile.id}`}
|
||||
>
|
||||
<div className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0 bg-primary/10">
|
||||
{IconComponent && (
|
||||
<IconComponent className="w-4 h-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{profile.name}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground truncate">
|
||||
{profile.model}
|
||||
{profile.thinkingLevel !== "none" &&
|
||||
` + ${profile.thinkingLevel}`}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Or customize below. Manage profiles in{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAddDialog(false);
|
||||
useAppStore.getState().setCurrentView("profiles");
|
||||
}}
|
||||
className="text-brand-500 hover:underline"
|
||||
>
|
||||
AI Profiles
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{aiProfiles.length > 0 &&
|
||||
(!showProfilesOnly || showAdvancedOptions) && (
|
||||
<div className="border-t border-border" />
|
||||
)}
|
||||
|
||||
{/* Claude Models Section - Hidden when showProfilesOnly is true and showAdvancedOptions is false */}
|
||||
{(!showProfilesOnly || showAdvancedOptions) && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-primary" />
|
||||
Claude (SDK)
|
||||
</Label>
|
||||
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary">
|
||||
Native
|
||||
</span>
|
||||
</div>
|
||||
{renderModelOptions(
|
||||
CLAUDE_MODELS,
|
||||
newFeature.model,
|
||||
(model) =>
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model,
|
||||
thinkingLevel: modelSupportsThinking(model)
|
||||
? newFeature.thinkingLevel
|
||||
: "none",
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Thinking Level - Only shown when Claude model is selected */}
|
||||
{newModelAllowsThinking && (
|
||||
<div className="space-y-2 pt-2 border-t border-border">
|
||||
<Label className="flex items-center gap-2 text-sm">
|
||||
<Brain className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
Thinking Level
|
||||
</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{(
|
||||
[
|
||||
"none",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"ultrathink",
|
||||
] as ThinkingLevel[]
|
||||
).map((level) => (
|
||||
<button
|
||||
key={level}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
thinkingLevel: level,
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
|
||||
newFeature.thinkingLevel === level
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-background hover:bg-accent border-input"
|
||||
)}
|
||||
data-testid={`thinking-level-${level}`}
|
||||
>
|
||||
{level === "none" && "None"}
|
||||
{level === "low" && "Low"}
|
||||
{level === "medium" && "Med"}
|
||||
{level === "high" && "High"}
|
||||
{level === "ultrathink" && "Ultra"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Higher levels give more time to reason through complex
|
||||
problems.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Testing Tab */}
|
||||
<TabsContent value="testing" className="space-y-4 overflow-y-auto">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="skip-tests"
|
||||
checked={!newFeature.skipTests}
|
||||
onCheckedChange={(checked) =>
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
skipTests: checked !== true,
|
||||
})
|
||||
}
|
||||
data-testid="skip-tests-checkbox"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
htmlFor="skip-tests"
|
||||
className="text-sm cursor-pointer"
|
||||
>
|
||||
Enable automated testing
|
||||
</Label>
|
||||
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, this feature will use automated TDD. When
|
||||
disabled, it will require manual verification.
|
||||
</p>
|
||||
|
||||
{/* Verification Steps - Only shown when skipTests is enabled */}
|
||||
{newFeature.skipTests && (
|
||||
<div className="space-y-2 pt-2 border-t border-border">
|
||||
<Label>Verification Steps</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Add manual steps to verify this feature works correctly.
|
||||
</p>
|
||||
{newFeature.steps.map((step, index) => (
|
||||
<Input
|
||||
key={index}
|
||||
placeholder={`Verification step ${index + 1}`}
|
||||
value={step}
|
||||
onChange={(e) => {
|
||||
const steps = [...newFeature.steps];
|
||||
steps[index] = e.target.value;
|
||||
setNewFeature({ ...newFeature, steps });
|
||||
}}
|
||||
data-testid={`feature-step-${index}-input`}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
steps: [...newFeature.steps, ""],
|
||||
})
|
||||
}
|
||||
data-testid="add-step-button"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Verification Step
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setShowAddDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={handleAddFeature}
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={showAddDialog}
|
||||
data-testid="confirm-add-feature"
|
||||
>
|
||||
Add Feature
|
||||
</HotkeyButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
onOpenChange={setShowAddDialog}
|
||||
onAdd={handleAddFeature}
|
||||
categorySuggestions={categorySuggestions}
|
||||
defaultSkipTests={defaultSkipTests}
|
||||
isMaximized={isMaximized}
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
aiProfiles={aiProfiles}
|
||||
/>
|
||||
|
||||
{/* Edit Feature Dialog */}
|
||||
<Dialog
|
||||
|
||||
578
apps/app/src/components/views/board-view/AddFeatureDialog.tsx
Normal file
578
apps/app/src/components/views/board-view/AddFeatureDialog.tsx
Normal file
@@ -0,0 +1,578 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CategoryAutocomplete } from "@/components/ui/category-autocomplete";
|
||||
import {
|
||||
DescriptionImageDropZone,
|
||||
FeatureImagePath as DescriptionImagePath,
|
||||
ImagePreviewMap,
|
||||
} from "@/components/ui/description-image-dropzone";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
MessageSquare,
|
||||
Settings2,
|
||||
FlaskConical,
|
||||
Plus,
|
||||
Brain,
|
||||
UserCircle,
|
||||
Zap,
|
||||
Scale,
|
||||
Cpu,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { cn, modelSupportsThinking } from "@/lib/utils";
|
||||
import {
|
||||
useAppStore,
|
||||
AgentModel,
|
||||
ThinkingLevel,
|
||||
FeatureImage,
|
||||
AIProfile,
|
||||
} from "@/store/app-store";
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
interface AddFeatureDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAdd: (feature: {
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
images: FeatureImage[];
|
||||
imagePaths: DescriptionImagePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
}) => void;
|
||||
categorySuggestions: string[];
|
||||
defaultSkipTests: boolean;
|
||||
isMaximized: boolean;
|
||||
showProfilesOnly: boolean;
|
||||
aiProfiles: AIProfile[];
|
||||
}
|
||||
|
||||
export function AddFeatureDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onAdd,
|
||||
categorySuggestions,
|
||||
defaultSkipTests,
|
||||
isMaximized,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
}: AddFeatureDialogProps) {
|
||||
const [newFeature, setNewFeature] = useState({
|
||||
category: "",
|
||||
description: "",
|
||||
steps: [""],
|
||||
images: [] as FeatureImage[],
|
||||
imagePaths: [] as DescriptionImagePath[],
|
||||
skipTests: false,
|
||||
model: "opus" as AgentModel,
|
||||
thinkingLevel: "none" as ThinkingLevel,
|
||||
});
|
||||
const [newFeaturePreviewMap, setNewFeaturePreviewMap] =
|
||||
useState<ImagePreviewMap>(() => new Map());
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [descriptionError, setDescriptionError] = useState(false);
|
||||
|
||||
// Sync skipTests default when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setNewFeature((prev) => ({
|
||||
...prev,
|
||||
skipTests: defaultSkipTests,
|
||||
}));
|
||||
}
|
||||
}, [open, defaultSkipTests]);
|
||||
|
||||
const handleAdd = () => {
|
||||
// Validate description is required
|
||||
if (!newFeature.description.trim()) {
|
||||
setDescriptionError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const category = newFeature.category || "Uncategorized";
|
||||
const selectedModel = newFeature.model;
|
||||
const normalizedThinking = modelSupportsThinking(selectedModel)
|
||||
? newFeature.thinkingLevel
|
||||
: "none";
|
||||
|
||||
onAdd({
|
||||
category,
|
||||
description: newFeature.description,
|
||||
steps: newFeature.steps.filter((s) => s.trim()),
|
||||
images: newFeature.images,
|
||||
imagePaths: newFeature.imagePaths,
|
||||
skipTests: newFeature.skipTests,
|
||||
model: selectedModel,
|
||||
thinkingLevel: normalizedThinking,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setNewFeature({
|
||||
category: "",
|
||||
description: "",
|
||||
steps: [""],
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: "opus",
|
||||
thinkingLevel: "none",
|
||||
});
|
||||
setNewFeaturePreviewMap(new Map());
|
||||
setShowAdvancedOptions(false);
|
||||
setDescriptionError(false);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleDialogClose = (open: boolean) => {
|
||||
onOpenChange(open);
|
||||
// Clear preview map, validation error, and reset advanced options when dialog closes
|
||||
if (!open) {
|
||||
setNewFeaturePreviewMap(new Map());
|
||||
setShowAdvancedOptions(false);
|
||||
setDescriptionError(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderModelOptions = (
|
||||
options: ModelOption[],
|
||||
selectedModel: AgentModel,
|
||||
onSelect: (model: AgentModel) => void,
|
||||
testIdPrefix = "model-select"
|
||||
) => (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedModel === option.id;
|
||||
// Shorter display names for compact view
|
||||
const shortName = option.label.replace("Claude ", "");
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(option.id)}
|
||||
title={option.description}
|
||||
className={cn(
|
||||
"flex-1 min-w-[80px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-background hover:bg-accent border-input"
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-${option.id}`}
|
||||
>
|
||||
{shortName}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const newModelAllowsThinking = modelSupportsThinking(newFeature.model);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogClose}>
|
||||
<DialogContent
|
||||
compact={!isMaximized}
|
||||
data-testid="add-feature-dialog"
|
||||
onPointerDownOutside={(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();
|
||||
}
|
||||
}}
|
||||
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();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Feature</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new feature card for the Kanban board.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs
|
||||
defaultValue="prompt"
|
||||
className="py-4 flex-1 min-h-0 flex flex-col"
|
||||
>
|
||||
<TabsList className="w-full grid grid-cols-3 mb-4">
|
||||
<TabsTrigger value="prompt" data-testid="tab-prompt">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Prompt
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="model" data-testid="tab-model">
|
||||
<Settings2 className="w-4 h-4 mr-2" />
|
||||
Model
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="testing" data-testid="tab-testing">
|
||||
<FlaskConical className="w-4 h-4 mr-2" />
|
||||
Testing
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Prompt Tab */}
|
||||
<TabsContent value="prompt" className="space-y-4 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<DescriptionImageDropZone
|
||||
value={newFeature.description}
|
||||
onChange={(value) => {
|
||||
setNewFeature({ ...newFeature, description: value });
|
||||
if (value.trim()) {
|
||||
setDescriptionError(false);
|
||||
}
|
||||
}}
|
||||
images={newFeature.imagePaths}
|
||||
onImagesChange={(images) =>
|
||||
setNewFeature({ ...newFeature, imagePaths: images })
|
||||
}
|
||||
placeholder="Describe the feature..."
|
||||
previewMap={newFeaturePreviewMap}
|
||||
onPreviewMapChange={setNewFeaturePreviewMap}
|
||||
autoFocus
|
||||
error={descriptionError}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category (optional)</Label>
|
||||
<CategoryAutocomplete
|
||||
value={newFeature.category}
|
||||
onChange={(value) =>
|
||||
setNewFeature({ ...newFeature, category: value })
|
||||
}
|
||||
suggestions={categorySuggestions}
|
||||
placeholder="e.g., Core, UI, API"
|
||||
data-testid="feature-category-input"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Model Tab */}
|
||||
<TabsContent value="model" className="space-y-4 overflow-y-auto">
|
||||
{/* Show Advanced Options Toggle - only when profiles-only mode is enabled */}
|
||||
{showProfilesOnly && (
|
||||
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Simple Mode Active
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only showing AI profiles. Advanced model tweaking is hidden.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
data-testid="show-advanced-options-toggle"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 mr-2" />
|
||||
{showAdvancedOptions ? "Hide" : "Show"} Advanced
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Select Profile Section */}
|
||||
{aiProfiles.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-2">
|
||||
<UserCircle className="w-4 h-4 text-brand-500" />
|
||||
Quick Select Profile
|
||||
</Label>
|
||||
<span className="text-[11px] px-2 py-0.5 rounded-full border border-brand-500/40 text-brand-500">
|
||||
Presets
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{aiProfiles.slice(0, 6).map((profile) => {
|
||||
const IconComponent = profile.icon
|
||||
? PROFILE_ICONS[profile.icon]
|
||||
: Brain;
|
||||
const isSelected =
|
||||
newFeature.model === profile.model &&
|
||||
newFeature.thinkingLevel === profile.thinkingLevel;
|
||||
return (
|
||||
<button
|
||||
key={profile.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: profile.model,
|
||||
thinkingLevel: profile.thinkingLevel,
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2 rounded-lg border text-left transition-all",
|
||||
isSelected
|
||||
? "bg-brand-500/10 border-brand-500 text-foreground"
|
||||
: "bg-background hover:bg-accent border-input"
|
||||
)}
|
||||
data-testid={`profile-quick-select-${profile.id}`}
|
||||
>
|
||||
<div className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0 bg-primary/10">
|
||||
{IconComponent && (
|
||||
<IconComponent className="w-4 h-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{profile.name}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground truncate">
|
||||
{profile.model}
|
||||
{profile.thinkingLevel !== "none" &&
|
||||
` + ${profile.thinkingLevel}`}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Or customize below. Manage profiles in{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
useAppStore.getState().setCurrentView("profiles");
|
||||
}}
|
||||
className="text-brand-500 hover:underline"
|
||||
>
|
||||
AI Profiles
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{aiProfiles.length > 0 &&
|
||||
(!showProfilesOnly || showAdvancedOptions) && (
|
||||
<div className="border-t border-border" />
|
||||
)}
|
||||
|
||||
{/* Claude Models Section - Hidden when showProfilesOnly is true and showAdvancedOptions is false */}
|
||||
{(!showProfilesOnly || showAdvancedOptions) && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-primary" />
|
||||
Claude (SDK)
|
||||
</Label>
|
||||
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary">
|
||||
Native
|
||||
</span>
|
||||
</div>
|
||||
{renderModelOptions(
|
||||
CLAUDE_MODELS,
|
||||
newFeature.model,
|
||||
(model) =>
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model,
|
||||
thinkingLevel: modelSupportsThinking(model)
|
||||
? newFeature.thinkingLevel
|
||||
: "none",
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Thinking Level - Only shown when Claude model is selected */}
|
||||
{newModelAllowsThinking && (
|
||||
<div className="space-y-2 pt-2 border-t border-border">
|
||||
<Label className="flex items-center gap-2 text-sm">
|
||||
<Brain className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
Thinking Level
|
||||
</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{(
|
||||
[
|
||||
"none",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"ultrathink",
|
||||
] as ThinkingLevel[]
|
||||
).map((level) => (
|
||||
<button
|
||||
key={level}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
thinkingLevel: level,
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
|
||||
newFeature.thinkingLevel === level
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-background hover:bg-accent border-input"
|
||||
)}
|
||||
data-testid={`thinking-level-${level}`}
|
||||
>
|
||||
{level === "none" && "None"}
|
||||
{level === "low" && "Low"}
|
||||
{level === "medium" && "Med"}
|
||||
{level === "high" && "High"}
|
||||
{level === "ultrathink" && "Ultra"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Higher levels give more time to reason through complex
|
||||
problems.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Testing Tab */}
|
||||
<TabsContent value="testing" className="space-y-4 overflow-y-auto">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="skip-tests"
|
||||
checked={!newFeature.skipTests}
|
||||
onCheckedChange={(checked) =>
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
skipTests: checked !== true,
|
||||
})
|
||||
}
|
||||
data-testid="skip-tests-checkbox"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
htmlFor="skip-tests"
|
||||
className="text-sm cursor-pointer"
|
||||
>
|
||||
Enable automated testing
|
||||
</Label>
|
||||
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, this feature will use automated TDD. When disabled,
|
||||
it will require manual verification.
|
||||
</p>
|
||||
|
||||
{/* Verification Steps - Only shown when skipTests is enabled */}
|
||||
{newFeature.skipTests && (
|
||||
<div className="space-y-2 pt-2 border-t border-border">
|
||||
<Label>Verification Steps</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Add manual steps to verify this feature works correctly.
|
||||
</p>
|
||||
{newFeature.steps.map((step, index) => (
|
||||
<Input
|
||||
key={index}
|
||||
placeholder={`Verification step ${index + 1}`}
|
||||
value={step}
|
||||
onChange={(e) => {
|
||||
const steps = [...newFeature.steps];
|
||||
steps[index] = e.target.value;
|
||||
setNewFeature({ ...newFeature, steps });
|
||||
}}
|
||||
data-testid={`feature-step-${index}-input`}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
steps: [...newFeature.steps, ""],
|
||||
})
|
||||
}
|
||||
data-testid="add-step-button"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Verification Step
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={handleAdd}
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={open}
|
||||
data-testid="confirm-add-feature"
|
||||
>
|
||||
Add Feature
|
||||
</HotkeyButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user