repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/strata/assets/app.js
Rawconst 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);
});