"use client"; import React from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ArrowRight, Bot, BrainCircuit, CheckCircle2, ListTodo, RefreshCw, RotateCcw, ShieldAlert, Upload } from "lucide-react"; import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, Progress, Switch, Textarea } from "@/components/ui"; import { autonomyApi, type AutoModePhase, type AutoModeState } from "@/shared/api/autonomyApi"; import { ApiError } from "@/shared/api/httpClient"; import { autoModeItemLabel, autoModePhaseLabel, autoModeStatusLabel, getAutoModeOperationalState } from "@/shared/lib/autoModePresentation"; import { autonomyProfileDefinitions, autonomyProfileOrder } from "@/shared/lib/autonomyProfiles"; import { showSuccess } from "@/utils/toast"; const phaseVariant = (phase: AutoModePhase) => phase.status === "completed" ? "default" : phase.status === "blocked" ? "destructive" : phase.status === "pending" ? "secondary" : "outline"; const readableError = (error: unknown) => error instanceof ApiError ? error.message : error instanceof Error ? error.message : "Onbekende fout."; const AutoMode = () => { const { projectId = "" } = useParams(); const navigate = useNavigate(); const [state, setState] = React.useState(); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(); const [roadmapTitle, setRoadmapTitle] = React.useState(""); const [roadmapMarkdown, setRoadmapMarkdown] = React.useState(""); const [confirmRoadmapReplacement, setConfirmRoadmapReplacement] = React.useState(false); const loadGeneration = React.useRef(0); const [policy, setPolicy] = React.useState({ profile: "normal" as AutoModeState["profile"], autoBrainstorm: true, maxBrainstormIterations: 5, maxConsecutiveFailures: 3, leaseMinutes: 30, maxWorkItemsPerRun: 8, maxRunMinutes: 45, maxRecoveryMinutes: 120 }); const load = React.useCallback(async (preservePolicy = false) => { if (!projectId) return; const generation = ++loadGeneration.current; setLoading(true); setError(undefined); try { const next = await autonomyApi.get(projectId); if (generation !== loadGeneration.current) return; setState(next); if (!preservePolicy) setPolicy({ profile: next.profile ?? "normal", autoBrainstorm: next.autoBrainstorm, maxBrainstormIterations: next.maxBrainstormIterations, maxConsecutiveFailures: next.maxConsecutiveFailures, leaseMinutes: next.leaseMinutes, maxWorkItemsPerRun: next.maxWorkItemsPerRun, maxRunMinutes: next.maxRunMinutes, maxRecoveryMinutes: next.maxRecoveryMinutes }); if (next.roadmap?.sourceMarkdown) { setRoadmapTitle((current) => current || next.roadmap?.title || ""); setRoadmapMarkdown((current) => current || next.roadmap?.sourceMarkdown || ""); } } catch (nextError) { if (generation === loadGeneration.current) setError(readableError(nextError)); } finally { if (generation === loadGeneration.current) setLoading(false); } }, [projectId]); React.useEffect(() => { void load(); return () => { loadGeneration.current += 1; }; }, [load]); const configure = async (patch: Parameters[1]) => { if (!projectId) return false; loadGeneration.current += 1; setLoading(false); setSaving(true); setError(undefined); try { const next = await autonomyApi.configure(projectId, patch); setState(next); if (patch.enabled === undefined) setPolicy({ profile: next.profile ?? "normal", autoBrainstorm: next.autoBrainstorm, maxBrainstormIterations: next.maxBrainstormIterations, maxConsecutiveFailures: next.maxConsecutiveFailures, leaseMinutes: next.leaseMinutes, maxWorkItemsPerRun: next.maxWorkItemsPerRun, maxRunMinutes: next.maxRunMinutes, maxRecoveryMinutes: next.maxRecoveryMinutes }); return true; } catch (nextError) { setError(readableError(nextError)); return false; } finally { setSaving(false); } }; const importRoadmap = async () => { if (!projectId || roadmapMarkdown.trim().length < 3) return; if (state?.roadmap && state.roadmap.sourceMarkdown.trim() === roadmapMarkdown.trim() && state.roadmap.title === roadmapTitle.trim()) { showSuccess("Deze roadmap is al opgeslagen. De voortgang blijft behouden."); return; } const replacesExistingRoadmap = Boolean(state?.roadmap && state.roadmap.sourceMarkdown.trim() !== roadmapMarkdown.trim()); if (replacesExistingRoadmap && !confirmRoadmapReplacement) { setConfirmRoadmapReplacement(true); return; } loadGeneration.current += 1; setLoading(false); setSaving(true); setError(undefined); try { setState(await autonomyApi.importRoadmap(projectId, { ...(roadmapTitle.trim() ? { title: roadmapTitle.trim() } : {}), markdown: roadmapMarkdown })); setConfirmRoadmapReplacement(false); showSuccess("Roadmap opgeslagen"); } catch (nextError) { setError(readableError(nextError)); } finally { setSaving(false); } }; const resume = async () => { if (!projectId) return; loadGeneration.current += 1; setLoading(false); setSaving(true); setError(undefined); try { setState(await autonomyApi.resume(projectId)); showSuccess("Auto Mode kan opnieuw werk claimen"); } catch (nextError) { setError(readableError(nextError)); } finally { setSaving(false); } }; const savePolicy = async () => { if (await configure(policy)) showSuccess("Autonomiebeleid opgeslagen"); }; const phases = state?.roadmap?.phases ?? []; const items = phases.flatMap((phase) => phase.items); const completedItems = items.filter((item) => item.status === "completed").length; const openItems = items.filter((item) => item.status !== "completed"); const progress = items.length ? Math.round((completedItems / items.length) * 100) : state?.status === "completed" ? 100 : 0; const activePhase = state?.roadmap ? phases[state.roadmap.currentPhaseIndex] : undefined; const inProgressItem = activePhase?.items.find((item) => item.status === "in_progress"); const activeItem = inProgressItem ?? activePhase?.items.find((item) => item.status !== "completed"); const nextItem = phases.flatMap((phase) => phase.items).find((item) => item.status === "pending"); const operational = state ? getAutoModeOperationalState(state) : undefined; const policyDirty = Boolean(state && (state.profile !== policy.profile || state.autoBrainstorm !== policy.autoBrainstorm || state.maxBrainstormIterations !== policy.maxBrainstormIterations || state.maxConsecutiveFailures !== policy.maxConsecutiveFailures || state.leaseMinutes !== policy.leaseMinutes || state.maxWorkItemsPerRun !== policy.maxWorkItemsPerRun || state.maxRunMinutes !== policy.maxRunMinutes || state.maxRecoveryMinutes !== policy.maxRecoveryMinutes)); const updatePolicyNumber = (key: "maxBrainstormIterations" | "maxConsecutiveFailures" | "leaseMinutes" | "maxWorkItemsPerRun" | "maxRunMinutes" | "maxRecoveryMinutes", value: string, minimum: number, maximum: number) => { const parsed = Number(value); if (Number.isFinite(parsed)) setPolicy((current) => ({ ...current, profile: "custom", [key]: Math.max(minimum, Math.min(maximum, Math.trunc(parsed))) })); }; const selectProfile = (profile: AutoModeState["profile"]) => { const settings = autonomyProfileDefinitions[profile].settings; setPolicy((current) => settings ? ({ ...current, profile, ...settings }) : ({ ...current, profile: "custom" })); }; React.useEffect(() => { if (!projectId) return; const interval = window.setInterval(() => { if (document.visibilityState === "visible" && !saving && !policyDirty) void load(true); }, 10_000); return () => window.clearInterval(interval); }, [load, policyDirty, projectId, saving]); if (loading && !state) return
Auto Mode laden…
; const controllerClass = operational?.tone === "blocked" ? "border-destructive/40" : operational?.tone === "retry" ? "border-amber-300" : operational?.tone === "active" ? "border-primary/50" : operational?.tone === "complete" ? "border-emerald-300" : "border-dashed"; return

Auto Mode

{state ? {autoModeStatusLabel[state.status]} : null}

ProjectBrain stuurt roadmap, afhankelijkheden en herstel; Gitea is de uitvoerings- en evidencebron voor repositorywerk.

{error ?
{error}
: null} {state && operational ?
{operational.tone === "blocked" ? : operational.tone === "retry" ? : }
{operational.label}Auto {state.enabled ? "aan" : "uit"}

{operational.headline}

{operational.detail}

void configure({ enabled })} />
{state.enabled ? : null}

Nu

{inProgressItem ? inProgressItem.title : state.activeLease ? "Compatibiliteitslease actief" : state.enabled ? "Wacht op Gitea-branch" : "Auto staat uit"}

Hierna

{nextItem?.title ?? (openItems.length ? activePhase?.title ?? "Volgende veilige stap wordt bepaald" : "Geen open committed werk")}

Voortgang

{completedItems}/{items.length} · {progress}%

Open

{openItems.length} werkitem{openItems.length === 1 ? "" : "s"}

{activePhase ? `Fase: ${activePhase.title}` : "Geen actieve fase"}

Repositorywerk wordt atomair geclaimd via projectbrain/auto/<work-id>. Een item wordt pas voltooid na succesvolle exact-head Gitea Actions-validatie en merge van de canonieke PR. Leases en callbacks blijven alleen voor compatibiliteit en herstel.

: null} {state?.blocker?.kind === "retryable" ?

Automatisch herstel

{state.blocker.reason} ProjectBrain probeert dit automatisch opnieuw; jouw actie is niet nodig.

{state.blocker.retryAt ?

Opnieuw uitvoerbaar vanaf {new Date(state.blocker.retryAt).toLocaleString("nl-BE")}

: null}
: null} {state?.blocker?.kind === "human" ?

Jouw beslissing of toegang nodig

{state.blocker.reason}

: null}
Roadmap en wachtrij

Committed werk

{state?.roadmap ? `${openItems.length} open · ${completedItems}/${items.length} voltooid` : "Nog geen roadmap of opdrachten."}

{progress}%
{phases.map((phase, index) => { const done = phase.items.filter((item) => item.status === "completed").length; return

{index + 1}. {phase.title}

{phase.objective}

{autoModePhaseLabel[phase.status]}

{done}/{phase.items.length} items

{phase.items.length ?
{phase.items.map((item) =>
{item.status === "completed" ? : }{item.title}

{autoModeItemLabel[item.status]} · {item.attempts} poging(en)

)}
: null}
; })}{!items.length ? : null}
Autonomiebeleid en geavanceerde instellingen
AutonomieprofielRoutinewerk automatiseren zonder echte risico- of productbeslissingen te verbergen.
{autonomyProfileOrder.map((profile) => { const definition = autonomyProfileDefinitions[profile]; return ; })}
Geavanceerde grenzen
updatePolicyNumber("maxBrainstormIterations", event.target.value, 1, 10)} />
updatePolicyNumber("maxConsecutiveFailures", event.target.value, 1, 10)} />
updatePolicyNumber("leaseMinutes", event.target.value, 5, 120)} />
updatePolicyNumber("maxWorkItemsPerRun", event.target.value, 1, 20)} />
updatePolicyNumber("maxRunMinutes", event.target.value, 5, 55)} />
updatePolicyNumber("maxRecoveryMinutes", event.target.value, 15, 1440)} />

Zoek na concrete taken nog naar echte gaten.

setPolicy((current) => ({ ...current, profile: "custom", autoBrainstorm: checked }))} />
Roadmap handmatig importerenAlleen voor geavanceerd beheer; gewone opdrachten gaan automatisch naar de duurzame wachtrij.
setRoadmapTitle(event.target.value)} />