import { useState } from "react"; import type { ReactNode } from "react"; import { applyPullMergeFix } from "~/fns/mergefix"; import type { MergeFix, PullParams } from "~/hooks/use-merge-fix"; import { useAction } from "~/hooks/use-action"; import { Badge } from "~/ui/Badge"; import { Button } from "~/ui/Button"; import { ErrorBanner } from "~/ui/Callout"; import { cn } from "~/utils/cn"; type FixFile = NonNullable["files"][number]; function percent(value: number): string { return `${Math.round(value * 100)}%`; } function DiffView({ diff }: { diff: string }) { return (
      {diff.split("\n").map((line, index) => (
        
{line || " "}
))}
); } function Reasoning({ file }: { file: Extract }) { const confidences = file.hunks.map((hunk) => (hunk.selected ? (hunk.probabilities[hunk.selected] ?? 0) : 0)); return (
risk: {file.decision.risk.level} needs a human: {percent(file.decision.needsHuman)} confidence per conflict: {confidences.map((value) => percent(value)).join(", ") || "n/a"} {file.decision.reasons.length === 0 ? ( No concerns raised ) : ( file.decision.reasons.map((reason) => ( {reason} )) )}
); } /** A reviewable proposal: per-file diff, reasoning and risk, with accept/reject per file before anything is pushed. */ export function MergeFixProposal({ params, fix, canApply, onApplied, renderHunks, }: { params: PullParams; fix: NonNullable; canApply: boolean; onApplied: () => void; renderHunks: (file: Extract) => ReactNode; }) { const apply = useAction(applyPullMergeFix, { invalidate: false }); const applicable = fix.files.flatMap((file) => file.status === "planned" && file.resolution !== null ? [file.path] : [], ); const [rejected, setRejected] = useState(new Set()); const accepted = applicable.filter((path) => !rejected.has(path)); const toggle = (path: string) => { const next = new Set(rejected); if (next.has(path)) next.delete(path); else next.add(path); setRejected(next); }; return (
{fix.files.map((file) => (
{file.status === "planned" && file.resolution !== null && ( )} {file.path} {file.status === "skipped" && {file.reason}} {file.status === "planned" && file.resolution === null && ( No clean resolution; resolve by hand )}
{file.status === "planned" && ( <> {file.diff && } {renderHunks(file)} )}
))} {canApply && applicable.length > 0 && (
{accepted.length < fix.files.length && ( Every conflicted file needs a resolution; edit the rest below before committing. )}
)}
); }