"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { api, ApiError } from "@/lib/api";
import type { ImportReport } from "@/lib/types";
import { ProvenanceBadge } from "./provenance-badge";
/**
* Pulling a pull request in, and being honest about what came with it.
*
* An import is the one moment where it would be easiest to lie. GitHub hands
* over a commit author and a description, and it would take nothing to render
* those as provenance and rationale — which is exactly what every other tool
* does, and exactly what this project exists to refuse.
*
* So the report leads with the gaps. What arrived is a *synthetic* history:
* real source, real diff, an inferred hand, and a decision nobody has signed.
* The only way to close it is for a person to answer, which is what Jackie's
* interview is for.
*/
export function GithubImport() {
const router = useRouter();
const [owner, setOwner] = useState("BurntSushi");
const [repo, setRepo] = useState("ripgrep");
const [pr, setPr] = useState("3496");
const [token, setToken] = useState("");
const [busy, setBusy] = useState(false);
const [report, setReport] = useState<ImportReport | null>(null);
const [error, setError] = useState<string | null>(null);
const run = async () => {
setBusy(true);
setError(null);
setReport(null);
try {
const out = await api.importGithub({
owner: owner.trim(),
repo: repo.trim(),
pr: Number(pr),
token: token.trim() || undefined,
});
setReport(out);
} catch (e) {
setError(e instanceof ApiError ? e.message : "the import failed");
} finally {
setBusy(false);
}
};
const valid = owner.trim() && repo.trim() && Number(pr) > 0 && !busy;
return (
<div className="jac-page">
<p className="jac-eyebrow">Import</p>
<h1 className="jac-h1">Bring a pull request in</h1>
<p className="jac-lede">
The source and the diff come across intact. Provenance does not — GitHub
records who pushed, which is a different question from whose hands made
the work. What arrives is a synthetic history that says so, and a list
of what a person still has to answer.
</p>
<div className="jac-import-form">
<label className="jac-label" htmlFor="imp-owner">
owner
</label>
<input
id="imp-owner"
className="jac-select"
value={owner}
onChange={(e) => setOwner(e.target.value)}
/>
<label className="jac-label" htmlFor="imp-repo">
repo
</label>
<input
id="imp-repo"
className="jac-select"
value={repo}
onChange={(e) => setRepo(e.target.value)}
/>
<label className="jac-label" htmlFor="imp-pr">
pull request
</label>
<input
id="imp-pr"
className="jac-select"
inputMode="numeric"
value={pr}
onChange={(e) => setPr(e.target.value)}
/>
<label className="jac-label" htmlFor="imp-token">
token — optional
</label>
<input
id="imp-token"
className="jac-select"
type="password"
placeholder="for private repos or a higher rate limit"
value={token}
onChange={(e) => setToken(e.target.value)}
/>
<p className="jac-small">
The token is used for this request and never stored. It goes to
jac-serve, which talks to GitHub; the browser never holds it after
the call.
</p>
<button
type="button"
className="jac-btn jac-btn--primary"
disabled={!valid}
onClick={() => void run()}
>
{busy ? "Reading GitHub…" : "Import it"}
</button>
{error ? <p className="jac-small jac-error">{error}</p> : null}
</div>
{report ? <Report report={report} onOpen={(s) => router.push(`/repos/${s}`)} /> : null}
</div>
);
}
function Report({
report,
onOpen,
}: {
report: ImportReport;
onOpen: (slug: string) => void;
}) {
const { source, imported, inference, gaps } = report;
return (
<section className="jac-import-report">
<h2 className="jac-h2">
{source.owner}/{source.repo} #{source.pull_request}
</h2>
<p className="jac-small">
opened by <strong>{source.author}</strong> ·{" "}
<code className="jac-mono">{source.base.slice(0, 7)}</code> →{" "}
<code className="jac-mono">{source.head.slice(0, 7)}</code>
</p>
<div className="jac-import-cols">
<div className="jac-import-col">
<p className="jac-panel-label">What came across</p>
<ul className="jac-import-facts">
<li>
<b>{imported.files}</b> files, at both their base and head states
— so the diff is real, not a patch blob
</li>
<li>
<b>{imported.changed_on_github}</b> changed on GitHub
{imported.truncated ? " — more than fit, so this is a slice" : ""}
</li>
{imported.skipped.length ? (
<li>
<b>{imported.skipped.length}</b> skipped as binary or oversized
</li>
) : null}
<li>one decision, proposed and unsettled</li>
</ul>
</div>
<div className="jac-import-col">
<p className="jac-panel-label">What was inferred</p>
<div className="jac-import-inference" data-assumed={inference.assumed}>
<div className="jac-meta-row" style={{ marginTop: 0 }}>
<ProvenanceBadge provenance={inference.provenance} />
{inference.assumed ? (
<span className="jac-tag jac-tag--warn">assumed</span>
) : (
<span className="jac-tag">from evidence</span>
)}
</div>
<p className="jac-small">{inference.because}</p>
</div>
</div>
</div>
<div className="jac-import-gaps">
<p className="jac-panel-label">
What only a person can answer — {gaps.length}
</p>
<ol>
{gaps.map((g) => (
<li key={g.kind}>
<span className="jac-import-ask">{g.ask}</span>
<span className="jac-small">{g.why}</span>
</li>
))}
</ol>
</div>
<p className="jac-import-honesty">{report.honesty}</p>
<div className="jac-remark-actions">
<button
type="button"
className="jac-btn jac-btn--primary"
onClick={() => onOpen(report.slug)}
>
Walk it with Jackie →
</button>
<Link href={`/repos/${report.slug}/weave`} className="jac-btn">
See everything it made
</Link>
<Link href={`/repos/${report.slug}/decisions/${report.decision}`} className="jac-btn">
The unsigned decision
</Link>
</div>
</section>
);
}