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.
- package/README.md +106 -273
- package/docs/architecture.md +16 -0
- package/docs/configuration.md +241 -0
- package/docs/integrations.md +135 -0
- package/docs/logo.svg +3 -0
- package/package.json +4 -2
- package/scripts/postinstall.js +23 -0
- package/src/browser-interview.js +74 -8
- package/src/browser-page.js +287 -32
- package/src/change-view.js +314 -0
- package/src/cli.js +57 -20
- package/src/config.js +57 -5
- package/src/evaluator.js +31 -5
- package/src/help.js +5 -4
- package/src/install.js +3 -1
- package/src/logo.js +6 -0
- package/src/mcp.js +2 -1
- package/src/openrouter.js +65 -35
- package/src/overrides.js +16 -5
- package/src/preparation.js +19 -3
- package/src/prompts.js +25 -1
- package/src/review-workspace.js +13 -10
- package/src/reviewer.js +9 -4
- package/src/routing.js +3 -1
- package/src/sessions.js +10 -1
- package/src/stream.js +59 -0
- package/src/terminal.js +16 -14
- package/src/version.js +4 -0
package/src/browser-page.js
CHANGED
|
@@ -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:
|
|
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:
|
|
12
|
-
.brand{display:flex;align-items:
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
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
|
-
.
|
|
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:
|
|
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
|
|
46
|
-
<
|
|
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"
|
|
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
|
-
|
|
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',
|
|
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'?'
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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)
|
|
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&¤t.question&¤t.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)&¤t.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>`;
|