From 0ab7b54ad755b46d54d1ffb55db1beddf9ecbb7f Mon Sep 17 00:00:00 2001 From: Lukas May Date: Tue, 3 Mar 2026 13:13:07 +0100 Subject: [PATCH] feat: Show detailing status in pipeline tab phase groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thread detail agent info through PipelineGraph → PipelineStageColumn → PipelinePhaseGroup. Phase groups now show spinner + "Detailing…" when a detail agent is active and "Review changes" when finished with no tasks. --- .../src/components/pipeline/PipelineGraph.tsx | 5 ++- .../pipeline/PipelinePhaseGroup.tsx | 23 ++++++++++-- .../pipeline/PipelineStageColumn.tsx | 6 ++-- .../src/components/pipeline/PipelineTab.tsx | 35 +++++++++++++++++++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/pipeline/PipelineGraph.tsx b/apps/web/src/components/pipeline/PipelineGraph.tsx index af90689..2fac66b 100644 --- a/apps/web/src/components/pipeline/PipelineGraph.tsx +++ b/apps/web/src/components/pipeline/PipelineGraph.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import type { PipelineColumn, DependencyEdge } from "@codewalk-district/shared"; import { PipelineStageColumn } from "./PipelineStageColumn"; import type { SerializedTask } from "@/components/TaskRow"; +import type { DetailAgentInfo } from "./PipelinePhaseGroup"; interface PipelineGraphProps { columns: PipelineColumn<{ @@ -12,9 +13,10 @@ interface PipelineGraphProps { }>[]; tasksByPhase: Record; dependencyEdges: DependencyEdge[]; + detailAgentByPhase?: Map; } -export function PipelineGraph({ columns, tasksByPhase, dependencyEdges }: PipelineGraphProps) { +export function PipelineGraph({ columns, tasksByPhase, dependencyEdges, detailAgentByPhase }: PipelineGraphProps) { // Build a set of phase IDs whose dependencies are all completed const blockedPhaseIds = useMemo(() => { const phaseStatusMap = new Map(); @@ -49,6 +51,7 @@ export function PipelineGraph({ columns, tasksByPhase, dependencyEdges }: Pipeli phases={column.phases} tasksByPhase={tasksByPhase} blockedPhaseIds={blockedPhaseIds} + detailAgentByPhase={detailAgentByPhase} /> ))} diff --git a/apps/web/src/components/pipeline/PipelinePhaseGroup.tsx b/apps/web/src/components/pipeline/PipelinePhaseGroup.tsx index 5f96481..4daebc7 100644 --- a/apps/web/src/components/pipeline/PipelinePhaseGroup.tsx +++ b/apps/web/src/components/pipeline/PipelinePhaseGroup.tsx @@ -1,10 +1,15 @@ -import { Play } from "lucide-react"; +import { Loader2, Play } from "lucide-react"; import { StatusDot } from "@/components/StatusDot"; import { trpc } from "@/lib/trpc"; import { sortByPriorityAndQueueTime } from "@codewalk-district/shared"; import { PipelineTaskCard } from "./PipelineTaskCard"; import type { SerializedTask } from "@/components/TaskRow"; +export interface DetailAgentInfo { + status: string; + createdAt: string | Date; +} + interface PipelinePhaseGroupProps { phase: { id: string; @@ -13,12 +18,17 @@ interface PipelinePhaseGroupProps { }; tasks: SerializedTask[]; isBlocked: boolean; + detailAgent?: DetailAgentInfo | null; } -export function PipelinePhaseGroup({ phase, tasks, isBlocked }: PipelinePhaseGroupProps) { +export function PipelinePhaseGroup({ phase, tasks, isBlocked, detailAgent }: PipelinePhaseGroupProps) { const queuePhase = trpc.queuePhase.useMutation(); const sorted = sortByPriorityAndQueueTime(tasks); const canExecute = phase.status === "approved" && !isBlocked; + const isDetailing = + detailAgent?.status === "running" || + detailAgent?.status === "waiting_for_input"; + const detailDone = detailAgent?.status === "idle"; return (
@@ -41,7 +51,14 @@ export function PipelinePhaseGroup({ phase, tasks, isBlocked }: PipelinePhaseGro {/* Tasks */}
- {sorted.length === 0 ? ( + {isDetailing ? ( +

+ + Detailing… +

+ ) : detailDone && sorted.length === 0 ? ( +

Review changes

+ ) : sorted.length === 0 ? (

No tasks

) : ( sorted.map((task) => ( diff --git a/apps/web/src/components/pipeline/PipelineStageColumn.tsx b/apps/web/src/components/pipeline/PipelineStageColumn.tsx index 9b8b18c..7b9fc03 100644 --- a/apps/web/src/components/pipeline/PipelineStageColumn.tsx +++ b/apps/web/src/components/pipeline/PipelineStageColumn.tsx @@ -1,4 +1,4 @@ -import { PipelinePhaseGroup } from "./PipelinePhaseGroup"; +import { PipelinePhaseGroup, type DetailAgentInfo } from "./PipelinePhaseGroup"; import type { SerializedTask } from "@/components/TaskRow"; interface PipelineStageColumnProps { @@ -9,9 +9,10 @@ interface PipelineStageColumnProps { }>; tasksByPhase: Record; blockedPhaseIds: Set; + detailAgentByPhase?: Map; } -export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds }: PipelineStageColumnProps) { +export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds, detailAgentByPhase }: PipelineStageColumnProps) { return (
{phases.map((phase) => ( @@ -20,6 +21,7 @@ export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds }: P phase={phase} tasks={tasksByPhase[phase.id] ?? []} isBlocked={blockedPhaseIds.has(phase.id)} + detailAgent={detailAgentByPhase?.get(phase.id) ?? null} /> ))}
diff --git a/apps/web/src/components/pipeline/PipelineTab.tsx b/apps/web/src/components/pipeline/PipelineTab.tsx index 0b41b83..f5a4702 100644 --- a/apps/web/src/components/pipeline/PipelineTab.tsx +++ b/apps/web/src/components/pipeline/PipelineTab.tsx @@ -16,6 +16,7 @@ import { } from "@/components/execution"; import type { SerializedTask } from "@/components/TaskRow"; import { PipelineGraph } from "./PipelineGraph"; +import type { DetailAgentInfo } from "./PipelinePhaseGroup"; interface PipelineTabProps { initiativeId: string; @@ -53,6 +54,10 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr ); const dependencyEdges: DependencyEdge[] = depsQuery.data ?? []; + // Fetch agents for detail agent tracking + const agentsQuery = trpc.listAgents.useQuery(); + const allAgents = agentsQuery.data ?? []; + // Group tasks by phaseId const tasksByPhase = useMemo(() => { const map: Record = {}; @@ -65,6 +70,35 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr return map; }, [allTasks]); + // Map phaseId → most recent active detail agent + const detailAgentByPhase = useMemo(() => { + const map = new Map(); + const taskPhaseMap = new Map(); + for (const t of allTasks) { + if (t.phaseId) taskPhaseMap.set(t.id, t.phaseId); + } + const candidates = allAgents.filter( + (a) => + a.mode === "detail" && + a.initiativeId === initiativeId && + ["running", "waiting_for_input", "idle"].includes(a.status) && + !a.userDismissedAt, + ); + for (const agent of candidates) { + const phaseId = taskPhaseMap.get(agent.taskId ?? ""); + if (!phaseId) continue; + const existing = map.get(phaseId); + if ( + !existing || + new Date(agent.createdAt).getTime() > + new Date(existing.createdAt).getTime() + ) { + map.set(phaseId, { status: agent.status, createdAt: agent.createdAt }); + } + } + return map; + }, [allAgents, allTasks, initiativeId]); + // Compute pipeline columns const columns = useMemo( () => groupPhasesByDependencyLevel(phases, dependencyEdges), @@ -141,6 +175,7 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr columns={columns} tasksByPhase={tasksByPhase} dependencyEdges={dependencyEdges} + detailAgentByPhase={detailAgentByPhase} />
);