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:
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|||||||
import type { PipelineColumn, DependencyEdge } from "@codewalk-district/shared";
|
import type { PipelineColumn, DependencyEdge } from "@codewalk-district/shared";
|
||||||
import { PipelineStageColumn } from "./PipelineStageColumn";
|
import { PipelineStageColumn } from "./PipelineStageColumn";
|
||||||
import type { SerializedTask } from "@/components/TaskRow";
|
import type { SerializedTask } from "@/components/TaskRow";
|
||||||
|
import type { DetailAgentInfo } from "./PipelinePhaseGroup";
|
||||||
|
|
||||||
interface PipelineGraphProps {
|
interface PipelineGraphProps {
|
||||||
columns: PipelineColumn<{
|
columns: PipelineColumn<{
|
||||||
@@ -12,9 +13,10 @@ interface PipelineGraphProps {
|
|||||||
}>[];
|
}>[];
|
||||||
tasksByPhase: Record<string, SerializedTask[]>;
|
tasksByPhase: Record<string, SerializedTask[]>;
|
||||||
dependencyEdges: DependencyEdge[];
|
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
|
// Build a set of phase IDs whose dependencies are all completed
|
||||||
const blockedPhaseIds = useMemo(() => {
|
const blockedPhaseIds = useMemo(() => {
|
||||||
const phaseStatusMap = new Map<string, string>();
|
const phaseStatusMap = new Map<string, string>();
|
||||||
@@ -49,6 +51,7 @@ export function PipelineGraph({ columns, tasksByPhase, dependencyEdges }: Pipeli
|
|||||||
phases={column.phases}
|
phases={column.phases}
|
||||||
tasksByPhase={tasksByPhase}
|
tasksByPhase={tasksByPhase}
|
||||||
blockedPhaseIds={blockedPhaseIds}
|
blockedPhaseIds={blockedPhaseIds}
|
||||||
|
detailAgentByPhase={detailAgentByPhase}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Play } from "lucide-react";
|
import { Loader2, Play } from "lucide-react";
|
||||||
import { StatusDot } from "@/components/StatusDot";
|
import { StatusDot } from "@/components/StatusDot";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
import { sortByPriorityAndQueueTime } from "@codewalk-district/shared";
|
import { sortByPriorityAndQueueTime } from "@codewalk-district/shared";
|
||||||
import { PipelineTaskCard } from "./PipelineTaskCard";
|
import { PipelineTaskCard } from "./PipelineTaskCard";
|
||||||
import type { SerializedTask } from "@/components/TaskRow";
|
import type { SerializedTask } from "@/components/TaskRow";
|
||||||
|
|
||||||
|
export interface DetailAgentInfo {
|
||||||
|
status: string;
|
||||||
|
createdAt: string | Date;
|
||||||
|
}
|
||||||
|
|
||||||
interface PipelinePhaseGroupProps {
|
interface PipelinePhaseGroupProps {
|
||||||
phase: {
|
phase: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,12 +18,17 @@ interface PipelinePhaseGroupProps {
|
|||||||
};
|
};
|
||||||
tasks: SerializedTask[];
|
tasks: SerializedTask[];
|
||||||
isBlocked: boolean;
|
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 queuePhase = trpc.queuePhase.useMutation();
|
||||||
const sorted = sortByPriorityAndQueueTime(tasks);
|
const sorted = sortByPriorityAndQueueTime(tasks);
|
||||||
const canExecute = phase.status === "approved" && !isBlocked;
|
const canExecute = phase.status === "approved" && !isBlocked;
|
||||||
|
const isDetailing =
|
||||||
|
detailAgent?.status === "running" ||
|
||||||
|
detailAgent?.status === "waiting_for_input";
|
||||||
|
const detailDone = detailAgent?.status === "idle";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-border bg-card overflow-hidden">
|
<div className="rounded-lg border border-border bg-card overflow-hidden">
|
||||||
@@ -41,7 +51,14 @@ export function PipelinePhaseGroup({ phase, tasks, isBlocked }: PipelinePhaseGro
|
|||||||
|
|
||||||
{/* Tasks */}
|
{/* Tasks */}
|
||||||
<div className="py-1">
|
<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>
|
<p className="px-3 py-1 text-xs text-muted-foreground">No tasks</p>
|
||||||
) : (
|
) : (
|
||||||
sorted.map((task) => (
|
sorted.map((task) => (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PipelinePhaseGroup } from "./PipelinePhaseGroup";
|
import { PipelinePhaseGroup, type DetailAgentInfo } from "./PipelinePhaseGroup";
|
||||||
import type { SerializedTask } from "@/components/TaskRow";
|
import type { SerializedTask } from "@/components/TaskRow";
|
||||||
|
|
||||||
interface PipelineStageColumnProps {
|
interface PipelineStageColumnProps {
|
||||||
@@ -9,9 +9,10 @@ interface PipelineStageColumnProps {
|
|||||||
}>;
|
}>;
|
||||||
tasksByPhase: Record<string, SerializedTask[]>;
|
tasksByPhase: Record<string, SerializedTask[]>;
|
||||||
blockedPhaseIds: Set<string>;
|
blockedPhaseIds: Set<string>;
|
||||||
|
detailAgentByPhase?: Map<string, DetailAgentInfo>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds }: PipelineStageColumnProps) {
|
export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds, detailAgentByPhase }: PipelineStageColumnProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-64 shrink-0 flex-col gap-3">
|
<div className="flex w-64 shrink-0 flex-col gap-3">
|
||||||
{phases.map((phase) => (
|
{phases.map((phase) => (
|
||||||
@@ -20,6 +21,7 @@ export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds }: P
|
|||||||
phase={phase}
|
phase={phase}
|
||||||
tasks={tasksByPhase[phase.id] ?? []}
|
tasks={tasksByPhase[phase.id] ?? []}
|
||||||
isBlocked={blockedPhaseIds.has(phase.id)}
|
isBlocked={blockedPhaseIds.has(phase.id)}
|
||||||
|
detailAgent={detailAgentByPhase?.get(phase.id) ?? null}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "@/components/execution";
|
} from "@/components/execution";
|
||||||
import type { SerializedTask } from "@/components/TaskRow";
|
import type { SerializedTask } from "@/components/TaskRow";
|
||||||
import { PipelineGraph } from "./PipelineGraph";
|
import { PipelineGraph } from "./PipelineGraph";
|
||||||
|
import type { DetailAgentInfo } from "./PipelinePhaseGroup";
|
||||||
|
|
||||||
interface PipelineTabProps {
|
interface PipelineTabProps {
|
||||||
initiativeId: string;
|
initiativeId: string;
|
||||||
@@ -53,6 +54,10 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
|||||||
);
|
);
|
||||||
const dependencyEdges: DependencyEdge[] = depsQuery.data ?? [];
|
const dependencyEdges: DependencyEdge[] = depsQuery.data ?? [];
|
||||||
|
|
||||||
|
// Fetch agents for detail agent tracking
|
||||||
|
const agentsQuery = trpc.listAgents.useQuery();
|
||||||
|
const allAgents = agentsQuery.data ?? [];
|
||||||
|
|
||||||
// Group tasks by phaseId
|
// Group tasks by phaseId
|
||||||
const tasksByPhase = useMemo(() => {
|
const tasksByPhase = useMemo(() => {
|
||||||
const map: Record<string, SerializedTask[]> = {};
|
const map: Record<string, SerializedTask[]> = {};
|
||||||
@@ -65,6 +70,35 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
|||||||
return map;
|
return map;
|
||||||
}, [allTasks]);
|
}, [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
|
// Compute pipeline columns
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => groupPhasesByDependencyLevel(phases, dependencyEdges),
|
() => groupPhasesByDependencyLevel(phases, dependencyEdges),
|
||||||
@@ -141,6 +175,7 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
tasksByPhase={tasksByPhase}
|
tasksByPhase={tasksByPhase}
|
||||||
dependencyEdges={dependencyEdges}
|
dependencyEdges={dependencyEdges}
|
||||||
|
detailAgentByPhase={detailAgentByPhase}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user