-
Notifications
You must be signed in to change notification settings - Fork 5
feat(web-ui): auto-run gates on execution completion, show summary in banner (#472) #494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,17 +1,17 @@ | ||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import { useState, useEffect, useCallback } from 'react'; | ||||||||||||||||||||||
| import { useState, useEffect, useCallback, useRef } from 'react'; | ||||||||||||||||||||||
| import { useParams, useRouter } from 'next/navigation'; | ||||||||||||||||||||||
| import Link from 'next/link'; | ||||||||||||||||||||||
| import { useExecutionMonitor } from '@/hooks/useExecutionMonitor'; | ||||||||||||||||||||||
| import { tasksApi } from '@/lib/api'; | ||||||||||||||||||||||
| import { tasksApi, gatesApi } from '@/lib/api'; | ||||||||||||||||||||||
| import { getSelectedWorkspacePath } from '@/lib/workspace-storage'; | ||||||||||||||||||||||
| import { ExecutionHeader } from '@/components/execution/ExecutionHeader'; | ||||||||||||||||||||||
| import { ProgressIndicator } from '@/components/execution/ProgressIndicator'; | ||||||||||||||||||||||
| import { EventStream } from '@/components/execution/EventStream'; | ||||||||||||||||||||||
| import { ChangesSidebar } from '@/components/execution/ChangesSidebar'; | ||||||||||||||||||||||
| import { Button } from '@/components/ui/button'; | ||||||||||||||||||||||
| import type { Task, CompletionBannerProps } from '@/types'; | ||||||||||||||||||||||
| import type { Task, CompletionBannerProps, GateResult } from '@/types'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export default function ExecutionPage() { | ||||||||||||||||||||||
| const params = useParams<{ taskId: string }>(); | ||||||||||||||||||||||
|
|
@@ -23,6 +23,12 @@ export default function ExecutionPage() { | |||||||||||||||||||||
| const [task, setTask] = useState<Task | null>(null); | ||||||||||||||||||||||
| const [taskError, setTaskError] = useState(false); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Gate auto-run state | ||||||||||||||||||||||
| const [gateResult, setGateResult] = useState<GateResult | null>(null); | ||||||||||||||||||||||
| const [gateRunning, setGateRunning] = useState(false); | ||||||||||||||||||||||
| const [gateError, setGateError] = useState(false); | ||||||||||||||||||||||
| const hasRunGatesRef = useRef(false); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Hydrate workspace path from localStorage | ||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
| setWorkspacePath(getSelectedWorkspacePath()); | ||||||||||||||||||||||
|
|
@@ -44,6 +50,21 @@ export default function ExecutionPage() { | |||||||||||||||||||||
| workspacePath | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Auto-run gates non-blocking when execution completes | ||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
| if (monitor.completionStatus !== 'completed' || !workspacePath || hasRunGatesRef.current) return; | ||||||||||||||||||||||
| hasRunGatesRef.current = true; | ||||||||||||||||||||||
| setGateRunning(true); | ||||||||||||||||||||||
| gatesApi.run(workspacePath) | ||||||||||||||||||||||
| .then(setGateResult) | ||||||||||||||||||||||
| .catch(() => setGateError(true)) | ||||||||||||||||||||||
| .finally(() => setGateRunning(false)); | ||||||||||||||||||||||
| }, [monitor.completionStatus, workspacePath]); | ||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Derive pending state immediately on first completed render (before effect commits) | ||||||||||||||||||||||
| const showGatePending = | ||||||||||||||||||||||
| monitor.completionStatus === 'completed' && !gateResult && !gateError; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Stop handler — may fail if run already completed or no active run | ||||||||||||||||||||||
| const handleStop = useCallback(async () => { | ||||||||||||||||||||||
| if (!workspacePath || !taskId) return; | ||||||||||||||||||||||
|
|
@@ -139,6 +160,9 @@ export default function ExecutionPage() { | |||||||||||||||||||||
| onViewChanges={() => router.push('/review')} | ||||||||||||||||||||||
| onBackToTasks={() => router.push('/tasks')} | ||||||||||||||||||||||
| onViewBlockers={() => router.push('/blockers')} | ||||||||||||||||||||||
| gateResult={gateResult} | ||||||||||||||||||||||
| gateRunning={gateRunning || showGatePending} | ||||||||||||||||||||||
| gateError={gateError} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -158,33 +182,83 @@ export default function ExecutionPage() { | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| // ── Completion Banner ───────────────────────────────────────────────── | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function GateSummary({ | ||||||||||||||||||||||
| gateRunning, | ||||||||||||||||||||||
| gateResult, | ||||||||||||||||||||||
| gateError, | ||||||||||||||||||||||
| }: { | ||||||||||||||||||||||
| gateRunning: boolean; | ||||||||||||||||||||||
| gateResult: CompletionBannerProps['gateResult']; | ||||||||||||||||||||||
| gateError: boolean; | ||||||||||||||||||||||
| }) { | ||||||||||||||||||||||
| if (gateRunning) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <p className="mt-2 text-xs text-green-700 dark:text-green-300"> | ||||||||||||||||||||||
| Running quality gates… | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if (gateError) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <p className="mt-2 text-xs text-amber-700 dark:text-amber-300"> | ||||||||||||||||||||||
| Gate check unavailable ·{' '} | ||||||||||||||||||||||
| <Link href="/review" className="underline hover:no-underline">View in Review →</Link> | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if (gateResult) { | ||||||||||||||||||||||
| const total = gateResult.checks.length; | ||||||||||||||||||||||
| const passed = gateResult.checks.filter((c) => c.status === 'PASSED').length; | ||||||||||||||||||||||
| if (gateResult.passed) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <p className="mt-2 text-xs text-green-700 dark:text-green-300"> | ||||||||||||||||||||||
| ✓ All {total} gate{total !== 1 ? 's' : ''} passed | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <p className="mt-2 text-xs text-amber-700 dark:text-amber-300"> | ||||||||||||||||||||||
| ⚠ {passed}/{total} gates passed ·{' '} | ||||||||||||||||||||||
| <Link href="/review" className="underline hover:no-underline">View full report →</Link> | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function CompletionBanner({ | ||||||||||||||||||||||
| status, | ||||||||||||||||||||||
| duration, | ||||||||||||||||||||||
| onViewProof, | ||||||||||||||||||||||
| onViewChanges, | ||||||||||||||||||||||
| onBackToTasks, | ||||||||||||||||||||||
| onViewBlockers, | ||||||||||||||||||||||
| gateResult, | ||||||||||||||||||||||
| gateRunning = false, | ||||||||||||||||||||||
| gateError = false, | ||||||||||||||||||||||
| }: CompletionBannerProps) { | ||||||||||||||||||||||
| const durationText = duration !== null ? `${Math.round(duration)}s` : ''; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (status === 'completed') { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <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"> | ||||||||||||||||||||||
| <p className="text-sm font-medium text-green-800 dark:text-green-200"> | ||||||||||||||||||||||
| Execution complete{durationText && ` in ${durationText}`}. Run PROOF9 gates to verify quality before shipping. | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| <div className="flex gap-2"> | ||||||||||||||||||||||
| <Button onClick={onViewProof} size="sm"> | ||||||||||||||||||||||
| Verify with PROOF9 | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| <Button onClick={onViewChanges} variant="outline" size="sm"> | ||||||||||||||||||||||
| View Changes | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| <Button onClick={onBackToTasks} variant="outline" size="sm"> | ||||||||||||||||||||||
| Back to Tasks | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| <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"> | ||||||||||||||||||||||
| <div className="flex items-center justify-between"> | ||||||||||||||||||||||
| <p className="text-sm font-medium text-green-800 dark:text-green-200"> | ||||||||||||||||||||||
| Execution complete{durationText && ` in ${durationText}`}. Run PROOF9 gates to verify quality before shipping. | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
|
Comment on lines
+244
to
+248
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The completed copy still implies a manual gate run. This state now auto-runs gates in the background, so “Run PROOF9 gates” is stale and conflicts with the summary rendered underneath. ✏️ Suggested wording- Execution complete{durationText && ` in ${durationText}`}. Run PROOF9 gates to verify quality before shipping.
+ Execution complete{durationText && ` in ${durationText}`}. Review the PROOF9 gate summary before shipping.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| <div className="flex gap-2"> | ||||||||||||||||||||||
| <Button onClick={onViewProof} size="sm"> | ||||||||||||||||||||||
| Verify with PROOF9 | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| <Button onClick={onViewChanges} variant="outline" size="sm"> | ||||||||||||||||||||||
| View Changes | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| <Button onClick={onBackToTasks} variant="outline" size="sm"> | ||||||||||||||||||||||
| Back to Tasks | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| <GateSummary gateRunning={gateRunning} gateResult={gateResult} gateError={gateError} /> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In Next.js App Router, when navigating between two/execution/[taskId]routes handled by the same clientpage.tsx, is local component state preserved acrosstaskIdchanges by default, or is the page remounted?💡 Result:
In the Next.js App Router, navigating from
/execution/1to/execution/2(i.e., changing the dynamic path param in the same[taskId]/page.tsx) will unmount and remount the page subtree, so local component state is not preserved by default. (github.com)Citations:
🏁 Script executed:
cat -n web-ui/src/app/execution/[taskId]/page.tsx | head -100Repository: frankbria/codeframe
Length of output: 4593
🏁 Script executed:
Repository: frankbria/codeframe
Length of output: 45
🏁 Script executed:
Repository: frankbria/codeframe
Length of output: 7865
Handle stale promise completions from
gatesApi.run()when navigating between tasks.The
gatesApi.run()call has no cleanup function, so if you navigate away (changingtaskId) before the promise resolves, the old promise will still callsetGateResult(),setGateError(), andsetGateRunning()on the unmounted component. Add a cleanup function with acancelledflag to prevent stale state updates.💡 Suggested fix
// Auto-run gates non-blocking when execution completes useEffect(() => { if (monitor.completionStatus !== 'completed' || !workspacePath || hasRunGatesRef.current) return; hasRunGatesRef.current = true; setGateRunning(true); + let cancelled = false; gatesApi.run(workspacePath) - .then(setGateResult) - .catch(() => setGateError(true)) - .finally(() => setGateRunning(false)); + .then((result) => { + if (!cancelled) setGateResult(result); + }) + .catch(() => { + if (!cancelled) setGateError(true); + }) + .finally(() => { + if (!cancelled) setGateRunning(false); + }); + + return () => { + cancelled = true; + }; }, [monitor.completionStatus, workspacePath]);🤖 Prompt for AI Agents