11'use client' ;
22
3- import { useState , useEffect , useCallback } from 'react' ;
3+ import { useState , useEffect , useCallback , useRef } from 'react' ;
44import { useParams , useRouter } from 'next/navigation' ;
55import Link from 'next/link' ;
66import { useExecutionMonitor } from '@/hooks/useExecutionMonitor' ;
7- import { tasksApi } from '@/lib/api' ;
7+ import { tasksApi , gatesApi } from '@/lib/api' ;
88import { getSelectedWorkspacePath } from '@/lib/workspace-storage' ;
99import { ExecutionHeader } from '@/components/execution/ExecutionHeader' ;
1010import { ProgressIndicator } from '@/components/execution/ProgressIndicator' ;
1111import { EventStream } from '@/components/execution/EventStream' ;
1212import { ChangesSidebar } from '@/components/execution/ChangesSidebar' ;
1313import { Button } from '@/components/ui/button' ;
14- import type { Task , CompletionBannerProps } from '@/types' ;
14+ import type { Task , CompletionBannerProps , GateResult } from '@/types' ;
1515
1616export 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+
161229function 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 }
0 commit comments