feat: Add approve+execute buttons to pipeline UI
Per-phase Play button now shows for pending phases (with tasks) and approves before queueing. Top-level Execute button counts both pending and approved phases, approving pending ones first.
This commit is contained in:
@@ -27,6 +27,7 @@ interface PipelinePhaseGroupProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detailAgent }: PipelinePhaseGroupProps) {
|
export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detailAgent }: PipelinePhaseGroupProps) {
|
||||||
|
const approvePhase = trpc.approvePhase.useMutation();
|
||||||
const queuePhase = trpc.queuePhase.useMutation();
|
const queuePhase = trpc.queuePhase.useMutation();
|
||||||
|
|
||||||
// Sort tasks topologically by dependency order
|
// Sort tasks topologically by dependency order
|
||||||
@@ -52,7 +53,12 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
|
|||||||
return { sorted: sortedTasks, blockedByCountMap: countMap };
|
return { sorted: sortedTasks, blockedByCountMap: countMap };
|
||||||
}, [tasks, taskDepsRaw]);
|
}, [tasks, taskDepsRaw]);
|
||||||
|
|
||||||
const canExecute = phase.status === "approved" && !isBlocked;
|
const hasNonDetailTasks = tasks.length > 0;
|
||||||
|
const canExecute =
|
||||||
|
(phase.status === "approved" || (phase.status === "pending" && hasNonDetailTasks)) &&
|
||||||
|
!isBlocked;
|
||||||
|
const isPending = phase.status === "pending";
|
||||||
|
const isMutating = approvePhase.isPending || queuePhase.isPending;
|
||||||
const isDetailing =
|
const isDetailing =
|
||||||
detailAgent?.status === "running" ||
|
detailAgent?.status === "running" ||
|
||||||
detailAgent?.status === "waiting_for_input";
|
detailAgent?.status === "waiting_for_input";
|
||||||
@@ -68,11 +74,21 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
|
|||||||
</span>
|
</span>
|
||||||
{canExecute && (
|
{canExecute && (
|
||||||
<button
|
<button
|
||||||
onClick={() => queuePhase.mutate({ phaseId: phase.id })}
|
onClick={async () => {
|
||||||
title="Queue phase"
|
if (isPending) {
|
||||||
|
await approvePhase.mutateAsync({ phaseId: phase.id });
|
||||||
|
}
|
||||||
|
queuePhase.mutate({ phaseId: phase.id });
|
||||||
|
}}
|
||||||
|
disabled={isMutating}
|
||||||
|
title={isPending ? "Approve & queue phase" : "Queue phase"}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
>
|
>
|
||||||
<Play className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
{isMutating ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -135,12 +135,25 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
|||||||
[phases, dependencyEdges],
|
[phases, dependencyEdges],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Count how many phases are approved and ready to execute
|
// Count phases that can be executed: approved OR pending with non-detail tasks
|
||||||
const approvedCount = useMemo(
|
const { actionableCount, pendingPhaseIds } = useMemo(() => {
|
||||||
() => phases.filter((p) => p.status === "approved").length,
|
const pending: string[] = [];
|
||||||
[phases],
|
let count = 0;
|
||||||
);
|
for (const p of phases) {
|
||||||
|
if (p.status === "approved") {
|
||||||
|
count++;
|
||||||
|
} else if (p.status === "pending") {
|
||||||
|
const phaseTasks = tasksByPhase[p.id] ?? [];
|
||||||
|
if (phaseTasks.length > 0) {
|
||||||
|
count++;
|
||||||
|
pending.push(p.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { actionableCount: count, pendingPhaseIds: pending };
|
||||||
|
}, [phases, tasksByPhase]);
|
||||||
|
|
||||||
|
const approvePhase = trpc.approvePhase.useMutation();
|
||||||
const queueAll = trpc.queueAllPhases.useMutation();
|
const queueAll = trpc.queueAllPhases.useMutation();
|
||||||
|
|
||||||
// Register tasks with ExecutionContext for TaskSlideOver
|
// Register tasks with ExecutionContext for TaskSlideOver
|
||||||
@@ -185,19 +198,24 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{approvedCount > 0 && (
|
{actionableCount > 0 && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => queueAll.mutate({ initiativeId })}
|
onClick={async () => {
|
||||||
disabled={queueAll.isPending}
|
for (const phaseId of pendingPhaseIds) {
|
||||||
|
await approvePhase.mutateAsync({ phaseId });
|
||||||
|
}
|
||||||
|
queueAll.mutate({ initiativeId });
|
||||||
|
}}
|
||||||
|
disabled={approvePhase.isPending || queueAll.isPending}
|
||||||
>
|
>
|
||||||
{queueAll.isPending ? (
|
{approvePhase.isPending || queueAll.isPending ? (
|
||||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Play className="mr-1.5 h-3.5 w-3.5" />
|
<Play className="mr-1.5 h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
Execute {approvedCount === 1 ? "1 phase" : `${approvedCount} phases`}
|
Execute {actionableCount === 1 ? "1 phase" : `${actionableCount} phases`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -162,8 +162,9 @@ Configured in `src/lib/trpc.ts`. Uses `@trpc/react-query` with TanStack Query fo
|
|||||||
### Pipeline Visualization
|
### Pipeline Visualization
|
||||||
1. Execution tab → pipeline DAG shows phases as nodes
|
1. Execution tab → pipeline DAG shows phases as nodes
|
||||||
2. Drag to add dependencies between phases
|
2. Drag to add dependencies between phases
|
||||||
3. Approve phases → queue for dispatch
|
3. Per-phase Play button: if phase is `pending` (with non-detail tasks), approves then queues in one click; if already `approved`, just queues
|
||||||
4. Tasks auto-queued when phase starts
|
4. "Execute N phases" top-level button: approves all `pending` phases that have tasks, then calls `queueAllPhases` — count includes both `pending` (with tasks) and `approved` phases
|
||||||
|
5. Tasks auto-queued when phase starts
|
||||||
|
|
||||||
### Detailing Phases
|
### Detailing Phases
|
||||||
1. Select phase → "Detail" button
|
1. Select phase → "Detail" button
|
||||||
|
|||||||
Reference in New Issue
Block a user