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) {
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>

View File

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

View File

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