diff --git a/.gitignore b/.gitignore index 590e1b67..fa484626 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ #added by trueheads > will remove once supercombo adds multi-os support launch.sh +# Claude Code settings +.claude/settings.local.json + # Dependencies node_modules/ diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index 3b7c361c..2836c917 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -445,6 +445,7 @@ export function BoardView() { isMaximized={isMaximized} showProfilesOnly={showProfilesOnly} aiProfiles={aiProfiles} + allFeatures={hookFeatures} /> {/* Agent Output Modal */} diff --git a/apps/app/src/components/views/board-view/components/kanban-card.tsx b/apps/app/src/components/views/board-view/components/kanban-card.tsx index 11e67ac6..d32bfefe 100644 --- a/apps/app/src/components/views/board-view/components/kanban-card.tsx +++ b/apps/app/src/components/views/board-view/components/kanban-card.tsx @@ -330,6 +330,49 @@ export const KanbanCard = memo(function KanbanCard({ /> )} + {/* Priority badge */} + {feature.priority && ( + + + +
+ P{feature.priority} +
+
+ +

+ {feature.priority === 1 + ? "High Priority" + : feature.priority === 2 + ? "Medium Priority" + : "Low Priority"} +

+
+
+
+ )} + + {/* Category text next to priority badge */} + {feature.priority && ( +
+ + {feature.category} + +
+ )} + {/* Skip Tests (Manual) indicator badge */} {feature.skipTests && !feature.error && ( @@ -338,7 +381,7 @@ export const KanbanCard = memo(function KanbanCard({
@@ -422,7 +471,10 @@ export const KanbanCard = memo(function KanbanCard({ )} - - {feature.category} - + {!feature.priority && ( + + {feature.category} + + )}
diff --git a/apps/app/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/add-feature-dialog.tsx index 7a91459b..f78e76c1 100644 --- a/apps/app/src/components/views/board-view/dialogs/add-feature-dialog.tsx +++ b/apps/app/src/components/views/board-view/dialogs/add-feature-dialog.tsx @@ -47,6 +47,7 @@ interface AddFeatureDialogProps { skipTests: boolean; model: AgentModel; thinkingLevel: ThinkingLevel; + priority: number; }) => void; categorySuggestions: string[]; defaultSkipTests: boolean; @@ -74,6 +75,7 @@ export function AddFeatureDialog({ skipTests: false, model: "opus" as AgentModel, thinkingLevel: "none" as ThinkingLevel, + priority: 2 as number, // Default to medium priority }); const [newFeaturePreviewMap, setNewFeaturePreviewMap] = useState(() => new Map()); @@ -111,6 +113,7 @@ export function AddFeatureDialog({ skipTests: newFeature.skipTests, model: selectedModel, thinkingLevel: normalizedThinking, + priority: newFeature.priority, }); // Reset form @@ -122,6 +125,7 @@ export function AddFeatureDialog({ imagePaths: [], skipTests: defaultSkipTests, model: "opus", + priority: 2, thinkingLevel: "none", }); setNewFeaturePreviewMap(new Map()); @@ -237,6 +241,55 @@ export function AddFeatureDialog({ data-testid="feature-category-input" /> + + {/* Priority Selector */} +
+ +
+ + + +
+
{/* Model Tab */} diff --git a/apps/app/src/components/views/board-view/dialogs/dependency-tree-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/dependency-tree-dialog.tsx new file mode 100644 index 00000000..7cd6e49f --- /dev/null +++ b/apps/app/src/components/views/board-view/dialogs/dependency-tree-dialog.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Feature } from "@/store/app-store"; +import { AlertCircle, CheckCircle2, Circle } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface DependencyTreeDialogProps { + open: boolean; + onClose: () => void; + feature: Feature | null; + allFeatures: Feature[]; +} + +export function DependencyTreeDialog({ + open, + onClose, + feature, + allFeatures, +}: DependencyTreeDialogProps) { + const [dependencyTree, setDependencyTree] = useState<{ + dependencies: Feature[]; + dependents: Feature[]; + }>({ dependencies: [], dependents: [] }); + + useEffect(() => { + if (!feature) return; + + // Find features this depends on + const dependencies = (feature.dependencies || []) + .map((depId) => allFeatures.find((f) => f.id === depId)) + .filter((f): f is Feature => f !== undefined); + + // Find features that depend on this one + const dependents = allFeatures.filter((f) => + f.dependencies?.includes(feature.id) + ); + + setDependencyTree({ dependencies, dependents }); + }, [feature, allFeatures]); + + if (!feature) return null; + + const getStatusIcon = (status: Feature["status"]) => { + switch (status) { + case "completed": + case "verified": + return ; + case "in_progress": + case "waiting_approval": + return ; + default: + return ; + } + }; + + const getPriorityBadge = (priority?: number) => { + if (!priority) return null; + return ( + + P{priority} + + ); + }; + + return ( + + + + Dependency Tree + + +
+ {/* Current Feature */} +
+
+ {getStatusIcon(feature.status)} +

Current Feature

+ {getPriorityBadge(feature.priority)} +
+

{feature.description}

+

+ Category: {feature.category} +

+
+ + {/* Dependencies (what this feature needs) */} +
+
+

+ Dependencies ({dependencyTree.dependencies.length}) +

+ + This feature requires: + +
+ + {dependencyTree.dependencies.length === 0 ? ( +
+ No dependencies - this feature can be started independently +
+ ) : ( +
+ {dependencyTree.dependencies.map((dep) => ( +
+
+ {getStatusIcon(dep.status)} + + {dep.description.slice(0, 100)} + {dep.description.length > 100 && "..."} + + {getPriorityBadge(dep.priority)} +
+
+ + {dep.category} + + + {dep.status.replace(/_/g, " ")} + +
+
+ ))} +
+ )} +
+ + {/* Dependents (what depends on this feature) */} +
+
+

+ Dependents ({dependencyTree.dependents.length}) +

+ + Features blocked by this: + +
+ + {dependencyTree.dependents.length === 0 ? ( +
+ No dependents - no other features are waiting on this one +
+ ) : ( +
+ {dependencyTree.dependents.map((dependent) => ( +
+
+ {getStatusIcon(dependent.status)} + + {dependent.description.slice(0, 100)} + {dependent.description.length > 100 && "..."} + + {getPriorityBadge(dependent.priority)} +
+
+ + {dependent.category} + + + {dependent.status.replace(/_/g, " ")} + +
+
+ ))} +
+ )} +
+ + {/* Warning for incomplete dependencies */} + {dependencyTree.dependencies.some( + (d) => d.status !== "completed" && d.status !== "verified" + ) && ( +
+ +
+

+ Incomplete Dependencies +

+

+ This feature has dependencies that aren't completed yet. + Consider completing them first for a smoother implementation. +

+
+
+ )} +
+
+
+ ); +} diff --git a/apps/app/src/components/views/board-view/dialogs/edit-feature-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/edit-feature-dialog.tsx index fdc13d60..68aea366 100644 --- a/apps/app/src/components/views/board-view/dialogs/edit-feature-dialog.tsx +++ b/apps/app/src/components/views/board-view/dialogs/edit-feature-dialog.tsx @@ -19,7 +19,7 @@ import { FeatureImagePath as DescriptionImagePath, ImagePreviewMap, } from "@/components/ui/description-image-dropzone"; -import { MessageSquare, Settings2, FlaskConical } from "lucide-react"; +import { MessageSquare, Settings2, FlaskConical, GitBranch } from "lucide-react"; import { modelSupportsThinking } from "@/lib/utils"; import { Feature, @@ -33,6 +33,7 @@ import { ProfileQuickSelect, TestingTabContent, } from "../shared"; +import { DependencyTreeDialog } from "./dependency-tree-dialog"; interface EditFeatureDialogProps { feature: Feature | null; @@ -47,12 +48,14 @@ interface EditFeatureDialogProps { model: AgentModel; thinkingLevel: ThinkingLevel; imagePaths: DescriptionImagePath[]; + priority: number; } ) => void; categorySuggestions: string[]; isMaximized: boolean; showProfilesOnly: boolean; aiProfiles: AIProfile[]; + allFeatures: Feature[]; } export function EditFeatureDialog({ @@ -63,11 +66,13 @@ export function EditFeatureDialog({ isMaximized, showProfilesOnly, aiProfiles, + allFeatures, }: EditFeatureDialogProps) { const [editingFeature, setEditingFeature] = useState(feature); const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState(() => new Map()); const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false); + const [showDependencyTree, setShowDependencyTree] = useState(false); useEffect(() => { setEditingFeature(feature); @@ -93,6 +98,7 @@ export function EditFeatureDialog({ model: selectedModel, thinkingLevel: normalizedThinking, imagePaths: editingFeature.imagePaths ?? [], + priority: editingFeature.priority ?? 2, }; onUpdate(editingFeature.id, updates); @@ -214,6 +220,64 @@ export function EditFeatureDialog({ data-testid="edit-feature-category" /> + + {/* Priority Selector */} +
+ +
+ + + +
+
{/* Model Tab */} @@ -297,20 +361,37 @@ export function EditFeatureDialog({ /> - - - + +
+ + + Save Changes + +
+ + setShowDependencyTree(false)} + feature={editingFeature} + allFeatures={allFeatures} + /> ); } diff --git a/apps/app/src/components/views/board-view/dialogs/feature-suggestions-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/feature-suggestions-dialog.tsx index e68ae57f..bc19e17f 100644 --- a/apps/app/src/components/views/board-view/dialogs/feature-suggestions-dialog.tsx +++ b/apps/app/src/components/views/board-view/dialogs/feature-suggestions-dialog.tsx @@ -239,6 +239,7 @@ export function FeatureSuggestionsDialog({ steps: s.steps, status: "backlog" as const, skipTests: true, // As specified, testing mode true + priority: s.priority, // Preserve priority from suggestion })); // Create each new feature using the features API diff --git a/apps/app/src/components/views/board-view/hooks/use-board-actions.ts b/apps/app/src/components/views/board-view/hooks/use-board-actions.ts index af121fa6..ccae297a 100644 --- a/apps/app/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/app/src/components/views/board-view/hooks/use-board-actions.ts @@ -66,6 +66,7 @@ export function useBoardActions({ skipTests: boolean; model: AgentModel; thinkingLevel: ThinkingLevel; + priority: number; }) => { const newFeatureData = { ...featureData, @@ -89,6 +90,7 @@ export function useBoardActions({ model: AgentModel; thinkingLevel: ThinkingLevel; imagePaths: DescriptionImagePath[]; + priority: number; } ) => { updateFeature(featureId, updates); diff --git a/apps/app/src/store/app-store.ts b/apps/app/src/store/app-store.ts index 6992dc2d..bb379de9 100644 --- a/apps/app/src/store/app-store.ts +++ b/apps/app/src/store/app-store.ts @@ -292,6 +292,7 @@ export interface Feature { thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none) error?: string; // Error message if the agent errored during processing priority?: number; // Priority: 1 = high, 2 = medium, 3 = low + dependencies?: string[]; // Array of feature IDs this feature depends on // Worktree info - set when a feature is being worked on in an isolated git worktree worktreePath?: string; // Path to the worktree directory branchName?: string; // Name of the feature branch diff --git a/apps/server/src/routes/app-spec/generate-features-from-spec.ts b/apps/server/src/routes/app-spec/generate-features-from-spec.ts index 84964a9c..adfa2dc5 100644 --- a/apps/server/src/routes/app-spec/generate-features-from-spec.ts +++ b/apps/server/src/routes/app-spec/generate-features-from-spec.ts @@ -56,17 +56,19 @@ ${spec} Generate a prioritized list of implementable features. For each feature provide: 1. **id**: A unique lowercase-hyphenated identifier -2. **title**: Short descriptive title -3. **description**: What this feature does (2-3 sentences) -4. **priority**: 1 (high), 2 (medium), or 3 (low) -5. **complexity**: "simple", "moderate", or "complex" -6. **dependencies**: Array of feature IDs this depends on (can be empty) +2. **category**: Functional category (e.g., "Core", "UI", "API", "Authentication", "Database") +3. **title**: Short descriptive title +4. **description**: What this feature does (2-3 sentences) +5. **priority**: 1 (high), 2 (medium), or 3 (low) +6. **complexity**: "simple", "moderate", or "complex" +7. **dependencies**: Array of feature IDs this depends on (can be empty) Format as JSON: { "features": [ { "id": "feature-id", + "category": "Feature Category", "title": "Feature Title", "description": "What it does", "priority": 1, diff --git a/apps/server/src/routes/app-spec/parse-and-create-features.ts b/apps/server/src/routes/app-spec/parse-and-create-features.ts index 8a119972..e0f6db6b 100644 --- a/apps/server/src/routes/app-spec/parse-and-create-features.ts +++ b/apps/server/src/routes/app-spec/parse-and-create-features.ts @@ -53,6 +53,7 @@ export async function parseAndCreateFeatures( const featureData = { id: feature.id, + category: feature.category || "Uncategorized", title: feature.title, description: feature.description, status: "backlog", // Features go to backlog - user must manually start them