Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/strata/assets/app.js

Raw
const workspace = document.querySelector("#workspace");
const status = document.querySelector("#status");
const cohorts = document.querySelector("#cohorts");
const diff = document.querySelector("#diff");
const allChanges = document.querySelector("#all-changes");
const notes = document.querySelector("#notes");
const questionInput = document.querySelector("#question");
const commentDialog = document.querySelector("#comment-dialog");
const commentForm = document.querySelector("#comment-form");
const askForm = document.querySelector("#ask-form");
const narrowLayout = window.matchMedia("(max-width: 900px)");
const keyboardHelp = document.querySelector("#keyboard-help");
const keyboardGuide = document.querySelector("#keyboard-guide");
const keyboardSearch = document.querySelector("#keyboard-search");

const NOTE_DEBOUNCE_MS = 300;
const EXPLICIT_JUMP_MS = 500;
const COMMANDS = [
	{
		group: "Navigation",
		keys: "j / k",
		label: "Next / previous line; scroll side pane",
	},
	{
		group: "Navigation",
		keys: "h / l",
		label: "Scroll focused pane horizontally",
	},
	{ group: "Navigation", keys: "Ctrl+d / Ctrl+u", label: "Scroll half a page" },
	{
		group: "Navigation",
		keys: "gg / G",
		label: "First / last line or pane edge",
		prefix: "g",
		continuation: "g",
		short: "first line or pane top",
	},
	{
		group: "Navigation",
		keys: "]c / [c",
		label: "Next / previous hunk",
		prefix: "]",
		continuation: "c",
		short: "next hunk",
	},
	{
		group: "Navigation",
		keys: "]l / [l",
		label: "Next / previous layer",
		prefix: "]",
		continuation: "l",
		short: "next layer",
	},
	{
		group: "Navigation",
		keys: "]u / [u",
		label: "Next / previous unreviewed hunk",
		prefix: "]",
		continuation: "u",
		short: "next unreviewed hunk",
	},
	{
		group: "Navigation",
		keys: "/ · n / N",
		label: "Search · next / previous match",
	},
	{
		group: "Workspace",
		keys: "gd / ga / gf",
		label: "Details / Ask / Feedback",
		prefix: "g",
		continuation: "d / a / f",
		short: "open Details / Ask / Feedback",
	},
	{
		group: "Review",
		keys: "Space r",
		label: "Toggle focused hunk reviewed",
		prefix: " ",
		continuation: "r",
		short: "toggle hunk reviewed",
	},
	{
		group: "Review",
		keys: "Space R",
		label: "Toggle selected layer reviewed",
		prefix: " ",
		continuation: "R",
		short: "toggle whole layer",
	},
	{
		group: "Writing",
		keys: "v · Shift-click",
		label: "Start or clear line range",
	},
	{
		group: "Writing",
		keys: "Space c",
		label: "Comment on focused line or selected range",
		prefix: " ",
		continuation: "c",
		short: "comment on focused line or range",
	},
	{
		group: "Writing",
		keys: "Space a",
		label: "Compose a question",
		prefix: " ",
		continuation: "a",
		short: "compose question",
	},
	{
		group: "Writing",
		keys: "Space n",
		label: "Edit review notes",
		prefix: " ",
		continuation: "n",
		short: "edit notes",
	},
	{
		group: "Workspace",
		keys: "Space h / Space l",
		label: "Previous / next visible pane",
		prefix: " ",
		continuation: "h / l",
		short: "focus previous / next pane",
	},
	{
		group: "Workspace",
		keys: "Space b / Space d",
		label: "Toggle layer sidebar / drawer",
		prefix: " ",
		continuation: "b / d",
		short: "toggle sidebar / drawer",
	},
	{
		group: "Workspace",
		keys: "Space s",
		label: "Confirm feedback submission",
		prefix: " ",
		continuation: "s",
		short: "send feedback…",
	},
	{
		group: "Workspace",
		keys: "Space f",
		label: "Confirm snapshot refresh",
		prefix: " ",
		continuation: "f",
		short: "refresh snapshot…",
	},
	{
		group: "Workspace",
		keys: "Space q",
		label: "Confirm review closure",
		prefix: " ",
		continuation: "q",
		short: "close review…",
	},
	{
		group: "Help",
		keys: "? / Escape",
		label: "Open help / dismiss transient UI",
	},
];
let review;
let threads = [];
let askDrafts = {};
let revision = 0;
let snapshotId = "";
let selectedLayerId = null;
let currentHunkId = null;
let commentTarget = null;
let observer;
let busy = false;
let askInFlight = false;
let pendingAsk = null;
let staleClient = false;
let noteTimer;
let notesPending = false;
let saveChain = Promise.resolve();
let draftGeneration = 0;
let acknowledgedDraft = "";
let layersOpen = !narrowLayout.matches;
let drawerOpen = !narrowLayout.matches;
let keyboardPane = "diff";
let currentLineKey = null;
let pendingPrefix = null;
let searchQuery = "";
let suppressObserverUntil = 0;
let explicitJumpLocked = false;
let renderedLines = [];
let currentLineRow = null;
let currentLineIndex = -1;
let renderedLineByKey = new Map();
let renderedLineIndexes = new WeakMap();
let renderedHunks = new Map();
let renderedHunkLines = new Map();
let forgeCommentStates = new Map();
let forgeInlineByAnchor = new Map();
const askErrors = new Map();
let rangeAnchor = null;
let selectedRangeRows = [];
let closing = false;
let readinessTimer;
let draftEpoch = 0;
let hunkIndex = new Map();
let layerIndex = new Map();
let hunkLayerIndex = new Map();
let reviewedHunks = new Set();
let mainView = "technical";
let diagramGeneration = 0;
let activeDiagram = null;

const byId = (id) => document.querySelector(`#${id}`);

function node(tag, className, text) {
	const value = document.createElement(tag);
	if (className) value.className = className;
	if (text !== undefined) value.textContent = text;
	return value;
}

function forgeStorageScope() {
	const forge = review?.forge;
	return forge ? `${forge.provider}:${forge.url}:` : "";
}

function storageKey() {
	return `strata:draft:${location.origin}:${forgeStorageScope()}${review?.snapshot.id ?? snapshotId}`;
}

function askStorageKey() {
	return `strata:questions:${location.origin}:${forgeStorageScope()}${review?.snapshot.id ?? snapshotId}`;
}

function persistDraft(draft) {
	try {
		sessionStorage.setItem(storageKey(), JSON.stringify(draft));
	} catch {}
}

function persistAskDrafts() {
	try {
		sessionStorage.setItem(askStorageKey(), JSON.stringify(askDrafts));
	} catch {}
}

function clearStoredDraft() {
	try {
		sessionStorage.removeItem(storageKey());
	} catch {}
}

function clearStoredAskDrafts() {
	try {
		sessionStorage.removeItem(askStorageKey());
	} catch {}
}

function recoveryKey() {
	return `strata:recovery:${location.origin}:${forgeStorageScope()}`;
}

function recoveryDraft(value) {
	try {
		const draft =
			typeof value?.text === "string" ? JSON.parse(value.text) : value?.draft;
		if (
			typeof value?.snapshotId === "string" &&
			Array.isArray(draft?.reviewed) &&
			Array.isArray(draft?.findings) &&
			typeof draft?.notes === "string"
		) {
			return { ...value, draft };
		}
	} catch {}
	return typeof value?.snapshotId === "string" &&
		typeof value?.text === "string"
		? { ...value, draft: null }
		: null;
}

function localRecoveries() {
	try {
		const stored = JSON.parse(sessionStorage.getItem(recoveryKey()));
		return (Array.isArray(stored) ? stored : [stored])
			.map(recoveryDraft)
			.filter(Boolean);
	} catch {
		return [];
	}
}

function rememberRecovery(draft, previousSnapshotId) {
	const value = {
		snapshotId: previousSnapshotId,
		draft: structuredClone(draft),
	};
	const pending = localRecoveries();
	if (
		!pending.some(
			(item) =>
				item.snapshotId === value.snapshotId &&
				draftSignature(item.draft) === draftSignature(value.draft),
		)
	) {
		if (pending.length >= 32)
			throw new Error(
				"Local recovery limit reached. Resolve retained drafts before closing.",
			);
		pending.push(value);
	}
	sessionStorage.setItem(recoveryKey(), JSON.stringify(pending));
	return value;
}

function forgetLocalRecovery(recovered) {
	const pending = localRecoveries().filter(
		(item) =>
			item.snapshotId !== recovered.snapshotId ||
			draftSignature(item.draft) !== draftSignature(recovered.draft),
	);
	if (pending.length)
		sessionStorage.setItem(recoveryKey(), JSON.stringify(pending));
	else sessionStorage.removeItem(recoveryKey());
}

function readRecovery() {
	const remote = Array.isArray(review?.recoveries)
		? review.recoveries.map(recoveryDraft).find(Boolean)
		: null;
	return localRecoveries()[0] ?? remote ?? null;
}

function renderRecovery() {
	const recovered = readRecovery();
	byId("local-recovery").hidden = !recovered;
	byId("recovery-content").textContent = recovered?.draft
		? JSON.stringify(
				{ snapshotId: recovered.snapshotId, draft: recovered.draft },
				null,
				2,
			)
		: (recovered?.text ?? "");
}

async function persistRecovery(draft, previousSnapshotId) {
	let value = { snapshotId: previousSnapshotId, draft: structuredClone(draft) };
	try {
		value = rememberRecovery(draft, previousSnapshotId);
	} catch {}
	renderRecovery();
	const text = JSON.stringify(value.draft);
	const payload = await request("/api/recovery", {
		recovery: { snapshotId: previousSnapshotId, text },
	});
	if (payload.review) review.recoveries = payload.review.recoveries;
	forgetLocalRecovery(value);
	renderRecovery();
	return review.recoveries.find(
		(item) => item.snapshotId === previousSnapshotId && item.text === text,
	);
}

function recoverableFinding(finding) {
	const hunk = hunkById(finding.hunkId);
	const end = finding.endLine ?? finding.line;
	if (
		!hunk ||
		!["old", "new"].includes(finding.side) ||
		!Number.isSafeInteger(finding.line) ||
		!Number.isSafeInteger(end) ||
		end < finding.line ||
		end - finding.line >= hunk.lines.length
	)
		return false;
	const present = new Set(
		hunk.lines.flatMap((line) => {
			const number = finding.side === "old" ? line.oldLine : line.newLine;
			return number === undefined ? [] : [number];
		}),
	);
	for (let line = finding.line; line <= end; line++) {
		if (!present.has(line)) return false;
	}
	return true;
}

async function mergeRecovery() {
	const recovered = readRecovery();
	if (!recovered || busy || staleClient || closing) return;
	if (!recovered.draft) {
		setStatus(
			"Recovery is raw text, not a valid draft. Copy it before discarding.",
			true,
		);
		return;
	}
	if (
		!window.confirm(
			"Merge compatible local findings, reviewed markers and notes into the current draft? Changed anchors remain in recovery.",
		)
	)
		return;
	const sameSnapshot = recovered.snapshotId === review.snapshot.id;
	const draft = currentDraft();
	const compatible = sameSnapshot
		? recovered.draft.findings.filter(
				(finding) =>
					recoverableFinding(finding) &&
					!draft.findings.some(
						(existing) =>
							existing.id === finding.id &&
							(existing.text !== finding.text ||
								existing.severity !== finding.severity),
					),
			)
		: [];
	for (const finding of compatible) {
		if (!draft.findings.some((existing) => existing.id === finding.id)) {
			draft.findings.push(finding);
		}
	}
	if (sameSnapshot)
		draft.reviewed = [
			...new Set([
				...draft.reviewed,
				...recovered.draft.reviewed.filter((id) => hunkById(id)),
			]),
		];
	if (
		recovered.draft.notes &&
		recovered.draft.notes !== draft.notes &&
		!draft.notes.includes(`\n\nRecovered notes:\n${recovered.draft.notes}`)
	) {
		draft.notes = draft.notes
			? `${draft.notes}\n\nRecovered notes:\n${recovered.draft.notes}`
			: recovered.draft.notes;
	}
	if (draft.notes.length > 20000) {
		setStatus(
			"Combined notes exceed the limit. Copy the local recovery text before discarding it.",
			true,
		);
		return;
	}
	if (noteTimer) clearTimeout(noteTimer);
	noteTimer = undefined;
	notesPending = false;
	notes.value = draft.notes;
	setBusy(true, "Merging local draft…");
	try {
		await queueDraftSave(draft);
		const remaining = {
			reviewed: recovered.draft.reviewed.filter(
				(id) => !sameSnapshot || !review.draft.reviewed.includes(id),
			),
			findings: recovered.draft.findings.filter(
				(finding) =>
					!compatible.includes(finding) &&
					!review.draft.findings.some(
						(existing) =>
							existing.id === finding.id &&
							existing.text === finding.text &&
							existing.severity === finding.severity &&
							existing.side === finding.side,
					),
			),
		};
		if (!remaining.reviewed.length && !remaining.findings.length) {
			if (recovered.id) {
				const payload = await request("/api/recovery", {
					remove: recovered.id,
				});
				if (payload.review) review.recoveries = payload.review.recoveries;
			}
			forgetLocalRecovery(recovered);
		}
		reviewedHunks = new Set(review.draft.reviewed);
		renderFindings();
		updateReviewControls(review.draft.reviewed);
		renderRecovery();
		setStatus(
			remaining.findings.length || remaining.reviewed.length
				? "Local notes merged. Changed comment anchors and review markers remain in recovery; re-anchor or discard them manually."
				: "Local draft merged",
		);
	} catch (error) {
		reportError(error);
	} finally {
		setBusy(false);
	}
}

function storedDraft() {
	try {
		const value = sessionStorage.getItem(storageKey());
		return value ? JSON.parse(value) : undefined;
	} catch {
		return undefined;
	}
}

function restoreAskDrafts() {
	try {
		const value = sessionStorage.getItem(askStorageKey());
		const parsed = value ? JSON.parse(value) : undefined;
		if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
		askDrafts = Object.fromEntries(
			Object.entries(parsed).filter(
				([key, draft]) =>
					typeof key === "string" &&
					typeof draft === "string" &&
					draft.length <= 4000,
			),
		);
	} catch {}
}

function applyVersion(payload) {
	if (Number.isSafeInteger(payload.revision) && payload.revision >= 0) {
		revision = payload.revision;
	}
	if (typeof payload.snapshotId === "string" && payload.snapshotId) {
		snapshotId = payload.snapshotId;
	}
}

function applyThreads(payload) {
	if (Array.isArray(payload.threads)) threads = payload.threads;
}

async function request(path, body) {
	const epoch = draftEpoch;
	const isPost = body !== undefined;
	const headers = isPost ? { "Content-Type": "application/json" } : {};
	if (isPost && path !== "/api/close") {
		headers["If-Match"] = String(revision);
		headers["X-Strata-Snapshot"] = snapshotId;
	}
	const response = await fetch(path, {
		method: isPost ? "POST" : "GET",
		headers,
		body: isPost ? JSON.stringify(body) : undefined,
	});
	let payload;
	try {
		payload = await response.json();
	} catch {
		throw new Error(`Request failed (${response.status})`);
	}
	if (!response.ok) {
		if (payload.code === "stale" && payload.snapshotId === review?.snapshot.id)
			applyVersion(payload);
		const error = new Error(
			payload.error || `Request failed (${response.status})`,
		);
		error.code = payload.code;
		throw error;
	}
	if (epoch === draftEpoch) {
		if (path !== "/api/state" && !(path === "/api/recovery" && body?.recovery))
			applyVersion(payload);
		applyThreads(payload);
	}
	return payload;
}

function setStatus(message, error = false) {
	if (closing && message !== "Review closed. You may close this tab.") return;
	status.textContent = message;
	status.classList.toggle("error", error);
}

function reportError(error) {
	if (
		closing ||
		(error?.draftGeneration !== undefined &&
			error.draftGeneration !== draftGeneration)
	)
		return;
	if (error?.code === "stale_client") {
		staleClient = true;
		++draftEpoch;
		persistDraft(currentDraft());
		setStatus(
			"Review changed in another tab. Refresh to recover these notes.",
			true,
		);
		return;
	}
	setStatus(error instanceof Error ? error.message : "Request failed", true);
}

function isNavigationControl(control) {
	return (
		control.id === "close-button" ||
		control.id === "refresh-button" ||
		control.id === "keyboard-help-button" ||
		control.id === "keyboard-help-close" ||
		control.id === "layers-toggle" ||
		control.id === "drawer-toggle" ||
		control.id === "all-changes" ||
		control.hasAttribute("data-layer-id") ||
		control.hasAttribute("data-drawer-tab")
	);
}

function setBusy(nextBusy, message) {
	if (closing) return;
	busy = nextBusy;
	if (nextBusy) cancelPendingPrefix();
	workspace.setAttribute("aria-busy", String(nextBusy));
	for (const control of document.querySelectorAll(
		"button, textarea, select, input",
	)) {
		if (!isNavigationControl(control)) control.disabled = nextBusy;
	}
	updateDetails();
	updateAskControls();
	updateLayerAction();
	updateKeyboardStatus();
	if (message) setStatus(message);
}

function sourceLabel() {
	const source = review.snapshot.source;
	const shortId = review.snapshot.id.slice(0, 12);
	if (review.forge)
		return `${review.forge.providerLabel} #${review.forge.number} · ${shortId}`;
	if (source.kind === "base") return `base ${source.ref} · ${shortId}`;
	return `${source.kind} · ${shortId}`;
}

function safeHttpsUrl(value) {
	if (typeof value !== "string") return null;
	try {
		const url = new URL(value);
		if (url.protocol !== "https:" || url.username || url.password) return null;
		return url.href;
	} catch {
		return null;
	}
}

function setSafeLink(link, value) {
	const href = safeHttpsUrl(value);
	link.hidden = href === null;
	if (href) link.href = href;
	else link.removeAttribute("href");
	return href !== null;
}

function forgeAnchorKey(hunkId, side, line) {
	return `${hunkId}\u0000${side}\u0000${line}`;
}

function forgeCheckoutMatches() {
	return (
		review?.forge &&
		review.snapshot.source.kind === "base" &&
		review.snapshot.source.ref === review.forge.baseSha &&
		review.snapshot.head === review.forge.headSha
	);
}

function indexForgeComments() {
	forgeCommentStates = new Map();
	forgeInlineByAnchor = new Map();
	const forge = review.forge;
	if (!forge) return;
	const anchors = new Map();
	for (const hunk of review.snapshot.hunks) {
		for (const line of hunk.lines) {
			if (line.oldLine !== undefined) {
				anchors.set(
					`${hunk.oldPath ?? hunk.path}\u0000old\u0000${line.oldLine}`,
					forgeAnchorKey(hunk.id, "old", line.oldLine),
				);
			}
			if (line.newLine !== undefined) {
				anchors.set(
					`${hunk.path}\u0000new\u0000${line.newLine}`,
					forgeAnchorKey(hunk.id, "new", line.newLine),
				);
			}
		}
	}
	for (const comment of forge.comments) {
		const anchor =
			comment.kind === "inline" &&
			comment.path &&
			comment.side &&
			comment.line !== undefined
				? anchors.get(
						`${comment.path}\u0000${comment.side}\u0000${comment.line}`,
					)
				: undefined;
		const outdated =
			comment.kind === "inline" &&
			(comment.outdated || !forgeCheckoutMatches());
		const state = { anchor, outdated, current: Boolean(anchor) && !outdated };
		forgeCommentStates.set(comment, state);
		if (state.current) {
			const comments = forgeInlineByAnchor.get(anchor) ?? [];
			comments.push(comment);
			forgeInlineByAnchor.set(anchor, comments);
		}
	}
}

function appendForgeSource(container, forge) {
	container.replaceChildren();
	if (forge.provider.toLocaleLowerCase() === "github") {
		const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
		svg.setAttribute("viewBox", "0 0 16 16");
		svg.setAttribute("aria-hidden", "true");
		svg.classList.add("forge-source-icon");
		const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
		path.setAttribute(
			"d",
			"M8 0C3.58 0 0 3.64 0 8.13c0 3.59 2.29 6.64 5.47 7.71.4.08.55-.18.55-.39 0-.19-.01-.83-.01-1.5-2.01.38-2.53-.5-2.69-.96-.09-.23-.48-.96-.82-1.15-.28-.15-.68-.53-.01-.54.63-.01 1.08.59 1.23.83.72 1.23 1.87.88 2.33.67.07-.53.28-.88.51-1.08-1.78-.21-3.64-.91-3.64-4.02 0-.89.31-1.62.82-2.19-.08-.2-.36-1.04.08-2.16 0 0 .67-.22 2.2.84A7.5 7.5 0 0 1 8 3.92a7.5 7.5 0 0 1 2 .27c1.53-1.06 2.2-.84 2.2-.84.44 1.12.16 1.96.08 2.16.51.57.82 1.3.82 2.19 0 3.12-1.87 3.81-3.65 4.02.29.25.54.74.54 1.51 0 1.09-.01 1.97-.01 2.24 0 .21.15.47.55.39A8.14 8.14 0 0 0 16 8.13C16 3.64 12.42 0 8 0Z",
		);
		svg.append(path);
		container.append(svg);
	}
	container.append(node("span", "", forge.providerLabel));
}

function rebuildReviewIndexes() {
	hunkIndex = new Map(review.snapshot.hunks.map((hunk) => [hunk.id, hunk]));
	layerIndex = new Map();
	hunkLayerIndex = new Map();
	for (const cohort of review.plan.cohorts) {
		for (const layer of cohort.layers) {
			layerIndex.set(layer.id, layer);
			for (const hunk of layer.hunks) hunkLayerIndex.set(hunk.id, layer);
		}
	}
	reviewedHunks = new Set(review.draft.reviewed);
}

function layers() {
	return [...layerIndex.values()];
}

function layerById(id) {
	return layerIndex.get(id);
}

function layerForHunk(hunkId) {
	return hunkLayerIndex.get(hunkId);
}

function hunkById(id) {
	return hunkIndex.get(id);
}

function visibleHunks() {
	if (!selectedLayerId) return review.snapshot.hunks;
	return (layerById(selectedLayerId)?.hunks ?? [])
		.map((reference) => hunkById(reference.id))
		.filter(Boolean);
}

function scopeKey(layerId = selectedLayerId) {
	return JSON.stringify(layerId);
}

function scopeTitle(layerId = selectedLayerId) {
	return layerId ? (layerById(layerId)?.title ?? layerId) : "All changes";
}

function threadForScope(layerId = selectedLayerId) {
	return threads.find((thread) => thread.layerId === layerId);
}

function currentDraft() {
	return {
		reviewed: [...review.draft.reviewed],
		findings: review.draft.findings.map((finding) => ({ ...finding })),
		notes: notes.value,
	};
}

function paneElement(name = keyboardPane) {
	return document.querySelector(`[data-keyboard-pane="${name}"]`);
}

function visiblePaneNames() {
	return ["layers", "diff", "drawer"].filter((name) => {
		const pane = paneElement(name);
		return pane && !pane.hidden;
	});
}

function setKeyboardPane(name, focus = false) {
	if (!visiblePaneNames().includes(name)) name = "diff";
	keyboardPane = name;
	for (const pane of document.querySelectorAll("[data-keyboard-pane]")) {
		pane.classList.toggle(
			"keyboard-focused",
			pane.dataset.keyboardPane === name,
		);
	}
	if (focus) paneElement()?.focus({ preventScroll: true });
	updateKeyboardStatus();
}

function anchorableLines() {
	return renderedLines;
}

function lineKey(row) {
	return row
		? `${row.dataset.hunkId}:${row.dataset.side}:${row.dataset.line}`
		: null;
}

function currentLine() {
	if (currentLineRow?.isConnected) return currentLineRow;
	currentLineRow = renderedLineByKey.get(currentLineKey) ?? null;
	currentLineIndex = currentLineRow
		? (renderedLineIndexes.get(currentLineRow) ?? -1)
		: -1;
	return currentLineRow;
}

function lineDescription(row = currentLine()) {
	if (!row) return "no lines";
	const hunk = hunkById(row.dataset.hunkId);
	return `${hunk?.path ?? row.dataset.hunkId} · line ${row.dataset.line}`;
}

function clearRangeSelection() {
	rangeAnchor = null;
	for (const row of selectedRangeRows) row.classList.remove("range-selected");
	selectedRangeRows = [];
}

function updateRangeSelection() {
	if (!rangeAnchor && selectedRangeRows.length === 0) return false;
	for (const row of selectedRangeRows) row.classList.remove("range-selected");
	selectedRangeRows = [];
	const current = currentLine();
	if (!rangeAnchor || !current) return false;
	if (
		rangeAnchor.hunkId !== current.dataset.hunkId ||
		rangeAnchor.side !== current.dataset.side
	) {
		return false;
	}
	const start = Math.min(rangeAnchor.line, Number(current.dataset.line));
	const end = Math.max(rangeAnchor.line, Number(current.dataset.line));
	for (const row of renderedHunkLines.get(rangeAnchor.hunkId) ?? []) {
		const line = Number(row.dataset.line);
		if (row.dataset.side === rangeAnchor.side && line >= start && line <= end) {
			row.classList.add("range-selected");
			selectedRangeRows.push(row);
		}
	}
	return true;
}

function toggleRangeSelection() {
	const row = currentLine();
	if (!row) return;
	if (
		rangeAnchor?.hunkId === row.dataset.hunkId &&
		rangeAnchor.side === row.dataset.side &&
		rangeAnchor.line === Number(row.dataset.line)
	) {
		clearRangeSelection();
		setStatus("Line range cleared");
		return;
	}
	rangeAnchor = {
		hunkId: row.dataset.hunkId,
		side: row.dataset.side,
		line: Number(row.dataset.line),
	};
	updateRangeSelection();
	setStatus("Range start selected; move to an endpoint, then Space c");
}

function updateKeyboardStatus() {
	const editing = isEditable(document.activeElement);
	byId("keyboard-mode").textContent = busy
		? "BUSY"
		: editing
			? "TEXT"
			: "NORMAL";
	byId("keyboard-cursor").textContent = editing
		? `Editing · ${document.activeElement?.getAttribute("aria-label") || document.activeElement?.id || "text"}`
		: `${keyboardPane[0].toUpperCase()}${keyboardPane.slice(1)} · ${lineDescription()}`;
	byId("keyboard-context").hidden = editing;
	byId("keyboard-return-hint").hidden = !editing;
}

function beginExplicitJump() {
	suppressObserverUntil = performance.now() + EXPLICIT_JUMP_MS;
	explicitJumpLocked = true;
}

function clearCurrentLine() {
	currentLineRow?.classList.remove("keyboard-current");
	currentLineKey = null;
	currentLineRow = null;
	currentLineIndex = -1;
}

function setCurrentLine(row, scroll = false) {
	if (!row?.matches(".diff-line[data-line]")) return;
	if (scroll) beginExplicitJump();
	if (currentLineRow !== row) {
		currentLineRow?.classList.remove("keyboard-current");
		row.classList.add("keyboard-current");
	}
	currentLineRow = row;
	currentLineKey = lineKey(row);
	currentLineIndex = renderedLineIndexes.get(row) ?? -1;
	selectCurrentHunk(row.dataset.hunkId, false, false);
	setKeyboardPane("diff");
	if (scroll) {
		row.scrollIntoView({
			block: "center",
			inline: "nearest",
			behavior: "instant",
		});
		row.focus({ preventScroll: true });
	}
	updateRangeSelection();
	updateKeyboardStatus();
}

function ensureCurrentLine(preferredHunkId) {
	const preferredLines = renderedHunkLines.get(preferredHunkId) ?? [];
	const retained = renderedLineByKey.get(currentLineKey);
	if (retained?.dataset.hunkId === preferredHunkId) {
		setCurrentLine(retained, false);
		return;
	}
	if (preferredHunkId && renderedHunks.has(preferredHunkId)) {
		if (preferredLines.length > 0) setCurrentLine(preferredLines[0], false);
		else {
			clearCurrentLine();
			updateKeyboardStatus();
		}
		return;
	}
	if (renderedLines.length > 0) setCurrentLine(renderedLines[0], false);
	else {
		clearCurrentLine();
		updateKeyboardStatus();
	}
}

function isEditable(target) {
	return (
		target instanceof HTMLInputElement ||
		target instanceof HTMLTextAreaElement ||
		target instanceof HTMLSelectElement ||
		target?.isContentEditable
	);
}

function renderKeyboardHelp() {
	const content = byId("keyboard-help-content");
	content.replaceChildren();
	for (const groupName of [
		...new Set(COMMANDS.map((command) => command.group)),
	]) {
		const group = node("section", "keyboard-help-group");
		group.append(node("h3", "", groupName));
		for (const command of COMMANDS.filter(
			(candidate) => candidate.group === groupName,
		)) {
			const row = node("div", "keyboard-help-row");
			row.append(node("span", "", command.label));
			const keys = node("span", "keyboard-help-keys");
			const keyLabels = command.keys.split(/\s+\/\s+|\s*·\s*/);
			for (const key of keyLabels) keys.append(node("kbd", "", key));
			row.append(keys);
			group.append(row);
		}
		content.append(group);
	}
}

function showPendingPrefix(prefix) {
	pendingPrefix = prefix;
	keyboardGuide.replaceChildren();
	const heading = node(
		"strong",
		"keyboard-guide-heading",
		prefix === " " ? "Space then…" : `${prefix} then…`,
	);
	keyboardGuide.append(heading);
	const commandPrefix = prefix === "[" ? "]" : prefix;
	const commands = COMMANDS.filter(
		(command) => command.prefix === commandPrefix,
	);
	for (const groupName of [
		...new Set(commands.map((command) => command.group)),
	]) {
		const group = node("div", "keyboard-guide-group");
		group.append(node("span", "keyboard-guide-group-name", groupName));
		for (const command of commands.filter(
			(candidate) => candidate.group === groupName,
		)) {
			const item = node("span", "keyboard-guide-command");
			const description =
				prefix === "["
					? command.short.replace(/^next\b/, "previous")
					: command.short;
			item.append(node("kbd", "", command.continuation), ` ${description}`);
			group.append(item);
		}
		keyboardGuide.append(group);
	}
	keyboardGuide.append(node("span", "muted", "Esc cancels"));
	keyboardGuide.hidden = false;
	updateKeyboardStatus();
}

function cancelPendingPrefix() {
	pendingPrefix = null;
	keyboardGuide.hidden = true;
	keyboardGuide.replaceChildren();
}

function showHelp() {
	cancelPendingPrefix();
	if (!keyboardHelp.open) keyboardHelp.showModal();
}

function openSearch() {
	cancelPendingPrefix();
	keyboardSearch.hidden = false;
	keyboardSearch.value = searchQuery;
	keyboardSearch.focus();
	keyboardSearch.select();
	updateKeyboardStatus();
}

function closeSearch(focusDiff = true) {
	searchQuery = keyboardSearch.value;
	keyboardSearch.hidden = true;
	if (focusDiff) currentLine()?.focus({ preventScroll: true });
	setKeyboardPane("diff");
}

function searchLines(direction = 1) {
	searchQuery = keyboardSearch.value || searchQuery;
	if (!searchQuery) return;
	const query = searchQuery.toLocaleLowerCase();
	const matches = anchorableLines().filter((row) =>
		row
			.querySelector(".line-code")
			?.textContent.toLocaleLowerCase()
			.includes(query),
	);
	if (matches.length === 0) {
		setStatus(`No matches for “${searchQuery}” in visible diff lines`, true);
		return;
	}
	const current = matches.indexOf(currentLine());
	const next =
		current < 0
			? direction > 0
				? 0
				: matches.length - 1
			: (current + direction + matches.length) % matches.length;
	setCurrentLine(matches[next], true);
	setStatus(`${next + 1} / ${matches.length} matches for “${searchQuery}”`);
}

function draftSignature(draft) {
	return JSON.stringify(draft);
}

function queueDraftSave(draft) {
	const copy = structuredClone(draft);
	const generation = ++draftGeneration;
	const epoch = draftEpoch;
	review.draft = copy;
	persistDraft(copy);
	if (staleClient) {
		setStatus(
			"Review changed in another tab. Refresh to recover these notes.",
			true,
		);
		return Promise.reject(
			new Error("Draft requires explicit stale-client recovery"),
		);
	}
	const pending = saveChain.then(async () => {
		try {
			if (epoch !== draftEpoch || staleClient || closing)
				throw new Error("Draft save superseded by newer review state");
			const payload = await request("/api/draft", copy);
			if (epoch !== draftEpoch || closing)
				throw new Error("Draft save superseded by newer review state");
			acknowledgedDraft = draftSignature(payload.draft);
			if (generation === draftGeneration) {
				review.draft = payload.draft;
				reviewedHunks = new Set(payload.draft.reviewed);
				persistDraft(currentDraft());
				updateProgress();
				setStatus("Draft saved");
			}
			return payload.draft;
		} catch (error) {
			if (error && typeof error === "object")
				error.draftGeneration = generation;
			throw error;
		}
	});
	saveChain = pending.catch((error) => {
		reportError(error);
	});
	return pending;
}

function scheduleNotesSave() {
	notesPending = true;
	if (noteTimer) clearTimeout(noteTimer);
	noteTimer = setTimeout(() => {
		noteTimer = undefined;
		notesPending = false;
		queueDraftSave(currentDraft()).catch(() => {});
	}, NOTE_DEBOUNCE_MS);
}

async function flushDraft() {
	if (noteTimer) clearTimeout(noteTimer);
	noteTimer = undefined;
	if (notesPending) notesPending = false;
	if (draftSignature(currentDraft()) !== acknowledgedDraft)
		await queueDraftSave(currentDraft());
	await saveChain;
	if (staleClient)
		throw new Error(
			"Review changed in another tab. Refresh to recover these notes.",
		);
	if (draftSignature(currentDraft()) !== acknowledgedDraft) {
		await queueDraftSave(currentDraft());
		if (draftSignature(currentDraft()) !== acknowledgedDraft)
			throw new Error("Draft save was not acknowledged");
	}
}

function applyPanelState() {
	const layerNav = byId("layer-nav");
	const detailRegion = byId("detail-region");
	layerNav.hidden = !layersOpen;
	detailRegion.hidden = !drawerOpen;
	workspace.classList.toggle("layers-collapsed", !layersOpen);
	workspace.classList.toggle("drawer-collapsed", !drawerOpen);
	byId("layers-toggle").setAttribute("aria-expanded", String(layersOpen));
	byId("layers-toggle").setAttribute(
		"aria-label",
		layersOpen ? "Hide review layers" : "Show review layers",
	);
	byId("drawer-toggle").setAttribute("aria-expanded", String(drawerOpen));
	byId("drawer-toggle").setAttribute(
		"aria-label",
		drawerOpen ? "Hide review drawer" : "Show review drawer",
	);
	if (!visiblePaneNames().includes(keyboardPane)) setKeyboardPane("diff");
	else setKeyboardPane(keyboardPane);
	if (review) {
		observer?.disconnect();
		observeHunks();
	}
}

function hasNarrativePlan() {
	return Boolean(review?.plan?.overview || review?.plan?.architecture);
}

function cancelDiagram() {
	++diagramGeneration;
	activeDiagram = null;
	byId("architecture-panel").replaceChildren();
}

function diagramFallback(message) {
	const fallback = node("p", "muted", message);
	fallback.setAttribute("role", "status");
	return fallback;
}

function renderOverview() {
	const panel = byId("overview-panel");
	panel.replaceChildren();
	const overview = review?.plan?.overview;
	if (!overview) {
		panel.append(diagramFallback("No overview is available for this review."));
		return;
	}
	for (const [heading, value] of [
		["Goal", overview.goal],
		["Intent", overview.intent],
	]) {
		const section = node("section", "narrative-section");
		section.append(node("h2", "narrative-heading", heading));
		section.append(node("p", "prose", value.text));
		section.append(node("p", "provenance", `Source: ${value.source}`));
		panel.append(section);
	}
	for (const [heading, value] of [
		["Before", overview.before],
		["After", overview.after],
	]) {
		const section = node("section", "narrative-section");
		section.append(node("h2", "narrative-heading", heading));
		section.append(node("p", "prose", value));
		panel.append(section);
	}
}

function renderArchitecture() {
	const panel = byId("architecture-panel");
	panel.replaceChildren();
	const architecture = review?.plan?.architecture;
	if (!architecture) {
		panel.append(
			diagramFallback("No architecture summary is available for this review."),
		);
		return;
	}
	panel.append(node("p", "prose", architecture.summary));
	if (typeof architecture.diagram !== "string" || !architecture.diagram) return;
	const generation = diagramGeneration;
	const frame = document.createElement("iframe");
	frame.className = "architecture-diagram";
	frame.title = "Architecture diagram";
	frame.src = "/diagram";
	frame.sandbox.add("allow-scripts");
	frame.setAttribute("referrerpolicy", "no-referrer");
	frame.hidden = true;
	frame.addEventListener("load", () => {
		if (generation !== diagramGeneration || frame.contentWindow === null)
			return;
		frame.contentWindow.postMessage(
			{
				type: "strata.diagram.render",
				source: architecture.diagram,
				dark: window.matchMedia("(prefers-color-scheme: dark)").matches,
			},
			"*",
		);
	});
	panel.append(frame);
	const fallback = diagramFallback(
		"Diagram unavailable. Architecture summary remains available.",
	);
	fallback.hidden = true;
	panel.append(fallback);
	activeDiagram = { generation, frame, fallback, source: architecture.diagram };
}

window.addEventListener("message", (event) => {
	const active = activeDiagram;
	if (
		!active ||
		active.generation !== diagramGeneration ||
		event.origin !== "null" ||
		event.source !== active.frame.contentWindow ||
		!event.data ||
		typeof event.data !== "object"
	)
		return;
	if (event.data.type === "strata.diagram.ready") {
		active.frame.contentWindow?.postMessage(
			{
				type: "strata.diagram.render",
				source: active.source,
				dark: window.matchMedia("(prefers-color-scheme: dark)").matches,
			},
			"*",
		);
	} else if (event.data.type === "strata.diagram.size") {
		const height = Number(event.data.height);
		if (Number.isFinite(height)) {
			active.frame.hidden = false;
			active.frame.style.height = `${Math.max(120, Math.min(1000, height))}px`;
		}
	} else if (event.data.type === "strata.diagram.error") {
		active.frame.hidden = true;
		active.fallback.hidden = false;
	}
});

function selectMainView(name, focus = false) {
	if (!["overview", "architecture", "technical"].includes(name)) return;
	if (name !== "architecture") cancelDiagram();
	mainView = name;
	for (const tab of document.querySelectorAll("[data-main-view]")) {
		const selected = tab.dataset.mainView === name;
		tab.setAttribute("aria-selected", String(selected));
		tab.tabIndex = selected ? 0 : -1;
		if (selected && focus) tab.focus();
	}
	for (const panel of document.querySelectorAll(".main-view-panel")) {
		panel.hidden = panel.id !== `${name}-panel`;
	}
	if (name === "overview") renderOverview();
	if (name === "architecture") renderArchitecture();
}

function selectDrawerTab(name, focus = false) {
	drawerOpen = true;
	for (const tab of document.querySelectorAll("[data-drawer-tab]")) {
		const selected = tab.dataset.drawerTab === name;
		tab.setAttribute("aria-selected", String(selected));
		tab.tabIndex = selected ? 0 : -1;
		if (selected && focus) tab.focus();
	}
	for (const panel of document.querySelectorAll(".drawer-panel")) {
		panel.hidden = panel.id !== `${name}-panel`;
	}
	applyPanelState();
	if (name === "ask") renderAsk();
}

function renderNavigation() {
	cohorts.replaceChildren();
	allChanges.classList.toggle("selected", selectedLayerId === null);
	allChanges.setAttribute(
		"aria-current",
		selectedLayerId === null ? "page" : "false",
	);
	allChanges.setAttribute("aria-label", "All changes");
	byId("all-count").textContent = String(review.snapshot.hunks.length);
	for (const cohort of review.plan.cohorts) {
		const group = node("section", "cohort");
		const heading = node("h2", "cohort-heading", cohort.title);
		group.append(heading);
		for (const layer of cohort.layers) {
			const button = node("button", "layer-button");
			button.type = "button";
			button.dataset.layerId = layer.id;
			button.classList.toggle("selected", selectedLayerId === layer.id);
			button.setAttribute(
				"aria-current",
				selectedLayerId === layer.id ? "page" : "false",
			);
			button.setAttribute("aria-label", layer.title);
			button.append(node("span", "", layer.title));
			button.append(node("span", "count", String(layer.hunks.length)));
			button.addEventListener("click", () => selectLayer(layer.id));
			group.append(button);
		}
		cohorts.append(group);
	}

	const omitted = byId("omitted-list");
	omitted.replaceChildren();
	if (review.snapshot.skipped.length === 0) {
		omitted.append(node("li", "muted", "None"));
	} else {
		for (const item of review.snapshot.skipped) {
			omitted.append(node("li", "", `${item.path}: ${item.reason}`));
		}
	}
}

function updateLayerAction() {
	if (!review) return;
	const button = byId("mark-layer-reviewed");
	const layer = selectedLayerId ? layerById(selectedLayerId) : undefined;
	const ids = layer?.hunks.map((hunk) => hunk.id) ?? [];
	const allReviewed =
		ids.length > 0 && ids.every((id) => reviewedHunks.has(id));
	button.disabled = busy || staleClient || !layer || ids.length === 0;
	button.setAttribute("aria-pressed", String(allReviewed));
	button.setAttribute(
		"aria-label",
		allReviewed ? "Unmark layer reviewed" : "Mark layer reviewed",
	);
	button.querySelector(".action-label").textContent = allReviewed
		? "Unmark layer reviewed"
		: "Mark layer reviewed";
}

function updateHunkReviewButton(hunkId) {
	const button = renderedHunks.get(hunkId)?.querySelector(".mark-reviewed");
	if (!button) return;
	const reviewed = reviewedHunks.has(hunkId);
	const label = reviewed ? "Reviewed ✓" : "Mark reviewed";
	button.classList.toggle("done", reviewed);
	button.setAttribute("aria-pressed", String(reviewed));
	button.setAttribute("aria-label", label);
	button.firstChild.textContent = label;
}

function updateReviewControls(hunkIds) {
	for (const hunkId of hunkIds) updateHunkReviewButton(hunkId);
	updateLayerAction();
	updateProgress();
}

function toggleHunkReviewed(hunkId) {
	if (busy || staleClient || !hunkById(hunkId)) return;
	const draft = currentDraft();
	const reviewed = new Set(draft.reviewed);
	const wasReviewed = reviewed.has(hunkId);
	if (wasReviewed) reviewed.delete(hunkId);
	else reviewed.add(hunkId);
	draft.reviewed = [...reviewed];
	reviewedHunks = reviewed;
	const save = queueDraftSave(draft);
	const generation = draftGeneration;
	save.catch((error) => {
		if (error?.draftGeneration !== generation || generation !== draftGeneration)
			return;
		const restored = currentDraft();
		const current = new Set(restored.reviewed);
		if (wasReviewed) current.add(hunkId);
		else current.delete(hunkId);
		restored.reviewed = [...current];
		review.draft = restored;
		reviewedHunks = current;
		persistDraft(restored);
		updateReviewControls([hunkId]);
	});
	updateReviewControls([hunkId]);
}

function toggleLayerReviewed() {
	if (busy || staleClient || !selectedLayerId) return;
	const layer = layerById(selectedLayerId);
	if (!layer) return;
	const ids = layer.hunks.map((hunk) => hunk.id);
	const draft = currentDraft();
	const reviewed = new Set(draft.reviewed);
	const previous = new Map(ids.map((id) => [id, reviewed.has(id)]));
	const remove = ids.every((id) => reviewed.has(id));
	for (const id of ids) {
		if (remove) reviewed.delete(id);
		else reviewed.add(id);
	}
	draft.reviewed = [...reviewed];
	reviewedHunks = reviewed;
	const save = queueDraftSave(draft);
	const generation = draftGeneration;
	save.catch((error) => {
		if (error?.draftGeneration !== generation || generation !== draftGeneration)
			return;
		const restored = currentDraft();
		const current = new Set(restored.reviewed);
		for (const [id, wasReviewed] of previous) {
			if (wasReviewed) current.add(id);
			else current.delete(id);
		}
		restored.reviewed = [...current];
		review.draft = restored;
		reviewedHunks = current;
		persistDraft(restored);
		updateReviewControls(ids);
	});
	updateReviewControls(ids);
}

function renderForgeComment(comment, compact = false) {
	const item = node(
		"article",
		compact ? "forge-comment compact" : "forge-comment",
	);
	item.dataset.forgeCommentId = comment.id;
	const heading = node("div", "forge-comment-heading");
	const kind = node("span", "forge-comment-kind", comment.kind);
	const author = node("strong", "", comment.author);
	heading.append(kind, author);
	if (comment.kind === "inline") {
		const state = forgeCommentStates.get(comment);
		const labels = [];
		if (state?.outdated) labels.push("Outdated");
		if (!state?.anchor) labels.push("Unmapped");
		if (state?.current) labels.push("Current anchor");
		heading.append(
			node(
				"span",
				`forge-comment-state${state?.current ? " current" : ""}`,
				labels.join(" · "),
			),
		);
	}
	item.append(heading);
	if (
		comment.kind === "inline" &&
		comment.path &&
		comment.side &&
		comment.line !== undefined
	) {
		item.append(
			node(
				"p",
				"forge-comment-anchor",
				`${comment.path} · ${comment.side}:${comment.line}`,
			),
		);
	}
	item.append(node("p", "forge-comment-body", comment.body));
	const footer = node("div", "forge-comment-footer");
	if (comment.replyTo) {
		footer.append(node("span", "muted", `Reply to ${comment.replyTo}`));
	}
	const link = node(
		"a",
		"forge-comment-link",
		`Open on ${review.forge.providerLabel}`,
	);
	link.rel = "noreferrer noopener";
	link.target = "_blank";
	if (setSafeLink(link, comment.url)) footer.append(link);
	if (footer.hasChildNodes()) item.append(footer);
	return item;
}

function renderForge() {
	const region = byId("forge-region");
	const forge = review.forge;
	const source = byId("forge-source");
	const sourceLink = byId("forge-link");
	const refs = byId("forge-refs");
	const checkout = byId("forge-checkout");
	const description = byId("forge-description");
	const discussions = byId("forge-discussions");
	const comments = byId("forge-comments");
	region.hidden = !forge;
	indexForgeComments();
	source.replaceChildren();
	sourceLink.textContent = "";
	setSafeLink(sourceLink, null);
	byId("forge-heading").textContent = "";
	byId("forge-identity").textContent = "";
	refs.replaceChildren();
	checkout.classList.remove("mismatch");
	checkout.textContent = "";
	byId("forge-body").textContent = "";
	description.open = false;
	byId("forge-discussions-summary").textContent = "";
	discussions.open = false;
	comments.replaceChildren();
	if (!forge) return;
	appendForgeSource(source, forge);
	sourceLink.textContent = `Open #${forge.number} on ${forge.providerLabel}`;
	setSafeLink(sourceLink, forge.url);
	byId("forge-heading").textContent = forge.title;
	byId("forge-identity").textContent =
		`${forge.repository} #${forge.number} · opened by ${forge.author}`;
	for (const [label, ref, sha] of [
		["Base tip", forge.baseRef, forge.baseSha],
		["Merge base", `${forge.baseRef} ↔ ${forge.headRef}`, review.snapshot.base],
		["Head", forge.headRef, forge.headSha],
	]) {
		const row = node("div", "forge-ref");
		row.append(node("strong", "", label), node("span", "", ref));
		row.append(node("code", "", sha));
		refs.append(row);
	}
	const matches = forgeCheckoutMatches();
	checkout.classList.toggle("mismatch", !matches);
	checkout.textContent = matches
		? `Checked out: ${forge.checkoutBranch}. Snapshot is bound to the exact PR base tip and head commit.`
		: `Checked out: ${forge.checkoutBranch}. Snapshot is not bound to the exact PR base tip and head commit.`;
	byId("forge-body").textContent = forge.body || "No description.";
	description.open = forge.body.length <= 240;
	byId("forge-discussions-summary").textContent =
		`Discussions (${forge.comments.length})`;
	if (forge.comments.length === 0) {
		comments.append(node("p", "muted", "No imported discussions."));
	} else {
		for (const comment of forge.comments) {
			comments.append(renderForgeComment(comment));
		}
	}
}

function renderDiff() {
	observer?.disconnect();
	diff.replaceChildren();
	currentLineRow = null;
	currentLineIndex = -1;
	renderedLines = [];
	renderedLineByKey = new Map();
	renderedLineIndexes = new WeakMap();
	renderedHunks = new Map();
	renderedHunkLines = new Map();
	const hunks = visibleHunks();
	const layer = selectedLayerId ? layerById(selectedLayerId) : undefined;
	byId("diff-heading").textContent = layer?.title || "All changes";
	byId("view-summary").textContent = layer?.summary || review.plan.summary;
	byId("visible-count").textContent =
		`${hunks.length} / ${review.snapshot.hunks.length} hunks`;
	updateLayerAction();
	if (hunks.length === 0) {
		diff.append(node("p", "empty-state", "No captured hunks in this view."));
		currentHunkId = null;
		currentLineKey = null;
		updateDetails();
		updateKeyboardStatus();
		return;
	}

	for (const hunk of hunks) diff.append(renderHunk(hunk));
	if (!hunks.some((hunk) => hunk.id === currentHunkId)) {
		currentHunkId = hunks[0].id;
	}
	selectCurrentHunk(currentHunkId, false, false, true);
	ensureCurrentLine(currentHunkId);
	observeHunks();
}

function renderHunk(hunk) {
	const article = node("article", "hunk");
	article.id = `hunk-${hunk.id}`;
	article.dataset.hunkId = hunk.id;
	article.tabIndex = -1;
	renderedHunks.set(hunk.id, article);
	renderedHunkLines.set(hunk.id, []);
	article.addEventListener("click", () => selectCurrentHunk(hunk.id, false));

	const heading = node("header", "file-heading");
	const path = hunk.oldPath ? `${hunk.oldPath} → ${hunk.path}` : hunk.path;
	heading.append(node("span", "file-name", path));
	const reviewed = reviewedHunks.has(hunk.id);
	const mark = node(
		"button",
		"mark-reviewed",
		reviewed ? "Reviewed ✓" : "Mark reviewed",
	);
	mark.type = "button";
	mark.dataset.hunkId = hunk.id;
	mark.classList.toggle("done", reviewed);
	mark.setAttribute("aria-pressed", String(reviewed));
	mark.setAttribute("aria-label", reviewed ? "Reviewed ✓" : "Mark reviewed");
	const hint = node("span", "action-hint");
	hint.setAttribute("aria-hidden", "true");
	hint.append(node("kbd", "", "Space"), node("kbd", "", "r"));
	mark.append(hint);
	mark.addEventListener("click", (event) => {
		event.stopPropagation();
		toggleHunkReviewed(hunk.id);
	});
	heading.append(mark);
	article.append(heading);
	article.append(node("pre", "hunk-header", hunk.header));

	const scroll = node("div", "diff-scroll");
	const table = node("table", "diff-table");
	const body = document.createElement("tbody");
	for (const [index, line] of hunk.lines.entries()) {
		body.append(renderLine(hunk, line, index));
		const imported = [
			...(line.oldLine === undefined
				? []
				: (forgeInlineByAnchor.get(
						forgeAnchorKey(hunk.id, "old", line.oldLine),
					) ?? [])),
			...(line.newLine === undefined
				? []
				: (forgeInlineByAnchor.get(
						forgeAnchorKey(hunk.id, "new", line.newLine),
					) ?? [])),
		];
		if (imported.length > 0) body.append(renderImportedComments(imported));
	}
	table.append(body);
	scroll.append(table);
	article.append(scroll);
	return article;
}

function renderImportedComments(comments) {
	const row = node("tr", "imported-comments-row");
	const cell = node("td", "imported-comments-cell");
	cell.colSpan = 3;
	const group = node("div", "imported-comments");
	for (const comment of comments)
		group.append(renderForgeComment(comment, true));
	cell.append(group);
	row.append(cell);
	return row;
}

function renderLine(hunk, line, index) {
	const row = node("tr", `diff-row diff-line ${line.kind}`);
	row.tabIndex = -1;
	row.dataset.kind = line.kind;
	row.dataset.hunkId = hunk.id;
	row.dataset.lineIndex = String(index);
	const side =
		line.kind === "delete"
			? "old"
			: line.newLine !== undefined
				? "new"
				: line.oldLine !== undefined
					? "old"
					: undefined;
	const number = side === "old" ? line.oldLine : line.newLine;
	if (side && number !== undefined) {
		row.dataset.side = side;
		row.dataset.line = String(number);
		const key = lineKey(row);
		renderedLineIndexes.set(row, renderedLines.length);
		renderedLines.push(row);
		renderedLineByKey.set(key, row);
		renderedHunkLines.get(hunk.id).push(row);
		row.addEventListener("click", (event) => {
			if (event.target instanceof HTMLButtonElement) return;
			setCurrentLine(row, false);
		});
	}
	row.append(
		lineNumberCell(hunk, line, "old"),
		lineNumberCell(hunk, line, "new"),
	);
	const code = node("td", "line-code");
	const prefix = line.kind === "add" ? "+" : line.kind === "delete" ? "−" : " ";
	code.append(
		node("span", "line-prefix", prefix),
		document.createTextNode(line.text),
	);
	row.append(code);
	return row;
}

function lineNumberCell(hunk, line, side) {
	const cell = node("td", "line-number");
	const number = side === "old" ? line.oldLine : line.newLine;
	if (number === undefined) return cell;
	const button = node("button", "", String(number));
	button.type = "button";
	button.id = `line-${hunk.id}-${side}-${number}`;
	button.setAttribute(
		"aria-label",
		`Comment on ${side} line ${number} in ${hunk.path}`,
	);
	button.addEventListener("click", (event) => {
		event.stopPropagation();
		const row = button.closest(".diff-line");
		if (row?.dataset.line) setCurrentLine(row, false);
		if (event.shiftKey) {
			if (rangeAnchor?.hunkId === hunk.id && rangeAnchor.side === side) {
				openComment(hunk.id, side, number, rangeAnchor.line);
			} else {
				rangeAnchor = { hunkId: hunk.id, side, line: number };
				updateRangeSelection();
				setStatus("Range start selected; Shift-click an endpoint");
			}
			return;
		}
		openComment(hunk.id, side, number);
	});
	cell.append(button);
	return cell;
}

function observeHunks() {
	if (!review || renderedHunks.size === 0) return;
	const intersections = new Map();
	const root = narrowLayout.matches ? null : paneElement("diff");
	observer = new IntersectionObserver(
		(entries) => {
			for (const entry of entries) {
				const id = entry.target.dataset.hunkId;
				if (entry.isIntersecting) intersections.set(id, entry);
				else intersections.delete(id);
			}
			if (performance.now() < suppressObserverUntil || explicitJumpLocked)
				return;
			const rootBounds = root?.getBoundingClientRect();
			const viewportTop = (rootBounds?.top ?? 0) + 75;
			const viewportBottom = rootBounds?.bottom ?? window.innerHeight;
			const scrollTop = root?.scrollTop ?? window.scrollY;
			const firstId = renderedHunks.keys().next().value;
			// A metadata-only first hunk still owns selection at the top of the review.
			if (
				scrollTop === 0 &&
				firstId &&
				intersections.has(firstId) &&
				renderedHunkLines.get(firstId)?.length === 0
			) {
				selectCurrentHunk(firstId, false, true);
				return;
			}
			const atBottom =
				root &&
				scrollTop > 0 &&
				scrollTop + root.clientHeight >= root.scrollHeight - 50;
			const lastVisibleId = [...renderedHunks.keys()]
				.reverse()
				.find((id) => intersections.has(id));
			if (atBottom && lastVisibleId) {
				selectCurrentHunk(lastVisibleId, false, true);
				return;
			}
			let bestId;
			let bestArea = 0;
			for (const id of intersections.keys()) {
				const rect = renderedHunks.get(id)?.getBoundingClientRect();
				if (!rect) continue;
				const visibleHeight = Math.max(
					0,
					Math.min(rect.bottom, viewportBottom) -
						Math.max(rect.top, viewportTop),
				);
				const visibleWidth = Math.max(
					0,
					Math.min(rect.right, rootBounds?.right ?? window.innerWidth) -
						Math.max(rect.left, rootBounds?.left ?? 0),
				);
				const area = visibleHeight * visibleWidth;
				if (area > bestArea) {
					bestId = id;
					bestArea = area;
				}
			}
			if (bestId) selectCurrentHunk(bestId, false, true);
		},
		{
			root,
			rootMargin: "-75px 0px 0px 0px",
			threshold: [0, 0.1, 0.3, 0.6, 1],
		},
	);
	for (const hunk of renderedHunks.values()) observer.observe(hunk);
}

function selectCurrentHunk(id, scroll, syncLine = true, force = false) {
	if (!id || !hunkById(id)) return;
	if (scroll) beginExplicitJump();
	const previousId = currentHunkId;
	const changed = previousId !== id;
	if (changed) renderedHunks.get(previousId)?.classList.remove("current");
	currentHunkId = id;
	renderedHunks.get(id)?.classList.add("current");
	if (syncLine && currentLine()?.dataset.hunkId !== id) {
		const first = renderedHunkLines.get(id)?.[0];
		if (first) setCurrentLine(first, false);
		else clearCurrentLine();
	}
	if (changed || force) updateDetails();
	else updateKeyboardStatus();
	if (scroll) {
		const target = renderedHunkLines.get(id)?.[0] ?? renderedHunks.get(id);
		target?.scrollIntoView({
			block: "start",
			inline: "nearest",
			behavior: "instant",
		});
		target?.focus({ preventScroll: true });
	}
}

function updateDetails() {
	if (!review) return;
	const hunk = hunkById(currentHunkId);
	const layer = hunk ? layerForHunk(hunk.id) : undefined;
	const plannedHunk = layer?.hunks.find((item) => item.id === hunk?.id);
	byId("current-path").textContent = hunk ? hunk.path : "No hunk selected";
	const explanation = byId("explanation");
	explanation.textContent = plannedHunk?.summary || "";
	explanation.hidden = !plannedHunk?.summary;
	const flow = byId("flow");
	flow.replaceChildren();
	for (const [index, step] of (layer?.flow ?? []).entries()) {
		if (index > 0) flow.append(node("div", "flow-arrow", "↓"));
		flow.append(node("div", "flow-step", step));
	}
	byId("flow-region").hidden = (layer?.flow?.length ?? 0) === 0;
	renderAsk();
}

function renderAskMessage(exchange) {
	const message = node("article", "chat-exchange");
	const context = exchange.hunkId
		? `Focused hunk: ${hunkById(exchange.hunkId)?.path ?? exchange.hunkId}`
		: "No focused hunk";
	message.append(
		node("p", "chat-context", context),
		node("p", "chat-question", exchange.question),
		node("p", "chat-answer", exchange.answer),
	);
	return message;
}

function renderAsk() {
	if (!review) return;
	byId("ask-scope").textContent = scopeTitle();
	const hunk = hunkById(currentHunkId);
	byId("ask-focus").textContent = hunk
		? `Focus: ${hunk.path} · ${hunk.id}`
		: "No focused hunk";
	const chat = byId("chat");
	const scrollTop = chat.scrollTop;
	const followLatest =
		chat.dataset.scope !== scopeKey() ||
		chat.scrollHeight - chat.clientHeight - scrollTop < 24;
	chat.dataset.scope = scopeKey();
	chat.replaceChildren();
	const thread = threadForScope();
	for (const exchange of thread?.messages ?? []) {
		chat.append(renderAskMessage(exchange));
	}
	if (pendingAsk?.scopeKey === scopeKey()) {
		const waiting = node("article", "chat-exchange pending");
		waiting.append(
			node(
				"p",
				"chat-context",
				pendingAsk.hunkId
					? `Focused hunk: ${hunkById(pendingAsk.hunkId)?.path ?? pendingAsk.hunkId}`
					: "No focused hunk",
			),
			node("p", "chat-question", pendingAsk.question),
			node("p", "chat-answer muted", "Waiting for answer…"),
		);
		chat.append(waiting);
	}
	const askError = askErrors.get(scopeKey());
	if (askError) {
		chat.append(node("p", "chat-error", askError));
	}
	if (!chat.hasChildNodes()) {
		chat.append(node("p", "empty-state", "No questions in this thread."));
	}
	chat.scrollTop = followLatest ? chat.scrollHeight : scrollTop;
	updateAskControls();
}

function updateAskControls() {
	const unavailable = busy || askInFlight || !review;
	byId("ask-button").disabled = unavailable;
	questionInput.disabled = unavailable;
}

function restoreQuestionForScope() {
	questionInput.value = askDrafts[scopeKey()] ?? "";
}

function jumpToFinding(finding) {
	selectMainView("technical");
	const layer = layerForHunk(finding.hunkId);
	if (layer) selectLayer(layer.id);
	else selectLayer(null);
	selectCurrentHunk(finding.hunkId, true);
	byId(`line-${finding.hunkId}-${finding.side}-${finding.line}`)?.focus({
		preventScroll: false,
	});
}

function renderFindings() {
	const container = byId("findings");
	container.replaceChildren();
	if (review.draft.findings.length === 0) {
		container.append(node("p", "muted", "No line comments."));
		return;
	}
	for (const finding of review.draft.findings) {
		const hunk = hunkById(finding.hunkId);
		const item = node("article", "finding");
		item.dataset.findingId = finding.id;
		const head = node("div", "finding-head");
		head.append(node("span", `severity ${finding.severity}`, finding.severity));
		const anchor = node(
			"button",
			"finding-anchor quiet",
			`${hunk?.path || finding.hunkId} · ${finding.side}:${finding.line}${finding.endLine ? `–${finding.endLine}` : ""}`,
		);
		anchor.type = "button";
		anchor.addEventListener("click", () => jumpToFinding(finding));
		head.append(anchor);
		const remove = node("button", "quiet", "Remove");
		remove.type = "button";
		remove.setAttribute(
			"aria-label",
			`Remove comment on ${finding.side} line ${finding.line}${finding.endLine ? ` through ${finding.endLine}` : ""}`,
		);
		remove.addEventListener("click", async () => {
			const draft = currentDraft();
			draft.findings = draft.findings.filter((item) => item.id !== finding.id);
			try {
				await queueDraftSave(draft);
				renderFindings();
			} catch {}
		});
		head.append(remove);
		item.append(head, node("p", "finding-text", finding.text));
		container.append(item);
	}
}

function updateProgress() {
	const reviewed = review.draft.reviewed.length;
	const total = review.snapshot.hunks.length;
	byId("progress-label").textContent = `${reviewed} / ${total} reviewed`;
}

function renderAll() {
	rebuildReviewIndexes();
	if (hasNarrativePlan() && mainView === "technical") mainView = "overview";
	byId("source-label").textContent = sourceLabel();
	notes.value = review.draft.notes;
	renderForge();
	renderNavigation();
	renderDiff();
	renderFindings();
	renderRecovery();
	updateProgress();
	restoreQuestionForScope();
	renderAsk();
	selectMainView(mainView);
	applyPanelState();
	updateLayerAction();
	updateKeyboardStatus();
}

function selectLayer(layerId, focusDiff = false) {
	if (focusDiff) selectMainView("technical");
	selectedLayerId = layerId;
	currentHunkId = layerId
		? (layerById(layerId)?.hunks[0]?.id ?? null)
		: (review.snapshot.hunks[0]?.id ?? null);
	currentLineKey = null;
	renderNavigation();
	renderDiff();
	restoreQuestionForScope();
	renderAsk();
	if (currentHunkId) {
		selectCurrentHunk(currentHunkId, true, true);
		ensureCurrentLine(currentHunkId);
	}
	if (focusDiff) setKeyboardPane("diff", true);
}

function openComment(hunkId, side, line, endpoint) {
	if (busy || staleClient || !hunkById(hunkId) || !Number.isSafeInteger(line)) {
		return;
	}
	const endLine = Number.isSafeInteger(endpoint) ? endpoint : undefined;
	selectDrawerTab("feedback");
	commentTarget = {
		hunkId,
		side,
		line: endLine === undefined ? line : Math.min(line, endLine),
		...(endLine === undefined || endLine === line
			? {}
			: { endLine: Math.max(line, endLine) }),
	};
	byId("comment-anchor").textContent =
		`${hunkById(hunkId)?.path} · ${side}:${commentTarget.line}${commentTarget.endLine ? `–${commentTarget.endLine}` : ""}`;
	byId("comment-text").value = "";
	byId("comment-severity").value = "question";
	commentDialog.showModal();
	byId("comment-text").focus();
}

async function restoreLocalDraft() {
	const draft = storedDraft();
	if (!draft || typeof draft !== "object") return;
	if (
		!Array.isArray(draft.reviewed) ||
		!Array.isArray(draft.findings) ||
		typeof draft.notes !== "string"
	)
		return;
	if (draftSignature(draft) === draftSignature(review.draft)) {
		clearStoredDraft();
		return;
	}
	++draftEpoch;
	try {
		await persistRecovery(draft, review.snapshot.id);
		clearStoredDraft();
		setStatus(
			"A local draft differs from the latest review. It is retained in recovery.",
		);
	} catch (error) {
		setStatus(
			`Local draft recovery was not saved: ${error instanceof Error ? error.message : "request failed"}`,
			true,
		);
	}
}

async function pollReadiness(attempt = 0) {
	readinessTimer = undefined;
	if (closing) return;
	try {
		let payload = await request("/api/state");
		if (closing) return;
		if (!payload.busy) {
			++draftEpoch;
			if (draftSignature(currentDraft()) !== acknowledgedDraft) {
				await persistRecovery(currentDraft(), review.snapshot.id);
				payload = await request("/api/state");
				if (payload.busy) {
					setBusy(
						true,
						"Review is busy. Refresh checks again; Close remains available.",
					);
					return;
				}
			}
			if (payload.snapshotId !== review.snapshot.id) {
				selectedLayerId = null;
				currentHunkId = null;
			}
			applyVersion(payload);
			review = payload.review;
			acknowledgedDraft = draftSignature(review.draft);
			staleClient = false;
			renderAll();
			setBusy(false, "Review ready");
			return;
		}
		setBusy(
			true,
			"Review is still busy. Refresh checks again; Close remains available.",
		);
	} catch (error) {
		setBusy(
			true,
			`${error instanceof Error ? error.message : "Readiness check failed"}. Refresh retries; Close remains available.`,
		);
		return;
	}
	if (attempt < READINESS_RETRIES) {
		readinessTimer = setTimeout(
			() => pollReadiness(attempt + 1),
			NOTE_DEBOUNCE_MS,
		);
	}
}

narrowLayout.addEventListener("change", (event) => {
	if (event.matches) {
		layersOpen = false;
		drawerOpen = false;
	}
	applyPanelState();
});

byId("layers-toggle").addEventListener("click", () => {
	layersOpen = !layersOpen;
	applyPanelState();
});

byId("drawer-toggle").addEventListener("click", () => {
	drawerOpen = !drawerOpen;
	applyPanelState();
});

for (const tab of document.querySelectorAll("[data-main-view]")) {
	tab.addEventListener("click", () => selectMainView(tab.dataset.mainView));
	tab.addEventListener("keydown", (event) => {
		const tabs = [...document.querySelectorAll("[data-main-view]")];
		const current = tabs.indexOf(event.currentTarget);
		let next = current;
		if (event.key === "ArrowRight") next = (current + 1) % tabs.length;
		else if (event.key === "ArrowLeft")
			next = (current - 1 + tabs.length) % tabs.length;
		else if (event.key === "Home") next = 0;
		else if (event.key === "End") next = tabs.length - 1;
		else return;
		event.preventDefault();
		selectMainView(tabs[next].dataset.mainView, true);
	});
}

for (const tab of document.querySelectorAll("[data-drawer-tab]")) {
	tab.addEventListener("click", () => selectDrawerTab(tab.dataset.drawerTab));
	tab.addEventListener("keydown", (event) => {
		const tabs = [...document.querySelectorAll("[data-drawer-tab]")];
		const current = tabs.indexOf(event.currentTarget);
		let next;
		if (event.key === "ArrowRight") next = (current + 1) % tabs.length;
		else if (event.key === "ArrowLeft") {
			next = (current - 1 + tabs.length) % tabs.length;
		} else if (event.key === "Home") next = 0;
		else if (event.key === "End") next = tabs.length - 1;
		else return;
		event.preventDefault();
		selectDrawerTab(tabs[next].dataset.drawerTab, true);
	});
}

allChanges.addEventListener("click", () => selectLayer(null));
byId("mark-layer-reviewed").addEventListener("click", toggleLayerReviewed);
byId("keyboard-help-button").addEventListener("click", showHelp);
byId("keyboard-help-close").addEventListener("click", () =>
	keyboardHelp.close(),
);
keyboardHelp.addEventListener("close", () => {
	if (keyboardHelp.open || isEditable(document.activeElement)) return;
	setKeyboardPane("diff");
	currentLine()?.focus({ preventScroll: true });
});
keyboardHelp.addEventListener("cancel", () => cancelPendingPrefix());
commentDialog.addEventListener("cancel", () => {
	commentTarget = null;
});
keyboardSearch.addEventListener("input", () => {
	searchQuery = keyboardSearch.value;
});
keyboardSearch.addEventListener("keydown", (event) => {
	if (event.isComposing) return;
	if (event.key === "Enter") {
		event.preventDefault();
		searchLines(event.shiftKey ? -1 : 1);
		closeSearch();
	}
});
for (const pane of document.querySelectorAll("[data-keyboard-pane]")) {
	pane.addEventListener("pointerdown", () =>
		setKeyboardPane(pane.dataset.keyboardPane),
	);
	pane.addEventListener("focusin", () =>
		setKeyboardPane(pane.dataset.keyboardPane),
	);
}
document.addEventListener("focusin", updateKeyboardStatus);
document.addEventListener("focusout", () =>
	queueMicrotask(updateKeyboardStatus),
);
document.addEventListener("pointerdown", cancelPendingPrefix);
window.addEventListener("blur", cancelPendingPrefix);
function releaseExplicitJump() {
	if (performance.now() >= suppressObserverUntil) explicitJumpLocked = false;
}
for (const eventName of ["wheel", "touchmove", "pointerdown"]) {
	paneElement("diff").addEventListener(eventName, releaseExplicitJump, {
		passive: true,
	});
}
renderKeyboardHelp();

notes.addEventListener("input", () => {
	review.draft.notes = notes.value;
	persistDraft(currentDraft());
	if (staleClient) {
		setStatus(
			"Review changed in another tab. Refresh to recover these notes.",
			true,
		);
		return;
	}
	scheduleNotesSave();
});

questionInput.addEventListener("input", () => {
	askDrafts[scopeKey()] = questionInput.value;
	persistAskDrafts();
});

questionInput.addEventListener("keydown", (event) => {
	if (event.isComposing || event.key === "Process") return;
	if (event.key === "Enter" && event.shiftKey) {
		event.preventDefault();
		askForm.requestSubmit();
	}
});

commentForm.addEventListener("submit", async (event) => {
	event.preventDefault();
	if (!commentTarget) return;
	const text = byId("comment-text").value;
	if (!text) return;
	const draft = currentDraft();
	draft.findings.push({
		id: crypto.randomUUID(),
		...commentTarget,
		severity: byId("comment-severity").value,
		text,
	});
	setStatus("Saving comment…");
	const saving = queueDraftSave(draft);
	commentDialog.close();
	commentTarget = null;
	clearRangeSelection();
	renderFindings();
	setKeyboardPane("drawer", true);
	try {
		await saving;
	} catch {}
});

byId("comment-cancel").addEventListener("click", () => {
	commentTarget = null;
	commentDialog.close();
	setKeyboardPane("drawer", true);
});

askForm.addEventListener("submit", async (event) => {
	event.preventDefault();
	const question = questionInput.value;
	if (!question.trim() || busy || askInFlight) return;
	const sent = {
		layerId: selectedLayerId,
		hunkId: currentHunkId,
		question,
		scopeKey: scopeKey(),
	};
	askInFlight = true;
	pendingAsk = sent;
	askErrors.delete(sent.scopeKey);
	setBusy(true, "Saving draft…");
	renderAsk();
	try {
		await flushDraft();
		setStatus("Asking model…");
		const payload = await request("/api/ask", {
			layerId: sent.layerId,
			hunkId: sent.hunkId,
			question: sent.question,
		});
		delete askDrafts[sent.scopeKey];
		persistAskDrafts();
		if (scopeKey() === sent.scopeKey) questionInput.value = "";
		setStatus("Answer received");
		applyThreads(payload);
	} catch (error) {
		if (!closing) {
			askErrors.set(
				sent.scopeKey,
				error instanceof Error ? error.message : "Question failed",
			);
			reportError(error);
		}
	} finally {
		pendingAsk = null;
		askInFlight = false;
		if (!closing) {
			setBusy(false);
			renderAsk();
			if (
				scopeKey() === sent.scopeKey &&
				drawerOpen &&
				!byId("ask-panel").hidden &&
				document.activeElement === document.body
			)
				questionInput.focus({ preventScroll: true });
		}
	}
});

async function refreshReview() {
	if (closing) return;
	if (busy) {
		if (!readinessTimer) pollReadiness();
		return;
	}
	if (
		!window.confirm(
			"Refresh the review snapshot? Unsaved local work stays in recovery.",
		)
	)
		return;
	const recovering = staleClient;
	const localDraft = currentDraft();
	const previousSnapshotId = review.snapshot.id;
	const previousDraftKey = storageKey();
	const previousAskKey = askStorageKey();
	let backup;
	setBusy(true, recovering ? "Resolving newer draft…" : "Saving draft…");
	try {
		if (recovering) {
			++draftEpoch;
			if (noteTimer) clearTimeout(noteTimer);
			noteTimer = undefined;
			notesPending = false;
			backup = await persistRecovery(localDraft, previousSnapshotId);
			const latest = await request("/api/state");
			applyVersion(latest);
			review = latest.review;
			acknowledgedDraft = draftSignature(review.draft);
			applyThreads(latest);
			staleClient = false;
			selectedLayerId = null;
			currentHunkId = null;
			renderAll();
			sessionStorage.removeItem(previousDraftKey);
		} else {
			await flushDraft();
			++draftEpoch;
			if (localDraft.findings.length)
				backup = await persistRecovery(localDraft, previousSnapshotId);
		}
		setStatus("Refreshing snapshot…");
		const payload = await request("/api/refresh", { confirm: true });
		++draftEpoch;
		review = payload.review;
		acknowledgedDraft = draftSignature(review.draft);
		threads = Array.isArray(payload.threads) ? payload.threads : [];
		if (previousSnapshotId !== review.snapshot.id) askDrafts = {};
		if (!recovering && localDraft.findings.length) {
			const retained = new Set(
				review.draft.findings.map((finding) => finding.id),
			);
			const orphaned = localDraft.findings.filter(
				(finding) => !retained.has(finding.id),
			);
			if (!orphaned.length && backup) {
				const removed = await request("/api/recovery", { remove: backup.id });
				review.recoveries = removed.review.recoveries;
			}
		}
		askErrors.clear();
		selectedLayerId = null;
		currentHunkId = null;
		currentLineKey = null;
		clearRangeSelection();
		searchQuery = "";
		if (!recovering) {
			try {
				sessionStorage.removeItem(previousDraftKey);
				sessionStorage.removeItem(previousAskKey);
			} catch {}
			clearStoredDraft();
			clearStoredAskDrafts();
		}
		renderAll();
		setKeyboardPane("diff", true);
		setStatus(
			recovering
				? "Snapshot refreshed. Local unsaved draft is retained in recovery."
				: payload.wasCurrent
					? "Review refreshed"
					: "Stale snapshot replaced",
		);
	} catch (error) {
		reportError(error);
	} finally {
		if (!closing) setBusy(false);
	}
}

async function submitFeedback() {
	if (busy || staleClient) {
		if (staleClient) reportError({ code: "stale_client" });
		return;
	}
	if (!window.confirm("Send feedback to Pi with the current draft?")) return;
	setBusy(true, "Saving draft…");
	try {
		await flushDraft();
		setStatus("Checking snapshot and sending feedback…");
		const payload = await request("/api/submit", { draft: currentDraft() });
		review.draft = payload.draft;
		acknowledgedDraft = draftSignature(payload.draft);
		reviewedHunks = new Set(payload.draft.reviewed);
		clearStoredDraft();
		notes.value = review.draft.notes;
		renderFindings();
		renderDiff();
		updateProgress();
		setStatus("Feedback sent to Pi");
	} catch (error) {
		reportError(error);
	} finally {
		setBusy(false);
	}
}

function enterClosedState() {
	closing = true;
	cancelDiagram();
	if (noteTimer) clearTimeout(noteTimer);
	if (readinessTimer) clearTimeout(readinessTimer);
	noteTimer = undefined;
	readinessTimer = undefined;
	cancelPendingPrefix();
	workspace.setAttribute("aria-busy", "false");
	for (const control of document.querySelectorAll(
		"button, textarea, select, input",
	))
		control.disabled = true;
	setStatus("Review closed. You may close this tab.");
}

async function closeReview() {
	if (closing || !window.confirm("Close this review workspace?")) return;
	const draft = currentDraft();
	persistDraft(draft);
	setBusy(true, "Closing review…");
	closing = true;
	cancelDiagram();
	++draftEpoch;
	if (noteTimer) clearTimeout(noteTimer);
	noteTimer = undefined;
	notesPending = false;
	try {
		if (staleClient || draftSignature(draft) !== acknowledgedDraft)
			rememberRecovery(draft, review.snapshot.id);
		const pending = localRecoveries();
		const backups = pending.map((item) => ({
			snapshotId: item.snapshotId,
			text: JSON.stringify(item.draft),
		}));
		await request("/api/close", backups.length ? { recoveries: backups } : {});
		for (const item of pending) forgetLocalRecovery(item);
		enterClosedState();
	} catch (error) {
		closing = false;
		reportError(error);
		setBusy(false);
	}
}

byId("refresh-button").addEventListener("click", refreshReview);
byId("submit-button").addEventListener("click", submitFeedback);
byId("close-button").addEventListener("click", closeReview);
byId("merge-recovery").addEventListener("click", mergeRecovery);
byId("discard-recovery").addEventListener("click", async () => {
	const recovered = readRecovery();
	if (
		!recovered ||
		!window.confirm("Discard the retained local recovery draft?")
	)
		return;
	try {
		if (recovered.id) {
			const payload = await request("/api/recovery", { remove: recovered.id });
			if (payload.review) review.recoveries = payload.review.recoveries;
		}
		forgetLocalRecovery(recovered);
		renderRecovery();
	} catch (error) {
		reportError(error);
	}
});

function moveLine(direction) {
	if (renderedLines.length === 0) return;
	if (currentLineIndex < 0) {
		const hunks = visibleHunks();
		const hunkIndex = hunks.findIndex((hunk) => hunk.id === currentHunkId);
		const candidates =
			direction > 0
				? hunks.slice(hunkIndex + 1)
				: hunks.slice(0, hunkIndex).reverse();
		for (const hunk of candidates) {
			const lines = renderedHunkLines.get(hunk.id) ?? [];
			const target = direction > 0 ? lines[0] : lines.at(-1);
			if (target) {
				setCurrentLine(target, true);
				return;
			}
		}
		return;
	}
	const next = Math.max(
		0,
		Math.min(renderedLines.length - 1, currentLineIndex + direction),
	);
	setCurrentLine(renderedLines[next], true);
}

function focusedScrollSurface(axis = "top") {
	if (keyboardPane === "drawer") {
		return document.querySelector(".drawer-panel:not([hidden])");
	}
	if (keyboardPane === "diff" && axis === "left") {
		return (
			currentLine()?.closest(".hunk")?.querySelector(".diff-scroll") ||
			paneElement()
		);
	}
	return paneElement();
}

function scrollFocusedPane(axis, amount) {
	focusedScrollSurface(axis)?.scrollBy({
		[axis]: amount,
		behavior: "instant",
	});
}

function jumpBoundary(last) {
	if (keyboardPane !== "diff") {
		const surface = focusedScrollSurface();
		surface?.scrollTo({
			top: last ? surface.scrollHeight : 0,
			behavior: "instant",
		});
		return;
	}
	const lines = anchorableLines();
	if (lines.length) setCurrentLine(lines[last ? lines.length - 1 : 0], true);
}

function jumpHunk(direction, unreviewed = false) {
	const hunks = visibleHunks();
	if (hunks.length === 0) return;
	const current = hunks.findIndex((hunk) => hunk.id === currentHunkId);
	let next = current;
	for (
		let index = current + direction;
		index >= 0 && index < hunks.length;
		index += direction
	) {
		if (!unreviewed || !review.draft.reviewed.includes(hunks[index].id)) {
			next = index;
			break;
		}
	}
	if (next === current || next < 0) return;
	const id = hunks[next].id;
	const row = renderedHunkLines.get(id)?.[0];
	if (row) setCurrentLine(row, true);
	else {
		clearCurrentLine();
		selectCurrentHunk(id, true, false);
		updateKeyboardStatus();
	}
}

function jumpLayer(direction) {
	const available = layers();
	if (available.length === 0) return;
	const current = available.findIndex((layer) => layer.id === selectedLayerId);
	const next =
		current < 0
			? direction > 0
				? 0
				: available.length - 1
			: Math.max(0, Math.min(available.length - 1, current + direction));
	if (next !== current) selectLayer(available[next].id, true);
}

function focusAdjacentPane(direction) {
	const available = visiblePaneNames();
	const current = Math.max(0, available.indexOf(keyboardPane));
	const next = Math.max(0, Math.min(available.length - 1, current + direction));
	setKeyboardPane(available[next], true);
}

function composeQuestion() {
	if (busy) return;
	selectDrawerTab("ask");
	setKeyboardPane("drawer");
	questionInput.focus();
}

function editNotes() {
	if (busy) return;
	selectDrawerTab("feedback");
	setKeyboardPane("drawer");
	notes.focus();
}

function runSequence(prefix, key) {
	cancelPendingPrefix();
	if (prefix === "g") {
		if (key === "g") jumpBoundary(false);
		else if (["d", "a", "f"].includes(key)) {
			const tab = key === "d" ? "details" : key === "a" ? "ask" : "feedback";
			selectDrawerTab(tab, true);
			setKeyboardPane("drawer");
		}
		return;
	}
	if (prefix === "[" || prefix === "]") {
		const direction = prefix === "]" ? 1 : -1;
		if (key === "c") jumpHunk(direction);
		else if (key === "l") jumpLayer(direction);
		else if (key === "u") jumpHunk(direction, true);
		return;
	}
	if (prefix !== " ") return;
	if (key === "q") {
		closeReview();
		return;
	}
	if (key === "f") {
		refreshReview();
		return;
	}
	if (busy || staleClient) return;
	if (key === "r" && currentHunkId) toggleHunkReviewed(currentHunkId);
	else if (key === "R") toggleLayerReviewed();
	else if (key === "c") {
		const row = currentLine();
		if (row)
			openComment(
				row.dataset.hunkId,
				row.dataset.side,
				Number(row.dataset.line),
				rangeAnchor?.hunkId === row.dataset.hunkId &&
					rangeAnchor.side === row.dataset.side
					? rangeAnchor.line
					: undefined,
			);
	} else if (key === "a") composeQuestion();
	else if (key === "n") editNotes();
	else if (key === "h") focusAdjacentPane(-1);
	else if (key === "l") focusAdjacentPane(1);
	else if (key === "b") {
		layersOpen = !layersOpen;
		applyPanelState();
	} else if (key === "d") {
		drawerOpen = !drawerOpen;
		applyPanelState();
	} else if (key === "s") submitFeedback();
}

function dismissTransient(event) {
	if (keyboardHelp.open) {
		event.preventDefault();
		keyboardHelp.close();
		return true;
	}
	if (commentDialog.open) {
		event.preventDefault();
		commentTarget = null;
		commentDialog.close();
		return true;
	}
	if (pendingPrefix) {
		event.preventDefault();
		cancelPendingPrefix();
		return true;
	}
	if (!keyboardSearch.hidden) {
		event.preventDefault();
		closeSearch();
		return true;
	}
	if (isEditable(event.target)) {
		event.preventDefault();
		event.target.blur();
		setKeyboardPane("diff");
		currentLine()?.focus({ preventScroll: true });
		return true;
	}
	return false;
}

function isHalfPageKey(event) {
	return (
		event.ctrlKey &&
		!event.altKey &&
		!event.metaKey &&
		!event.shiftKey &&
		["d", "u"].includes(event.key.toLowerCase())
	);
}

function hasUnrelatedModifiers(event) {
	if (event.metaKey) return true;
	const printableModifiedKey = ["?", "/", "[", "]"].includes(event.key);
	const altGraph =
		event.ctrlKey && event.altKey && event.getModifierState("AltGraph");
	if (event.ctrlKey || event.altKey) {
		return !(altGraph && printableModifiedKey);
	}
	return (
		event.shiftKey && !["?", "/", "[", "]", "G", "N", "R"].includes(event.key)
	);
}

document.addEventListener("keydown", (event) => {
	if (event.isComposing || event.key === "Process") return;
	if (event.key === "Escape" && dismissTransient(event)) return;
	if (keyboardHelp.open || commentDialog.open || isEditable(event.target))
		return;
	if (
		event.target instanceof HTMLButtonElement &&
		(event.key === " " || event.key === "Enter")
	) {
		cancelPendingPrefix();
		return;
	}
	if (isHalfPageKey(event)) {
		cancelPendingPrefix();
		if (busy) return;
		event.preventDefault();
		scrollFocusedPane(
			"top",
			((event.key.toLowerCase() === "d" ? 1 : -1) *
				(paneElement()?.clientHeight ?? 400)) /
				2,
		);
		return;
	}
	if (hasUnrelatedModifiers(event)) {
		cancelPendingPrefix();
		return;
	}
	if (pendingPrefix && event.key === "Tab") {
		cancelPendingPrefix();
		return;
	}
	if (busy && !pendingPrefix) {
		if (event.key === " ") {
			event.preventDefault();
			showPendingPrefix(" ");
		}
		return;
	}
	if (pendingPrefix) {
		event.preventDefault();
		runSequence(pendingPrefix, event.key);
		return;
	}
	if ([" ", "g", "[", "]"].includes(event.key)) {
		event.preventDefault();
		showPendingPrefix(event.key);
		return;
	}
	if (event.key === "v") {
		event.preventDefault();
		toggleRangeSelection();
	} else if (event.key === "?") {
		event.preventDefault();
		showHelp();
	} else if (event.key === "/") {
		event.preventDefault();
		openSearch();
	} else if (event.key === "n" || event.key === "N") {
		event.preventDefault();
		searchLines(event.key === "n" ? 1 : -1);
	} else if (event.key === "j" || event.key === "k") {
		event.preventDefault();
		if (keyboardPane === "diff") moveLine(event.key === "j" ? 1 : -1);
		else scrollFocusedPane("top", event.key === "j" ? 48 : -48);
	} else if (event.key === "h" || event.key === "l") {
		event.preventDefault();
		scrollFocusedPane("left", event.key === "l" ? 96 : -96);
	} else if (event.key === "G") {
		event.preventDefault();
		jumpBoundary(true);
	}
});

request("/api/state")
	.then(async (payload) => {
		applyVersion(payload);
		review = payload.review;
		acknowledgedDraft = draftSignature(review.draft);
		applyThreads(payload);
		await restoreLocalDraft();
		restoreAskDrafts();
		renderAll();
		if (payload.busy) {
			setBusy(true, "Review operation in progress…");
			readinessTimer = setTimeout(pollReadiness, NOTE_DEBOUNCE_MS);
		} else {
			setBusy(false, "Review ready");
		}
	})
	.catch((error) => {
		workspace.setAttribute("aria-busy", "false");
		diff.replaceChildren(node("p", "empty-state", error.message));
		reportError(error);
	});