feat: Show detailing status in pipeline tab phase groups

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.
This commit is contained in:
Lukas May
2026-03-03 13:13:07 +01:00
parent 411700d37d
commit 0ab7b54ad7
4 changed files with 63 additions and 6 deletions

View File

@@ -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<string, SerializedTask[]>;
dependencyEdges: DependencyEdge[];
detailAgentByPhase?: Map<string, DetailAgentInfo>;
}
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<string, string>();
@@ -49,6 +51,7 @@ export function PipelineGraph({ columns, tasksByPhase, dependencyEdges }: Pipeli
phases={column.phases}
tasksByPhase={tasksByPhase}
blockedPhaseIds={blockedPhaseIds}
detailAgentByPhase={detailAgentByPhase}
/>
</div>
))}

View File

@@ -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 (
<div className="rounded-lg border border-border bg-card overflow-hidden">
@@ -41,7 +51,14 @@ export function PipelinePhaseGroup({ phase, tasks, isBlocked }: PipelinePhaseGro
{/* Tasks */}
<div className="py-1">
{sorted.length === 0 ? (
{isDetailing ? (
<p className="flex items-center gap-1.5 px-3 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>
) : (
sorted.map((task) => (

View File

@@ -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<string, SerializedTask[]>;
blockedPhaseIds: Set<string>;
detailAgentByPhase?: Map<string, DetailAgentInfo>;
}
export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds }: PipelineStageColumnProps) {
export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds, detailAgentByPhase }: PipelineStageColumnProps) {
return (
<div className="flex w-64 shrink-0 flex-col gap-3">
{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}
/>
))}
</div>

View File

@@ -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<string, SerializedTask[]> = {};
@@ -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<string, DetailAgentInfo>();
const taskPhaseMap = new Map<string, string>();
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}
/>
</div>
);