sphinx-understand 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,47 @@
1
+ import { sphinxMark } from './logo.js';
1
2
  /** Static UI only. Model-authored content always enters the DOM as textContent. */
2
3
  export function browserPage({ token, nonce }) {
3
4
  return `<!doctype html>
4
5
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sphinx — understanding check</title>
5
6
  <style nonce="${nonce}">
6
- :root{color-scheme:light;--ink:#292524;--muted:#6b6259;--line:#dfd8cf;--paper:#faf8f5;--amber:#a45009;--focus:#b45309}
7
+ :root{color-scheme:light;--ink:#292524;--muted:#6b6259;--line:#dfd8cf;--paper:#faf8f5;--amber:#a45009;--focus:#b45309;--pass:#3d6b35;--retry:#8d271e;--add:#e8f1e3;--add-ink:#2f5d28;--del:#f8e6e2;--del-ink:#8d271e;--mark:#fdf0cc;--mono:ui-monospace,SFMono-Regular,Menlo,monospace}
7
8
  *{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.6 "Avenir Next",Avenir,sans-serif}
8
9
  button,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.5}
9
- button:focus-visible,textarea:focus-visible{outline:2px solid var(--focus);outline-offset:3px}
10
+ button:focus-visible,textarea:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
10
11
  .shell{height:100vh;height:100dvh;display:flex;flex-direction:column}
11
- header{min-height:60px;border-bottom:1px solid var(--line);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;gap:16px}
12
- .brand{display:flex;align-items:baseline;gap:14px}.brand strong{font:600 19px ui-monospace,SFMono-Regular,Menlo,monospace}.brand span{color:var(--muted);font-size:14px}
13
- main{width:100%;max-width:800px;margin:0 auto;padding:0 24px;display:flex;flex:1;flex-direction:column;min-height:0}
14
- .context{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 0;border-bottom:1px solid var(--line);font-size:13px;color:var(--muted)}
15
- #meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#usage{white-space:nowrap;font-variant-numeric:tabular-nums}
12
+ header{min-height:52px;border-bottom:1px solid var(--line);padding:8px 24px;display:flex;align-items:center;justify-content:space-between;gap:16px}
13
+ .brand{display:flex;align-items:center;gap:14px}.brand strong{display:flex;align-items:center;gap:9px;font:600 19px ui-monospace,SFMono-Regular,Menlo,monospace}.brand svg{color:var(--amber);flex-shrink:0}.brand span{color:var(--muted);font-size:14px}
14
+ .scope{border-bottom:1px solid var(--line);padding:8px 24px 9px;font-size:13px;line-height:1.45;background:#f5f1eb}
15
+ .facts{display:flex;flex-wrap:wrap;gap:2px 20px;margin:0;align-items:baseline}.facts div{display:flex;gap:6px;min-width:0}.facts dt{color:var(--muted)}.facts dd{margin:0;font-weight:600;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
16
+ .facts .mono{font:600 12.5px var(--mono)}.scope-note{margin:2px 0 0;color:var(--muted);font-size:12px}#usage{color:var(--muted);font-weight:400;font-variant-numeric:tabular-nums}
17
+ .tabs{display:none}.scope-short{display:none}
18
+ .workspace{flex:1;min-height:0;display:grid;grid-template-columns:minmax(210px,20%) minmax(0,45%) minmax(340px,35%)}
19
+ .pane{min-width:0;min-height:0;display:flex;flex-direction:column;border-right:1px solid var(--line)}.pane:last-child{border-right:0}
20
+ .pane h2{font-size:13px;font-weight:600;margin:0;overflow-wrap:anywhere;line-height:1.4}.pane-note{font-size:12px;color:var(--muted);margin:0;padding:8px 14px;overflow-wrap:anywhere}
21
+ .filters{display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:10px 12px 6px}.filters button{min-height:44px;font-size:12.5px;line-height:1.25;padding:4px 8px}
22
+ .filters button[aria-pressed=true]{background:#efe5d8;border-color:var(--amber);font-weight:600}.filters .count{display:block;font:11.5px var(--mono);color:var(--muted)}
23
+ #file-search{margin:4px 12px 6px;font:inherit;font-size:13px;padding:8px 10px;border:1px solid #b9afa3;border-radius:6px;background:#fff;color:var(--ink);min-height:40px}
24
+ .filelist{list-style:none;margin:0;padding:0 0 8px;overflow-y:auto;flex:1;min-height:0}
25
+ .file{display:flex;align-items:flex-start;gap:8px;width:100%;min-height:44px;border:0;border-radius:0;padding:7px 12px;text-align:left;font-size:13px;line-height:1.35}
26
+ .file[aria-current=true]{background:#efe5d8;box-shadow:inset 3px 0 0 var(--amber)}.file .name{flex:1;min-width:0;overflow-wrap:anywhere}.file .from{display:block;color:var(--muted);font-size:12px}
27
+ .badge{flex-shrink:0;min-width:5.6em;font:600 10.5px/1.9 var(--mono);text-transform:uppercase;letter-spacing:.02em;text-align:center;border:1px solid #cfc4b6;border-radius:4px;color:var(--muted)}
28
+ .badge.added{color:var(--add-ink);border-color:#a9c39f}.badge.deleted{color:var(--del-ink);border-color:#d6aaa3}.counts{flex-shrink:0;font:12px/1.6 var(--mono);white-space:nowrap}.counts .plus{color:var(--add-ink)}.counts .minus{color:var(--del-ink)}
29
+ .file .relevant{display:block;color:var(--amber);font-size:11.5px}
30
+ .diffhead{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:flex-start;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--line)}.diffhead>div:first-child{min-width:0;flex:1 1 220px}
31
+ #diff-meta{margin:2px 0 0;font-size:12px;color:var(--muted)}.diffactions{display:flex;gap:6px;flex-wrap:wrap}.diffactions button{min-height:44px;font-size:13px}
32
+ .diffbody{flex:1;min-height:0;overflow:auto;background:#fff}.diffbody:empty{background:transparent}
33
+ table.diff{border-collapse:collapse;font:12.5px/1.55 var(--mono);min-width:100%;tab-size:4}
34
+ .diff td{padding:0 8px;vertical-align:top}.diff .num{width:1%;text-align:right;color:#8a7f73;white-space:nowrap;user-select:none}.diff .sign{width:1%;padding:0 2px;user-select:none;text-align:center}
35
+ .diff .code{white-space:pre;padding-right:16px}.diff .mark{width:1%;padding:0 2px;color:var(--amber);user-select:none}
36
+ .diff tr.addition{background:var(--add)}.diff tr.addition .sign{color:var(--add-ink)}.diff tr.deletion{background:var(--del)}.diff tr.deletion .sign{color:var(--del-ink)}
37
+ .diff tr.evidence .mark{background:var(--mark);box-shadow:inset 3px 0 0 var(--amber)}.diff tr.evidence .num{color:var(--ink)}
38
+ .diff tr.hunk td{background:#f1ece5;color:var(--muted);padding:3px 12px;white-space:pre-wrap;overflow-wrap:anywhere}.diff tr.gap td{background:#f7f3ee;color:var(--muted);padding:2px 12px;font-family:inherit}
39
+ .diff tr:focus{outline:2px solid var(--focus);outline-offset:-2px}.legend{font-size:12px;color:var(--muted);margin:0;padding:6px 16px;border-bottom:1px solid var(--line)}
40
+ main{width:100%;padding:0 20px;display:flex;flex-direction:column;min-height:0}
41
+ .evidence{flex-shrink:0;border-top:1px solid var(--line);padding:10px 0 4px;max-height:34%;overflow-y:auto}.evidence ul{list-style:none;margin:6px 0 0;padding:0;display:grid;gap:4px}
42
+ .evidence li button{width:100%;min-height:44px;text-align:left;font-size:13px;line-height:1.35;padding:6px 10px}.evidence li .ref-path{display:block;overflow-wrap:anywhere;font:12.5px var(--mono)}.evidence li .ref-lines{display:block;color:var(--muted);font-size:12px}
43
+ .evidence li.static{border:1px dashed #cfc4b6;border-radius:6px;padding:6px 10px;font-size:13px}.evidence .pane-note{padding:6px 0 0}
44
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
16
45
  .conversation{flex:1;min-height:0;overflow-y:auto;padding:24px 8px 8px 0;scrollbar-width:thin}
17
46
  #messages{list-style:none;margin:0;padding:0}.turn{margin:0 0 24px}.turn.human{margin-left:32px;padding:12px 16px;background:#f0ebe4;border-left:2px solid #c6b7a5}
18
47
  .speaker{font-size:13px;font-weight:600;margin-bottom:6px}.turn-text{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}.turn.question .turn-text{font-size:18px;line-height:1.6;font-weight:500}
@@ -23,10 +52,11 @@ ul{padding-left:22px}.status{font-size:13px;color:var(--muted);margin:0 0 12px}
23
52
  label{display:block;font-size:14px;font-weight:600;margin-bottom:8px}
24
53
  textarea{display:block;width:100%;min-height:76px;max-height:180px;resize:vertical;padding:12px 14px;border:1px solid #b9afa3;border-radius:6px;background:#fff;color:var(--ink);line-height:1.5}
25
54
  textarea:disabled{background:#f6f2ed;color:var(--muted)}textarea::placeholder{color:#82796f}
26
- .actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}
55
+ .actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}.actions button,.recovery button,.skipbar button{min-height:44px}
27
56
  button{border:1px solid #c3b8aa;border-radius:6px;padding:7px 12px;background:transparent;color:var(--ink);font-size:14px;line-height:1.5}
28
- button.primary{background:var(--amber);color:#fff;border-color:var(--amber);margin-left:auto;min-width:72px}
57
+ button.primary{background:var(--amber);color:#fff;border-color:var(--amber);margin-left:auto;min-width:72px}.actions button,header .tools button{min-height:44px}
29
58
  button:hover:not(:disabled){background:#eee7dc}button.primary:hover:not(:disabled){background:#8b4208}
59
+ .disputing textarea{border-color:#8d271e}.disputing #submit{background:#8d271e;border-color:#8d271e}
30
60
  button.quiet{border-color:transparent;color:var(--muted)}button.quiet:hover:not(:disabled){color:var(--ink)}
31
61
  .hint{font-size:12px;color:var(--muted);margin:10px 0 0}.recovery{display:grid;gap:12px;margin:0 0 20px}.recovery button{text-align:left;width:100%}.recovery p{margin:4px 0 0;font-size:13px;color:var(--muted)}
32
62
  header .tools{display:flex;gap:4px}
@@ -37,28 +67,49 @@ button.danger{background:#8d271e;border-color:#8d271e;color:#fff}button.danger:h
37
67
  .activity summary .latest::before{content:"✓ ";color:#7c8a5c}.activity.live summary .latest::before{content:"● ";color:var(--amber);animation:pulse 1.2s ease-in-out infinite}
38
68
  .activity .count{color:#8f857a;white-space:nowrap}.activity .count::after{content:" ▸"}.activity details[open] .count::after{content:" ▾"}
39
69
  .activity ol{list-style:none;margin:8px 0 0;padding:0 0 0 14px;border-left:1px solid var(--line);max-height:220px;overflow-y:auto}.activity ol li{padding:1px 0 1px 8px}
40
- .status.busy::before{content:"● ";color:var(--amber);animation:pulse 1.2s ease-in-out infinite}@keyframes pulse{50%{opacity:.25}}
70
+ .turn.streaming .turn-text::after{content:"▍";color:var(--amber);animation:pulse 1s steps(2) infinite}
71
+ .status.busy::before{content:"● ";color:var(--amber);animation:pulse 1.2s ease-in-out infinite}.status.phase-question,.status.phase-passed,.status.phase-paused,.status.phase-error,.status.phase-skipped{display:inline-block;font-size:13px;font-weight:600;line-height:1;padding:6px 10px;border:1px solid currentColor;border-radius:999px}.status.phase-question{color:var(--amber)}.status.phase-question::before{content:"◆ "}.status.phase-question.retry{color:var(--retry)}.status.phase-question.retry::before{content:"↺ "}.status.phase-passed{color:var(--pass)}.status.phase-passed::before{content:"✓ "}.status.phase-paused,.status.phase-skipped{color:var(--muted)}.status.phase-paused::before{content:"‖ "}.status.phase-skipped::before{content:"↷ "}.status.phase-error{color:var(--retry)}.status.phase-error::before{content:"! "}@keyframes pulse{50%{opacity:.25}}
41
72
  @media(prefers-reduced-motion:reduce){.status.busy::before,.activity.live summary .latest::before{animation:none}}
42
73
  footer{flex-shrink:0;padding:0 0 14px;color:var(--muted);font-size:12px;line-height:1.5}footer p{margin:0}#waiting{margin-top:3px}[hidden]{display:none!important}
43
- @media(max-width:600px){header{padding:12px 16px}.brand{gap:8px}.brand span{display:none}main{padding:0 16px}.context{align-items:flex-start;flex-direction:column;gap:4px}.conversation{padding-top:20px}.turn.human{margin-left:16px}.actions{gap:4px}button{padding:7px 10px}.hint{max-width:34em}}
74
+ @media(max-width:1099px){.workspace{grid-template-columns:minmax(200px,26%) minmax(0,1fr) minmax(320px,40%)}}
75
+ @media(max-width:899px){.tabs{display:flex;gap:4px;padding:6px 12px;border-bottom:1px solid var(--line)}.tabs button{flex:1;min-height:44px;font-size:14px}.tabs button[aria-selected=true]{background:#efe5d8;border-color:var(--amber);font-weight:600}
76
+ .workspace{display:flex}.pane{display:none;flex:1;border-right:0}.workspace.show-files #files-pane,.workspace.show-diff #diff-pane,.workspace.show-question #question-pane{display:flex}
77
+ #question-pane{overflow-y:auto}.conversation{flex:none;overflow:visible;padding-right:0}.evidence{max-height:none;overflow:visible}}
78
+ @media(max-width:600px){header{padding:8px 12px}.brand{gap:8px}.brand span{display:none}.scope{padding:6px 12px 7px;font-size:12.5px}.facts{gap:0 12px}.facts .mono{font-size:12px}#scope-note{display:none}.scope-short{display:block}.hint,footer{font-size:11.5px}main{padding:0 14px}.conversation{padding-top:16px}.turn.human{margin-left:16px}.actions{gap:4px}button{padding:7px 10px}.hint{max-width:34em}.diffhead{padding:8px 12px}.legend{padding:6px 12px}}
44
79
  </style></head><body><div class="shell">
45
- <header><div class="brand"><strong>sphinx</strong><span>Understanding check</span></div><div class="tools"><button id="skip" class="quiet" type="button">Skip this time</button><button id="pause" class="quiet" type="button">Pause check</button></div></header>
46
- <main><div class="context"><span id="meta"></span><span id="usage"></span></div>
80
+ <header><div class="brand"><strong>${sphinxMark({ size: 26, id: 'brand-mark' })}sphinx</strong><span>Understanding check</span></div><div class="tools"><button id="skip" class="quiet" type="button">Skip this time</button><button id="pause" class="quiet" type="button">Pause check</button></div></header>
81
+ <section class="scope" aria-label="Change under review"><dl class="facts"><div><dt>Repository</dt><dd id="meta"></dd></div><div><dt id="scope-op-label">Operation</dt><dd id="scope-op">Loading…</dd></div><div><dt>Comparison</dt><dd id="scope-range" class="mono">Loading…</dd></div><div><dt>Changed</dt><dd id="scope-stats">Loading…</dd></div><div id="scope-excluded-row" hidden><dt>Excluded</dt><dd id="scope-excluded"></dd></div><div><dt>Progress</dt><dd id="scope-progress">Connecting…</dd></div><div><dd id="usage"></dd></div></dl>
82
+ <p id="scope-note" class="scope-note">Checkpoint changes: the exact range Sphinx froze for this check, not necessarily an entire pull request.</p><p class="scope-note scope-short">Checkpoint range only — not necessarily a whole pull request.</p></section>
83
+ <nav class="tabs" role="tablist" aria-label="Questionnaire views"><button id="tab-question" role="tab" type="button" aria-controls="question-pane" aria-selected="true">Question</button><button id="tab-files" role="tab" type="button" aria-controls="files-pane" aria-selected="false">Files</button><button id="tab-diff" role="tab" type="button" aria-controls="diff-pane" aria-selected="false">Diff</button></nav>
84
+ <div id="announce" class="sr" aria-live="polite"></div>
85
+ <div id="workspace" class="workspace show-question">
86
+ <aside id="files-pane" class="pane" aria-label="Changed files">
87
+ <div class="filters" role="group" aria-label="Which files to list"><button id="filter-all" type="button" aria-pressed="true">All changed files<span id="count-all" class="count"></span></button><button id="filter-relevant" type="button" aria-pressed="false">Relevant to this question<span id="count-relevant" class="count"></span></button></div>
88
+ <input id="file-search" type="search" autocomplete="off" spellcheck="false" placeholder="Filter by path" aria-label="Filter files by path">
89
+ <p id="filter-total" class="pane-note" aria-live="polite"></p>
90
+ <ul id="file-list" class="filelist" aria-label="Files"></ul><p id="files-note" class="pane-note"></p></aside>
91
+ <section id="diff-pane" class="pane" aria-label="Diff">
92
+ <div class="diffhead"><div><h2 id="diff-title">No file selected</h2><p id="diff-meta">Choose a file from the list, or a code reference beside the question.</p></div>
93
+ <div class="diffactions"><button id="context-more" type="button" hidden>More context</button><button id="context-all" type="button" hidden>Full file</button><button id="back-question" type="button">Back to question</button></div></div>
94
+ <p id="diff-legend" class="legend" hidden>+ added · − removed · ▸ lines the reviewer read for the current question</p>
95
+ <p id="diff-note" class="pane-note" role="status" hidden></p><div id="diff-body" class="diffbody"></div></section>
96
+ <main id="question-pane" class="pane" aria-label="Question">
47
97
  <div id="skipbar" class="skipbar" hidden><input id="skipreason" maxlength="500" autocomplete="off" placeholder="Why is this urgent? (optional)" aria-label="Reason for skipping"><button id="skipconfirm" class="danger" type="button">Skip and continue</button><button id="skipcancel" class="quiet" type="button">Cancel</button><p>The push goes through without the check. The skip is logged.</p></div>
48
98
  <div id="conversation" class="conversation" aria-label="Understanding check conversation">
49
99
  <ol id="messages" aria-label="Conversation"></ol>
50
100
  <p id="feedback" class="feedback" hidden></p><section id="question" hidden><p id="prompt" class="question"></p><ul id="choices" hidden></ul></section>
51
101
  <p id="message" class="message" hidden></p><div id="status" class="status" role="status" aria-live="polite">Connecting to your local checkpoint…</div><div id="recovery" class="recovery" hidden></div>
52
102
  </div>
103
+ <section id="evidence" class="evidence" aria-labelledby="evidence-title" hidden><h2 id="evidence-title">Code behind this question</h2><ul id="evidence-list"></ul><p id="evidence-note" class="pane-note"></p></section>
53
104
  <div id="composer" class="composer"><p id="error" class="error" role="alert" hidden></p>
54
- <form id="form"><label for="response">Your answer</label><textarea id="response" rows="2" maxlength="8000" autocomplete="off" spellcheck="true" placeholder="Your prediction and a short reason…"></textarea>
55
- <div class="actions"><button type="button" class="quiet" data-action="learn">Explain this</button><button type="button" class="quiet" data-action="dispute">Dispute question</button><button id="submit" class="primary" type="submit">Send</button></div>
105
+ <form id="form"><label id="label" for="response">Your answer</label><textarea id="response" rows="2" maxlength="8000" autocomplete="off" spellcheck="true" placeholder="Your prediction and a short reason…"></textarea>
106
+ <div class="actions"><button type="button" class="quiet" data-action="learn">Explain this</button><button type="button" class="quiet" id="dispute">Dispute question</button><button id="submit" class="primary" type="submit">Send</button></div>
56
107
  <p class="hint">Enter to send · Shift+Enter for a new line. Open-book—inspect the code whenever useful.</p></form></div>
57
108
  <footer><p>Your answers go directly to Sphinx. The coding agent receives status only.</p><p id="waiting">Closing this tab leaves the check waiting. Use Pause check to stop.</p></footer>
58
- </main></div>
109
+ </main></div></div>
59
110
  <script nonce="${nonce}">
60
111
  const token=${JSON.stringify(token)},base='/'+token,$=id=>document.getElementById(id);
61
- let current,submitting=false,finished=false,connected=false,lastQuestion,lastGraded,lastMessages='';const openGroups=new Set();
112
+ let disputing=false,current,submitting=false,finished=false,connected=false,lastQuestion,lastGraded,lastMessages='';const openGroups=new Set();
62
113
  const text=(id,value)=>{$(id).textContent=value||'';$(id).hidden=!value;};
63
114
  const compact=n=>n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'k':String(n);
64
115
  const usageText=(u,t)=>{if(!t)return '';const total=(t.input||0)+(t.output||0);if(!total&&t.cost==null)return '';
@@ -67,7 +118,8 @@ const resize=()=>{$('response').style.height='auto';$('response').style.height=M
67
118
  function controls(){
68
119
  if(!current)return;
69
120
  const canAnswer=current.phase==='question'&&!current.operationClosed&&connected&&!submitting;
70
- for(const button of document.querySelectorAll('button'))button.disabled=!canAnswer;
121
+ // Only answer controls follow the question state; browsing the change never locks.
122
+ for(const button of document.querySelectorAll('#composer button'))button.disabled=!canAnswer;
71
123
  $('pause').disabled=!connected||submitting||finished;
72
124
  for(const id of ['skip','skipconfirm','skipcancel'])$(id).disabled=!connected||finished;
73
125
  for(const button of $('recovery').querySelectorAll('button'))button.disabled=!connected||submitting||finished;
@@ -94,7 +146,7 @@ function history(messages){
94
146
  details.append(summary,list);row.append(details);$('messages').append(row);continue;
95
147
  }
96
148
  const row=document.createElement('li'),name=document.createElement('div'),body=document.createElement('p');
97
- row.className='turn '+(item.role==='human'?'human':'sphinx')+(item.kind==='question'?' question':item.kind==='notice'?' notice':'');
149
+ row.className='turn '+(item.role==='human'?'human':'sphinx')+(item.kind==='question'?' question':item.kind==='notice'?' notice':'')+(item.streaming?' streaming':'');
98
150
  name.className='speaker';name.textContent=item.role==='human'?'You':'Sphinx';body.className='turn-text';body.textContent=item.text;
99
151
  row.append(name);
100
152
  if(item.kind==='question'&&item.number){const caption=document.createElement('div');caption.className='question-number';caption.textContent='Question '+item.number+(item.followUp?' · Follow-up':'');row.append(caption);}
@@ -109,14 +161,15 @@ function render(state){
109
161
  if(current&&state.sequence<current.sequence)return;
110
162
  const previous=current;current=state;finished=state.operationClosed||['passed','paused','error','skipped'].includes(state.phase);
111
163
  const repo=state.repoRoot?.split(/[\\\\/]/).filter(Boolean).at(-1),branch=state.target?.replace(/^refs\\/heads\\//,'');
112
- text('meta',[repo,branch].filter(Boolean).join(' / '));$('meta').title=state.repoRoot||'';
113
- text('usage',usageText(state.usage,state.tokens));
114
- text('waiting',state.waitingGit?'Git continues after you pass. Closing this tab leaves it waiting; Pause check stops it.':'Closing this tab leaves the check waiting. Use Pause check to stop.');
115
- const label=state.phase==='question'?'Question '+(state.progress.completed+1)+' of '+state.progress.total:state.phase==='working'?'Working…':state.phase==='recovery'?'Review needs your decision':state.phase==='passed'?'Complete':state.phase==='paused'?'Paused':state.phase==='skipped'?'Skipped':state.phase==='error'?'Check stopped':'Waiting…';
116
- text('status',label);$('status').className='status'+(state.phase==='working'?' busy':'');text('message',state.message);text('error',state.error);
117
- const messages=state.messages||[];history(messages);
164
+ text('meta',repo||state.change?.header?.repository||'');$('meta').title=state.repoRoot||'';
165
+ text('usage',usageText(state.usage,state.tokens));const messages=state.messages||[];
166
+ text('waiting',finished?'':state.waitingGit?'Git continues after you pass. Closing this tab leaves it waiting; Pause check stops it.':'Closing this tab leaves the check waiting. Use Pause check to stop.');
167
+ const label=state.phase==='question'?'Question '+(state.progress.completed+1)+' of '+state.progress.total:state.phase==='working'?'Reviewing…':state.phase==='recovery'?'Review needs your decision':state.phase==='passed'?'Passed':state.phase==='paused'?'Paused':state.phase==='skipped'?'Skipped':state.phase==='error'?'Check stopped':'Waiting…';
168
+ const retry=state.phase==='question'&&[...messages].reverse().find(item=>item.kind==='question')?.followUp;
169
+ text('status',label+(retry?' · Needs another answer':''));scope(state,branch,label);$('status').className='status phase-'+state.phase+(state.phase==='working'?' busy':'')+(retry?' retry':'');text('message',state.message);text('error',state.error);
170
+ history(messages);
118
171
  // Live activity already shows progress; avoid stacking generic spinners under it.
119
- if(state.phase==='working'&&messages.at(-1)?.kind==='activity'){$('status').hidden=true;$('message').hidden=true;}
172
+ if(state.phase==='working'&&(messages.at(-1)?.kind==='activity'||messages.at(-1)?.streaming)){$('status').hidden=true;$('message').hidden=true;}
120
173
  text('feedback',messages.length?'':state.feedback);
121
174
  $('question').hidden=state.phase!=='question'||messages.some(item=>item.kind==='question'&&item.questionId===state.question?.id);
122
175
  $('composer').hidden=!['question','working'].includes(state.phase)&&!state.error;
@@ -127,19 +180,20 @@ function render(state){
127
180
  for(const choice of state.question.choices||[]){const li=document.createElement('li');li.textContent=choice;$('choices').append(li);}
128
181
  $('choices').hidden=!(state.question.choices||[]).length;
129
182
  const key=state.id+':'+state.question.id;
130
- if(lastQuestion!==key){$('response').value='';lastQuestion=key;resize();}
183
+ if(lastQuestion!==key){$('response').value='';lastQuestion=key;resize();if(disputing)setDispute(false);}
131
184
  }
132
185
  const graded=[...messages].reverse().find(item=>item.role==='human'&&item.status==='graded');
133
- if(graded&&lastGraded!==graded.id){lastGraded=graded.id;$('response').value='';resize();}
186
+ if(graded&&lastGraded!==graded.id){lastGraded=graded.id;$('response').value='';resize();if(disputing)setDispute(false);}
134
187
  $('recovery').replaceChildren();
135
188
  if(state.phase==='recovery')for(const choice of state.choices||[]){
136
189
  if(choice.value==='pause')continue;
137
190
  const row=document.createElement('div'),button=document.createElement('button'),description=document.createElement('p');
138
191
  button.type='button';button.textContent=choice.label;button.onclick=()=>send(choice.value);description.textContent=choice.description;row.append(button,description);$('recovery').append(row);
139
192
  }
140
- controls();
193
+ checkpoint(state);evidenceList();controls();
141
194
  // A new step (question, recovery decision, result) must never sit below the fold.
142
- if(previous?.phase!==state.phase||previous?.question?.id!==state.question?.id){const scroll=$('conversation');scroll.scrollTop=scroll.scrollHeight;}
195
+ if(previous?.phase!==state.phase||previous?.question?.id!==state.question?.id){const scroll=$('conversation');scroll.scrollTop=scroll.scrollHeight;
196
+ if(globalThis.matchMedia?.('(max-width: 899px)').matches)[...document.querySelectorAll('#messages .turn.question')].at(-1)?.scrollIntoView?.({block:'start'});}
143
197
  if(state.phase==='question'&&(previous?.phase!=='question'||previous?.question?.id!==state.question?.id)&&document.visibilityState!=='hidden')$('response').focus({preventScroll:true});
144
198
  }
145
199
  async function send(action){
@@ -154,11 +208,21 @@ async function send(action){
154
208
  }catch{if(!finished)text('error','Connection interrupted. Your response is not resent automatically. Wait for the current state before trying again.');}
155
209
  finally{submitting=false;controls();}
156
210
  }
157
- $('form').onsubmit=event=>{event.preventDefault();send('answer');};
211
+ // Dispute is a reversible mode: the same box and Send button carry the reason.
212
+ function setDispute(on){
213
+ disputing=on;$('form').classList.toggle('disputing',on);
214
+ $('label').textContent=on?'What is ambiguous or wrong about this question?':'Your answer';
215
+ $('response').placeholder=on?'Explain the missing assumption or the evidence that contradicts it…':'Your prediction and a short reason…';
216
+ $('submit').textContent=on?'Send dispute':'Send';$('dispute').textContent=on?'Back to answering':'Dispute question';
217
+ text('error','');$('response').focus();
218
+ }
219
+ $('dispute').onclick=()=>setDispute(!disputing);
220
+ const submit=()=>send(disputing?'dispute':'answer');
221
+ $('form').onsubmit=event=>{event.preventDefault();submit();};
158
222
  $('response').oninput=resize;
159
223
  $('response').onkeydown=event=>{
160
224
  if(event.key!=='Enter'||event.shiftKey||event.isComposing||event.keyCode===229)return;
161
- event.preventDefault();if(!event.repeat)send('answer');
225
+ event.preventDefault();if(!event.repeat)submit();
162
226
  };
163
227
  for(const button of document.querySelectorAll('[data-action]'))button.onclick=()=>send(button.dataset.action);
164
228
  $('pause').onclick=()=>send('pause');
@@ -166,6 +230,197 @@ $('skip').onclick=()=>{$('skipbar').hidden=false;$('skipreason').focus();};
166
230
  $('skipcancel').onclick=()=>{$('skipbar').hidden=true;};
167
231
  $('skipconfirm').onclick=()=>send('skip');
168
232
  $('skipreason').onkeydown=event=>{if(event.key==='Enter'&&!event.isComposing){event.preventDefault();send('skip');}else if(event.key==='Escape')$('skipbar').hidden=true;};
233
+
234
+ // Change context: a read-only view of the exact frozen comparison. Every value
235
+ // from the repository enters the DOM through textContent, never markup.
236
+ let change,changeFor,changeLoading=false,filter='all',query='',selected,diff,diffSeq=0,tab='question',lastFiles='',lastEvidence='';const contextFor=new Map();
237
+ const el=(tag,className,value)=>{const node=document.createElement(tag);if(className)node.className=className;if(value!=null)node.textContent=value;return node;};
238
+ const num=n=>Number(n).toLocaleString('en-US');
239
+ const plural=(n,word)=>num(n)+' '+word+(n===1?'':'s');
240
+ const short=oid=>oid?oid.slice(0,10):'';
241
+ const announce=value=>{$('announce').textContent='';$('announce').textContent=value;};
242
+ function totalsText(t){
243
+ if(!t)return '';
244
+ const parts=[plural(t.files,'file'),'+'+num(t.additions)+' −'+num(t.deletions)+' lines'];
245
+ if(t.binaryFiles)parts.push(plural(t.binaryFiles,'binary file')+' (no line counts)');
246
+ if(t.uncountedFiles)parts.push(num(t.uncountedFiles)+' without line counts');
247
+ return parts.join(' · ');
248
+ }
249
+ function scope(state,branch,label){
250
+ const c=state.change,h=c?.header;
251
+ $('scope-progress').textContent=label;
252
+ if(!c){$('scope-op').textContent=branch?(state.event==='pre-commit'?'Commit to ':'Push to ')+branch:'Waiting…';$('scope-range').textContent='Loading…';$('scope-stats').textContent='Loading…';return;}
253
+ if(h){
254
+ const dest=(h.destination||'').replace(/^refs\\/heads\\//,'');
255
+ $('scope-op').textContent=h.event==='pre-commit'?'Commit to '+dest+' (staged changes)':'Push to '+(h.remote&&h.remote!=='.'?h.remote+' / ':'')+dest;
256
+ $('scope-range').textContent=h.staged?(h.base?short(h.base):'empty repository')+' → staged index':(h.base?short(h.base):'new branch (no destination history)')+' → '+short(h.head);
257
+ $('scope-range').title='Checkpoint '+h.checkpoint+(h.base?'\\nBase '+h.base:'')+(h.head?'\\nHead '+h.head:'');
258
+ $('scope-stats').textContent=totalsText(c.totals);
259
+ $('scope-excluded-row').hidden=!h.excludedFiles;$('scope-excluded').textContent=h.excludedFiles?plural(h.excludedFiles,'file')+' not shown or counted':'';
260
+ $('scope-note').textContent='Checkpoint changes: the exact range Sphinx froze for this check, not necessarily an entire pull request.'+(h.excludedFiles?' '+plural(h.excludedFiles,'more changed file')+' excluded by privacy or ignore rules; names, contents and line counts are not shown or counted.':'');
261
+ }else{
262
+ $('scope-op').textContent=branch?(state.event==='pre-commit'?'Commit to ':'Push to ')+branch:'—';
263
+ $('scope-range').textContent=c.status==='loading'?'Loading…':'Unavailable';
264
+ $('scope-stats').textContent=c.status==='loading'?'Loading changed files…':'Unavailable'+(c.reason?': '+c.reason:'');
265
+ }
266
+ }
267
+ function checkpoint(state){
268
+ if(changeFor&&changeFor!==state.id){change=undefined;changeFor=undefined;selected=undefined;diff=undefined;contextFor.clear();lastFiles='';clearDiff();}
269
+ if(state.change?.status==='ready'&&changeFor!==state.id&&!changeLoading)loadChange(state.id);
270
+ if(state.change?.status!=='ready'&&!change)$('files-note').textContent=state.change?.status==='unavailable'?'Changed files are unavailable: '+(state.change.reason||'unknown reason')+' The question can still be answered.':'Loading changed files…';
271
+ files();
272
+ }
273
+ async function getJSON(path){const res=await fetch(base+path);let data={};try{data=await res.json();}catch{}return {ok:res.ok,status:res.status,data};}
274
+ async function loadChange(id){
275
+ changeLoading=true;
276
+ try{
277
+ const r=await getJSON('/change?id='+encodeURIComponent(id));
278
+ if(current?.id!==id)return;
279
+ if(r.ok&&r.data.status==='ready'){change=r.data;changeFor=id;lastFiles='';$('files-note').textContent=filesNote();files();evidenceList();}
280
+ else $('files-note').textContent='Changed files are unavailable: '+(r.data.error||r.data.reason||'the request failed.');
281
+ }catch{$('files-note').textContent='Changed files could not be loaded. The question can still be answered.';}
282
+ finally{changeLoading=false;}
283
+ }
284
+ function filesNote(){
285
+ if(!change)return '';
286
+ const notes=[];
287
+ if(change.listed<change.totals.files)notes.push('Listing the first '+num(change.listed)+' of '+plural(change.totals.files,'file')+'. Totals count every file.');
288
+ if(change.header.excludedFiles)notes.push(plural(change.header.excludedFiles,'changed file')+' excluded by privacy or ignore rules are not listed or counted.');
289
+ return notes.join(' ');
290
+ }
291
+ function refs(){return current?.evidence&&current.question&&current.evidence.questionId===current.question.id?current.evidence.refs:null;}
292
+ function relevantPaths(){return new Set((refs()||[]).filter(ref=>ref.inChange).map(ref=>ref.viewPath));}
293
+ function statsOf(list){return list.reduce((t,f)=>{t.files++;if(f.binary)t.binaryFiles++;else if(f.additions==null)t.uncountedFiles++;else{t.additions+=f.additions;t.deletions+=f.deletions;}return t;},{files:0,additions:0,deletions:0,binaryFiles:0,uncountedFiles:0});}
294
+ const STATUS={added:'Added',modified:'Modified',deleted:'Deleted',renamed:'Renamed'};
295
+ function files(){
296
+ const relevant=relevantPaths(),all=change?.files||[];
297
+ const subset=filter==='relevant'?all.filter(f=>relevant.has(f.path)):all;
298
+ const shown=query?subset.filter(f=>(f.path+' '+(f.oldPath||'')).toLowerCase().includes(query.toLowerCase())):subset;
299
+ const signature=JSON.stringify([changeFor,filter,query,selected,[...relevant],all.length]);
300
+ $('filter-all').setAttribute('aria-pressed',String(filter==='all'));$('filter-relevant').setAttribute('aria-pressed',String(filter==='relevant'));
301
+ const cited=statsOf(all.filter(f=>relevant.has(f.path)));
302
+ $('count-all').textContent=change?plural(change.totals.files,'file'):'';
303
+ $('count-relevant').textContent=change?(refs()?plural(cited.files,'file'):'—'):'';
304
+ if(signature===lastFiles)return;lastFiles=signature;
305
+ const list=$('file-list');list.replaceChildren();
306
+ if(!change){$('filter-total').textContent='';return;}
307
+ $('filter-total').textContent=filter==='relevant'
308
+ ?(relevant.size?'Cited by this question: '+totalsText(cited)+'. Question evidence, not everything that changed.':(refs()?'This question cites no changed file with a known location.':'No question is active.'))
309
+ :'All changed files: '+totalsText(change.totals)+(query?'. Showing paths matching “'+query+'”.':'.');
310
+ for(const f of shown){
311
+ const li=el('li'),button=el('button','file');button.type='button';button.dataset.path=f.path;
312
+ if(f.path===selected)button.setAttribute('aria-current','true');
313
+ const badge=el('span','badge '+f.status,f.binary?'Binary':STATUS[f.status]);
314
+ const name=el('span','name',f.path);
315
+ if(f.oldPath)name.append(el('span','from','from '+f.oldPath));
316
+ if(relevant.has(f.path))name.append(el('span','relevant','▸ cited by this question'));
317
+ const counts=el('span','counts');
318
+ if(f.binary||f.additions==null)counts.textContent=f.binary?'binary':'n/a';
319
+ else{counts.append(el('span','plus','+'+num(f.additions)),' ',el('span','minus','−'+num(f.deletions)));}
320
+ button.setAttribute('aria-label',(f.binary?'Binary file ':'')+STATUS[f.status]+' '+f.path+(f.oldPath?' from '+f.oldPath:'')+(f.binary||f.additions==null?', no line counts':', '+f.additions+' added, '+f.deletions+' removed lines')+(relevant.has(f.path)?', cited by this question':''));
321
+ button.append(badge,name,counts);button.onclick=()=>openFile(f.path,{show:true});
322
+ li.append(button);list.append(li);
323
+ }
324
+ if(!shown.length)list.append(el('li','pane-note',filter==='relevant'?'No changed files are linked to this question.':'No files match this filter.'));
325
+ }
326
+ $('filter-all').onclick=()=>{filter='all';files();announce('Showing all changed files.');};
327
+ $('filter-relevant').onclick=()=>{filter='relevant';files();announce('Showing files relevant to this question.');};
328
+ $('file-search').oninput=()=>{query=$('file-search').value.trim();files();};
329
+ $('file-list').onkeydown=event=>{
330
+ if(!['ArrowDown','ArrowUp','Home','End'].includes(event.key))return;
331
+ const buttons=[...$('file-list').querySelectorAll('button')];if(!buttons.length)return;
332
+ const at=buttons.indexOf(document.activeElement);event.preventDefault();
333
+ const next=event.key==='Home'?0:event.key==='End'?buttons.length-1:Math.max(0,Math.min(buttons.length-1,at+(event.key==='ArrowDown'?1:-1)));
334
+ buttons[next].focus();
335
+ };
336
+ function setTab(name){
337
+ tab=name;$('workspace').className='workspace show-'+name;
338
+ for(const id of ['question','files','diff'])$('tab-'+id).setAttribute('aria-selected',String(id===name));
339
+ }
340
+ for(const id of ['question','files','diff'])$('tab-'+id).onclick=()=>{setTab(id);announce(id==='question'?'Question view. Your answer is kept.':id==='files'?'Changed files view.':'Diff view.');};
341
+ $('back-question').onclick=()=>{setTab('question');$('response').focus({preventScroll:true});announce('Back to the question. Your answer is kept.');};
342
+ function clearDiff(){$('diff-title').textContent='No file selected';$('diff-meta').textContent='Choose a file from the list, or a code reference beside the question.';$('diff-body').replaceChildren();$('diff-note').hidden=true;$('diff-legend').hidden=true;$('context-more').hidden=true;$('context-all').hidden=true;}
343
+ const NOTES={binary:'Binary file. Its contents are not shown and no line counts are reported.',symlink:'Symbolic link changed. The link target is not shown.',submodule:'Submodule pointer changed. Its contents are outside this repository diff.',unsupported:'This file type is not shown.','rename-only':'Renamed without content changes.','mode-only':'Only the file mode changed; no lines changed.',empty:'Empty file. There are no lines to show.','too-large':'This diff is too large to display here. The totals above still count it.','non-utf8':'Not valid UTF-8 text, so the contents are not shown.'};
344
+ async function openFile(path,{show=false,range,quiet=false}={}){
345
+ if(!current||!change)return;
346
+ selected=path;files();if(show)setTab('diff');
347
+ const level=contextFor.get(path)||'3',id=current.id,seq=++diffSeq;
348
+ $('diff-title').textContent=path;$('diff-meta').textContent='Loading diff…';
349
+ try{
350
+ const r=await getJSON('/diff?id='+encodeURIComponent(id)+'&path='+encodeURIComponent(path)+'&context='+encodeURIComponent(level));
351
+ if(seq!==diffSeq||current?.id!==id)return;
352
+ if(!r.ok){diff=undefined;$('diff-body').replaceChildren();$('diff-meta').textContent='';$('diff-note').textContent=r.data.error||'This diff is unavailable.';$('diff-note').hidden=false;return;}
353
+ diff=r.data;
354
+ if(range&&level!=='all'&&!visible(range)&&!diff.note){contextFor.set(path,'all');return openFile(path,{range,quiet});}
355
+ drawDiff(quiet?undefined:range,quiet);
356
+ }catch{if(seq===diffSeq){$('diff-meta').textContent='';$('diff-note').textContent='The diff could not be loaded. Your answer is unaffected.';$('diff-note').hidden=false;}}
357
+ }
358
+ function rowsOf(d){return (d?.hunks||[]).flatMap(h=>h.rows);}
359
+ function visible(range){return rowsOf(diff).some(row=>row[range.side]!=null&&row[range.side]>=range.start&&row[range.side]<=range.end);}
360
+ function marked(row,ranges){return ranges.some(r=>row[r.side]!=null&&row[r.side]>=r.start&&row[r.side]<=r.end);}
361
+ function drawDiff(focus,quiet=false){
362
+ const d=diff,ranges=(refs()||[]).filter(ref=>ref.viewPath===d.path).flatMap(ref=>ref.ranges);
363
+ const level=d.context;
364
+ $('diff-title').textContent=d.path;
365
+ $('diff-meta').textContent=[(d.binary?'Binary · ':'')+STATUS[d.status]+(d.oldPath?' from '+d.oldPath:''),d.binary||d.additions==null?'no line counts':'+'+num(d.additions)+' −'+num(d.deletions),d.modeChange?'mode '+d.modeChange.from+' → '+d.modeChange.to:'',d.hunks.length?(level==='all'?'full file':level+' lines of context'):''].filter(Boolean).join(' · ');
366
+ const notes=[];if(d.note)notes.push(NOTES[d.note]||'Contents are not shown.');
367
+ if(d.truncated)notes.push('Showing the first '+num(d.truncated.shownRows)+' of '+num(d.truncated.totalRows)+' diff lines. The rest of this file is not displayed; the totals still count it.');
368
+ if(ranges.length&&!rowsOf(d).some(row=>marked(row,ranges)))notes.push('The lines the reviewer read in this file are outside the displayed content.');
369
+ $('diff-note').textContent=notes.join(' ');$('diff-note').hidden=!notes.length;
370
+ $('context-more').hidden=$('context-all').hidden=!d.hunks.length||level==='all';
371
+ $('diff-legend').hidden=!d.hunks.length;
372
+ const table=el('table','diff'),body=el('tbody');table.append(body);table.setAttribute('aria-label','Diff of '+d.path);
373
+ let previous,first;
374
+ for(const hunk of d.hunks){
375
+ const hiddenLines=previous?hunk.newStart-(previous.newStart+previous.newLines):Math.max(hunk.newStart,hunk.oldStart)-1;
376
+ if(hiddenLines>0&&level!=='all'){const tr=el('tr','gap'),td=el('td',null,'⋯ '+plural(hiddenLines,'unchanged line')+' not shown');td.colSpan=5;tr.append(td);body.append(tr);}
377
+ const head=el('tr','hunk'),cell=el('td',null,'@@ −'+hunk.oldStart+','+hunk.oldLines+' +'+hunk.newStart+','+hunk.newLines+' @@'+(hunk.section?' '+hunk.section:''));cell.colSpan=5;head.append(cell);body.append(head);
378
+ for(const row of hunk.rows){
379
+ const hit=ranges.length&&marked(row,ranges);
380
+ const tr=el('tr',row.type+(hit?' evidence':''));
381
+ const sign=row.type==='addition'?'+':row.type==='deletion'?'−':' ';
382
+ tr.append(el('td','mark',hit?'▸':''),el('td','num',row.old??''),el('td','num',row.new??''),el('td','sign',sign),el('td','code',row.text+(row.noNewline?' ⏎ no newline at end of file':'')));
383
+ tr.setAttribute('aria-label',(row.type==='addition'?'Added line '+row.new:row.type==='deletion'?'Removed line '+row.old:'Unchanged line '+row.new)+(hit?', read by the reviewer for this question':''));
384
+ if(hit&&!first&&focus&&marked(row,[focus]))first=tr;
385
+ body.append(tr);
386
+ }
387
+ if(hunk.cut){const tr=el('tr','gap'),td=el('td',null,'⋯ display limit reached');td.colSpan=5;tr.append(td);body.append(tr);}
388
+ previous=hunk;
389
+ }
390
+ $('diff-body').replaceChildren(...(d.hunks.length?[table]:[]));
391
+ if(first){first.tabIndex=-1;first.scrollIntoView?.({block:'center'});first.focus?.({preventScroll:true});announce('Showing '+d.path+', lines the reviewer read for this question are marked.');}
392
+ else if(!quiet)announce('Showing '+d.path+'.');
393
+ }
394
+ $('context-more').onclick=()=>{if(!diff)return;const now=contextFor.get(diff.path)||'3';contextFor.set(diff.path,now==='3'?'25':'all');openFile(diff.path);};
395
+ $('context-all').onclick=()=>{if(!diff)return;contextFor.set(diff.path,'all');openFile(diff.path);};
396
+ function rangeText(r){return (r.side==='old'?'old ':'')+'line'+(r.end>r.start?'s '+r.start+'–'+r.end:' '+r.start);}
397
+ function evidenceList(){
398
+ const active=current&&['question','working'].includes(current.phase)&&current.question;
399
+ $('evidence').hidden=!active;
400
+ if(!active){if(lastEvidence!=='inactive'){lastEvidence='inactive';if(diff&&diff.path===selected)drawDiff(undefined,true);}return;}
401
+ const list=refs(),signature=JSON.stringify([current.id,current.question.id,list,Boolean(change),current.change?.status]);
402
+ if(signature===lastEvidence)return;lastEvidence=signature;
403
+ $('evidence-list').replaceChildren();
404
+ if(!list){$('evidence-note').textContent=current.change?.status==='unavailable'?'Code references are unavailable because the change could not be loaded.':'Loading code references…';return;}
405
+ for(const ref of list){
406
+ const li=el('li'),path=el('span','ref-path',ref.path),lines=el('span','ref-lines',ref.ranges.length?ref.ranges.map(rangeText).join(', '):'No verified line range; the file as a whole is cited.');
407
+ if(ref.inChange&&change){
408
+ const button=el('button');button.type='button';button.append(path,lines);
409
+ button.setAttribute('aria-label','Open '+ref.path+(ref.ranges.length?', '+ref.ranges.map(rangeText).join(', '):''));
410
+ button.onclick=()=>openFile(ref.viewPath,{show:true,range:ref.ranges[0]});li.append(button);
411
+ }else{li.className='static';lines.textContent=(ref.inChange?'':'Unchanged file, not part of this change. ')+lines.textContent;li.append(path,lines);}
412
+ $('evidence-list').append(li);
413
+ }
414
+ $('evidence-note').textContent=list.length
415
+ ?'Ranges the reviewer read while preparing this question. They point to context; they are not a claim that every changed line was read or checked.'
416
+ :'No specific code location is available for this question.';
417
+ files();
418
+ // Keep the code beside the question: open its first cited hunk once, without
419
+ // switching views. Never replace a file the person chose; refresh its markers.
420
+ const firstRef=list.find(ref=>ref.inChange);
421
+ if(change&&!selected&&firstRef)openFile(firstRef.viewPath,{range:firstRef.ranges[0],quiet:true});
422
+ else if(diff&&diff.path===selected)drawDiff(undefined,true);
423
+ }
169
424
  const events=new EventSource(base+'/events');events.onmessage=event=>{try{connected=true;render(JSON.parse(event.data));if(current?.operationClosed){connected=false;events.close();}}catch{}};
170
425
  events.onerror=()=>{connected=false;controls();if(current?.operationClosed){events.close();return;}text('error','Connection interrupted. Reconnecting… No response is retried automatically. If Git has stopped, check its output.');};
171
426
  </script></body></html>`;