diff --git a/src/components/bead-card.tsx b/src/components/bead-card.tsx index f438b57..72843fa 100644 --- a/src/components/bead-card.tsx +++ b/src/components/bead-card.tsx @@ -11,7 +11,7 @@ import { import { cn } from "@/lib/utils"; import type { Bead, WorktreeStatus, PRStatus } from "@/types"; import type { BranchStatus } from "@/lib/git"; -import { FolderOpen, GitPullRequest, MessageSquare, Check, X, Clock } from "lucide-react"; +import { FolderOpen, GitPullRequest, Link2, MessageSquare, Check, X, Clock } from "lucide-react"; export interface BeadCardProps { bead: Bead; @@ -191,6 +191,7 @@ function getTypeLabel(bead: Bead): string { export function BeadCard({ bead, ticketNumber, branchStatus, worktreeStatus, prStatus, isSelected = false, onSelect }: BeadCardProps) { const blocked = isBlocked(bead); const commentCount = (bead.comments ?? []).length; + const relatedCount = (bead.relates_to ?? []).length; // Prefer worktree status over branch status const hasWorktree = worktreeStatus?.exists ?? false; @@ -319,13 +320,21 @@ export function BeadCard({ bead, ticketNumber, branchStatus, worktreeStatus, prS )} - {/* Footer: comment count */} - {commentCount > 0 && ( + {/* Footer: comment count + related count */} + {(commentCount > 0 || relatedCount > 0) && ( - - - {commentCount} {commentCount === 1 ? "comment" : "comments"} - + {commentCount > 0 && ( + + + {commentCount} {commentCount === 1 ? "comment" : "comments"} + + )} + {relatedCount > 0 && ( + + + {relatedCount} related + + )} )} diff --git a/src/components/bead-detail.tsx b/src/components/bead-detail.tsx index 213735b..4c86aa7 100644 --- a/src/components/bead-detail.tsx +++ b/src/components/bead-detail.tsx @@ -23,6 +23,7 @@ import { X, Clock, GitMerge, + Link2, Trash2, Loader2, Upload, @@ -293,6 +294,15 @@ export function BeadDetail({ .filter((b): b is Bead => b !== undefined); }, [isEpic, bead.children, allBeads]); + // Resolve related tasks from IDs (skip unknown IDs gracefully) + const relatedTasks = useMemo(() => { + if (!allBeads || !bead.relates_to || bead.relates_to.length === 0) return []; + const beadMap = new Map(allBeads.map(b => [b.id, b])); + return bead.relates_to + .map(id => beadMap.get(id)) + .filter((b): b is Bead => b !== undefined); + }, [bead.relates_to, allBeads]); + // PR status for child tasks const [childPRStatuses, setChildPRStatuses] = useState>(new Map()); @@ -908,6 +918,55 @@ export function BeadDetail({ )} + {/* Related Tasks */} + {relatedTasks.length > 0 && onChildClick && ( + + + + Related Tasks ({relatedTasks.length}) + + + + + {relatedTasks.map((related) => ( + onChildClick(related)} + aria-label={`Open related task: ${related.title}`} + className={cn( + "w-full flex items-center gap-2 px-2 py-1.5 rounded-md", + "hover:bg-zinc-800 transition-colors text-left", + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-400", + "group" + )} + > + + + {formatBeadId(related.id)} + + + {related.title} + + + {formatStatus(related.status)} + + + ))} + + + + )} + {/* Subtasks (for epics) */} {isEpic && onChildClick && ( diff --git a/src/components/subtask-list.tsx b/src/components/subtask-list.tsx index 6c29488..be172b7 100644 --- a/src/components/subtask-list.tsx +++ b/src/components/subtask-list.tsx @@ -2,7 +2,7 @@ import { cn } from "@/lib/utils"; import type { Bead, BeadStatus, PRChecks } from "@/types"; -import { Check, Circle, Clock, FileCheck, GitPullRequest, GitMerge } from "lucide-react"; +import { Check, Circle, Clock, FileCheck, GitPullRequest, GitMerge, Link2 } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; /** @@ -208,6 +208,12 @@ export function SubtaskList({ )} + {(child.relates_to ?? []).length > 0 && ( + + + {child.relates_to!.length} + + )}