feat: Show detailing status in initiative overview and phase sidebar
Add 'detailing' activity state derived from active detail agents (mode=detail, status running/waiting_for_input). Initiative cards show pulsing "Detailing" indicator. Phase sidebar items show spinner during active detailing and "Review changes" when the agent finishes.
This commit is contained in:
@@ -232,6 +232,7 @@ export function ExecutionTab({
|
||||
dependencies={depNamesByPhase.get(phase.id) ?? []}
|
||||
isSelected={phase.id === activePhaseId}
|
||||
onClick={() => setSelectedPhaseId(phase.id)}
|
||||
detailAgent={detailAgentByPhase.get(phase.id) ?? null}
|
||||
/>
|
||||
))}
|
||||
{isAddingPhase && (
|
||||
|
||||
@@ -33,6 +33,7 @@ function activityVisual(state: string): { label: string; variant: StatusVariant;
|
||||
switch (state) {
|
||||
case "executing": return { label: "Executing", variant: "active", pulse: true };
|
||||
case "pending_review": return { label: "Pending Review", variant: "warning", pulse: true };
|
||||
case "detailing": return { label: "Detailing", variant: "active", pulse: true };
|
||||
case "ready": return { label: "Ready", variant: "active", pulse: false };
|
||||
case "blocked": return { label: "Blocked", variant: "error", pulse: false };
|
||||
case "complete": return { label: "Complete", variant: "success", pulse: false };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -12,6 +13,7 @@ interface PhaseSidebarItemProps {
|
||||
dependencies: string[];
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
detailAgent?: { status: string } | null;
|
||||
}
|
||||
|
||||
export function PhaseSidebarItem({
|
||||
@@ -21,7 +23,35 @@ export function PhaseSidebarItem({
|
||||
dependencies,
|
||||
isSelected,
|
||||
onClick,
|
||||
detailAgent,
|
||||
}: PhaseSidebarItemProps) {
|
||||
const isDetailing =
|
||||
detailAgent?.status === "running" ||
|
||||
detailAgent?.status === "waiting_for_input";
|
||||
const detailDone = detailAgent?.status === "idle";
|
||||
|
||||
function renderTaskStatus() {
|
||||
if (isDetailing) {
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-status-active-fg">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Detailing…
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (detailDone && taskCount.total === 0) {
|
||||
return <span className="text-status-warning-fg">Review changes</span>;
|
||||
}
|
||||
if (taskCount.total === 0) {
|
||||
return <span>Needs decomposition</span>;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
{taskCount.complete}/{taskCount.total} tasks
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
@@ -40,11 +70,7 @@ export function PhaseSidebarItem({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>
|
||||
{taskCount.total === 0
|
||||
? "Needs decomposition"
|
||||
: `${taskCount.complete}/${taskCount.total} tasks`}
|
||||
</span>
|
||||
{renderTaskStatus()}
|
||||
</div>
|
||||
|
||||
{dependencies.length > 0 && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PipelineColumn } from "@codewalk-district/shared";
|
||||
import { useMemo } from "react";
|
||||
import type { PipelineColumn, DependencyEdge } from "@codewalk-district/shared";
|
||||
import { PipelineStageColumn } from "./PipelineStageColumn";
|
||||
import type { SerializedTask } from "@/components/TaskRow";
|
||||
|
||||
@@ -10,9 +11,28 @@ interface PipelineGraphProps {
|
||||
createdAt: string | Date;
|
||||
}>[];
|
||||
tasksByPhase: Record<string, SerializedTask[]>;
|
||||
dependencyEdges: DependencyEdge[];
|
||||
}
|
||||
|
||||
export function PipelineGraph({ columns, tasksByPhase }: PipelineGraphProps) {
|
||||
export function PipelineGraph({ columns, tasksByPhase, dependencyEdges }: PipelineGraphProps) {
|
||||
// Build a set of phase IDs whose dependencies are all completed
|
||||
const blockedPhaseIds = useMemo(() => {
|
||||
const phaseStatusMap = new Map<string, string>();
|
||||
for (const col of columns) {
|
||||
for (const phase of col.phases) {
|
||||
phaseStatusMap.set(phase.id, phase.status);
|
||||
}
|
||||
}
|
||||
const blocked = new Set<string>();
|
||||
for (const edge of dependencyEdges) {
|
||||
const depStatus = phaseStatusMap.get(edge.dependsOnPhaseId);
|
||||
if (depStatus !== "completed") {
|
||||
blocked.add(edge.phaseId);
|
||||
}
|
||||
}
|
||||
return blocked;
|
||||
}, [columns, dependencyEdges]);
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto pb-4">
|
||||
<div className="flex min-w-max items-start gap-0">
|
||||
@@ -28,6 +48,7 @@ export function PipelineGraph({ columns, tasksByPhase }: PipelineGraphProps) {
|
||||
<PipelineStageColumn
|
||||
phases={column.phases}
|
||||
tasksByPhase={tasksByPhase}
|
||||
blockedPhaseIds={blockedPhaseIds}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -12,11 +12,13 @@ interface PipelinePhaseGroupProps {
|
||||
status: string;
|
||||
};
|
||||
tasks: SerializedTask[];
|
||||
isBlocked: boolean;
|
||||
}
|
||||
|
||||
export function PipelinePhaseGroup({ phase, tasks }: PipelinePhaseGroupProps) {
|
||||
export function PipelinePhaseGroup({ phase, tasks, isBlocked }: PipelinePhaseGroupProps) {
|
||||
const queuePhase = trpc.queuePhase.useMutation();
|
||||
const sorted = sortByPriorityAndQueueTime(tasks);
|
||||
const canExecute = phase.status === "approved" && !isBlocked;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-card overflow-hidden">
|
||||
@@ -26,7 +28,7 @@ export function PipelinePhaseGroup({ phase, tasks }: PipelinePhaseGroupProps) {
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium">
|
||||
{phase.name}
|
||||
</span>
|
||||
{phase.status === "pending" && (
|
||||
{canExecute && (
|
||||
<button
|
||||
onClick={() => queuePhase.mutate({ phaseId: phase.id })}
|
||||
title="Queue phase"
|
||||
|
||||
@@ -8,9 +8,10 @@ interface PipelineStageColumnProps {
|
||||
status: string;
|
||||
}>;
|
||||
tasksByPhase: Record<string, SerializedTask[]>;
|
||||
blockedPhaseIds: Set<string>;
|
||||
}
|
||||
|
||||
export function PipelineStageColumn({ phases, tasksByPhase }: PipelineStageColumnProps) {
|
||||
export function PipelineStageColumn({ phases, tasksByPhase, blockedPhaseIds }: PipelineStageColumnProps) {
|
||||
return (
|
||||
<div className="flex w-64 shrink-0 flex-col gap-3">
|
||||
{phases.map((phase) => (
|
||||
@@ -18,6 +19,7 @@ export function PipelineStageColumn({ phases, tasksByPhase }: PipelineStageColum
|
||||
key={phase.id}
|
||||
phase={phase}
|
||||
tasks={tasksByPhase[phase.id] ?? []}
|
||||
isBlocked={blockedPhaseIds.has(phase.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Loader2, Play } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import {
|
||||
groupPhasesByDependencyLevel,
|
||||
@@ -70,6 +71,14 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
||||
[phases, dependencyEdges],
|
||||
);
|
||||
|
||||
// Count how many phases are approved and ready to execute
|
||||
const approvedCount = useMemo(
|
||||
() => phases.filter((p) => p.status === "approved").length,
|
||||
[phases],
|
||||
);
|
||||
|
||||
const queueAll = trpc.queueAllPhases.useMutation();
|
||||
|
||||
// Register tasks with ExecutionContext for TaskModal
|
||||
useEffect(() => {
|
||||
for (const phase of phases) {
|
||||
@@ -110,5 +119,29 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
||||
);
|
||||
}
|
||||
|
||||
return <PipelineGraph columns={columns} tasksByPhase={tasksByPhase} />;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{approvedCount > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => queueAll.mutate({ initiativeId })}
|
||||
disabled={queueAll.isPending}
|
||||
>
|
||||
{queueAll.isPending ? (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Play className="mr-1.5 h-3.5 w-3.5" />
|
||||
)}
|
||||
Execute {approvedCount === 1 ? "1 phase" : `${approvedCount} phases`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<PipelineGraph
|
||||
columns={columns}
|
||||
tasksByPhase={tasksByPhase}
|
||||
dependencyEdges={dependencyEdges}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user