refactor: Replace PipelineTaskCard with TaskGraph in pipeline phases

Pipeline phase cards now use the same TaskGraph component from the plan
tab's phase detail panel. This gives tasks a two-line layout with rail
dot, status badge, category badge, priority, and dependency layer
connectors instead of the flat single-line display.

Removes PipelineTaskCard (no longer imported anywhere).
This commit is contained in:
Lukas May
2026-03-04 13:11:12 +01:00
parent 685b2cb4ec
commit bf635375af
3 changed files with 24 additions and 114 deletions

View File

@@ -1,13 +1,10 @@
import { useMemo, useState } from "react";
import { ChevronDown, Loader2, Play } from "lucide-react";
import { useMemo } from "react";
import { Loader2, Play } from "lucide-react";
import { StatusDot, mapEntityStatus } from "@/components/StatusDot";
import { trpc } from "@/lib/trpc";
import { cn } from "@/lib/utils";
import {
topologicalSortPhases,
type DependencyEdge,
} from "@codewalk-district/shared";
import { PipelineTaskCard } from "./PipelineTaskCard";
import { TaskGraph } from "@/components/execution/TaskGraph";
import { useExecutionContext } from "@/components/execution";
import type { SerializedTask } from "@/components/TaskRow";
export interface DetailAgentInfo {
@@ -27,8 +24,6 @@ interface PipelinePhaseGroupProps {
detailAgent?: DetailAgentInfo | null;
}
const COLLAPSE_THRESHOLD = 5;
const statusBorderColor: Record<string, string> = {
active: "border-l-status-active-dot",
success: "border-l-status-success-dot",
@@ -38,22 +33,15 @@ const statusBorderColor: Record<string, string> = {
urgent: "border-l-status-urgent-dot",
};
const emptyAgentMap = new Map<string, string>();
export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detailAgent }: PipelinePhaseGroupProps) {
const approvePhase = trpc.approvePhase.useMutation();
const queuePhase = trpc.queuePhase.useMutation();
const [expanded, setExpanded] = useState(false);
const { setSelectedTaskId } = useExecutionContext();
// Sort tasks topologically by dependency order
const { sorted, blockedByCountMap } = useMemo(() => {
const edges: DependencyEdge[] = [];
for (const raw of taskDepsRaw) {
for (const depId of raw.dependsOn) {
edges.push({ phaseId: raw.taskId, dependsOnPhaseId: depId });
}
}
const sortedTasks = topologicalSortPhases(tasks, edges);
// Compute blocked-by counts (incomplete dependencies)
// Compute blocked-by counts for TaskGraph
const blockedByCountMap = useMemo(() => {
const countMap = new Map<string, number>();
for (const raw of taskDepsRaw) {
const incomplete = raw.dependsOn.filter((depId) => {
@@ -62,8 +50,7 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
}).length;
if (incomplete > 0) countMap.set(raw.taskId, incomplete);
}
return { sorted: sortedTasks, blockedByCountMap: countMap };
return countMap;
}, [tasks, taskDepsRaw]);
const completedCount = tasks.filter((t) => t.status === "completed").length;
@@ -84,13 +71,6 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
const variant = mapEntityStatus(phase.status);
const borderClass = statusBorderColor[variant] ?? statusBorderColor.neutral;
// Collapsible logic
const needsCollapse = sorted.length > COLLAPSE_THRESHOLD;
const visibleTasks = needsCollapse && !expanded
? sorted.slice(0, COLLAPSE_THRESHOLD)
: sorted;
const hiddenCount = sorted.length - COLLAPSE_THRESHOLD;
return (
<div className={cn(
"rounded-lg border border-border border-l-2 bg-card overflow-hidden",
@@ -144,38 +124,24 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
)}
{/* Tasks */}
<div className="py-1">
<div className="px-1 py-1">
{isDetailing ? (
<p className="flex items-center gap-1.5 px-3 py-1 text-xs text-status-active-fg">
<p className="flex items-center gap-1.5 px-2 py-1 text-xs text-status-active-fg">
<Loader2 className="h-3 w-3 animate-spin" />
Detailing
</p>
) : detailDone && sorted.length === 0 ? (
<p className="px-3 py-1 text-xs text-status-warning-fg">Review changes</p>
) : sorted.length === 0 ? (
<p className="px-3 py-1 text-xs text-muted-foreground">No tasks</p>
) : detailDone && tasks.length === 0 ? (
<p className="px-2 py-1 text-xs text-status-warning-fg">Review changes</p>
) : tasks.length === 0 ? (
<p className="px-2 py-1 text-xs text-muted-foreground">No tasks</p>
) : (
<>
{visibleTasks.map((task) => (
<PipelineTaskCard
key={task.id}
task={task}
blockedByCount={blockedByCountMap.get(task.id) ?? 0}
/>
))}
{needsCollapse && (
<button
onClick={() => setExpanded((v) => !v)}
className="flex w-full items-center gap-1 px-3 py-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
>
<ChevronDown className={cn(
"h-3 w-3 transition-transform",
expanded && "rotate-180",
)} />
{expanded ? "Show less" : `${hiddenCount} more task${hiddenCount === 1 ? "" : "s"}`}
</button>
)}
</>
<TaskGraph
tasks={tasks}
taskDepsRaw={taskDepsRaw}
blockedByCountMap={blockedByCountMap}
agentNameMap={emptyAgentMap}
onClickTask={setSelectedTaskId}
/>
)}
</div>
</div>

View File

@@ -1,55 +0,0 @@
import { CheckCircle2, Loader2, Clock, Ban, Play, AlertTriangle } from "lucide-react";
import { cn } from "@/lib/utils";
import { trpc } from "@/lib/trpc";
import { useExecutionContext } from "@/components/execution";
import type { SerializedTask } from "@/components/TaskRow";
const statusConfig: Record<string, { icon: typeof Clock; color: string; spin?: boolean }> = {
pending: { icon: Clock, color: "text-status-neutral-fg" },
pending_approval: { icon: AlertTriangle, color: "text-status-warning-dot" },
in_progress: { icon: Loader2, color: "text-status-active-dot", spin: true },
completed: { icon: CheckCircle2, color: "text-status-success-dot" },
blocked: { icon: Ban, color: "text-status-error-dot" },
};
interface PipelineTaskCardProps {
task: SerializedTask;
blockedByCount?: number;
}
export function PipelineTaskCard({ task, blockedByCount = 0 }: PipelineTaskCardProps) {
const { setSelectedTaskId } = useExecutionContext();
const queueTask = trpc.queueTask.useMutation();
const config = statusConfig[task.status] ?? statusConfig.pending;
const Icon = config.icon;
return (
<div
className="flex items-center gap-2 rounded px-2 py-1 cursor-pointer hover:bg-accent/50 group"
onClick={() => setSelectedTaskId(task.id)}
>
<Icon
className={cn("h-3.5 w-3.5 shrink-0", config.color, config.spin && "animate-spin")}
/>
<span className="min-w-0 flex-1 truncate text-xs">{task.name}</span>
{blockedByCount > 0 && (
<span className="shrink-0 rounded-full bg-muted px-1.5 py-px text-[10px] text-muted-foreground">
{blockedByCount} dep{blockedByCount === 1 ? "" : "s"}
</span>
)}
{task.status === "pending" && (
<button
className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
queueTask.mutate({ taskId: task.id });
}}
title="Queue task"
>
<Play className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
)}
</div>
);
}