mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
Merge pull request #243 from JBotwina/JBotwina/task-deps-spawn
feat: Add task dependencies and spawn sub-task functionality
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Cpu,
|
||||
GitFork,
|
||||
} from 'lucide-react';
|
||||
import { CountUpTimer } from '@/components/ui/count-up-timer';
|
||||
import { formatModelName, DEFAULT_MODEL } from '@/lib/agent-context-parser';
|
||||
@@ -31,6 +32,7 @@ interface CardHeaderProps {
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onViewOutput?: () => void;
|
||||
onSpawnTask?: () => void;
|
||||
}
|
||||
|
||||
export function CardHeaderSection({
|
||||
@@ -40,6 +42,7 @@ export function CardHeaderSection({
|
||||
onEdit,
|
||||
onDelete,
|
||||
onViewOutput,
|
||||
onSpawnTask,
|
||||
}: CardHeaderProps) {
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
@@ -92,6 +95,17 @@ export function CardHeaderSection({
|
||||
<Edit className="w-3 h-3 mr-2" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSpawnTask?.();
|
||||
}}
|
||||
data-testid={`spawn-running-${feature.id}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<GitFork className="w-3 h-3 mr-2" />
|
||||
Spawn Sub-Task
|
||||
</DropdownMenuItem>
|
||||
{/* Model info in dropdown */}
|
||||
<div className="px-2 py-1.5 text-[10px] text-muted-foreground border-t mt-1 pt-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -106,7 +120,21 @@ export function CardHeaderSection({
|
||||
|
||||
{/* Backlog header */}
|
||||
{!isCurrentAutoTask && feature.status === 'backlog' && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 hover:bg-white/10 text-muted-foreground hover:text-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSpawnTask?.();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`spawn-backlog-${feature.id}`}
|
||||
title="Spawn Sub-Task"
|
||||
>
|
||||
<GitFork className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -141,6 +169,22 @@ export function CardHeaderSection({
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 hover:bg-white/10 text-muted-foreground hover:text-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSpawnTask?.();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`spawn-${
|
||||
feature.status === 'waiting_approval' ? 'waiting' : 'verified'
|
||||
}-${feature.id}`}
|
||||
title="Spawn Sub-Task"
|
||||
>
|
||||
<GitFork className="w-4 h-4" />
|
||||
</Button>
|
||||
{onViewOutput && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -229,6 +273,17 @@ export function CardHeaderSection({
|
||||
View Logs
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSpawnTask?.();
|
||||
}}
|
||||
data-testid={`spawn-feature-${feature.id}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<GitFork className="w-3 h-3 mr-2" />
|
||||
Spawn Sub-Task
|
||||
</DropdownMenuItem>
|
||||
{/* Model info in dropdown */}
|
||||
<div className="px-2 py-1.5 text-[10px] text-muted-foreground border-t mt-1 pt-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -25,6 +25,7 @@ interface KanbanCardProps {
|
||||
onComplete?: () => void;
|
||||
onViewPlan?: () => void;
|
||||
onApprovePlan?: () => void;
|
||||
onSpawnTask?: () => void;
|
||||
hasContext?: boolean;
|
||||
isCurrentAutoTask?: boolean;
|
||||
shortcutKey?: string;
|
||||
@@ -51,6 +52,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onComplete,
|
||||
onViewPlan,
|
||||
onApprovePlan,
|
||||
onSpawnTask,
|
||||
hasContext,
|
||||
isCurrentAutoTask,
|
||||
shortcutKey,
|
||||
@@ -146,6 +148,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onViewOutput={onViewOutput}
|
||||
onSpawnTask={onSpawnTask}
|
||||
/>
|
||||
|
||||
<CardContent className="px-3 pt-0 pb-0">
|
||||
|
||||
@@ -19,14 +19,7 @@ import {
|
||||
FeatureTextFilePath as DescriptionTextFilePath,
|
||||
ImagePreviewMap,
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import {
|
||||
MessageSquare,
|
||||
Settings2,
|
||||
SlidersHorizontal,
|
||||
FlaskConical,
|
||||
Sparkles,
|
||||
ChevronDown,
|
||||
} from 'lucide-react';
|
||||
import { MessageSquare, Settings2, SlidersHorizontal, Sparkles, ChevronDown } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
@@ -37,6 +30,7 @@ import {
|
||||
FeatureImage,
|
||||
AIProfile,
|
||||
PlanningMode,
|
||||
Feature,
|
||||
} from '@/store/app-store';
|
||||
import {
|
||||
ModelSelector,
|
||||
@@ -46,6 +40,7 @@ import {
|
||||
PrioritySelector,
|
||||
BranchSelector,
|
||||
PlanningModeSelector,
|
||||
AncestorContextSection,
|
||||
} from '../shared';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -54,6 +49,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
getAncestors,
|
||||
formatAncestorContextForPrompt,
|
||||
type AncestorContext,
|
||||
} from '@automaker/dependency-resolver';
|
||||
|
||||
interface AddFeatureDialogProps {
|
||||
open: boolean;
|
||||
@@ -72,6 +72,7 @@ interface AddFeatureDialogProps {
|
||||
priority: number;
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
dependencies?: string[];
|
||||
}) => void;
|
||||
categorySuggestions: string[];
|
||||
branchSuggestions: string[];
|
||||
@@ -82,6 +83,9 @@ interface AddFeatureDialogProps {
|
||||
isMaximized: boolean;
|
||||
showProfilesOnly: boolean;
|
||||
aiProfiles: AIProfile[];
|
||||
// Spawn task mode props
|
||||
parentFeature?: Feature | null;
|
||||
allFeatures?: Feature[];
|
||||
}
|
||||
|
||||
export function AddFeatureDialog({
|
||||
@@ -97,7 +101,10 @@ export function AddFeatureDialog({
|
||||
isMaximized,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
parentFeature = null,
|
||||
allFeatures = [],
|
||||
}: AddFeatureDialogProps) {
|
||||
const isSpawnMode = !!parentFeature;
|
||||
const navigate = useNavigate();
|
||||
const [useCurrentBranch, setUseCurrentBranch] = useState(true);
|
||||
const [newFeature, setNewFeature] = useState({
|
||||
@@ -125,6 +132,10 @@ export function AddFeatureDialog({
|
||||
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
|
||||
const [requirePlanApproval, setRequirePlanApproval] = useState(false);
|
||||
|
||||
// Spawn mode state
|
||||
const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
|
||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Get enhancement model, planning mode defaults, and worktrees setting from store
|
||||
const {
|
||||
enhancementModel,
|
||||
@@ -153,6 +164,17 @@ export function AddFeatureDialog({
|
||||
setUseCurrentBranch(true);
|
||||
setPlanningMode(defaultPlanningMode);
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
|
||||
// Initialize ancestors for spawn mode
|
||||
if (parentFeature) {
|
||||
const ancestorList = getAncestors(parentFeature, allFeatures);
|
||||
setAncestors(ancestorList);
|
||||
// Only select parent by default - ancestors are optional context
|
||||
setSelectedAncestorIds(new Set([parentFeature.id]));
|
||||
} else {
|
||||
setAncestors([]);
|
||||
setSelectedAncestorIds(new Set());
|
||||
}
|
||||
}
|
||||
}, [
|
||||
open,
|
||||
@@ -162,6 +184,8 @@ export function AddFeatureDialog({
|
||||
defaultRequirePlanApproval,
|
||||
defaultAIProfileId,
|
||||
aiProfiles,
|
||||
parentFeature,
|
||||
allFeatures,
|
||||
]);
|
||||
|
||||
const handleAdd = () => {
|
||||
@@ -187,10 +211,34 @@ export function AddFeatureDialog({
|
||||
// Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary)
|
||||
const finalBranchName = useCurrentBranch ? currentBranch || '' : newFeature.branchName || '';
|
||||
|
||||
// Build final description - prepend ancestor context in spawn mode
|
||||
let finalDescription = newFeature.description;
|
||||
if (isSpawnMode && parentFeature && selectedAncestorIds.size > 0) {
|
||||
// Create parent context as an AncestorContext
|
||||
const parentContext: AncestorContext = {
|
||||
id: parentFeature.id,
|
||||
title: parentFeature.title,
|
||||
description: parentFeature.description,
|
||||
spec: parentFeature.spec,
|
||||
summary: parentFeature.summary,
|
||||
depth: -1,
|
||||
};
|
||||
|
||||
const allAncestorsWithParent = [parentContext, ...ancestors];
|
||||
const contextText = formatAncestorContextForPrompt(
|
||||
allAncestorsWithParent,
|
||||
selectedAncestorIds
|
||||
);
|
||||
|
||||
if (contextText) {
|
||||
finalDescription = `${contextText}\n\n---\n\n## Task Description\n\n${newFeature.description}`;
|
||||
}
|
||||
}
|
||||
|
||||
onAdd({
|
||||
title: newFeature.title,
|
||||
category,
|
||||
description: newFeature.description,
|
||||
description: finalDescription,
|
||||
images: newFeature.images,
|
||||
imagePaths: newFeature.imagePaths,
|
||||
textFilePaths: newFeature.textFilePaths,
|
||||
@@ -201,6 +249,8 @@ export function AddFeatureDialog({
|
||||
priority: newFeature.priority,
|
||||
planningMode,
|
||||
requirePlanApproval,
|
||||
// In spawn mode, automatically add parent as dependency
|
||||
dependencies: isSpawnMode && parentFeature ? [parentFeature.id] : undefined,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
@@ -299,8 +349,12 @@ export function AddFeatureDialog({
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Feature</DialogTitle>
|
||||
<DialogDescription>Create a new feature card for the Kanban board.</DialogDescription>
|
||||
<DialogTitle>{isSpawnMode ? 'Spawn Sub-Task' : 'Add New Feature'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isSpawnMode
|
||||
? `Create a sub-task that depends on "${parentFeature?.title || parentFeature?.description.slice(0, 50)}..."`
|
||||
: '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">
|
||||
@@ -320,6 +374,22 @@ export function AddFeatureDialog({
|
||||
|
||||
{/* Prompt Tab */}
|
||||
<TabsContent value="prompt" className="space-y-4 overflow-y-auto cursor-default">
|
||||
{/* Ancestor Context Section - only in spawn mode */}
|
||||
{isSpawnMode && parentFeature && (
|
||||
<AncestorContextSection
|
||||
parentFeature={{
|
||||
id: parentFeature.id,
|
||||
title: parentFeature.title,
|
||||
description: parentFeature.description,
|
||||
spec: parentFeature.spec,
|
||||
summary: parentFeature.summary,
|
||||
}}
|
||||
ancestors={ancestors}
|
||||
selectedAncestorIds={selectedAncestorIds}
|
||||
onSelectionChange={setSelectedAncestorIds}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<DescriptionImageDropZone
|
||||
@@ -512,7 +582,7 @@ export function AddFeatureDialog({
|
||||
data-testid="confirm-add-feature"
|
||||
disabled={useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()}
|
||||
>
|
||||
Add Feature
|
||||
{isSpawnMode ? 'Spawn Task' : 'Add Feature'}
|
||||
</HotkeyButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -98,6 +98,7 @@ export function useBoardActions({
|
||||
priority: number;
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
dependencies?: string[];
|
||||
}) => {
|
||||
// Empty string means "unassigned" (show only on primary worktree) - convert to undefined
|
||||
// Non-empty string is the actual branch name (for non-primary worktrees)
|
||||
@@ -150,6 +151,7 @@ export function useBoardActions({
|
||||
titleGenerating: needsTitleGeneration,
|
||||
status: 'backlog' as const,
|
||||
branchName: finalBranchName,
|
||||
dependencies: featureData.dependencies || [],
|
||||
};
|
||||
const createdFeature = addFeature(newFeatureData);
|
||||
// Must await to ensure feature exists on server before user can drag it
|
||||
|
||||
@@ -41,6 +41,7 @@ interface KanbanBoardProps {
|
||||
onImplement: (feature: Feature) => void;
|
||||
onViewPlan: (feature: Feature) => void;
|
||||
onApprovePlan: (feature: Feature) => void;
|
||||
onSpawnTask?: (feature: Feature) => void;
|
||||
featuresWithContext: Set<string>;
|
||||
runningAutoTasks: string[];
|
||||
shortcuts: ReturnType<typeof useKeyboardShortcutsConfig>;
|
||||
@@ -73,6 +74,7 @@ export function KanbanBoard({
|
||||
onImplement,
|
||||
onViewPlan,
|
||||
onApprovePlan,
|
||||
onSpawnTask,
|
||||
featuresWithContext,
|
||||
runningAutoTasks,
|
||||
shortcuts,
|
||||
@@ -184,6 +186,7 @@ export function KanbanBoard({
|
||||
onImplement={() => onImplement(feature)}
|
||||
onViewPlan={() => onViewPlan(feature)}
|
||||
onApprovePlan={() => onApprovePlan(feature)}
|
||||
onSpawnTask={() => onSpawnTask?.(feature)}
|
||||
hasContext={featuresWithContext.has(feature.id)}
|
||||
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
||||
shortcutKey={shortcutKey}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { useState } from 'react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronDown, ChevronRight, Users, CheckCircle2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AncestorContext } from '@automaker/dependency-resolver';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
|
||||
interface ParentFeatureContext {
|
||||
id: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
spec?: string;
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
interface AncestorContextSectionProps {
|
||||
parentFeature: ParentFeatureContext;
|
||||
ancestors: AncestorContext[];
|
||||
selectedAncestorIds: Set<string>;
|
||||
onSelectionChange: (ids: Set<string>) => void;
|
||||
}
|
||||
|
||||
export function AncestorContextSection({
|
||||
parentFeature,
|
||||
ancestors,
|
||||
selectedAncestorIds,
|
||||
onSelectionChange,
|
||||
}: AncestorContextSectionProps) {
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleExpanded = (id: string) => {
|
||||
const newExpanded = new Set(expandedIds);
|
||||
if (newExpanded.has(id)) {
|
||||
newExpanded.delete(id);
|
||||
} else {
|
||||
newExpanded.add(id);
|
||||
}
|
||||
setExpandedIds(newExpanded);
|
||||
};
|
||||
|
||||
const toggleSelected = (id: string) => {
|
||||
const newSelected = new Set(selectedAncestorIds);
|
||||
if (newSelected.has(id)) {
|
||||
newSelected.delete(id);
|
||||
} else {
|
||||
newSelected.add(id);
|
||||
}
|
||||
onSelectionChange(newSelected);
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
const allIds = new Set([parentFeature.id, ...ancestors.map((a) => a.id)]);
|
||||
onSelectionChange(allIds);
|
||||
};
|
||||
|
||||
const selectNone = () => {
|
||||
onSelectionChange(new Set());
|
||||
};
|
||||
|
||||
// Combine parent and ancestors into a single list
|
||||
const allAncestorItems: Array<
|
||||
(AncestorContext | ParentFeatureContext) & { isParent: boolean; depth: number }
|
||||
> = [
|
||||
{ ...parentFeature, depth: -1, isParent: true },
|
||||
...ancestors.map((a) => ({ ...a, isParent: false })),
|
||||
];
|
||||
|
||||
const totalCount = allAncestorItems.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-muted-foreground" />
|
||||
<Label className="text-sm font-medium">Ancestor Context</Label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({selectedAncestorIds.size}/{totalCount} selected)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={selectAll}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={selectNone}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
None
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The parent task context will be included to help the AI understand the background.
|
||||
Additional ancestors can optionally be included for more context.
|
||||
</p>
|
||||
|
||||
<div className="space-y-1 max-h-[200px] overflow-y-auto border rounded-lg p-2 bg-muted/20">
|
||||
{allAncestorItems.map((item) => {
|
||||
const isSelected = selectedAncestorIds.has(item.id);
|
||||
const isExpanded = expandedIds.has(item.id);
|
||||
const hasContent =
|
||||
item.description ||
|
||||
('spec' in item && item.spec) ||
|
||||
('summary' in item && item.summary);
|
||||
const displayTitle =
|
||||
item.title ||
|
||||
item.description.slice(0, 50) + (item.description.length > 50 ? '...' : '');
|
||||
|
||||
return (
|
||||
<Collapsible key={item.id} open={isExpanded}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start gap-2 p-2 rounded-md transition-colors',
|
||||
item.isParent
|
||||
? isSelected
|
||||
? 'bg-[var(--status-success-bg)] border border-[var(--status-success)]/30'
|
||||
: 'bg-muted/30 border border-border hover:bg-muted/50'
|
||||
: isSelected
|
||||
? 'bg-primary/10'
|
||||
: 'hover:bg-muted/50'
|
||||
)}
|
||||
style={{ marginLeft: item.isParent ? 0 : `${item.depth * 12}px` }}
|
||||
>
|
||||
<Checkbox
|
||||
id={`ancestor-${item.id}`}
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => toggleSelected(item.id)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1">
|
||||
{hasContent && (
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={() => toggleExpanded(item.id)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
<label
|
||||
htmlFor={`ancestor-${item.id}`}
|
||||
className="text-sm font-medium cursor-pointer truncate flex-1"
|
||||
>
|
||||
{displayTitle}
|
||||
</label>
|
||||
{item.isParent && (
|
||||
<span className="ml-2 inline-flex items-center gap-1 text-xs text-[var(--status-success)] font-medium">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
Completed Parent
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CollapsibleContent>
|
||||
<div className="mt-2 space-y-2 text-xs text-muted-foreground pl-5">
|
||||
{item.description && (
|
||||
<div>
|
||||
<span className="font-medium text-foreground">Description:</span>
|
||||
<p className="mt-0.5 line-clamp-3">{item.description}</p>
|
||||
</div>
|
||||
)}
|
||||
{'spec' in item && item.spec && (
|
||||
<div>
|
||||
<span className="font-medium text-foreground">Specification:</span>
|
||||
<p className="mt-0.5 line-clamp-3">{item.spec}</p>
|
||||
</div>
|
||||
)}
|
||||
{'summary' in item && item.summary && (
|
||||
<div>
|
||||
<span className="font-medium text-foreground">Summary:</span>
|
||||
<p className="mt-0.5 line-clamp-3">{item.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
|
||||
{ancestors.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground text-center py-2">
|
||||
Parent task has no additional ancestors
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export * from './testing-tab-content';
|
||||
export * from './priority-selector';
|
||||
export * from './branch-selector';
|
||||
export * from './planning-mode-selector';
|
||||
export * from './ancestor-context-section';
|
||||
|
||||
Reference in New Issue
Block a user