/** Server-rendered review page: escaped HTML, one stylesheet, one script. */ import type { ReviewComment, ReviewState, SectionState, WalkthroughDocument, WalkthroughSection, } from "./contract.ts"; export function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } const INLINE_CODE = /`([^`\n]+)`/g; const LINK = /\[([^\]\n]+)\]\(([^()\s]+)\)/g; const BOLD = /\*\*([^*\n]+?)\*\*/g; const ITALIC = /(?$2') .replace(BOLD, "$1") .replace(ITALIC, "$1"); } function inline(value: string): string { const escaped = escapeHtml(value); const parts: string[] = []; let position = 0; for (const match of escaped.matchAll(INLINE_CODE)) { parts.push(emphasis(escaped.slice(position, match.index))); parts.push(`${match[1]}`); position = match.index + match[0].length; } parts.push(emphasis(escaped.slice(position))); return parts.join(""); } /** Render a safe structured Markdown subset to fully escaped HTML. */ export function renderMarkdown(value: string): string { const blocks: string[] = []; let paragraph: string[] = []; let items: string[] = []; let listTag = ""; let quote: string[] = []; const flushParagraph = () => { if (paragraph.length > 0) blocks.push(`

${inline(paragraph.join(" "))}

`); paragraph = []; }; const flushList = () => { if (items.length > 0) blocks.push( `<${listTag}>${items.map((item) => `
  • ${item}
  • `).join("")}`, ); items = []; listTag = ""; }; const flushQuote = () => { if (quote.length > 0) blocks.push(`

    ${inline(quote.join(" "))}

    `); quote = []; }; const flush = () => { flushParagraph(); flushList(); flushQuote(); }; const lines = value.split("\n"); for (let index = 0; index < lines.length; index++) { const stripped = lines[index]!.trim(); if (stripped.startsWith("```")) { flush(); const code: string[] = []; index++; while (index < lines.length && !lines[index]!.trim().startsWith("```")) { code.push(lines[index]!); index++; } blocks.push( `
    ${escapeHtml(code.join("\n"))}
    `, ); continue; } const heading = HEADING.exec(stripped); const ordered = ORDERED.exec(stripped); if (heading) { flush(); const level = Math.min(heading[1]!.length + 2, 6); blocks.push(`${inline(heading[2]!)}`); } else if (["---", "***", "___"].includes(stripped)) { flush(); blocks.push("
    "); } else if (/^[-*+] /.test(stripped)) { flushParagraph(); flushQuote(); if (listTag !== "ul") { flushList(); listTag = "ul"; } items.push(inline(stripped.slice(2).trim())); } else if (ordered) { flushParagraph(); flushQuote(); if (listTag !== "ol") { flushList(); listTag = "ol"; } items.push(inline(ordered[1]!)); } else if (stripped.startsWith(">")) { flushParagraph(); flushList(); quote.push(stripped.slice(1).trim()); } else if (!stripped) flush(); else { flushList(); flushQuote(); paragraph.push(stripped); } } flush(); return blocks.join(""); } export function diffClass(line: string): string { if (line.startsWith("@@")) return "hunk"; if ( /^(?:diff |index |--- |\+\+\+ |new file|deleted file|similarity|rename)/.test( line, ) ) return "file"; if (line.startsWith("+")) return "add"; if (line.startsWith("-")) return "del"; return "context"; } export function renderDiff(value: string): string { return value .split("\n") .map( (line) => `${escapeHtml(line) || " "}`, ) .join(""); } const STATE_LABELS: Record = { "not-reviewed": "Not viewed", viewed: "Viewed", "needs-explanation": "Needs explanation", }; const OUTCOME_LABELS: Record = { approved: "Approved. The session agent has the result.", "changes-requested": "Changes requested. This review is closed and the walkthrough is invalidated.", }; function title(id: string): string { return id .split("-") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function commentHtml(comment: ReviewComment): string { return ( '
    ' + `
    Comment on ${escapeHtml(comment.file)}:${escapeHtml(comment.lines)}
    ` + `
    ${escapeHtml(comment.body)}
    ` ); } function masthead(document: WalkthroughDocument): string { const warnings = document.warnings .map( (item) => `
    warn: ${escapeHtml(item)}
    `, ) .join(""); return ( '
    ' + '
    quick review' + `${escapeHtml(document.revision.slice(0, 12))}
    ` + `

    ${escapeHtml(document.title)}

    ` + `
    ${renderMarkdown(document.summary)}
    ` + '
    ' + `
    revision
    ${escapeHtml(document.revision)}
    ` + `
    base
    ${escapeHtml(document.baseRevision)}
    ` + `
    files
    ${document.files}
    ` + `
    lines
    +${document.added} -${document.removed}
    ` + `
    contract
    walkthrough v${document.version}
    ` + "
    " + warnings + '
    ' + '' + '' + '' + "
    " ); } function index(document: WalkthroughDocument, state: ReviewState): string { const rows = document.sections .map((section) => { const viewed = state.viewed[section.id] === true; return ( `
  • ` + `${viewed ? "[x]" : "[ ]"}` + `${escapeHtml(title(section.id))}` + `${escapeHtml(section.file)}:${escapeHtml(section.lines)}` + `${section.importance}` + "
  • " ); }) .join(""); const viewed = Object.values(state.viewed).filter(Boolean).length; const total = document.sections.length; return ( '" ); } function card( position: number, total: number, section: WalkthroughSection, state: ReviewState, ): string { const id = escapeHtml(section.id); const value = state.sections[section.id] ?? "not-reviewed"; const viewed = state.viewed[section.id] === true; const answers = state.questions .filter((item) => item.sectionId === section.id) .map( (item) => `
    ${escapeHtml(item.question)}
    ${escapeHtml(item.answer)}
    `, ) .join(""); const comments = state.comments .filter((item) => item.sectionId === section.id) .map(commentHtml) .join(""); return ( `
    ` + '
    ' + `

    change ${position} of ${total}

    ` + `

    ${escapeHtml(title(section.id))}

    ` + `

    ${escapeHtml(section.file)}:${escapeHtml(section.lines)}

    ` + `

    ${section.importance}` + `${STATE_LABELS[value]}

    ` + "
    " + `
    ' + '
    ' + `
    ${renderMarkdown(section.markdown)}
    ` + `
    ${renderDiff(section.diff)}
    ` + `
    Review prompt${escapeHtml(section.prompt)}
    ` + `
    ${answers}
    ` + `
    ` + `
    ${comments}
    ` + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '' + '' + "
    " ); } function final(document: WalkthroughDocument, state: ReviewState): string { const viewed = Object.values(state.viewed).filter(Boolean).length; const total = document.sections.length; const canApprove = viewed === total && state.outcome === "open"; return ( '
    ' + "

    Final review

    " + '
    ' + `${viewed}/${total} viewed` + `${state.comments.length} change comments
    ` + `
    ${state.comments.map(commentHtml).join("")}
    ` + '' + '' + '

    Approval requires every change to be viewed. Request changes requires an overall explanation and carries every comment above back to the session agent.

    ' + `
    ` + '
    ' + '' + `
    ${ state.outcome === "open" ? "" : OUTCOME_LABELS[state.outcome] }
    ` + "
    " ); } export function renderPage( document: WalkthroughDocument, state: ReviewState, ): string { const total = document.sections.length; const cards = document.sections .map((section, position) => card(position + 1, total, section, state)) .join(""); return ( '' + '' + '' + `${escapeHtml(document.title)}` + '' + '' + `${masthead(document)}${index(document, state)}${cards}${final(document, state)}` + '' ); } export const PAGE_CSS = String.raw` :root { color-scheme: light dark; --bg: #eceeed; --panel: #fbfcfb; --ink: #1a1f20; --muted: #535e5d; --line: #c3cbc9; --strong: #87958f; --accent: #00587a; --ok: #17632a; --err: #9e1b26; --warn: #7a4e00; --add-bg: #e1f1e3; --del-bg: #f6e3e3; --hunk-bg: #e2ecf1; --code-bg: #f0f2f1; --hover: #e5e9e8; --on-solid: #ffffff; } @media (prefers-color-scheme: dark) { :root { --bg: #111514; --panel: #171c1b; --ink: #d9e1de; --muted: #939d99; --line: #2e3634; --strong: #4d5a57; --accent: #4fb3d9; --ok: #46a05e; --err: #e06058; --warn: #cf9a3d; --add-bg: #142b1c; --del-bg: #33191b; --hunk-bg: #122733; --code-bg: #131817; --hover: #1f2624; --on-solid: #101413; } } * { box-sizing: border-box; } html { scroll-behavior: smooth; } body { margin: 0; background: var(--bg); color: var(--ink); font: 13px/1.6 ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace; } main { max-width: 1080px; margin: 0 auto; padding: 28px 20px 90px; } :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } .skip { position: absolute; left: -9999px; top: 0; z-index: 10; background: var(--panel); border: 1px solid var(--strong); padding: 8px 12px; color: var(--ink); } .skip:focus { left: 12px; top: 12px; } .masthead { background: var(--panel); border: 1px solid var(--line); padding: 22px 24px; margin-bottom: 18px; } .mast-line { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; } .eyebrow { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; } .mast-rev { color: var(--muted); font-size: 12px; border: 1px solid var(--line); background: var(--code-bg); padding: 1px 8px; } h1 { margin: 0 0 10px; font-size: 19px; line-height: 1.35; } .facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); border-top: 1px solid var(--line); border-left: 1px solid var(--line); margin: 16px 0 0; } .fact { background: var(--panel); padding: 8px 12px; min-width: 0; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); } .fact dt { font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin: 0 0 3px; } .fact dd { margin: 0; font-size: 12px; overflow-wrap: anywhere; } .added { color: var(--ok); font-weight: 700; } .removed { color: var(--err); font-weight: 700; } .warning { margin: 14px 0 0; padding: 8px 12px; border: 1px solid var(--warn); border-left-width: 3px; color: var(--warn); } .mast-tools { margin-top: 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } .full-diff { flex-basis: 100%; max-height: 520px; overflow: auto; margin: 4px 0 0; } .index { background: var(--panel); border: 1px solid var(--line); padding: 16px 20px; margin-bottom: 18px; } .index-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; } .index h2, .final h2 { margin: 0; font-size: 13px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; } .index-progress { color: var(--muted); font-size: 12px; } .progress { appearance: none; display: block; width: 100%; height: 10px; border: 1px solid var(--line); background: var(--code-bg); } .progress::-webkit-progress-bar { background: var(--code-bg); } .progress::-webkit-progress-value { background: var(--ok); } .progress::-moz-progress-bar { background: var(--ok); } .index-list { list-style: none; margin: 12px 0 0; padding: 0; border-top: 1px solid var(--line); } .index-row { display: flex; gap: 10px; align-items: baseline; padding: 7px 4px; border-bottom: 1px solid var(--line); color: inherit; text-decoration: none; min-width: 0; } .index-row:hover { background: var(--hover); } .index-mark { color: var(--muted); flex: none; } .index-mark.done { color: var(--ok); font-weight: 700; } .index-title { font-weight: 600; flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; } .index-file { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; min-width: 0; flex: 1; } .kbd-hint { margin: 10px 0 0; color: var(--muted); font-size: 11px; } kbd { border: 1px solid var(--strong); background: var(--code-bg); padding: 0 5px; font: inherit; font-size: 11px; } .tag, .badge { display: inline-block; border: 1px solid var(--line); background: var(--code-bg); color: var(--muted); padding: 1px 8px; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; } .importance-critical { color: var(--err); border-color: var(--err); } .importance-important { color: var(--warn); border-color: var(--warn); } .importance-supporting { color: var(--muted); border-color: var(--strong); } .state-not-reviewed { color: var(--muted); } .state-viewed { color: var(--ok); border-color: var(--ok); } .state-needs-explanation { color: var(--accent); border-color: var(--accent); } .card { background: var(--panel); border: 1px solid var(--line); margin: 0 0 18px; scroll-margin-top: 16px; } .card.viewed { border-left: 3px solid var(--ok); } .card.viewed .card-details { display: none; } .card.viewed .card-head { border-bottom: 0; } .card.viewed h2 { color: var(--muted); } .card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; padding: 16px 20px; border-bottom: 1px solid var(--line); } .card-title { min-width: 0; } .card-count { margin: 0 0 4px; color: var(--muted); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; } .card h2 { margin: 0 0 6px; font-size: 15px; } .meta { margin: 4px 0 0; display: flex; gap: 8px; flex-wrap: wrap; align-items: baseline; min-width: 0; } .card-file { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; } .view-control { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 8px; color: var(--muted); font-weight: 600; white-space: nowrap; flex: none; cursor: pointer; } .view-control input { width: 15px; height: 15px; margin: 0; accent-color: var(--ok); } .prose { padding: 14px 20px; overflow-wrap: break-word; } .prose.intro { padding: 0; } .prose p { margin: 8px 0; } .prose h3, .prose h4, .prose h5, .prose h6 { margin: 14px 0 6px; font-size: 13px; letter-spacing: 0.05em; text-transform: uppercase; } .prose h3 { font-size: 14px; } .prose ul, .prose ol { margin: 8px 0; padding-left: 22px; } .prose li { margin: 3px 0; } .prose blockquote { margin: 10px 0; padding: 2px 12px; border-left: 3px solid var(--strong); color: var(--muted); } .prose hr { border: 0; border-top: 1px solid var(--line); margin: 14px 0; } .prose code { background: var(--code-bg); border: 1px solid var(--line); padding: 0 4px; font-size: 12px; overflow-wrap: anywhere; } .prose .md-code { margin: 10px 0; padding: 10px 12px; border: 1px solid var(--line); background: var(--code-bg); overflow-x: auto; font-size: 12px; line-height: 18px; } .prose .md-code code { border: 0; background: none; padding: 0; } .diff { margin: 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); overflow-x: auto; background: var(--code-bg); font-size: 12px; line-height: 20px; } .diff-line { display: block; white-space: pre; min-width: max-content; padding: 0 14px; border-left: 3px solid transparent; } .diff-add { background: var(--add-bg); border-left-color: var(--ok); } .diff-del { background: var(--del-bg); border-left-color: var(--err); } .diff-hunk { background: var(--hunk-bg); color: var(--accent); } .diff-file { color: var(--muted); font-weight: 600; } .prompt { margin: 14px 20px; padding: 10px 14px; border: 1px solid var(--line); border-left: 3px solid var(--accent); background: var(--code-bg); } .prompt strong { display: block; color: var(--accent); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 4px; } .answers { margin: 0 20px; } .answer { border: 1px solid var(--line); background: var(--code-bg); padding: 10px 14px; margin: 10px 0; } .answer strong { display: block; margin-bottom: 4px; } .answer div { white-space: pre-wrap; overflow-wrap: anywhere; } .controls { padding: 14px 20px 20px; } .comment-thread { border: 1px solid var(--line); margin: 10px 0; } .comment-thread:empty { display: none; } .review-comment + .review-comment { border-top: 1px solid var(--line); } .review-comment-head { padding: 6px 12px; background: var(--code-bg); border-bottom: 1px solid var(--line); font-size: 11px; color: var(--muted); overflow-wrap: anywhere; } .review-comment-body { padding: 10px 12px; white-space: pre-wrap; overflow-wrap: anywhere; } .comment, .question, .overall-comment { width: 100%; border: 1px solid var(--strong); background: var(--panel); color: var(--ink); padding: 8px 10px; margin: 0 0 10px; font: inherit; } .comment, .overall-comment { min-height: 84px; resize: vertical; } ::placeholder { color: var(--muted); opacity: 0.8; } .comment-composer, .question-composer { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; } .actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; } .button { appearance: none; border: 1px solid var(--strong); background: var(--panel); color: var(--ink); font: inherit; font-weight: 600; padding: 6px 14px; cursor: pointer; } .button:hover { background: var(--hover); } .button:disabled { opacity: 0.45; cursor: not-allowed; } .button.primary { background: var(--ok); border-color: var(--ok); color: var(--on-solid); } .button.danger { background: var(--err); border-color: var(--err); color: var(--on-solid); } .button.comment-action { background: var(--accent); border-color: var(--accent); color: var(--on-solid); } .button.primary:hover, .button.danger:hover, .button.comment-action:hover { filter: brightness(1.12); } .feedback { min-height: 20px; margin-top: 8px; color: var(--muted); font-size: 12px; } .feedback.error { color: var(--err); font-weight: 600; } .feedback.success { color: var(--ok); } .feedback.compact { width: 100%; min-height: 0; margin: 0; white-space: normal; text-align: right; } .feedback.compact:empty { display: none; } .spinner { display: inline-block; width: 9px; height: 9px; background: currentColor; margin-right: 7px; vertical-align: -1px; animation: pulse 1s steps(2, start) infinite; } @keyframes pulse { to { opacity: 0; } } .context-view { max-height: 420px; overflow: auto; margin: 12px 0 0; padding: 10px 12px; border: 1px solid var(--line); background: var(--code-bg); font-size: 12px; line-height: 18px; white-space: pre; } .final { background: var(--panel); border: 1px solid var(--line); padding: 20px 24px; margin-top: 24px; } .final-counts { display: flex; gap: 16px; flex-wrap: wrap; color: var(--muted); font-weight: 600; font-size: 12px; margin-top: 12px; } .review-summary { margin-top: 12px; } .review-summary:empty { display: none; } .overall-label { display: block; font-weight: 700; margin: 16px 0 6px; } .final-note { color: var(--muted); font-size: 12px; margin: 10px 0; } .outcome { margin-top: 14px; padding: 10px 14px; border: 1px solid var(--ok); border-left-width: 3px; color: var(--ok); font-weight: 700; } .outcome.changes-requested { border-color: var(--err); color: var(--err); } main.closed .view-control, main.closed .comment-composer, main.closed .question-composer, main.closed .card .actions, main.closed .final .actions, main.closed .overall-label, main.closed .overall-comment, main.closed .final-note { display: none; } main.closed .card.viewed .card-details { display: block; } @media (max-width: 680px) { main { padding: 14px 10px 60px; } .masthead, .index, .final { padding: 14px; } .card-head { flex-direction: column; gap: 10px; } .view-control { justify-content: flex-start; } .prose, .controls { padding-left: 14px; padding-right: 14px; } .prompt, .answers { margin-left: 14px; margin-right: 14px; } .actions .button { flex: 1; } .index-row { flex-wrap: wrap; } .index-file { flex-basis: 100%; } .index-row .tag { margin-left: auto; } } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } .spinner { animation: none; } } `; export const PAGE_JS = String.raw` const root=document.querySelector('main'); const cards=[...root.querySelectorAll('[data-card]')]; let currentState=null; let activeCard=-1; const stateLabels={'not-reviewed':'Not viewed','viewed':'Viewed','needs-explanation':'Needs explanation'}; const outcomeLabels={'approved':'Approved. The session agent has the result.','changes-requested':'Changes requested. This review is closed and the walkthrough is invalidated.'}; function commentBox(item){const box=document.createElement('div');box.className='review-comment';const head=document.createElement('div');head.className='review-comment-head';head.textContent='Comment on '+item.file+':'+item.lines;const body=document.createElement('div');body.className='review-comment-body';body.textContent=item.body;box.append(head,body);return box} function diffKind(line){if(line.startsWith('@@'))return 'hunk';if(/^(?:diff |index |--- |\+\+\+ |new file|deleted file|similarity|rename)/.test(line))return 'file';if(line.startsWith('+'))return 'add';if(line.startsWith('-'))return 'del';return 'context'} function renderDiff(target,value){target.replaceChildren();for(const line of value.split('\n')){const span=document.createElement('span');span.className='diff-line diff-'+diffKind(line);span.textContent=line||' ';target.append(span)}} function syncFinalActions(){ const overall=document.querySelector('.overall-comment');const hasOverall=Boolean(overall&&overall.value.trim());const closed=Boolean(currentState&¤tState.outcome!=='open'); const approve=document.querySelector('[data-action="approve"]'); if(approve){const count=currentState?currentState.comments.length:0;approve.textContent=(count>0||hasOverall)?'Approve with comments':'Approve';if(currentState){const viewed=Object.values(currentState.viewed).filter(Boolean).length;approve.disabled=closed||viewed!==Object.keys(currentState.sections).length}} const request=document.querySelector('[data-action="request-changes"]');if(request){request.hidden=!hasOverall||closed;request.disabled=closed} const outcome=document.querySelector('[data-outcome]'); if(outcome){if(closed){outcome.hidden=false;outcome.className='outcome '+currentState.outcome;outcome.textContent=outcomeLabels[currentState.outcome]||currentState.outcome}else{outcome.hidden=true}} } function renderState(state){ currentState=state; root.classList.toggle('closed',state.outcome!=='open'); let viewed=0; for(const [id,value] of Object.entries(state.sections)){ if(state.viewed[id])viewed++; const card=document.querySelector('[data-card="'+CSS.escape(id)+'"]');if(card)card.classList.toggle('viewed',state.viewed[id]); const checkbox=document.querySelector('[data-viewed="'+CSS.escape(id)+'"]');if(checkbox)checkbox.checked=state.viewed[id]; const mark=document.querySelector('[data-nav-viewed="'+CSS.escape(id)+'"]');if(mark){mark.textContent=state.viewed[id]?'[x]':'[ ]';mark.classList.toggle('done',state.viewed[id])} const badge=document.querySelector('[data-state="'+CSS.escape(id)+'"]'); if(badge){badge.className='badge state-'+value;badge.textContent=stateLabels[value]||value} const answers=document.querySelector('[data-answers="'+CSS.escape(id)+'"]'); if(answers){answers.replaceChildren();for(const item of state.questions.filter(q=>q.sectionId===id)){ const box=document.createElement('div');box.className='answer';const strong=document.createElement('strong');strong.textContent=item.question;const text=document.createElement('div');text.textContent=item.answer;box.append(strong,text);answers.append(box) }} for(const thread of document.querySelectorAll('[data-comments="'+CSS.escape(id)+'"]')){thread.replaceChildren();for(const item of state.comments.filter(n=>n.sectionId===id))thread.append(commentBox(item))} } const total=Object.keys(state.sections).length; document.querySelectorAll('[data-reviewed]').forEach(x=>x.textContent=viewed); document.querySelectorAll('[data-total]').forEach(x=>x.textContent=total); document.querySelectorAll('[data-note-count]').forEach(x=>x.textContent=state.comments.length); document.querySelectorAll('[data-progress]').forEach(x=>{x.max=total;x.value=viewed}); const summary=document.querySelector('[data-review-summary]');if(summary){summary.replaceChildren();for(const item of state.comments)summary.append(commentBox(item))} syncFinalActions(); } async function act(control,action=control.dataset.action){ const scope=control.closest('[data-scope]');const feedback=scope.querySelector('.feedback');const controls=scope.querySelectorAll('button,input,textarea');const input=control.dataset.input?scope.querySelector(control.dataset.input):null;const payload={action};if(scope.dataset.section)payload.section=scope.dataset.section;if(input)payload.comment=input.value; controls.forEach(x=>x.disabled=true);feedback.className='feedback';feedback.replaceChildren(Object.assign(document.createElement('span'),{className:'spinner'}),document.createTextNode('Working...')); try{ const response=await fetch('action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)}); let result;try{result=await response.json()}catch{throw new Error('Request failed ('+response.status+')')} if(result.state)renderState(result.state); if(!response.ok||!result.ok)throw new Error(result.error||'Request failed ('+response.status+')'); if(typeof result.context==='string'){const view=scope.querySelector('.context-view');if(view){view.querySelector('code').textContent=result.context;view.hidden=false}} if(typeof result.diff==='string'){const view=scope.querySelector('[data-full-diff]');if(view){renderDiff(view,result.diff);view.hidden=false}} feedback.textContent=result.message||'Updated.';feedback.className='feedback success'; if(input&&['ask','add-comment'].includes(payload.action))input.value=''; } catch(error){feedback.textContent=error instanceof Error?error.message:String(error);feedback.className='feedback error'} finally{controls.forEach(x=>x.disabled=false);if(currentState)renderState(currentState)} } root.addEventListener('click',event=>{const button=event.target.closest('button[data-action]');if(button){event.preventDefault();act(button)}}); root.addEventListener('change',event=>{const checkbox=event.target.closest('input[data-viewed]');if(checkbox)act(checkbox,checkbox.checked?'mark-viewed':'reopen')}); root.addEventListener('input',event=>{if(event.target.matches('.overall-comment'))syncFinalActions()}); root.addEventListener('focusin',event=>{const card=event.target.closest('[data-card]');if(card)activeCard=cards.indexOf(card)}); function focusCard(index){if(cards.length===0)return;activeCard=Math.min(cards.length-1,Math.max(0,index));const card=cards[activeCard];card.focus({preventScroll:true});card.scrollIntoView({block:'start'})} function typingTarget(target){return target instanceof HTMLElement&&(target.isContentEditable||['INPUT','TEXTAREA','SELECT'].includes(target.tagName))} document.addEventListener('keydown',event=>{ if(event.defaultPrevented||event.altKey||event.ctrlKey||event.metaKey||typingTarget(event.target))return; if(event.key==='j'){event.preventDefault();focusCard(activeCard+1)} else if(event.key==='k'){event.preventDefault();focusCard(activeCard<=0?0:activeCard-1)} else if(event.key==='v'&&activeCard>=0){const checkbox=cards[activeCard].querySelector('input[data-viewed]');if(checkbox&&!checkbox.disabled){event.preventDefault();checkbox.checked=!checkbox.checked;act(checkbox,checkbox.checked?'mark-viewed':'reopen')}} }); `;