diff --git a/apps/web/src/components/pipeline/PipelineGraph.tsx b/apps/web/src/components/pipeline/PipelineGraph.tsx index 790cb4b..be9d74a 100644 --- a/apps/web/src/components/pipeline/PipelineGraph.tsx +++ b/apps/web/src/components/pipeline/PipelineGraph.tsx @@ -43,9 +43,9 @@ export function PipelineGraph({ columns, tasksByPhase, taskDepsByPhase, dependen
{/* Connector arrow between columns */} {idx > 0 && ( -
-
-
+
+
+
)} = { + active: "border-l-status-active-dot", + success: "border-l-status-success-dot", + warning: "border-l-status-warning-dot", + error: "border-l-status-error-dot", + neutral: "border-l-border", + urgent: "border-l-status-urgent-dot", +}; + export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detailAgent }: PipelinePhaseGroupProps) { const approvePhase = trpc.approvePhase.useMutation(); const queuePhase = trpc.queuePhase.useMutation(); + const [expanded, setExpanded] = useState(false); // Sort tasks topologically by dependency order const { sorted, blockedByCountMap } = useMemo(() => { @@ -53,6 +66,10 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai return { sorted: sortedTasks, blockedByCountMap: countMap }; }, [tasks, taskDepsRaw]); + const completedCount = tasks.filter((t) => t.status === "completed").length; + const totalCount = tasks.length; + const progressPct = totalCount > 0 ? (completedCount / totalCount) * 100 : 0; + const hasNonDetailTasks = tasks.length > 0; const canExecute = (phase.status === "approved" || (phase.status === "pending" && hasNonDetailTasks)) && @@ -64,14 +81,32 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai detailAgent?.status === "waiting_for_input"; const detailDone = detailAgent?.status === "idle"; + 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 ( -
+
{/* Header */} -
+
{phase.name} + {totalCount > 0 && ( + + {completedCount}/{totalCount} + + )} {canExecute && (
+ {/* Progress bar */} + {totalCount > 0 && ( +
+
+
+ )} + {/* Tasks */}
{isDetailing ? ( @@ -105,13 +155,27 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai ) : sorted.length === 0 ? (

No tasks

) : ( - sorted.map((task) => ( - - )) + <> + {visibleTasks.map((task) => ( + + ))} + {needsCollapse && ( + + )} + )}
diff --git a/apps/web/src/components/pipeline/PipelineStageColumn.tsx b/apps/web/src/components/pipeline/PipelineStageColumn.tsx index 78f7868..a963e15 100644 --- a/apps/web/src/components/pipeline/PipelineStageColumn.tsx +++ b/apps/web/src/components/pipeline/PipelineStageColumn.tsx @@ -15,7 +15,7 @@ interface PipelineStageColumnProps { export function PipelineStageColumn({ phases, tasksByPhase, taskDepsByPhase, blockedPhaseIds, detailAgentByPhase }: PipelineStageColumnProps) { return ( -
+
{phases.map((phase) => ( {task.name} {blockedByCount > 0 && ( - - blocked by {blockedByCount} + + {blockedByCount} dep{blockedByCount === 1 ? "" : "s"} )} {task.status === "pending" && (