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:
Lukas May
2026-03-04 13:04:20 +01:00
parent 1c7d6f20ee
commit 029b5bf0f6
3 changed files with 51 additions and 16 deletions

View File

@@ -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>

View File

@@ -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>
)} )}

View File

@@ -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