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 { 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>
|
||||
))}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user