Skip to content

Commit 0a25641

Browse files
authored
feat(web-ui): auto-run gates on execution completion, show summary in banner (#472)
## Summary - Gates trigger automatically (non-blocking) via useEffect when execution status becomes 'completed' - useRef guard prevents duplicate runs; showGatePending derived var eliminates blank intermediate frame - GateSummary renders below action buttons: running / all-passed (green) / some-failed (amber + link) / error (amber + link) - CompletionBannerProps extended with gateResult?, gateRunning?, gateError? ## Validation - Review feedback: 2 Minor items addressed (pending state derivation, error color fix) - Demo: All 5 acceptance criteria verified via screenshot - Tests: Build passes clean - CI: All checks green (9 pass, 3 skip, 0 fail) Closes #472
1 parent 6dd9aa8 commit 0a25641

File tree

2 files changed

+94
-17
lines changed

2 files changed

+94
-17
lines changed

web-ui/src/app/execution/[taskId]/page.tsx

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
'use client';
22

3-
import { useState, useEffect, useCallback } from 'react';
3+
import { useState, useEffect, useCallback, useRef } from 'react';
44
import { useParams, useRouter } from 'next/navigation';
55
import Link from 'next/link';
66
import { useExecutionMonitor } from '@/hooks/useExecutionMonitor';
7-
import { tasksApi } from '@/lib/api';
7+
import { tasksApi, gatesApi } from '@/lib/api';
88
import { getSelectedWorkspacePath } from '@/lib/workspace-storage';
99
import { ExecutionHeader } from '@/components/execution/ExecutionHeader';
1010
import { ProgressIndicator } from '@/components/execution/ProgressIndicator';
1111
import { EventStream } from '@/components/execution/EventStream';
1212
import { ChangesSidebar } from '@/components/execution/ChangesSidebar';
1313
import { Button } from '@/components/ui/button';
14-
import type { Task, CompletionBannerProps } from '@/types';
14+
import type { Task, CompletionBannerProps, GateResult } from '@/types';
1515

1616
export default function ExecutionPage() {
1717
const params = useParams<{ taskId: string }>();
@@ -23,6 +23,12 @@ export default function ExecutionPage() {
2323
const [task, setTask] = useState<Task | null>(null);
2424
const [taskError, setTaskError] = useState(false);
2525

26+
// Gate auto-run state
27+
const [gateResult, setGateResult] = useState<GateResult | null>(null);
28+
const [gateRunning, setGateRunning] = useState(false);
29+
const [gateError, setGateError] = useState(false);
30+
const hasRunGatesRef = useRef(false);
31+
2632
// Hydrate workspace path from localStorage
2733
useEffect(() => {
2834
setWorkspacePath(getSelectedWorkspacePath());
@@ -44,6 +50,21 @@ export default function ExecutionPage() {
4450
workspacePath
4551
);
4652

53+
// Auto-run gates non-blocking when execution completes
54+
useEffect(() => {
55+
if (monitor.completionStatus !== 'completed' || !workspacePath || hasRunGatesRef.current) return;
56+
hasRunGatesRef.current = true;
57+
setGateRunning(true);
58+
gatesApi.run(workspacePath)
59+
.then(setGateResult)
60+
.catch(() => setGateError(true))
61+
.finally(() => setGateRunning(false));
62+
}, [monitor.completionStatus, workspacePath]);
63+
64+
// Derive pending state immediately on first completed render (before effect commits)
65+
const showGatePending =
66+
monitor.completionStatus === 'completed' && !gateResult && !gateError;
67+
4768
// Stop handler — may fail if run already completed or no active run
4869
const handleStop = useCallback(async () => {
4970
if (!workspacePath || !taskId) return;
@@ -139,6 +160,9 @@ export default function ExecutionPage() {
139160
onViewChanges={() => router.push('/review')}
140161
onBackToTasks={() => router.push('/tasks')}
141162
onViewBlockers={() => router.push('/blockers')}
163+
gateResult={gateResult}
164+
gateRunning={gateRunning || showGatePending}
165+
gateError={gateError}
142166
/>
143167
)}
144168

@@ -158,33 +182,83 @@ export default function ExecutionPage() {
158182

159183
// ── Completion Banner ─────────────────────────────────────────────────
160184

185+
function GateSummary({
186+
gateRunning,
187+
gateResult,
188+
gateError,
189+
}: {
190+
gateRunning: boolean;
191+
gateResult: CompletionBannerProps['gateResult'];
192+
gateError: boolean;
193+
}) {
194+
if (gateRunning) {
195+
return (
196+
<p className="mt-2 text-xs text-green-700 dark:text-green-300">
197+
Running quality gates…
198+
</p>
199+
);
200+
}
201+
if (gateError) {
202+
return (
203+
<p className="mt-2 text-xs text-amber-700 dark:text-amber-300">
204+
Gate check unavailable ·{' '}
205+
<Link href="/review" className="underline hover:no-underline">View in Review →</Link>
206+
</p>
207+
);
208+
}
209+
if (gateResult) {
210+
const total = gateResult.checks.length;
211+
const passed = gateResult.checks.filter((c) => c.status === 'PASSED').length;
212+
if (gateResult.passed) {
213+
return (
214+
<p className="mt-2 text-xs text-green-700 dark:text-green-300">
215+
✓ All {total} gate{total !== 1 ? 's' : ''} passed
216+
</p>
217+
);
218+
}
219+
return (
220+
<p className="mt-2 text-xs text-amber-700 dark:text-amber-300">
221+
{passed}/{total} gates passed ·{' '}
222+
<Link href="/review" className="underline hover:no-underline">View full report →</Link>
223+
</p>
224+
);
225+
}
226+
return null;
227+
}
228+
161229
function CompletionBanner({
162230
status,
163231
duration,
164232
onViewProof,
165233
onViewChanges,
166234
onBackToTasks,
167235
onViewBlockers,
236+
gateResult,
237+
gateRunning = false,
238+
gateError = false,
168239
}: CompletionBannerProps) {
169240
const durationText = duration !== null ? `${Math.round(duration)}s` : '';
170241

171242
if (status === 'completed') {
172243
return (
173-
<div role="alert" className="flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3 dark:border-green-900 dark:bg-green-950/30">
174-
<p className="text-sm font-medium text-green-800 dark:text-green-200">
175-
Execution complete{durationText && ` in ${durationText}`}. Run PROOF9 gates to verify quality before shipping.
176-
</p>
177-
<div className="flex gap-2">
178-
<Button onClick={onViewProof} size="sm">
179-
Verify with PROOF9
180-
</Button>
181-
<Button onClick={onViewChanges} variant="outline" size="sm">
182-
View Changes
183-
</Button>
184-
<Button onClick={onBackToTasks} variant="outline" size="sm">
185-
Back to Tasks
186-
</Button>
244+
<div role="alert" className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 dark:border-green-900 dark:bg-green-950/30">
245+
<div className="flex items-center justify-between">
246+
<p className="text-sm font-medium text-green-800 dark:text-green-200">
247+
Execution complete{durationText && ` in ${durationText}`}. Run PROOF9 gates to verify quality before shipping.
248+
</p>
249+
<div className="flex gap-2">
250+
<Button onClick={onViewProof} size="sm">
251+
Verify with PROOF9
252+
</Button>
253+
<Button onClick={onViewChanges} variant="outline" size="sm">
254+
View Changes
255+
</Button>
256+
<Button onClick={onBackToTasks} variant="outline" size="sm">
257+
Back to Tasks
258+
</Button>
259+
</div>
187260
</div>
261+
<GateSummary gateRunning={gateRunning} gateResult={gateResult} gateError={gateError} />
188262
</div>
189263
);
190264
}

web-ui/src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ export interface CompletionBannerProps {
352352
onViewChanges: () => void;
353353
onBackToTasks: () => void;
354354
onViewBlockers: () => void;
355+
gateResult?: GateResult | null;
356+
gateRunning?: boolean;
357+
gateError?: boolean;
355358
}
356359

357360
// Pipeline progress types

0 commit comments

Comments
 (0)