feat: TaskDetailDialog — verify_result display + verify_only checkbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-01 12:51:14 +02:00
parent f33903d207
commit 0069c600f2

View file

@ -46,16 +46,25 @@ interface TaskDetailContentProps {
onClose: () => void
}
const VERIFY_RESULT_CONFIG: Record<string, { label: string; className: string }> = {
aligned: { label: 'Aligned', className: 'text-status-done' },
partial: { label: 'Gedeeltelijk', className: 'text-warning' },
divergent: { label: 'Divergent', className: 'text-error' },
empty: { label: 'Geen wijzigingen', className: 'text-muted-foreground' },
}
type SaveState = 'idle' | 'saving' | 'saved'
function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDetailContentProps) {
const { updatePlan } = useSoloStore()
const { updatePlan, updateVerifyOnly } = useSoloStore()
const job = useSoloStore(s => s.claudeJobsByTaskId[task.id])
const connectedWorkers = useSoloStore(s => s.connectedWorkers)
const [localPlan, setLocalPlan] = useState(task.implementation_plan ?? '')
const [localVerifyOnly, setLocalVerifyOnly] = useState(task.verify_only)
const [saveState, setSaveState] = useState<SaveState>('idle')
const [, startTransition] = useTransition()
const [jobPending, startJobTransition] = useTransition()
const [verifyOnlyPending, startVerifyOnlyTransition] = useTransition()
const fadeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const savedPlanRef = useRef(task.implementation_plan ?? '')
@ -111,6 +120,31 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
})
}
function handleVerifyOnlyToggle() {
if (isDemo) return
const newValue = !localVerifyOnly
setLocalVerifyOnly(newValue)
startVerifyOnlyTransition(async () => {
try {
const res = await fetch(`/api/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ verify_only: newValue }),
})
if (!res.ok) {
setLocalVerifyOnly(!newValue)
toast.error('Verify-only bijwerken mislukt')
return
}
updateVerifyOnly(task.id, newValue)
} catch {
setLocalVerifyOnly(!newValue)
toast.error('Verify-only bijwerken mislukt')
}
})
}
return (
<>
<DialogHeader>
@ -162,6 +196,30 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
</div>
</div>
<div className="flex items-center gap-2">
<DemoTooltip show={isDemo}>
<button
type="button"
role="checkbox"
aria-checked={localVerifyOnly}
onClick={handleVerifyOnlyToggle}
disabled={isDemo || verifyOnlyPending}
className={cn(
'h-4 w-4 rounded border border-border flex items-center justify-center shrink-0',
'disabled:cursor-not-allowed disabled:opacity-50',
localVerifyOnly && 'bg-primary border-primary',
)}
>
{localVerifyOnly && (
<svg className="h-3 w-3 text-primary-foreground" viewBox="0 0 12 12" fill="none">
<path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</button>
</DemoTooltip>
<span className="text-xs text-muted-foreground">Alleen verifiëren (niet implementeren)</span>
</div>
<div className="-mx-4 -mb-4 flex flex-wrap items-center gap-2 border-t bg-muted/50 px-4 py-3 rounded-b-xl">
<Link
href={`/products/${productId}/sprint/planning`}
@ -209,7 +267,7 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
)}
{job?.status === 'done' && (
<span className="text-xs text-status-done flex items-center gap-2">
<span className="text-xs text-status-done flex items-center gap-2 flex-wrap">
Klaar{job.branch && !job.pushed_at ? ` — branch ${job.branch}` : ''}
{job.pushed_at && job.branch && repoUrl && (
<a
@ -221,6 +279,26 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
Open op GitHub
</a>
)}
{job.verify_result && (() => {
const cfg = VERIFY_RESULT_CONFIG[job.verify_result]
return cfg ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={
<span className={cn('font-medium cursor-default', cfg.className)}>
{cfg.label}
</span>
} />
<TooltipContent side="top" className="max-w-xs text-xs">
{job.verify_result === 'aligned' && 'De implementatie komt overeen met het plan.'}
{job.verify_result === 'partial' && 'De implementatie wijkt gedeeltelijk af van het plan.'}
{job.verify_result === 'divergent' && 'De implementatie wijkt significant af van het plan.'}
{job.verify_result === 'empty' && 'Er zijn geen codewijzigingen gedetecteerd.'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : null
})()}
</span>
)}