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) {
|
||||
const approvePhase = trpc.approvePhase.useMutation();
|
||||
const queuePhase = trpc.queuePhase.useMutation();
|
||||
|
||||
// Sort tasks topologically by dependency order
|
||||
@@ -52,7 +53,12 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
|
||||
return { sorted: sortedTasks, blockedByCountMap: countMap };
|
||||
}, [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 =
|
||||
detailAgent?.status === "running" ||
|
||||
detailAgent?.status === "waiting_for_input";
|
||||
@@ -68,11 +74,21 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
|
||||
</span>
|
||||
{canExecute && (
|
||||
<button
|
||||
onClick={() => queuePhase.mutate({ phaseId: phase.id })}
|
||||
title="Queue phase"
|
||||
onClick={async () => {
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -135,12 +135,25 @@ 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],
|
||||
);
|
||||
// Count phases that can be executed: approved OR pending with non-detail tasks
|
||||
const { actionableCount, pendingPhaseIds } = useMemo(() => {
|
||||
const pending: string[] = [];
|
||||
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();
|
||||
|
||||
// Register tasks with ExecutionContext for TaskSlideOver
|
||||
@@ -185,19 +198,24 @@ function PipelineTabInner({ initiativeId, phases, phasesLoading }: PipelineTabPr
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{approvedCount > 0 && (
|
||||
{actionableCount > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => queueAll.mutate({ initiativeId })}
|
||||
disabled={queueAll.isPending}
|
||||
onClick={async () => {
|
||||
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" />
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -162,8 +162,9 @@ Configured in `src/lib/trpc.ts`. Uses `@trpc/react-query` with TanStack Query fo
|
||||
### Pipeline Visualization
|
||||
1. Execution tab → pipeline DAG shows phases as nodes
|
||||
2. Drag to add dependencies between phases
|
||||
3. Approve phases → queue for dispatch
|
||||
4. Tasks auto-queued when phase starts
|
||||
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. "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
|
||||
1. Select phase → "Detail" button
|
||||
|
||||
Reference in New Issue
Block a user