import { useEffect, useRef, useState } from "react"; import { getPullMergeFix, startPullMergeFix } from "~/fns/mergefix"; import { useAction } from "~/hooks/use-action"; import { Badge, Panel } from "~/components/ui/Primitives"; import { Button } from "~/components/ui/Button"; import { ErrorBanner } from "~/components/ui/Field"; import { cn } from "~/utils/cn"; type MergeFix = Awaited>; type PullParams = { owner: string; repo: string; number: number }; const POLL_MS = 4000; const ACTIVE = new Set(["queued", "running", "applying"]); const STATUS_COPY = { queued: "Queued", running: "Analyzing conflicts", applying: "Committing fix", applied: "Fix committed", suggested: "Suggestions ready", failed: "Failed", } as const; const REQUEUE_COPY = { waiting: "Rejoins the merge queue once CI passes", enqueued: "Back in the merge queue", gave_up: "Not re-queued", } as const; function percent(value: number | undefined): string { return `${Math.round((value ?? 0) * 100)}%`; } /** Polls the latest run while it is active and reports when it settles. */ function useMergeFix(params: PullParams, initial: MergeFix, onSettled: (fix: MergeFix) => void) { const [fix, setFix] = useState(initial); const active = fix !== null && ACTIVE.has(fix.status); const latest = useRef({ params, onSettled }); latest.current = { params, onSettled }; useEffect(() => { if (!active) return; const timer = setInterval(async () => { const next = await getPullMergeFix({ data: latest.current.params }).catch(() => null); if (!next) return; setFix(next); if (!ACTIVE.has(next.status)) latest.current.onSettled(next); }, POLL_MS); return () => clearInterval(timer); }, [active]); return [fix, setFix] as const; } export function MergeFixPanel({ params, initial, canStart, onPick, onApplied, }: { params: PullParams; initial: MergeFix; canStart: boolean; onPick: (path: string, hunk: number, text: string) => void; onApplied: () => void; }) { const start = useAction(startPullMergeFix, { invalidate: false }); const [fix, setFix] = useMergeFix(params, initial, (next) => { if (next?.status === "applied") onApplied(); }); const [picked, setPicked] = useState(new Map()); const busy = fix !== null && ACTIVE.has(fix.status); const pick = (path: string, hunk: number, id: string, text: string) => { setPicked(new Map(picked).set(`${path}#${hunk}`, id)); onPick(path, hunk, text); }; return ( { const result = await start.run({ data: params }); if (result) setFix(await getPullMergeFix({ data: params })); }} > {fix ? "Run again" : "Fix conflicts with AI"} ) } >
{!fix ? (

Generates candidate resolutions per conflict and asks a decision model to rank them. Confident, low-risk fixes are committed to the head branch and run through CI; anything else is offered here for review.

) : (
{STATUS_COPY[fix.status]} {fix.commitSha && {fix.commitSha.slice(0, 7)}} {fix.stale && The head branch changed since this run.} {fix.error && {fix.error}} {fix.requeue && ( {REQUEUE_COPY[fix.requeue.status]} {fix.requeue.error ? `: ${fix.requeue.error}` : ""} )} {fix.calls} model call(s)
)} {fix?.status === "suggested" && !fix.stale && fix.files.map((file) => (
{file.path} {file.status === "skipped" ? ( {file.reason} ) : ( <> risk: {file.decision.risk.level} {file.decision.reasons.map((reason) => ( {reason} ))} )}
{file.status === "planned" && file.hunks .map((hunk, index) => ({ hunk, index, key: `${file.path}#${index}` })) .map(({ hunk, index, key }) => (
Conflict {index + 1}
{[...hunk.candidates] .sort( (left, right) => (hunk.probabilities[right.id] ?? 0) - (hunk.probabilities[left.id] ?? 0), ) .map((candidate) => ( ))}
))}
))}
); }