diff --git a/apps/web/src/components/execution/PhaseDetailPanel.tsx b/apps/web/src/components/execution/PhaseDetailPanel.tsx
index 99f3995..b9add77 100644
--- a/apps/web/src/components/execution/PhaseDetailPanel.tsx
+++ b/apps/web/src/components/execution/PhaseDetailPanel.tsx
@@ -5,7 +5,8 @@ import { trpc } from "@/lib/trpc";
import { StatusBadge } from "@/components/StatusBadge";
import { mapEntityStatus } from "@/components/StatusDot";
import { PhaseNumberBadge } from "@/components/DependencyChip";
-import { TaskRow, type SerializedTask } from "@/components/TaskRow";
+import { type SerializedTask } from "@/components/TaskRow";
+import { TaskGraph } from "./TaskGraph";
import { PhaseContentEditor } from "@/components/editor/PhaseContentEditor";
import { ChangeSetBanner } from "@/components/ChangeSetBanner";
import { Skeleton } from "@/components/Skeleton";
@@ -16,7 +17,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
-import { sortByPriorityAndQueueTime } from "@codewalk-district/shared";
import { useExecutionContext, type FlatTaskEntry } from "./ExecutionContext";
import { cn } from "@/lib/utils";
@@ -196,7 +196,6 @@ export function PhaseDetailPanel({
const isPendingReview = phase.status === "pending_review";
- const sortedTasks = sortByPriorityAndQueueTime(tasks);
const hasTasks = tasks.length > 0;
const isDetailRunning =
detailAgent?.status === "running" ||
@@ -406,22 +405,15 @@ export function PhaseDetailPanel({
))}
- ) : sortedTasks.length === 0 ? (
+ ) : tasks.length === 0 ? (
No tasks yet
) : (
-
- {sortedTasks.map((task, idx) => (
- setSelectedTaskId(task.id)}
- onDelete={() => deleteTask.mutate({ id: task.id })}
- />
- ))}
-
+ deleteTask.mutate({ id })}
+ />
)}
diff --git a/apps/web/src/components/execution/TaskGraph.tsx b/apps/web/src/components/execution/TaskGraph.tsx
new file mode 100644
index 0000000..40ec706
--- /dev/null
+++ b/apps/web/src/components/execution/TaskGraph.tsx
@@ -0,0 +1,205 @@
+import { useMemo } from "react";
+import { X } from "lucide-react";
+import {
+ groupPhasesByDependencyLevel,
+ type DependencyEdge,
+} from "@codewalk-district/shared";
+import { StatusBadge } from "@/components/StatusBadge";
+import { mapEntityStatus, type StatusVariant } from "@/components/StatusDot";
+import { cn } from "@/lib/utils";
+import type { SerializedTask } from "@/components/TaskRow";
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+interface TaskGraphProps {
+ tasks: SerializedTask[];
+ /** Raw edges: { taskId, dependsOn: string[] } */
+ taskDepsRaw: Array<{ taskId: string; dependsOn: string[] }>;
+ onClickTask: (taskId: string) => void;
+ onDeleteTask?: (taskId: string) => void;
+}
+
+// ---------------------------------------------------------------------------
+// Styling maps
+// ---------------------------------------------------------------------------
+
+const railDotClasses: Record = {
+ active: "bg-status-active-dot",
+ success: "bg-status-success-dot",
+ warning: "bg-status-warning-dot",
+ error: "bg-status-error-dot",
+ neutral: "bg-muted-foreground/30",
+ urgent: "bg-status-urgent-dot",
+};
+
+// ---------------------------------------------------------------------------
+// TaskGraph — vertical execution graph for tasks within a phase
+// ---------------------------------------------------------------------------
+
+export function TaskGraph({
+ tasks,
+ taskDepsRaw,
+ onClickTask,
+ onDeleteTask,
+}: TaskGraphProps) {
+ // Flatten task dep edges to DependencyEdge format for reuse of groupPhasesByDependencyLevel
+ const columns = useMemo(() => {
+ const edges: DependencyEdge[] = [];
+ for (const raw of taskDepsRaw) {
+ for (const depId of raw.dependsOn) {
+ edges.push({ phaseId: raw.taskId, dependsOnPhaseId: depId });
+ }
+ }
+ // groupPhasesByDependencyLevel accepts anything with { id, createdAt }
+ return groupPhasesByDependencyLevel(tasks, edges);
+ }, [tasks, taskDepsRaw]);
+
+ if (columns.length === 0) return null;
+
+ return (
+
+ {columns.map((col, colIdx) => {
+ const isParallel = col.phases.length > 1;
+ const isFirst = colIdx === 0;
+
+ const prevAllCompleted =
+ colIdx > 0 &&
+ columns
+ .slice(0, colIdx)
+ .every((c) =>
+ c.phases.every(
+ (t) => mapEntityStatus(t.status) === "success",
+ ),
+ );
+
+ return (
+
+ {!isFirst && (
+
+ )}
+
+ {isParallel ? (
+
+
+ Parallel · {col.phases.length}
+
+
+ {col.phases.map((task) => (
+ onClickTask(task.id)}
+ onDelete={
+ onDeleteTask
+ ? () => onDeleteTask(task.id)
+ : undefined
+ }
+ />
+ ))}
+
+
+ ) : (
+
onClickTask(col.phases[0].id)}
+ onDelete={
+ onDeleteTask
+ ? () => onDeleteTask(col.phases[0].id)
+ : undefined
+ }
+ />
+ )}
+
+ );
+ })}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// TaskNode — single task in the execution graph
+// ---------------------------------------------------------------------------
+
+function TaskNode({
+ task,
+ onClick,
+ onDelete,
+}: {
+ task: SerializedTask;
+ onClick: () => void;
+ onDelete?: () => void;
+}) {
+ const variant = mapEntityStatus(task.status);
+
+ return (
+
+ {/* Rail dot */}
+
+
+ {/* Task name */}
+
+ {task.name}
+
+
+ {/* Status badge */}
+
+
+ {/* Delete button */}
+ {onDelete && (
+
+ )}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// TaskLayerConnector
+// ---------------------------------------------------------------------------
+
+function TaskLayerConnector({ completed }: { completed: boolean }) {
+ return (
+
+ );
+}