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: "Space c",
label: "Comment on focused line",
prefix: " ",
continuation: "c",
short: "comment on focused line",
},
{
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",
},
];
const READINESS_RETRIES = 12;
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 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();
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 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 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})`);
}
applyVersion(payload);
if (!response.ok) {
const error = new Error(
payload.error || `Request failed (${response.status})`,
);
error.code = payload.code;
throw error;
}
applyThreads(payload);
return payload;
}
function setStatus(message, error = false) {
status.textContent = message;
status.classList.toggle("error", error);
}
function reportError(error) {
if (error?.code === "stale_client") {
staleClient = true;
persistDraft(currentDraft());
setStatus(
"Review changed in another tab. Reload to keep these notes.",
true,
);
return;
}
setStatus(error instanceof Error ? error.message : "Request failed", true);
}
function isNavigationControl(control) {
return (
control.id === "close-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) {
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 layers() {
return review.plan.cohorts.flatMap((cohort) => cohort.layers);
}
function layerById(id) {
return layers().find((layer) => layer.id === id);
}
function layerForHunk(hunkId) {
return layers().find((layer) =>
layer.hunks.some((hunk) => hunk.id === hunkId),
);
}
function hunkById(id) {
return review.snapshot.hunks.find((hunk) => hunk.id === 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 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 });
}
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 queueDraftSave(draft) {
const copy = structuredClone(draft);
const generation = ++draftGeneration;
review.draft = copy;
persistDraft(copy);
if (staleClient) {
setStatus(
"Review changed in another tab. Reload to keep these notes.",
true,
);
return Promise.resolve();
}
const pending = saveChain.then(async () => {
try {
const payload = await request("/api/draft", copy);
if (generation === draftGeneration) {
review.draft = payload.draft;
persistDraft(payload.draft);
updateProgress();
setStatus("Draft saved");
}
return payload.draft;
} catch (error) {
if (error && typeof error === "object") {
error.draftGeneration = generation;
}
throw error;
}
});
saveChain = pending.catch(reportError);
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;
await queueDraftSave(currentDraft());
}
await saveChain;
if (staleClient) {
throw new Error(
"Review changed in another tab. Reload to keep these notes.",
);
}
}
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 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) => review.draft.reviewed.includes(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 = review.draft.reviewed.includes(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];
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;
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];
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;
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 = review.draft.reviewed.includes(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);
const plannedHunk = layerForHunk(hunk.id)?.hunks.find(
(item) => item.id === hunk.id,
);
article.append(heading);
if (plannedHunk) {
article.append(node("p", "hunk-summary", plannedHunk.summary));
}
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);
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 atBottom =
root && root.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";
byId("explanation").textContent =
plannedHunk?.summary || "No explanation available.";
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) {
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}`,
);
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}`,
);
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() {
byId("source-label").textContent = sourceLabel();
notes.value = review.draft.notes;
renderForge();
renderNavigation();
renderDiff();
renderFindings();
updateProgress();
restoreQuestionForScope();
renderAsk();
applyPanelState();
updateLayerAction();
updateKeyboardStatus();
}
function selectLayer(layerId, focusDiff = false) {
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) {
if (busy || staleClient || !hunkById(hunkId) || !Number.isSafeInteger(line)) {
return;
}
selectDrawerTab("feedback");
commentTarget = { hunkId, side, line };
byId("comment-anchor").textContent =
`${hunkById(hunkId)?.path} · ${side}:${line}`;
byId("comment-text").value = "";
byId("comment-severity").value = "question";
commentDialog.showModal();
byId("comment-text").focus();
}
function restoreLocalDraft() {
const draft = storedDraft();
if (!draft || typeof draft !== "object") return;
if (!Array.isArray(draft.reviewed) || !Array.isArray(draft.findings)) return;
if (typeof draft.notes !== "string") return;
const needsSave = JSON.stringify(draft) !== JSON.stringify(review.draft);
review.draft = draft;
if (needsSave) {
notesPending = true;
scheduleNotesSave();
}
}
async function pollReadiness(attempt = 0) {
try {
const payload = await request("/api/state");
if (!payload.busy) {
if (payload.snapshotId === snapshotId) review = payload.review;
renderAll();
setBusy(false, "Review ready");
return;
}
} catch (error) {
setBusy(true);
setStatus(
`${error instanceof Error ? error.message : "Readiness check failed"}. Reload this page to retry; Close remains available.`,
true,
);
return;
}
if (attempt >= READINESS_RETRIES) {
setBusy(
true,
"Review is still busy. Reload this page to retry; Close remains available.",
);
return;
}
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-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. Reload to keep 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,
});
try {
await queueDraftSave(draft);
commentDialog.close();
commentTarget = null;
renderFindings();
setKeyboardPane("drawer", true);
} 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) {
askErrors.set(
sent.scopeKey,
error instanceof Error ? error.message : "Question failed",
);
reportError(error);
} finally {
pendingAsk = null;
askInFlight = false;
setBusy(false);
renderAsk();
if (
scopeKey() === sent.scopeKey &&
drawerOpen &&
!byId("ask-panel").hidden &&
document.activeElement === document.body
)
questionInput.focus({ preventScroll: true });
}
});
async function refreshReview() {
if (busy || staleClient) {
if (staleClient) reportError({ code: "stale_client" });
return;
}
if (!window.confirm("Discard the current draft and refresh the review?"))
return;
setBusy(true, "Saving draft…");
try {
await flushDraft();
setStatus("Refreshing snapshot…");
const previousDraftKey = storageKey();
const previousAskKey = askStorageKey();
const payload = await request("/api/refresh", { confirm: true });
review = payload.review;
threads = Array.isArray(payload.threads) ? payload.threads : [];
askDrafts = {};
askErrors.clear();
selectedLayerId = null;
currentHunkId = null;
currentLineKey = null;
searchQuery = "";
try {
sessionStorage.removeItem(previousDraftKey);
sessionStorage.removeItem(previousAskKey);
} catch {}
clearStoredDraft();
clearStoredAskDrafts();
renderAll();
setKeyboardPane("diff", true);
setStatus(
payload.wasCurrent ? "Review refreshed" : "Stale snapshot replaced",
);
} catch (error) {
reportError(error);
} finally {
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;
clearStoredDraft();
notes.value = review.draft.notes;
renderFindings();
renderDiff();
updateProgress();
setStatus("Feedback sent to Pi");
} catch (error) {
reportError(error);
} finally {
setBusy(false);
}
}
async function closeReview() {
if (!window.confirm("Close this review workspace?")) return;
if (busy) {
try {
await request("/api/close", {});
setStatus("Review closed. You may close this tab.");
} catch (error) {
reportError(error);
}
return;
}
setBusy(true, "Saving draft…");
try {
await flushDraft();
await request("/api/close", {});
setStatus("Review closed. You may close this tab.");
} catch (error) {
reportError(error);
setBusy(false);
}
}
byId("refresh-button").addEventListener("click", refreshReview);
byId("submit-button").addEventListener("click", submitFeedback);
byId("close-button").addEventListener("click", closeReview);
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 (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),
);
} 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();
else if (key === "f") refreshReview();
else if (key === "q") closeReview();
}
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) return;
if (pendingPrefix) {
event.preventDefault();
runSequence(pendingPrefix, event.key);
return;
}
if ([" ", "g", "[", "]"].includes(event.key)) {
event.preventDefault();
showPendingPrefix(event.key);
return;
}
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((payload) => {
review = payload.review;
applyThreads(payload);
restoreLocalDraft();
restoreAskDrafts();
renderAll();
if (payload.busy) {
setBusy(true, "Review operation in progress…");
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);
});