fix: Sort pipeline tasks by dependency order instead of priority+createdAt
The Execution tab's PipelinePhaseGroup was using sortByPriorityAndQueueTime which ignores task dependencies entirely. Tasks now sort topologically via topologicalSortPhases, matching the Plan tab's TaskGraph behavior. - Add listInitiativeTaskDependencies tRPC procedure (bulk fetch) - Fetch task deps in PipelineTab, group by phase, pass through - Replace priority sort with topological sort in PipelinePhaseGroup - Show "blocked by N" count on PipelineTaskCard
This commit is contained in:
@@ -12,11 +12,12 @@ interface PipelineGraphProps {
|
||||
createdAt: string | Date;
|
||||
}>[];
|
||||
tasksByPhase: Record<string, SerializedTask[]>;
|
||||
taskDepsByPhase: Record<string, Array<{ taskId: string; dependsOn: string[] }>>;
|
||||
dependencyEdges: DependencyEdge[];
|
||||
detailAgentByPhase?: Map<string, DetailAgentInfo>;
|
||||
}
|
||||
|
||||
export function PipelineGraph({ columns, tasksByPhase, dependencyEdges, detailAgentByPhase }: PipelineGraphProps) {
|
||||
export function PipelineGraph({ columns, tasksByPhase, taskDepsByPhase, dependencyEdges, detailAgentByPhase }: PipelineGraphProps) {
|
||||
// Build a set of phase IDs whose dependencies are all completed
|
||||
const blockedPhaseIds = useMemo(() => {
|
||||
const phaseStatusMap = new Map<string, string>();
|
||||
@@ -50,6 +51,7 @@ export function PipelineGraph({ columns, tasksByPhase, dependencyEdges, detailAg
|
||||
<PipelineStageColumn
|
||||
phases={column.phases}
|
||||
tasksByPhase={tasksByPhase}
|
||||
taskDepsByPhase={taskDepsByPhase}
|
||||
blockedPhaseIds={blockedPhaseIds}
|
||||
detailAgentByPhase={detailAgentByPhase}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useMemo } from "react";
|
||||
import { Loader2, Play } from "lucide-react";
|
||||
import { StatusDot } from "@/components/StatusDot";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { sortByPriorityAndQueueTime } from "@codewalk-district/shared";
|
||||
import {
|
||||
topologicalSortPhases,
|
||||
type DependencyEdge,
|
||||
} from "@codewalk-district/shared";
|
||||
import { PipelineTaskCard } from "./PipelineTaskCard";
|
||||
import type { SerializedTask } from "@/components/TaskRow";
|
||||
|
||||
@@ -17,13 +21,37 @@ interface PipelinePhaseGroupProps {
|
||||
status: string;
|
||||
};
|
||||
tasks: SerializedTask[];
|
||||
taskDepsRaw: Array<{ taskId: string; dependsOn: string[] }>;
|
||||
isBlocked: boolean;
|
||||
detailAgent?: DetailAgentInfo | null;
|
||||
}
|
||||
|
||||
export function PipelinePhaseGroup({ phase, tasks, isBlocked, detailAgent }: PipelinePhaseGroupProps) {
|
||||
export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detailAgent }: PipelinePhaseGroupProps) {
|
||||
const queuePhase = trpc.queuePhase.useMutation();
|
||||
const sorted = sortByPriorityAndQueueTime(tasks);
|
||||
|
||||
// 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)
|
||||
const countMap = new Map<string, number>();
|
||||
for (const raw of taskDepsRaw) {
|
||||
const incomplete = raw.dependsOn.filter((depId) => {
|
||||
const dep = tasks.find((t) => t.id === depId);
|
||||
return dep && dep.status !== "completed";
|
||||
}).length;
|
||||
if (incomplete > 0) countMap.set(raw.taskId, incomplete);
|
||||
}
|
||||
|
||||
return { sorted: sortedTasks, blockedByCountMap: countMap };
|
||||
}, [tasks, taskDepsRaw]);
|
||||
|
||||
const canExecute = phase.status === "approved" && !isBlocked;
|
||||
const isDetailing =
|
||||
detailAgent?.status === "running" ||
|
||||
@@ -62,7 +90,11 @@ export function PipelinePhaseGroup({ phase, tasks, isBlocked, detailAgent }: Pip
|
||||
<p className="px-3 py-1 text-xs text-muted-foreground">No tasks</p>
|
||||
) : (
|
||||
sorted.map((task) => (
|
||||
<PipelineTaskCard key={task.id} task={task} />
|
||||
<PipelineTaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
blockedByCount={blockedByCountMap.get(task.id) ?? 0}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,12 @@ interface PipelineStageColumnProps {
|
||||
status: string;
|
||||
}>;
|
||||
tasksByPhase: Record<string, SerializedTask[]>;
|
||||
taskDepsByPhase: Record<string, Array<{ taskId: string; dependsOn: string[] }>>;
|
||||
blockedPhaseIds: Set<string>;
|
||||
detailAgentByPhase?: Map<string, DetailAgentInfo>;
|
||||
}
|
||||
|
||||
export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds, detailAgentByPhase }: PipelineStageColumnProps) {
|
||||
export function PipelineStageColumn({ phases, tasksByPhase, taskDepsByPhase, 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, det
|
||||
key={phase.id}
|
||||
phase={phase}
|
||||
tasks={tasksByPhase[phase.id] ?? []}
|
||||
taskDepsRaw={taskDepsByPhase[phase.id] ?? []}
|
||||
isBlocked={blockedPhaseIds.has(phase.id)}
|
||||
detailAgent={detailAgentByPhase?.get(phase.id) ?? null}
|
||||
/>
|
||||
|
||||
@@ -47,13 +47,20 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
||||
);
|
||||
const allTasks = (tasksQuery.data ?? []) as SerializedTask[];
|
||||
|
||||
// Fetch dependency edges
|
||||
// Fetch dependency edges (phase-level)
|
||||
const depsQuery = trpc.listInitiativePhaseDependencies.useQuery(
|
||||
{ initiativeId },
|
||||
{ enabled: phases.length > 0 },
|
||||
);
|
||||
const dependencyEdges: DependencyEdge[] = depsQuery.data ?? [];
|
||||
|
||||
// Fetch task-level dependency edges
|
||||
const taskDepsQuery = trpc.listInitiativeTaskDependencies.useQuery(
|
||||
{ initiativeId },
|
||||
{ enabled: phases.length > 0 },
|
||||
);
|
||||
const taskDepsRaw = taskDepsQuery.data ?? [];
|
||||
|
||||
// Fetch agents for detail agent tracking
|
||||
const agentsQuery = trpc.listAgents.useQuery();
|
||||
const allAgents = agentsQuery.data ?? [];
|
||||
@@ -76,6 +83,23 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
||||
return map;
|
||||
}, [displayTasks]);
|
||||
|
||||
// Group task dependency edges by phaseId
|
||||
const taskDepsByPhase = useMemo(() => {
|
||||
const taskPhaseMap = new Map<string, string>();
|
||||
for (const t of allTasks) {
|
||||
if (t.phaseId) taskPhaseMap.set(t.id, t.phaseId);
|
||||
}
|
||||
const map: Record<string, Array<{ taskId: string; dependsOn: string[] }>> = {};
|
||||
for (const edge of taskDepsRaw) {
|
||||
const phaseId = taskPhaseMap.get(edge.taskId);
|
||||
if (phaseId) {
|
||||
if (!map[phaseId]) map[phaseId] = [];
|
||||
map[phaseId].push(edge);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [allTasks, taskDepsRaw]);
|
||||
|
||||
// Map phaseId → most recent active detail agent
|
||||
const detailAgentByPhase = useMemo(() => {
|
||||
const map = new Map<string, DetailAgentInfo>();
|
||||
@@ -180,6 +204,7 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
||||
<PipelineGraph
|
||||
columns={columns}
|
||||
tasksByPhase={tasksByPhase}
|
||||
taskDepsByPhase={taskDepsByPhase}
|
||||
dependencyEdges={dependencyEdges}
|
||||
detailAgentByPhase={detailAgentByPhase}
|
||||
/>
|
||||
|
||||
@@ -14,9 +14,10 @@ const statusConfig: Record<string, { icon: typeof Clock; color: string; spin?: b
|
||||
|
||||
interface PipelineTaskCardProps {
|
||||
task: SerializedTask;
|
||||
blockedByCount?: number;
|
||||
}
|
||||
|
||||
export function PipelineTaskCard({ task }: PipelineTaskCardProps) {
|
||||
export function PipelineTaskCard({ task, blockedByCount = 0 }: PipelineTaskCardProps) {
|
||||
const { setSelectedTaskId } = useExecutionContext();
|
||||
const queueTask = trpc.queueTask.useMutation();
|
||||
|
||||
@@ -32,6 +33,11 @@ export function PipelineTaskCard({ task }: PipelineTaskCardProps) {
|
||||
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 text-[10px] text-muted-foreground">
|
||||
blocked by {blockedByCount}
|
||||
</span>
|
||||
)}
|
||||
{task.status === "pending" && (
|
||||
<button
|
||||
className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
|
||||
Reference in New Issue
Block a user