From 685b2cb4ec7706c54c3e5c5199b78dc3c6079f63 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Wed, 4 Mar 2026 13:07:39 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Refine=20pipeline=20UI=20=E2=80=94=20pr?= =?UTF-8?q?ogress=20bars,=20status=20borders,=20collapsible=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wider phase cards (w-64 → w-72) to reduce task name truncation - Left border accent colored by phase status for quick visual scanning - Task count (completed/total) in phase header - Thin progress bar below header showing completion percentage - Collapsible task lists: phases with >5 tasks collapse with "N more" toggle - Blocked-by count styled as a pill badge ("3 deps" instead of "blocked by 3") - Dashed column connectors with better spacing between pipeline stages --- .../src/components/pipeline/PipelineGraph.tsx | 6 +- .../pipeline/PipelinePhaseGroup.tsx | 88 ++++++++++++++++--- .../pipeline/PipelineStageColumn.tsx | 2 +- .../components/pipeline/PipelineTaskCard.tsx | 4 +- 4 files changed, 82 insertions(+), 18 deletions(-) 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" && (