Added UI features back for priority, added/fixed category generation. Added dependency trees for stories, see PR for rest

This commit is contained in:
trueheads
2025-12-16 00:42:55 -06:00
parent 25044d40b9
commit ff4887773e
11 changed files with 459 additions and 27 deletions

3
.gitignore vendored
View File

@@ -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/

View File

@@ -445,6 +445,7 @@ export function BoardView() {
isMaximized={isMaximized}
showProfilesOnly={showProfilesOnly}
aiProfiles={aiProfiles}
allFeatures={hookFeatures}
/>
{/* Agent Output Modal */}

View File

@@ -330,6 +330,49 @@ export const KanbanCard = memo(function KanbanCard({
/>
)}
{/* Priority badge */}
{feature.priority && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"absolute px-2 py-1 text-sm font-bold rounded-md flex items-center justify-center z-10",
"top-2 left-2 min-w-[36px]",
feature.priority === 1 &&
"bg-red-500/20 text-red-500 border-2 border-red-500/50",
feature.priority === 2 &&
"bg-yellow-500/20 text-yellow-500 border-2 border-yellow-500/50",
feature.priority === 3 &&
"bg-blue-500/20 text-blue-500 border-2 border-blue-500/50"
)}
data-testid={`priority-badge-${feature.id}`}
>
P{feature.priority}
</div>
</TooltipTrigger>
<TooltipContent side="right" className="text-xs">
<p>
{feature.priority === 1
? "High Priority"
: feature.priority === 2
? "Medium Priority"
: "Low Priority"}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Category text next to priority badge */}
{feature.priority && (
<div className="absolute top-2 left-[54px] right-12 z-10 flex items-center h-[32px]">
<span className="text-[11px] text-muted-foreground/70 font-medium truncate">
{feature.category}
</span>
</div>
)}
{/* Skip Tests (Manual) indicator badge */}
{feature.skipTests && !feature.error && (
<TooltipProvider delayDuration={200}>
@@ -338,7 +381,7 @@ export const KanbanCard = memo(function KanbanCard({
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
"top-2 left-2",
feature.priority ? "top-11 left-2" : "top-2 left-2",
"bg-[var(--status-warning-bg)] border border-[var(--status-warning)]/40 text-[var(--status-warning)]"
)}
data-testid={`skip-tests-badge-${feature.id}`}
@@ -361,7 +404,7 @@ export const KanbanCard = memo(function KanbanCard({
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
"top-2 left-2",
feature.priority ? "top-11 left-2" : "top-2 left-2",
"bg-[var(--status-error-bg)] border border-[var(--status-error)]/40 text-[var(--status-error)]"
)}
data-testid={`error-badge-${feature.id}`}
@@ -381,7 +424,11 @@ export const KanbanCard = memo(function KanbanCard({
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
feature.skipTests ? "top-8 left-2" : "top-2 left-2",
feature.priority
? "top-11 left-2"
: feature.skipTests
? "top-8 left-2"
: "top-2 left-2",
"bg-[var(--status-success-bg)] border border-[var(--status-success)]/40 text-[var(--status-success)]",
"animate-pulse"
)}
@@ -401,9 +448,11 @@ export const KanbanCard = memo(function KanbanCard({
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10 cursor-default",
"bg-[var(--status-info-bg)] border border-[var(--status-info)]/40 text-[var(--status-info)]",
feature.error || feature.skipTests || isJustFinished
? "top-8 left-2"
: "top-2 left-2"
feature.priority
? "top-11 left-2"
: feature.error || feature.skipTests || isJustFinished
? "top-8 left-2"
: "top-2 left-2"
)}
data-testid={`branch-badge-${feature.id}`}
>
@@ -422,7 +471,10 @@ export const KanbanCard = memo(function KanbanCard({
<CardHeader
className={cn(
"p-3 pb-2 block",
(feature.skipTests || feature.error || isJustFinished) && "pt-10",
feature.priority && "pt-12",
!feature.priority &&
(feature.skipTests || feature.error || isJustFinished) &&
"pt-10",
hasWorktree &&
(feature.skipTests || feature.error || isJustFinished) &&
"pt-14"
@@ -613,9 +665,11 @@ export const KanbanCard = memo(function KanbanCard({
)}
</button>
)}
<CardDescription className="text-[11px] mt-1.5 truncate text-muted-foreground/70">
{feature.category}
</CardDescription>
{!feature.priority && (
<CardDescription className="text-[11px] mt-1.5 truncate text-muted-foreground/70">
{feature.category}
</CardDescription>
)}
</div>
</div>
</CardHeader>

View File

@@ -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<ImagePreviewMap>(() => 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"
/>
</div>
{/* Priority Selector */}
<div className="space-y-2">
<Label>Priority</Label>
<div className="flex gap-2">
<button
type="button"
onClick={() =>
setNewFeature({ ...newFeature, priority: 1 })
}
className={`flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
newFeature.priority === 1
? "bg-red-500/20 text-red-500 border-2 border-red-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
}`}
data-testid="priority-high-button"
>
High
</button>
<button
type="button"
onClick={() =>
setNewFeature({ ...newFeature, priority: 2 })
}
className={`flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
newFeature.priority === 2
? "bg-yellow-500/20 text-yellow-500 border-2 border-yellow-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
}`}
data-testid="priority-medium-button"
>
Medium
</button>
<button
type="button"
onClick={() =>
setNewFeature({ ...newFeature, priority: 3 })
}
className={`flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
newFeature.priority === 3
? "bg-blue-500/20 text-blue-500 border-2 border-blue-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
}`}
data-testid="priority-low-button"
>
Low
</button>
</div>
</div>
</TabsContent>
{/* Model Tab */}

View File

@@ -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 <CheckCircle2 className="w-4 h-4 text-green-500" />;
case "in_progress":
case "waiting_approval":
return <Circle className="w-4 h-4 text-blue-500 fill-blue-500/20" />;
default:
return <Circle className="w-4 h-4 text-muted-foreground/50" />;
}
};
const getPriorityBadge = (priority?: number) => {
if (!priority) return null;
return (
<span
className={cn(
"text-xs px-1.5 py-0.5 rounded font-medium",
priority === 1 && "bg-red-500/20 text-red-500",
priority === 2 && "bg-yellow-500/20 text-yellow-500",
priority === 3 && "bg-blue-500/20 text-blue-500"
)}
>
P{priority}
</span>
);
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Dependency Tree</DialogTitle>
</DialogHeader>
<div className="space-y-6 mt-4">
{/* Current Feature */}
<div className="border-2 border-primary rounded-lg p-4 bg-primary/5">
<div className="flex items-center gap-3 mb-2">
{getStatusIcon(feature.status)}
<h3 className="font-semibold text-sm">Current Feature</h3>
{getPriorityBadge(feature.priority)}
</div>
<p className="text-sm text-muted-foreground">{feature.description}</p>
<p className="text-xs text-muted-foreground/70 mt-2">
Category: {feature.category}
</p>
</div>
{/* Dependencies (what this feature needs) */}
<div>
<div className="flex items-center gap-2 mb-3">
<h3 className="font-semibold text-sm">
Dependencies ({dependencyTree.dependencies.length})
</h3>
<span className="text-xs text-muted-foreground">
This feature requires:
</span>
</div>
{dependencyTree.dependencies.length === 0 ? (
<div className="text-sm text-muted-foreground/70 italic border border-dashed rounded-lg p-4 text-center">
No dependencies - this feature can be started independently
</div>
) : (
<div className="space-y-2">
{dependencyTree.dependencies.map((dep) => (
<div
key={dep.id}
className={cn(
"border rounded-lg p-3 transition-colors",
dep.status === "completed" || dep.status === "verified"
? "bg-green-500/5 border-green-500/20"
: "bg-muted/30 border-border"
)}
>
<div className="flex items-center gap-3 mb-1">
{getStatusIcon(dep.status)}
<span className="text-sm font-medium flex-1">
{dep.description.slice(0, 100)}
{dep.description.length > 100 && "..."}
</span>
{getPriorityBadge(dep.priority)}
</div>
<div className="flex items-center gap-3 ml-7">
<span className="text-xs text-muted-foreground">
{dep.category}
</span>
<span
className={cn(
"text-xs px-2 py-0.5 rounded-full",
dep.status === "completed" || dep.status === "verified"
? "bg-green-500/20 text-green-600"
: dep.status === "in_progress"
? "bg-blue-500/20 text-blue-600"
: "bg-muted text-muted-foreground"
)}
>
{dep.status.replace(/_/g, " ")}
</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Dependents (what depends on this feature) */}
<div>
<div className="flex items-center gap-2 mb-3">
<h3 className="font-semibold text-sm">
Dependents ({dependencyTree.dependents.length})
</h3>
<span className="text-xs text-muted-foreground">
Features blocked by this:
</span>
</div>
{dependencyTree.dependents.length === 0 ? (
<div className="text-sm text-muted-foreground/70 italic border border-dashed rounded-lg p-4 text-center">
No dependents - no other features are waiting on this one
</div>
) : (
<div className="space-y-2">
{dependencyTree.dependents.map((dependent) => (
<div
key={dependent.id}
className="border rounded-lg p-3 bg-muted/30"
>
<div className="flex items-center gap-3 mb-1">
{getStatusIcon(dependent.status)}
<span className="text-sm font-medium flex-1">
{dependent.description.slice(0, 100)}
{dependent.description.length > 100 && "..."}
</span>
{getPriorityBadge(dependent.priority)}
</div>
<div className="flex items-center gap-3 ml-7">
<span className="text-xs text-muted-foreground">
{dependent.category}
</span>
<span
className={cn(
"text-xs px-2 py-0.5 rounded-full",
dependent.status === "completed" ||
dependent.status === "verified"
? "bg-green-500/20 text-green-600"
: dependent.status === "in_progress"
? "bg-blue-500/20 text-blue-600"
: "bg-muted text-muted-foreground"
)}
>
{dependent.status.replace(/_/g, " ")}
</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Warning for incomplete dependencies */}
{dependencyTree.dependencies.some(
(d) => d.status !== "completed" && d.status !== "verified"
) && (
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<AlertCircle className="w-5 h-5 text-yellow-600 shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-yellow-700 dark:text-yellow-500">
Incomplete Dependencies
</p>
<p className="text-yellow-600 dark:text-yellow-400 mt-1">
This feature has dependencies that aren't completed yet.
Consider completing them first for a smoother implementation.
</p>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -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 | null>(feature);
const [editFeaturePreviewMap, setEditFeaturePreviewMap] =
useState<ImagePreviewMap>(() => 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"
/>
</div>
{/* Priority Selector */}
<div className="space-y-2">
<Label>Priority</Label>
<div className="flex gap-2">
<button
type="button"
onClick={() =>
setEditingFeature({
...editingFeature,
priority: 1,
})
}
className={`flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
(editingFeature.priority ?? 2) === 1
? "bg-red-500/20 text-red-500 border-2 border-red-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
}`}
data-testid="edit-priority-high-button"
>
High
</button>
<button
type="button"
onClick={() =>
setEditingFeature({
...editingFeature,
priority: 2,
})
}
className={`flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
(editingFeature.priority ?? 2) === 2
? "bg-yellow-500/20 text-yellow-500 border-2 border-yellow-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
}`}
data-testid="edit-priority-medium-button"
>
Medium
</button>
<button
type="button"
onClick={() =>
setEditingFeature({
...editingFeature,
priority: 3,
})
}
className={`flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
(editingFeature.priority ?? 2) === 3
? "bg-blue-500/20 text-blue-500 border-2 border-blue-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
}`}
data-testid="edit-priority-low-button"
>
Low
</button>
</div>
</div>
</TabsContent>
{/* Model Tab */}
@@ -297,20 +361,37 @@ export function EditFeatureDialog({
/>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<HotkeyButton
onClick={handleUpdate}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={!!editingFeature}
data-testid="confirm-edit-feature"
<DialogFooter className="sm:!justify-between">
<Button
variant="outline"
onClick={() => setShowDependencyTree(true)}
className="gap-2 h-10"
>
Save Changes
</HotkeyButton>
<GitBranch className="w-4 h-4" />
View Dependency Tree
</Button>
<div className="flex gap-2">
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<HotkeyButton
onClick={handleUpdate}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={!!editingFeature}
data-testid="confirm-edit-feature"
>
Save Changes
</HotkeyButton>
</div>
</DialogFooter>
</DialogContent>
<DependencyTreeDialog
open={showDependencyTree}
onClose={() => setShowDependencyTree(false)}
feature={editingFeature}
allFeatures={allFeatures}
/>
</Dialog>
);
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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,

View File

@@ -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