diffback-review 1.1.0 → 1.3.0
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 +43 -16
- package/dist/cli.js +866 -148
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,85 @@ import { execSync } from "child_process";
|
|
|
6
6
|
import { createHash } from "crypto";
|
|
7
7
|
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "fs";
|
|
8
8
|
import { resolve, basename } from "path";
|
|
9
|
+
|
|
10
|
+
// src/feedback.ts
|
|
11
|
+
function generateFeedback(state) {
|
|
12
|
+
const lines = [];
|
|
13
|
+
const filesWithFeedback = [];
|
|
14
|
+
for (const [path, review] of Object.entries(state.files)) {
|
|
15
|
+
if (review.comments.length > 0) {
|
|
16
|
+
filesWithFeedback.push(path);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const totalComments = filesWithFeedback.reduce((sum, p) => sum + state.files[p].comments.length, 0) + state.generalComments.length;
|
|
20
|
+
lines.push("# Code Review Feedback");
|
|
21
|
+
lines.push("");
|
|
22
|
+
lines.push(
|
|
23
|
+
`${filesWithFeedback.length} files need changes. ${totalComments} comments total.`
|
|
24
|
+
);
|
|
25
|
+
for (const path of filesWithFeedback) {
|
|
26
|
+
const review = state.files[path];
|
|
27
|
+
lines.push("");
|
|
28
|
+
lines.push(`## ${path}`);
|
|
29
|
+
for (const comment of review.comments) {
|
|
30
|
+
const lineRef = comment.line ? `L${comment.line}` : "General";
|
|
31
|
+
if (comment.suggestion) {
|
|
32
|
+
lines.push(`- ${lineRef}: ${comment.text}`);
|
|
33
|
+
lines.push(" ```");
|
|
34
|
+
lines.push(` ${comment.suggestion}`);
|
|
35
|
+
lines.push(" ```");
|
|
36
|
+
} else {
|
|
37
|
+
lines.push(`- ${lineRef}: ${comment.text}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (state.generalComments.length > 0) {
|
|
42
|
+
lines.push("");
|
|
43
|
+
lines.push("## General");
|
|
44
|
+
for (const comment of state.generalComments) {
|
|
45
|
+
lines.push(`- ${comment.text}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/state.ts
|
|
52
|
+
function reconcileState(state, changedFiles, hashFile2) {
|
|
53
|
+
const currentPaths = new Set(changedFiles.map((f) => f.path));
|
|
54
|
+
let hasChanges = false;
|
|
55
|
+
for (const path of Object.keys(state.files)) {
|
|
56
|
+
if (!currentPaths.has(path)) {
|
|
57
|
+
delete state.files[path];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const path of currentPaths) {
|
|
61
|
+
const currentHash = hashFile2(path);
|
|
62
|
+
const existing = state.files[path];
|
|
63
|
+
if (existing) {
|
|
64
|
+
if (existing.hash !== currentHash) {
|
|
65
|
+
if (existing.comments.length > 0) {
|
|
66
|
+
const archived = existing.comments.map((c) => ({
|
|
67
|
+
...c,
|
|
68
|
+
archivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
69
|
+
round: state.round
|
|
70
|
+
}));
|
|
71
|
+
existing.archivedComments = [...existing.archivedComments || [], ...archived];
|
|
72
|
+
}
|
|
73
|
+
existing.status = "pending";
|
|
74
|
+
existing.hash = currentHash;
|
|
75
|
+
existing.comments = [];
|
|
76
|
+
existing.changedSinceReview = true;
|
|
77
|
+
hasChanges = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (hasChanges) {
|
|
82
|
+
state.round = (state.round || 1) + 1;
|
|
83
|
+
}
|
|
84
|
+
return state;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/cli.ts
|
|
9
88
|
var cwd = process.cwd();
|
|
10
89
|
var projectName = basename(cwd);
|
|
11
90
|
function getBranchName() {
|
|
@@ -91,6 +170,22 @@ function getChangedFiles() {
|
|
|
91
170
|
}
|
|
92
171
|
}
|
|
93
172
|
}
|
|
173
|
+
if (hasCommits()) {
|
|
174
|
+
try {
|
|
175
|
+
const numstat = execSync("git diff --numstat HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
176
|
+
if (numstat) {
|
|
177
|
+
for (const line of numstat.split("\n")) {
|
|
178
|
+
const [add, del, path] = line.split(" ");
|
|
179
|
+
const file = files.find((f) => f.path === path);
|
|
180
|
+
if (file && add !== "-") {
|
|
181
|
+
file.additions = parseInt(add) || 0;
|
|
182
|
+
file.deletions = parseInt(del) || 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
94
189
|
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
95
190
|
}
|
|
96
191
|
function getFileDiff(filePath) {
|
|
@@ -151,74 +246,17 @@ function hashFile(filePath) {
|
|
|
151
246
|
function loadState() {
|
|
152
247
|
try {
|
|
153
248
|
const data = readFileSync(getStateFile(), "utf-8");
|
|
154
|
-
|
|
249
|
+
const state = JSON.parse(data);
|
|
250
|
+
if (!state.round) state.round = 1;
|
|
251
|
+
return state;
|
|
155
252
|
} catch {
|
|
156
|
-
return { files: {}, generalComments: [] };
|
|
253
|
+
return { round: 1, files: {}, generalComments: [] };
|
|
157
254
|
}
|
|
158
255
|
}
|
|
159
256
|
function saveState(state) {
|
|
160
257
|
mkdirSync(getStateDir(), { recursive: true });
|
|
161
258
|
writeFileSync(getStateFile(), JSON.stringify(state, null, 2));
|
|
162
259
|
}
|
|
163
|
-
function reconcileState(state, changedFiles) {
|
|
164
|
-
const currentPaths = new Set(changedFiles.map((f) => f.path));
|
|
165
|
-
for (const path of Object.keys(state.files)) {
|
|
166
|
-
if (!currentPaths.has(path)) {
|
|
167
|
-
delete state.files[path];
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
for (const path of currentPaths) {
|
|
171
|
-
const currentHash = hashFile(path);
|
|
172
|
-
const existing = state.files[path];
|
|
173
|
-
if (existing) {
|
|
174
|
-
if (existing.hash !== currentHash) {
|
|
175
|
-
existing.status = "pending";
|
|
176
|
-
existing.hash = currentHash;
|
|
177
|
-
existing.changedSinceReview = true;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return state;
|
|
182
|
-
}
|
|
183
|
-
function generateFeedback(state) {
|
|
184
|
-
const lines = [];
|
|
185
|
-
const filesWithFeedback = [];
|
|
186
|
-
for (const [path, review] of Object.entries(state.files)) {
|
|
187
|
-
if (review.comments.length > 0) {
|
|
188
|
-
filesWithFeedback.push(path);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
const totalComments = filesWithFeedback.reduce((sum, p) => sum + state.files[p].comments.length, 0) + state.generalComments.length;
|
|
192
|
-
lines.push("# Code Review Feedback");
|
|
193
|
-
lines.push("");
|
|
194
|
-
lines.push(
|
|
195
|
-
`${filesWithFeedback.length} files need changes. ${totalComments} comments total.`
|
|
196
|
-
);
|
|
197
|
-
for (const path of filesWithFeedback) {
|
|
198
|
-
const review = state.files[path];
|
|
199
|
-
lines.push("");
|
|
200
|
-
lines.push(`## ${path}`);
|
|
201
|
-
for (const comment of review.comments) {
|
|
202
|
-
const lineRef = comment.line ? `L${comment.line}` : "General";
|
|
203
|
-
if (comment.suggestion) {
|
|
204
|
-
lines.push(`- ${lineRef}: ${comment.text}`);
|
|
205
|
-
lines.push(" ```");
|
|
206
|
-
lines.push(` ${comment.suggestion}`);
|
|
207
|
-
lines.push(" ```");
|
|
208
|
-
} else {
|
|
209
|
-
lines.push(`- ${lineRef}: ${comment.text}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (state.generalComments.length > 0) {
|
|
214
|
-
lines.push("");
|
|
215
|
-
lines.push("## General");
|
|
216
|
-
for (const comment of state.generalComments) {
|
|
217
|
-
lines.push(`- ${comment.text}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return lines.join("\n");
|
|
221
|
-
}
|
|
222
260
|
function copyToClipboard(text) {
|
|
223
261
|
try {
|
|
224
262
|
const platform = process.platform;
|
|
@@ -255,7 +293,7 @@ function startServer(port) {
|
|
|
255
293
|
process.exit(0);
|
|
256
294
|
}
|
|
257
295
|
let state = loadState();
|
|
258
|
-
state = reconcileState(state, changedFiles);
|
|
296
|
+
state = reconcileState(state, changedFiles, hashFile);
|
|
259
297
|
saveState(state);
|
|
260
298
|
const server = http.createServer(async (req, res) => {
|
|
261
299
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
@@ -280,10 +318,12 @@ function startServer(port) {
|
|
|
280
318
|
<title>Code Review</title>
|
|
281
319
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>\u{1F50D}</text></svg>">
|
|
282
320
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css">
|
|
321
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/base16/solarized-dark.min.css">
|
|
283
322
|
<script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
|
284
323
|
<style>
|
|
285
|
-
/* Solarized Dark */
|
|
286
324
|
:root {
|
|
325
|
+
--sidebar-width: 280px;
|
|
326
|
+
/* Theme colors - overridden by theme classes */
|
|
287
327
|
--bg: #002b36;
|
|
288
328
|
--bg-secondary: #073642;
|
|
289
329
|
--bg-tertiary: #0a4050;
|
|
@@ -299,8 +339,133 @@ function startServer(port) {
|
|
|
299
339
|
--red: #dc322f;
|
|
300
340
|
--magenta: #d33682;
|
|
301
341
|
--violet: #6c71c4;
|
|
302
|
-
|
|
303
|
-
|
|
342
|
+
/* Diff backgrounds */
|
|
343
|
+
--diff-ins-bg: #0a3d0a;
|
|
344
|
+
--diff-ins-highlight: #1a5c1a;
|
|
345
|
+
--diff-del-bg: #3d0a0a;
|
|
346
|
+
--diff-del-highlight: #5c1a1a;
|
|
347
|
+
--diff-ins-linenumber: #082e08;
|
|
348
|
+
--diff-del-linenumber: #2e0808;
|
|
349
|
+
/* Syntax highlighting */
|
|
350
|
+
--hl-keyword: #859900;
|
|
351
|
+
--hl-string: #2aa198;
|
|
352
|
+
--hl-type: #b58900;
|
|
353
|
+
--hl-function: #268bd2;
|
|
354
|
+
--hl-number: #d33682;
|
|
355
|
+
--hl-comment: #586e75;
|
|
356
|
+
--hl-attr: #b58900;
|
|
357
|
+
--hl-meta: #cb4b16;
|
|
358
|
+
--hl-variable: #268bd2;
|
|
359
|
+
--hl-regexp: #dc322f;
|
|
360
|
+
--hl-symbol: #6c71c4;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Solarized Dark (default) */
|
|
364
|
+
.theme-solarized-dark {
|
|
365
|
+
--bg: #002b36; --bg-secondary: #073642; --bg-tertiary: #0a4050;
|
|
366
|
+
--border: #586e75; --text: #93a1a1; --text-bright: #eee8d5; --text-muted: #657b83;
|
|
367
|
+
--accent: #268bd2; --cyan: #2aa198; --green: #859900; --yellow: #b58900;
|
|
368
|
+
--orange: #cb4b16; --red: #dc322f; --magenta: #d33682; --violet: #6c71c4;
|
|
369
|
+
--diff-ins-bg: #0a3d0a; --diff-ins-highlight: #1a5c1a;
|
|
370
|
+
--diff-del-bg: #3d0a0a; --diff-del-highlight: #5c1a1a;
|
|
371
|
+
--diff-ins-linenumber: #082e08; --diff-del-linenumber: #2e0808;
|
|
372
|
+
--hl-keyword: #859900; --hl-string: #2aa198; --hl-type: #b58900;
|
|
373
|
+
--hl-function: #268bd2; --hl-number: #d33682; --hl-comment: #586e75;
|
|
374
|
+
--hl-attr: #b58900; --hl-meta: #cb4b16; --hl-variable: #268bd2;
|
|
375
|
+
--hl-regexp: #dc322f; --hl-symbol: #6c71c4;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* Monokai */
|
|
379
|
+
.theme-monokai {
|
|
380
|
+
--bg: #272822; --bg-secondary: #1e1f1c; --bg-tertiary: #3e3d32;
|
|
381
|
+
--border: #49483e; --text: #f8f8f2; --text-bright: #f8f8f0; --text-muted: #75715e;
|
|
382
|
+
--accent: #66d9ef; --cyan: #66d9ef; --green: #a6e22e; --yellow: #e6db74;
|
|
383
|
+
--orange: #fd971f; --red: #f92672; --magenta: #f92672; --violet: #ae81ff;
|
|
384
|
+
--diff-ins-bg: #2b3d0a; --diff-ins-highlight: #3d5c1a;
|
|
385
|
+
--diff-del-bg: #3d0a1a; --diff-del-highlight: #5c1a2a;
|
|
386
|
+
--diff-ins-linenumber: #232e08; --diff-del-linenumber: #2e0815;
|
|
387
|
+
--hl-keyword: #f92672; --hl-string: #e6db74; --hl-type: #66d9ef;
|
|
388
|
+
--hl-function: #a6e22e; --hl-number: #ae81ff; --hl-comment: #75715e;
|
|
389
|
+
--hl-attr: #a6e22e; --hl-meta: #f92672; --hl-variable: #f8f8f2;
|
|
390
|
+
--hl-regexp: #e6db74; --hl-symbol: #ae81ff;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* GitHub Light */
|
|
394
|
+
.theme-github-light {
|
|
395
|
+
--bg: #ffffff; --bg-secondary: #f6f8fa; --bg-tertiary: #eaeef2;
|
|
396
|
+
--border: #d0d7de; --text: #1f2328; --text-bright: #1f2328; --text-muted: #656d76;
|
|
397
|
+
--accent: #0969da; --cyan: #0969da; --green: #1a7f37; --yellow: #9a6700;
|
|
398
|
+
--orange: #bc4c00; --red: #cf222e; --magenta: #8250df; --violet: #8250df;
|
|
399
|
+
--diff-ins-bg: #e6ffec; --diff-ins-highlight: #ccffd8;
|
|
400
|
+
--diff-del-bg: #ffebe9; --diff-del-highlight: #ffd7d5;
|
|
401
|
+
--diff-ins-linenumber: #e6ffec; --diff-del-linenumber: #ffebe9;
|
|
402
|
+
--hl-keyword: #cf222e; --hl-string: #0a3069; --hl-type: #8250df;
|
|
403
|
+
--hl-function: #6639ba; --hl-number: #0550ae; --hl-comment: #6e7781;
|
|
404
|
+
--hl-attr: #953800; --hl-meta: #cf222e; --hl-variable: #953800;
|
|
405
|
+
--hl-regexp: #0550ae; --hl-symbol: #8250df;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* GitHub Light: force all diff2html backgrounds to white */
|
|
409
|
+
.theme-github-light .d2h-wrapper,
|
|
410
|
+
.theme-github-light .d2h-file-wrapper,
|
|
411
|
+
.theme-github-light .d2h-file-diff .d2h-code-pane,
|
|
412
|
+
.theme-github-light .d2h-code-line,
|
|
413
|
+
.theme-github-light .d2h-code-side-line,
|
|
414
|
+
.theme-github-light .d2h-diff-table,
|
|
415
|
+
.theme-github-light .d2h-diff-tbody,
|
|
416
|
+
.theme-github-light .d2h-code-line-ctn,
|
|
417
|
+
.theme-github-light td.d2h-code-line-ctn,
|
|
418
|
+
.theme-github-light tr {
|
|
419
|
+
background: #ffffff !important;
|
|
420
|
+
}
|
|
421
|
+
.theme-github-light .d2h-code-line-ctn,
|
|
422
|
+
.theme-github-light .d2h-code-line-ctn * {
|
|
423
|
+
color: #1f2328 !important;
|
|
424
|
+
}
|
|
425
|
+
/* Re-apply syntax colors over the forced text color */
|
|
426
|
+
.theme-github-light .d2h-code-line-ctn .hljs-keyword,
|
|
427
|
+
.theme-github-light .d2h-code-line-ctn .hljs-selector-tag { color: #cf222e !important; }
|
|
428
|
+
.theme-github-light .d2h-code-line-ctn .hljs-string { color: #0a3069 !important; }
|
|
429
|
+
.theme-github-light .d2h-code-line-ctn .hljs-built_in,
|
|
430
|
+
.theme-github-light .d2h-code-line-ctn .hljs-type,
|
|
431
|
+
.theme-github-light .d2h-code-line-ctn .hljs-title.class_ { color: #8250df !important; }
|
|
432
|
+
.theme-github-light .d2h-code-line-ctn .hljs-function,
|
|
433
|
+
.theme-github-light .d2h-code-line-ctn .hljs-title { color: #6639ba !important; }
|
|
434
|
+
.theme-github-light .d2h-code-line-ctn .hljs-number,
|
|
435
|
+
.theme-github-light .d2h-code-line-ctn .hljs-literal { color: #0550ae !important; }
|
|
436
|
+
.theme-github-light .d2h-code-line-ctn .hljs-comment,
|
|
437
|
+
.theme-github-light .d2h-code-line-ctn .hljs-quote { color: #6e7781 !important; }
|
|
438
|
+
.theme-github-light .d2h-code-line-ctn .hljs-attr,
|
|
439
|
+
.theme-github-light .d2h-code-line-ctn .hljs-variable { color: #953800 !important; }
|
|
440
|
+
.theme-github-light .d2h-code-line-ctn .hljs-meta { color: #cf222e !important; }
|
|
441
|
+
/* Light theme diff backgrounds */
|
|
442
|
+
.theme-github-light .d2h-ins,
|
|
443
|
+
.theme-github-light .d2h-ins .d2h-code-line-ctn { background: #e6ffec !important; }
|
|
444
|
+
.theme-github-light .d2h-ins .d2h-code-line-ctn ins { background: #ccffd8 !important; }
|
|
445
|
+
.theme-github-light .d2h-del,
|
|
446
|
+
.theme-github-light .d2h-del .d2h-code-line-ctn { background: #ffebe9 !important; }
|
|
447
|
+
.theme-github-light .d2h-del .d2h-code-line-ctn del { background: #ffd7d5 !important; }
|
|
448
|
+
.theme-github-light .d2h-ins .d2h-code-linenumber { background: #e6ffec !important; }
|
|
449
|
+
.theme-github-light .d2h-del .d2h-code-linenumber { background: #ffebe9 !important; }
|
|
450
|
+
.theme-github-light .d2h-file-header,
|
|
451
|
+
.theme-github-light .d2h-file-wrapper {
|
|
452
|
+
border-color: #d0d7de !important;
|
|
453
|
+
}
|
|
454
|
+
.theme-github-light .d2h-code-linenumber {
|
|
455
|
+
background: #f6f8fa !important;
|
|
456
|
+
color: #8c959f !important;
|
|
457
|
+
border-color: #d0d7de !important;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* GitHub Light: scrollbar and fold overrides */
|
|
461
|
+
.theme-github-light ::-webkit-scrollbar-thumb { background: #d0d7de; }
|
|
462
|
+
.theme-github-light ::-webkit-scrollbar-thumb:hover { background: #8c959f; }
|
|
463
|
+
.theme-github-light .fold-indicator { color: #0969da; background: #f6f8fa; }
|
|
464
|
+
.theme-github-light .fold-indicator:hover { background: #eaeef2; }
|
|
465
|
+
.theme-github-light .fold-lines { color: #656d76; }
|
|
466
|
+
.theme-github-light .fold-line-num { color: #8c959f; }
|
|
467
|
+
.theme-github-light .comment-marker { background: #9a6700; }
|
|
468
|
+
.theme-github-light .inline-comment-bubble { background: #f6f8fa; border-left-color: #9a6700; color: #1f2328; }
|
|
304
469
|
|
|
305
470
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
306
471
|
|
|
@@ -318,8 +483,8 @@ function startServer(port) {
|
|
|
318
483
|
.toolbar {
|
|
319
484
|
display: flex;
|
|
320
485
|
align-items: center;
|
|
321
|
-
gap:
|
|
322
|
-
padding:
|
|
486
|
+
gap: 8px;
|
|
487
|
+
padding: 6px 12px;
|
|
323
488
|
background: var(--bg-secondary);
|
|
324
489
|
border-bottom: 1px solid var(--border);
|
|
325
490
|
flex-shrink: 0;
|
|
@@ -328,11 +493,22 @@ function startServer(port) {
|
|
|
328
493
|
font-weight: 600;
|
|
329
494
|
font-size: 14px;
|
|
330
495
|
color: var(--text-bright);
|
|
331
|
-
|
|
496
|
+
white-space: nowrap;
|
|
497
|
+
overflow: hidden;
|
|
498
|
+
text-overflow: ellipsis;
|
|
499
|
+
min-width: 0;
|
|
500
|
+
}
|
|
501
|
+
.toolbar-spacer { flex: 1; }
|
|
502
|
+
.toolbar-actions {
|
|
503
|
+
display: flex;
|
|
504
|
+
align-items: center;
|
|
505
|
+
gap: 8px;
|
|
506
|
+
flex-shrink: 0;
|
|
332
507
|
}
|
|
333
508
|
.toolbar-stats {
|
|
334
509
|
font-size: 13px;
|
|
335
510
|
color: var(--text-muted);
|
|
511
|
+
white-space: nowrap;
|
|
336
512
|
}
|
|
337
513
|
.toolbar-stats .count { color: var(--cyan); font-weight: 600; }
|
|
338
514
|
.btn {
|
|
@@ -346,6 +522,17 @@ function startServer(port) {
|
|
|
346
522
|
font-weight: 500;
|
|
347
523
|
}
|
|
348
524
|
.btn:hover { border-color: var(--accent); }
|
|
525
|
+
select.btn {
|
|
526
|
+
appearance: none;
|
|
527
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='6'%3E%3Cpath d='M0 0l4 6 4-6z' fill='%2393a1a1'/%3E%3C/svg%3E");
|
|
528
|
+
background-repeat: no-repeat;
|
|
529
|
+
background-position: right 8px center;
|
|
530
|
+
padding-right: 22px;
|
|
531
|
+
}
|
|
532
|
+
select.btn option {
|
|
533
|
+
background: var(--bg-secondary);
|
|
534
|
+
color: var(--text);
|
|
535
|
+
}
|
|
349
536
|
.btn-primary {
|
|
350
537
|
background: var(--accent);
|
|
351
538
|
color: var(--bg);
|
|
@@ -358,6 +545,7 @@ function startServer(port) {
|
|
|
358
545
|
color: var(--orange);
|
|
359
546
|
}
|
|
360
547
|
.btn-danger:hover { background: var(--orange); color: var(--bg); }
|
|
548
|
+
.btn { white-space: nowrap; }
|
|
361
549
|
|
|
362
550
|
/* Main layout */
|
|
363
551
|
.main {
|
|
@@ -369,14 +557,30 @@ function startServer(port) {
|
|
|
369
557
|
/* Sidebar */
|
|
370
558
|
.sidebar {
|
|
371
559
|
width: var(--sidebar-width);
|
|
560
|
+
min-width: 180px;
|
|
561
|
+
max-width: 600px;
|
|
372
562
|
border-right: 1px solid var(--border);
|
|
373
563
|
display: flex;
|
|
374
564
|
flex-direction: column;
|
|
375
565
|
flex-shrink: 0;
|
|
376
566
|
overflow: hidden;
|
|
567
|
+
position: relative;
|
|
568
|
+
}
|
|
569
|
+
.sidebar-resize {
|
|
570
|
+
position: absolute;
|
|
571
|
+
top: 0;
|
|
572
|
+
right: 0;
|
|
573
|
+
width: 4px;
|
|
574
|
+
height: 100%;
|
|
575
|
+
cursor: col-resize;
|
|
576
|
+
z-index: 20;
|
|
577
|
+
}
|
|
578
|
+
.sidebar-resize:hover,
|
|
579
|
+
.sidebar-resize.dragging {
|
|
580
|
+
background: var(--accent);
|
|
377
581
|
}
|
|
378
582
|
.sidebar-header {
|
|
379
|
-
padding:
|
|
583
|
+
padding: 6px 12px;
|
|
380
584
|
font-size: 12px;
|
|
381
585
|
font-weight: 600;
|
|
382
586
|
color: var(--text-muted);
|
|
@@ -384,7 +588,29 @@ function startServer(port) {
|
|
|
384
588
|
letter-spacing: 0.5px;
|
|
385
589
|
border-bottom: 1px solid var(--border);
|
|
386
590
|
background: var(--bg-secondary);
|
|
591
|
+
display: flex;
|
|
592
|
+
align-items: center;
|
|
593
|
+
justify-content: space-between;
|
|
594
|
+
gap: 8px;
|
|
595
|
+
}
|
|
596
|
+
.filter-bar {
|
|
597
|
+
display: flex;
|
|
598
|
+
gap: 2px;
|
|
387
599
|
}
|
|
600
|
+
.filter-btn {
|
|
601
|
+
background: none;
|
|
602
|
+
border: 1px solid transparent;
|
|
603
|
+
color: var(--text-muted);
|
|
604
|
+
font-size: 10px;
|
|
605
|
+
padding: 2px 6px;
|
|
606
|
+
border-radius: 3px;
|
|
607
|
+
cursor: pointer;
|
|
608
|
+
text-transform: none;
|
|
609
|
+
letter-spacing: 0;
|
|
610
|
+
font-weight: 400;
|
|
611
|
+
}
|
|
612
|
+
.filter-btn:hover { color: var(--text); border-color: var(--border); }
|
|
613
|
+
.filter-btn.active { color: var(--cyan); border-color: var(--cyan); font-weight: 600; }
|
|
388
614
|
.file-list {
|
|
389
615
|
flex: 1;
|
|
390
616
|
overflow-y: auto;
|
|
@@ -423,16 +649,35 @@ function startServer(port) {
|
|
|
423
649
|
font-size: 12px;
|
|
424
650
|
}
|
|
425
651
|
.file-dir { color: var(--text-muted); }
|
|
652
|
+
.file-stats {
|
|
653
|
+
font-family: "SF Mono", "Fira Code", monospace;
|
|
654
|
+
font-size: 10px;
|
|
655
|
+
flex-shrink: 0;
|
|
656
|
+
display: flex;
|
|
657
|
+
gap: 4px;
|
|
658
|
+
}
|
|
659
|
+
.file-stats .stat-add { color: var(--green); }
|
|
660
|
+
.file-stats .stat-del { color: var(--orange); }
|
|
426
661
|
|
|
427
662
|
/* Shortcuts legend in sidebar */
|
|
428
663
|
.shortcuts-legend {
|
|
429
|
-
padding:
|
|
664
|
+
padding: 0;
|
|
430
665
|
border-top: 1px solid var(--border);
|
|
431
666
|
background: var(--bg-secondary);
|
|
432
667
|
font-size: 11px;
|
|
433
668
|
color: var(--text-muted);
|
|
434
669
|
flex-shrink: 0;
|
|
435
670
|
}
|
|
671
|
+
.shortcuts-toggle {
|
|
672
|
+
padding: 6px 12px;
|
|
673
|
+
cursor: pointer;
|
|
674
|
+
display: flex;
|
|
675
|
+
justify-content: space-between;
|
|
676
|
+
}
|
|
677
|
+
.shortcuts-toggle:hover { color: var(--text); }
|
|
678
|
+
.shortcuts-body {
|
|
679
|
+
padding: 0 12px 6px;
|
|
680
|
+
}
|
|
436
681
|
.shortcuts-legend div {
|
|
437
682
|
display: flex;
|
|
438
683
|
justify-content: space-between;
|
|
@@ -467,6 +712,7 @@ function startServer(port) {
|
|
|
467
712
|
border-bottom: 1px solid var(--border);
|
|
468
713
|
flex-shrink: 0;
|
|
469
714
|
z-index: 10;
|
|
715
|
+
overflow-x: auto;
|
|
470
716
|
}
|
|
471
717
|
.file-nav-btn {
|
|
472
718
|
background: none;
|
|
@@ -588,8 +834,7 @@ function startServer(port) {
|
|
|
588
834
|
gap: 8px;
|
|
589
835
|
align-items: flex-start;
|
|
590
836
|
}
|
|
591
|
-
.add-comment input[type="
|
|
592
|
-
width: 60px;
|
|
837
|
+
.add-comment input[type="text"] {
|
|
593
838
|
padding: 6px 8px;
|
|
594
839
|
background: var(--bg);
|
|
595
840
|
border: 1px solid var(--border);
|
|
@@ -623,6 +868,79 @@ function startServer(port) {
|
|
|
623
868
|
margin-top: 4px;
|
|
624
869
|
display: inline-block;
|
|
625
870
|
}
|
|
871
|
+
.quick-comments {
|
|
872
|
+
display: flex;
|
|
873
|
+
flex-wrap: wrap;
|
|
874
|
+
gap: 4px;
|
|
875
|
+
padding: 6px 16px;
|
|
876
|
+
border-top: 1px solid var(--border);
|
|
877
|
+
}
|
|
878
|
+
.quick-btn {
|
|
879
|
+
background: var(--bg);
|
|
880
|
+
border: 1px solid var(--border);
|
|
881
|
+
color: var(--text-muted);
|
|
882
|
+
font-size: 11px;
|
|
883
|
+
padding: 2px 8px;
|
|
884
|
+
border-radius: 12px;
|
|
885
|
+
cursor: pointer;
|
|
886
|
+
}
|
|
887
|
+
.quick-btn:hover { color: var(--text); border-color: var(--accent); }
|
|
888
|
+
.archived-section {
|
|
889
|
+
border-top: 1px dashed var(--border);
|
|
890
|
+
padding: 6px 16px;
|
|
891
|
+
}
|
|
892
|
+
.archived-header {
|
|
893
|
+
font-size: 11px;
|
|
894
|
+
color: var(--text-muted);
|
|
895
|
+
cursor: pointer;
|
|
896
|
+
padding: 4px 0;
|
|
897
|
+
}
|
|
898
|
+
.archived-header:hover { color: var(--text); }
|
|
899
|
+
.archived-item {
|
|
900
|
+
display: flex;
|
|
901
|
+
align-items: flex-start;
|
|
902
|
+
gap: 8px;
|
|
903
|
+
padding: 6px 0;
|
|
904
|
+
font-size: 12px;
|
|
905
|
+
color: var(--text-muted);
|
|
906
|
+
border-bottom: 1px solid var(--border);
|
|
907
|
+
}
|
|
908
|
+
.archived-item:last-child { border-bottom: none; }
|
|
909
|
+
.archived-item .archived-round {
|
|
910
|
+
font-size: 10px;
|
|
911
|
+
background: var(--violet);
|
|
912
|
+
color: var(--bg);
|
|
913
|
+
border-radius: 3px;
|
|
914
|
+
padding: 1px 6px;
|
|
915
|
+
flex-shrink: 0;
|
|
916
|
+
font-weight: 600;
|
|
917
|
+
}
|
|
918
|
+
.archived-item .archived-text { flex: 1; }
|
|
919
|
+
/* Inline archived comment bubble (dimmed, dashed border) */
|
|
920
|
+
.inline-archived-bubble {
|
|
921
|
+
margin: 4px 8px 4px 60px;
|
|
922
|
+
padding: 5px 10px;
|
|
923
|
+
background: transparent;
|
|
924
|
+
border: 1px dashed var(--border);
|
|
925
|
+
border-left: 3px dashed var(--violet);
|
|
926
|
+
border-radius: 4px;
|
|
927
|
+
font-size: 11px;
|
|
928
|
+
color: var(--text-muted);
|
|
929
|
+
line-height: 1.4;
|
|
930
|
+
display: flex;
|
|
931
|
+
align-items: flex-start;
|
|
932
|
+
gap: 6px;
|
|
933
|
+
opacity: 0.7;
|
|
934
|
+
}
|
|
935
|
+
.inline-archived-bubble .archived-round {
|
|
936
|
+
font-size: 9px;
|
|
937
|
+
background: var(--violet);
|
|
938
|
+
color: var(--bg);
|
|
939
|
+
border-radius: 3px;
|
|
940
|
+
padding: 1px 5px;
|
|
941
|
+
flex-shrink: 0;
|
|
942
|
+
font-weight: 600;
|
|
943
|
+
}
|
|
626
944
|
.suggestion-input {
|
|
627
945
|
margin-top: 4px;
|
|
628
946
|
}
|
|
@@ -719,11 +1037,13 @@ function startServer(port) {
|
|
|
719
1037
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
720
1038
|
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
|
721
1039
|
|
|
722
|
-
/* Line click hint */
|
|
723
|
-
.d2h-code-linenumber { cursor: pointer; }
|
|
1040
|
+
/* Line number click hint - only the number column is clickable */
|
|
1041
|
+
.d2h-code-linenumber { cursor: pointer; user-select: none; }
|
|
724
1042
|
.d2h-code-linenumber:hover {
|
|
725
1043
|
background: rgba(38, 139, 210, 0.25) !important;
|
|
726
1044
|
}
|
|
1045
|
+
/* Code content is freely selectable */
|
|
1046
|
+
.d2h-code-line-ctn { cursor: text; user-select: text; }
|
|
727
1047
|
|
|
728
1048
|
/* ---- diff2html Solarized Dark overrides ---- */
|
|
729
1049
|
.d2h-file-header, .d2h-file-wrapper {
|
|
@@ -741,33 +1061,58 @@ function startServer(port) {
|
|
|
741
1061
|
}
|
|
742
1062
|
.d2h-code-line, .d2h-code-side-line {
|
|
743
1063
|
background: var(--bg) !important;
|
|
744
|
-
color: var(--text) !important;
|
|
745
1064
|
width: 100% !important;
|
|
746
1065
|
}
|
|
747
1066
|
.d2h-code-line-ctn {
|
|
748
|
-
color: var(--text) !important;
|
|
749
1067
|
width: 100% !important;
|
|
750
1068
|
}
|
|
1069
|
+
/* Default text color for non-highlighted code */
|
|
1070
|
+
.d2h-code-line-ctn, .d2h-code-line-ctn * {
|
|
1071
|
+
color: var(--text);
|
|
1072
|
+
}
|
|
1073
|
+
/* Syntax highlighting via CSS variables (theme-aware) */
|
|
1074
|
+
.d2h-code-line-ctn .hljs-keyword,
|
|
1075
|
+
.d2h-code-line-ctn .hljs-selector-tag,
|
|
1076
|
+
.d2h-code-line-ctn .hljs-deletion { color: var(--hl-keyword) !important; }
|
|
1077
|
+
.d2h-code-line-ctn .hljs-string,
|
|
1078
|
+
.d2h-code-line-ctn .hljs-addition { color: var(--hl-string) !important; }
|
|
1079
|
+
.d2h-code-line-ctn .hljs-built_in,
|
|
1080
|
+
.d2h-code-line-ctn .hljs-type { color: var(--hl-type) !important; }
|
|
1081
|
+
.d2h-code-line-ctn .hljs-function,
|
|
1082
|
+
.d2h-code-line-ctn .hljs-title { color: var(--hl-function) !important; }
|
|
1083
|
+
.d2h-code-line-ctn .hljs-number,
|
|
1084
|
+
.d2h-code-line-ctn .hljs-literal { color: var(--hl-number) !important; }
|
|
1085
|
+
.d2h-code-line-ctn .hljs-comment,
|
|
1086
|
+
.d2h-code-line-ctn .hljs-quote { color: var(--hl-comment) !important; font-style: italic; }
|
|
1087
|
+
.d2h-code-line-ctn .hljs-attr,
|
|
1088
|
+
.d2h-code-line-ctn .hljs-attribute { color: var(--hl-attr) !important; }
|
|
1089
|
+
.d2h-code-line-ctn .hljs-params { color: var(--text) !important; }
|
|
1090
|
+
.d2h-code-line-ctn .hljs-meta,
|
|
1091
|
+
.d2h-code-line-ctn .hljs-meta .hljs-keyword { color: var(--hl-meta) !important; }
|
|
1092
|
+
.d2h-code-line-ctn .hljs-class .hljs-title,
|
|
1093
|
+
.d2h-code-line-ctn .hljs-title.class_ { color: var(--hl-type) !important; }
|
|
1094
|
+
.d2h-code-line-ctn .hljs-variable,
|
|
1095
|
+
.d2h-code-line-ctn .hljs-template-variable { color: var(--hl-variable) !important; }
|
|
1096
|
+
.d2h-code-line-ctn .hljs-regexp { color: var(--hl-regexp) !important; }
|
|
1097
|
+
.d2h-code-line-ctn .hljs-symbol { color: var(--hl-symbol) !important; }
|
|
1098
|
+
.d2h-code-line-ctn .hljs-selector-class,
|
|
1099
|
+
.d2h-code-line-ctn .hljs-selector-id { color: var(--hl-function) !important; }
|
|
751
1100
|
/* Added lines */
|
|
752
1101
|
.d2h-ins, .d2h-ins .d2h-code-line-ctn {
|
|
753
|
-
background:
|
|
754
|
-
color: #eee8d5 !important;
|
|
1102
|
+
background: var(--diff-ins-bg) !important;
|
|
755
1103
|
}
|
|
756
1104
|
.d2h-ins .d2h-code-line-ctn ins,
|
|
757
1105
|
.d2h-ins.d2h-change .d2h-code-line-ctn ins {
|
|
758
|
-
background:
|
|
759
|
-
color: #eee8d5 !important;
|
|
1106
|
+
background: var(--diff-ins-highlight) !important;
|
|
760
1107
|
text-decoration: none !important;
|
|
761
1108
|
}
|
|
762
1109
|
/* Deleted lines */
|
|
763
1110
|
.d2h-del, .d2h-del .d2h-code-line-ctn {
|
|
764
|
-
background:
|
|
765
|
-
color: #eee8d5 !important;
|
|
1111
|
+
background: var(--diff-del-bg) !important;
|
|
766
1112
|
}
|
|
767
1113
|
.d2h-del .d2h-code-line-ctn del,
|
|
768
1114
|
.d2h-del.d2h-change .d2h-code-line-ctn del {
|
|
769
|
-
background:
|
|
770
|
-
color: #eee8d5 !important;
|
|
1115
|
+
background: var(--diff-del-highlight) !important;
|
|
771
1116
|
text-decoration: none !important;
|
|
772
1117
|
}
|
|
773
1118
|
/* Info/hunk headers - visually hidden but in DOM for fold indicator positioning */
|
|
@@ -781,17 +1126,17 @@ function startServer(port) {
|
|
|
781
1126
|
}
|
|
782
1127
|
/* Line numbers */
|
|
783
1128
|
.d2h-code-linenumber {
|
|
784
|
-
background:
|
|
785
|
-
color:
|
|
786
|
-
border-color:
|
|
1129
|
+
background: var(--bg-secondary) !important;
|
|
1130
|
+
color: var(--text-muted) !important;
|
|
1131
|
+
border-color: var(--border) !important;
|
|
787
1132
|
}
|
|
788
1133
|
.d2h-ins .d2h-code-linenumber {
|
|
789
|
-
background:
|
|
790
|
-
color:
|
|
1134
|
+
background: var(--diff-ins-linenumber) !important;
|
|
1135
|
+
color: var(--text-muted) !important;
|
|
791
1136
|
}
|
|
792
1137
|
.d2h-del .d2h-code-linenumber {
|
|
793
|
-
background:
|
|
794
|
-
color:
|
|
1138
|
+
background: var(--diff-del-linenumber) !important;
|
|
1139
|
+
color: var(--text-muted) !important;
|
|
795
1140
|
}
|
|
796
1141
|
/* Empty placeholder cells */
|
|
797
1142
|
.d2h-code-side-emptyplaceholder, .d2h-emptyplaceholder {
|
|
@@ -864,7 +1209,25 @@ function startServer(port) {
|
|
|
864
1209
|
width: 8px;
|
|
865
1210
|
height: 8px;
|
|
866
1211
|
border-radius: 50%;
|
|
867
|
-
|
|
1212
|
+
cursor: pointer;
|
|
1213
|
+
z-index: 5;
|
|
1214
|
+
}
|
|
1215
|
+
.comment-marker.marker-current {
|
|
1216
|
+
background: var(--orange);
|
|
1217
|
+
}
|
|
1218
|
+
.comment-marker.marker-archived {
|
|
1219
|
+
background: transparent;
|
|
1220
|
+
border: 2px solid var(--violet);
|
|
1221
|
+
}
|
|
1222
|
+
.comment-marker:hover { opacity: 0.8; }
|
|
1223
|
+
/* Selected line range highlighting */
|
|
1224
|
+
tr.line-selected td {
|
|
1225
|
+
outline: 1px solid var(--accent);
|
|
1226
|
+
outline-offset: -1px;
|
|
1227
|
+
}
|
|
1228
|
+
tr.line-selected .d2h-code-linenumber {
|
|
1229
|
+
background: rgba(38, 139, 210, 0.25) !important;
|
|
1230
|
+
color: var(--text-bright) !important;
|
|
868
1231
|
}
|
|
869
1232
|
.inline-comment-row td {
|
|
870
1233
|
padding: 0 !important;
|
|
@@ -881,7 +1244,20 @@ function startServer(port) {
|
|
|
881
1244
|
font-size: 12px;
|
|
882
1245
|
color: var(--text-bright);
|
|
883
1246
|
line-height: 1.4;
|
|
1247
|
+
display: flex;
|
|
1248
|
+
align-items: flex-start;
|
|
1249
|
+
gap: 6px;
|
|
1250
|
+
}
|
|
1251
|
+
.inline-comment-bubble .inline-content { flex: 1; }
|
|
1252
|
+
.inline-comment-delete {
|
|
1253
|
+
color: var(--text-muted);
|
|
1254
|
+
cursor: pointer;
|
|
1255
|
+
font-size: 15px;
|
|
1256
|
+
line-height: 1;
|
|
1257
|
+
padding: 0 2px;
|
|
1258
|
+
flex-shrink: 0;
|
|
884
1259
|
}
|
|
1260
|
+
.inline-comment-delete:hover { color: var(--orange); }
|
|
885
1261
|
.inline-comment-bubble .inline-line-ref {
|
|
886
1262
|
color: var(--cyan);
|
|
887
1263
|
font-family: "SF Mono", "Fira Code", monospace;
|
|
@@ -904,21 +1280,43 @@ function startServer(port) {
|
|
|
904
1280
|
<body>
|
|
905
1281
|
<div class="toolbar">
|
|
906
1282
|
<div class="toolbar-title" id="toolbar-title">diffback</div>
|
|
907
|
-
<div class="toolbar-
|
|
908
|
-
<
|
|
909
|
-
|
|
1283
|
+
<div class="toolbar-spacer"></div>
|
|
1284
|
+
<div class="toolbar-actions">
|
|
1285
|
+
<div class="toolbar-stats" id="toolbar-stats"></div>
|
|
1286
|
+
<select id="theme-selector" class="btn" title="Change theme" style="padding:4px 8px;cursor:pointer;">
|
|
1287
|
+
<option value="solarized-dark">Solarized Dark</option>
|
|
1288
|
+
<option value="monokai">Monokai</option>
|
|
1289
|
+
<option value="github-light">GitHub Light</option>
|
|
1290
|
+
</select>
|
|
1291
|
+
<button class="btn btn-primary" id="btn-generate" title="Generate feedback prompt">Generate</button>
|
|
1292
|
+
<button class="btn btn-danger" id="btn-finish" title="Finish review and clean up state">Finish</button>
|
|
1293
|
+
</div>
|
|
910
1294
|
</div>
|
|
911
1295
|
|
|
912
1296
|
<div class="main">
|
|
913
|
-
<div class="sidebar">
|
|
914
|
-
<div class="sidebar-
|
|
1297
|
+
<div class="sidebar" id="sidebar">
|
|
1298
|
+
<div class="sidebar-resize" id="sidebar-resize"></div>
|
|
1299
|
+
<div class="sidebar-header">
|
|
1300
|
+
<span>Files</span>
|
|
1301
|
+
<div class="filter-bar" id="filter-bar">
|
|
1302
|
+
<button class="filter-btn active" data-filter="all">All</button>
|
|
1303
|
+
<button class="filter-btn" data-filter="pending">Pending</button>
|
|
1304
|
+
<button class="filter-btn" data-filter="viewed">Viewed</button>
|
|
1305
|
+
<button class="filter-btn" data-filter="feedback">Feedback</button>
|
|
1306
|
+
</div>
|
|
1307
|
+
</div>
|
|
915
1308
|
<div class="file-list" id="file-list"></div>
|
|
916
1309
|
<div class="shortcuts-legend">
|
|
917
|
-
<div
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
<div
|
|
921
|
-
|
|
1310
|
+
<div class="shortcuts-toggle" id="shortcuts-toggle">
|
|
1311
|
+
<span>Shortcuts</span> <span id="shortcuts-arrow">▸</span>
|
|
1312
|
+
</div>
|
|
1313
|
+
<div class="shortcuts-body" id="shortcuts-body" style="display:none;">
|
|
1314
|
+
<div><span>Prev/Next file</span> <span><kbd>k</kbd> <kbd>j</kbd></span></div>
|
|
1315
|
+
<div><span>Viewed file</span> <kbd>a</kbd></div>
|
|
1316
|
+
<div><span>Add comment</span> <kbd>c</kbd></div>
|
|
1317
|
+
<div><span>Generate feedback</span> <kbd>g</kbd></div>
|
|
1318
|
+
<div><span>Send comment</span> <kbd>Cmd+Enter</kbd></div>
|
|
1319
|
+
</div>
|
|
922
1320
|
</div>
|
|
923
1321
|
</div>
|
|
924
1322
|
|
|
@@ -927,8 +1325,7 @@ function startServer(port) {
|
|
|
927
1325
|
</div>
|
|
928
1326
|
</div>
|
|
929
1327
|
|
|
930
|
-
<script>
|
|
931
|
-
// --- State ---
|
|
1328
|
+
<script> // --- State ---
|
|
932
1329
|
let appState = {
|
|
933
1330
|
files: [],
|
|
934
1331
|
generalComments: [],
|
|
@@ -936,7 +1333,9 @@ function startServer(port) {
|
|
|
936
1333
|
projectName: '',
|
|
937
1334
|
selectedFile: null,
|
|
938
1335
|
currentDiff: null,
|
|
939
|
-
fileContents: {},
|
|
1336
|
+
fileContents: {},
|
|
1337
|
+
filter: 'all',
|
|
1338
|
+
round: 1,
|
|
940
1339
|
};
|
|
941
1340
|
|
|
942
1341
|
// --- API ---
|
|
@@ -955,6 +1354,7 @@ function startServer(port) {
|
|
|
955
1354
|
appState.generalComments = data.generalComments || [];
|
|
956
1355
|
appState.summary = data.summary;
|
|
957
1356
|
appState.projectName = data.projectName;
|
|
1357
|
+
appState.round = data.round || 1;
|
|
958
1358
|
renderToolbar();
|
|
959
1359
|
renderFileList();
|
|
960
1360
|
}
|
|
@@ -994,8 +1394,19 @@ function startServer(port) {
|
|
|
994
1394
|
}
|
|
995
1395
|
|
|
996
1396
|
// --- Navigation helpers ---
|
|
1397
|
+
function getFilteredFiles() {
|
|
1398
|
+
return appState.files.filter(f => {
|
|
1399
|
+
if (appState.filter === 'all') return true;
|
|
1400
|
+
const status = f.review?.status;
|
|
1401
|
+
if (appState.filter === 'pending') return !status || status === 'pending';
|
|
1402
|
+
if (appState.filter === 'viewed') return status === 'reviewed';
|
|
1403
|
+
if (appState.filter === 'feedback') return status === 'has-feedback' || (f.review?.comments?.length > 0);
|
|
1404
|
+
return true;
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
997
1408
|
function navigateFile(direction) {
|
|
998
|
-
const files =
|
|
1409
|
+
const files = getFilteredFiles();
|
|
999
1410
|
if (files.length === 0) return;
|
|
1000
1411
|
const currentIdx = files.findIndex(f => f.path === appState.selectedFile);
|
|
1001
1412
|
let next;
|
|
@@ -1023,7 +1434,9 @@ function startServer(port) {
|
|
|
1023
1434
|
const container = document.getElementById('file-list');
|
|
1024
1435
|
container.innerHTML = '';
|
|
1025
1436
|
|
|
1026
|
-
|
|
1437
|
+
const filtered = getFilteredFiles();
|
|
1438
|
+
|
|
1439
|
+
for (const file of filtered) {
|
|
1027
1440
|
const el = document.createElement('div');
|
|
1028
1441
|
el.className = 'file-item' + (appState.selectedFile === file.path ? ' active' : '');
|
|
1029
1442
|
el.dataset.path = file.path;
|
|
@@ -1064,11 +1477,36 @@ function startServer(port) {
|
|
|
1064
1477
|
el.appendChild(statusIcon);
|
|
1065
1478
|
el.appendChild(reviewIcon);
|
|
1066
1479
|
el.appendChild(pathEl);
|
|
1480
|
+
|
|
1481
|
+
// Line stats (+/-)
|
|
1482
|
+
if (file.additions !== undefined || file.deletions !== undefined) {
|
|
1483
|
+
const stats = document.createElement('span');
|
|
1484
|
+
stats.className = 'file-stats';
|
|
1485
|
+
const add = file.additions || 0;
|
|
1486
|
+
const del = file.deletions || 0;
|
|
1487
|
+
if (add > 0) stats.innerHTML += \`<span class="stat-add">+\${add}</span>\`;
|
|
1488
|
+
if (del > 0) stats.innerHTML += \`<span class="stat-del">-\${del}</span>\`;
|
|
1489
|
+
el.appendChild(stats);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1067
1492
|
el.addEventListener('click', () => loadDiff(file.path));
|
|
1068
1493
|
container.appendChild(el);
|
|
1069
1494
|
}
|
|
1495
|
+
|
|
1496
|
+
// Update filter button counts
|
|
1497
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
1498
|
+
btn.classList.toggle('active', btn.dataset.filter === appState.filter);
|
|
1499
|
+
});
|
|
1070
1500
|
}
|
|
1071
1501
|
|
|
1502
|
+
// --- Filter handlers ---
|
|
1503
|
+
document.getElementById('filter-bar').addEventListener('click', (e) => {
|
|
1504
|
+
const btn = e.target.closest('.filter-btn');
|
|
1505
|
+
if (!btn) return;
|
|
1506
|
+
appState.filter = btn.dataset.filter;
|
|
1507
|
+
renderFileList();
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1072
1510
|
// --- Parse hunk ranges from @@ header ---
|
|
1073
1511
|
function parseHunkHeader(headerText) {
|
|
1074
1512
|
// @@ -oldStart,oldCount +newStart,newCount @@
|
|
@@ -1157,51 +1595,171 @@ function startServer(port) {
|
|
|
1157
1595
|
}
|
|
1158
1596
|
|
|
1159
1597
|
// --- Insert inline comment markers & bubbles ---
|
|
1160
|
-
function
|
|
1161
|
-
|
|
1162
|
-
if (
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1598
|
+
function getCommentEndLine(comment) {
|
|
1599
|
+
// Returns the last line number for a comment (for range "15-22" returns 22, for "15" returns 15)
|
|
1600
|
+
if (!comment.line) return null;
|
|
1601
|
+
const parts = String(comment.line).split('-');
|
|
1602
|
+
return parseInt(parts[parts.length - 1]);
|
|
1603
|
+
}
|
|
1604
|
+
function getCommentStartLine(comment) {
|
|
1605
|
+
if (!comment.line) return null;
|
|
1606
|
+
return parseInt(String(comment.line).split('-')[0]);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function insertInlineComments(diffContainer, comments, archivedComments) {
|
|
1610
|
+
// Build a map of line -> { current: [...], archived: [...] }
|
|
1611
|
+
const lineData = {};
|
|
1612
|
+
|
|
1613
|
+
for (const c of comments.filter(c => c.line !== null)) {
|
|
1614
|
+
const endLine = getCommentEndLine(c);
|
|
1615
|
+
const startLine = getCommentStartLine(c);
|
|
1616
|
+
if (!endLine) continue;
|
|
1617
|
+
if (!lineData[endLine]) lineData[endLine] = { current: [], archived: [], markerLines: new Set() };
|
|
1618
|
+
lineData[endLine].current.push(c);
|
|
1619
|
+
for (let l = startLine; l <= endLine; l++) lineData[endLine].markerLines.add(l);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
for (const c of (archivedComments || []).filter(c => c.line !== null)) {
|
|
1623
|
+
const endLine = getCommentEndLine(c);
|
|
1624
|
+
if (!endLine) continue;
|
|
1625
|
+
if (!lineData[endLine]) lineData[endLine] = { current: [], archived: [], markerLines: new Set() };
|
|
1626
|
+
lineData[endLine].archived.push(c);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Collect all marker lines (for range highlighting)
|
|
1630
|
+
const allMarkerLines = new Set();
|
|
1631
|
+
for (const data of Object.values(lineData)) {
|
|
1632
|
+
for (const l of data.markerLines) allMarkerLines.add(l);
|
|
1169
1633
|
}
|
|
1170
1634
|
|
|
1171
|
-
// Find all line number cells and match
|
|
1172
1635
|
diffContainer.querySelectorAll('.d2h-code-linenumber').forEach(el => {
|
|
1173
1636
|
const lineNum2 = el.querySelector('.line-num2')?.textContent?.trim();
|
|
1174
1637
|
const num = parseInt(lineNum2);
|
|
1175
|
-
if (isNaN(num)
|
|
1638
|
+
if (isNaN(num)) return;
|
|
1639
|
+
|
|
1640
|
+
const data = lineData[num];
|
|
1641
|
+
const hasCurrent = data?.current.length > 0;
|
|
1642
|
+
const hasArchived = data?.archived.length > 0;
|
|
1643
|
+
const isInRange = allMarkerLines.has(num) && !data; // part of a range but not the end line
|
|
1644
|
+
|
|
1645
|
+
if (!hasCurrent && !hasArchived && !isInRange) return;
|
|
1176
1646
|
|
|
1177
|
-
// Add marker dot
|
|
1178
1647
|
el.style.position = 'relative';
|
|
1179
|
-
const marker = document.createElement('span');
|
|
1180
|
-
marker.className = 'comment-marker';
|
|
1181
|
-
el.appendChild(marker);
|
|
1182
1648
|
|
|
1183
|
-
//
|
|
1649
|
+
// Add marker dots (can have both current and archived on same line)
|
|
1650
|
+
if (hasCurrent) {
|
|
1651
|
+
const marker = document.createElement('span');
|
|
1652
|
+
marker.className = 'comment-marker marker-current';
|
|
1653
|
+
marker.title = 'Toggle comments';
|
|
1654
|
+
el.appendChild(marker);
|
|
1655
|
+
}
|
|
1656
|
+
if (hasArchived) {
|
|
1657
|
+
const marker = document.createElement('span');
|
|
1658
|
+
marker.className = 'comment-marker marker-archived';
|
|
1659
|
+
marker.style.left = hasCurrent ? '12px' : '2px'; // offset if both
|
|
1660
|
+
marker.title = 'Toggle archived comments';
|
|
1661
|
+
el.appendChild(marker);
|
|
1662
|
+
}
|
|
1663
|
+
if (isInRange) {
|
|
1664
|
+
const marker = document.createElement('span');
|
|
1665
|
+
marker.className = 'comment-marker marker-current';
|
|
1666
|
+
marker.style.opacity = '0.4';
|
|
1667
|
+
el.appendChild(marker);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (!data) return;
|
|
1184
1671
|
const tr = el.closest('tr');
|
|
1185
1672
|
if (!tr) return;
|
|
1186
1673
|
|
|
1187
|
-
|
|
1674
|
+
// Create current comment bubbles (visible by default)
|
|
1675
|
+
const currentRows = [];
|
|
1676
|
+
for (const comment of data.current) {
|
|
1188
1677
|
const commentTr = document.createElement('tr');
|
|
1189
|
-
commentTr.className = 'inline-comment-row';
|
|
1678
|
+
commentTr.className = 'inline-comment-row inline-current';
|
|
1190
1679
|
const td = document.createElement('td');
|
|
1191
1680
|
td.colSpan = 99;
|
|
1192
1681
|
td.innerHTML = \`
|
|
1193
1682
|
<div class="inline-comment-bubble">
|
|
1194
|
-
<
|
|
1195
|
-
|
|
1196
|
-
|
|
1683
|
+
<div class="inline-content">
|
|
1684
|
+
<span class="inline-line-ref">L\${comment.line}</span>
|
|
1685
|
+
\${escapeHtml(comment.text)}
|
|
1686
|
+
\${comment.suggestion ? \`<div class="inline-suggestion">\${escapeHtml(comment.suggestion)}</div>\` : ''}
|
|
1687
|
+
</div>
|
|
1688
|
+
<span class="inline-comment-delete" data-id="\${comment.id}" title="Delete comment">×</span>
|
|
1197
1689
|
</div>
|
|
1198
1690
|
\`;
|
|
1691
|
+
td.querySelector('.inline-comment-delete').addEventListener('click', () => {
|
|
1692
|
+
const file = appState.files.find(f => f.path === appState.selectedFile);
|
|
1693
|
+
if (!file) return;
|
|
1694
|
+
const review = file.review || { comments: [] };
|
|
1695
|
+
const newComments = (review.comments || []).filter(c => c.id !== comment.id);
|
|
1696
|
+
const newStatus = newComments.length > 0 ? 'has-feedback' : 'pending';
|
|
1697
|
+
const diffEl2 = document.querySelector('.diff-container');
|
|
1698
|
+
const scrollTop = diffEl2 ? diffEl2.scrollTop : 0;
|
|
1699
|
+
saveReview(file.path, newStatus, newComments).then(() => {
|
|
1700
|
+
loadDiff(file.path).then(() => {
|
|
1701
|
+
const newDiffEl = document.querySelector('.diff-container');
|
|
1702
|
+
if (newDiffEl) newDiffEl.scrollTop = scrollTop;
|
|
1703
|
+
});
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1199
1706
|
commentTr.appendChild(td);
|
|
1200
|
-
|
|
1707
|
+
currentRows.push(commentTr);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// Create archived comment bubbles (hidden by default)
|
|
1711
|
+
const archivedRows = [];
|
|
1712
|
+
for (const ac of data.archived) {
|
|
1713
|
+
const archTr = document.createElement('tr');
|
|
1714
|
+
archTr.className = 'inline-comment-row inline-archived';
|
|
1715
|
+
archTr.style.display = 'none';
|
|
1716
|
+
const td = document.createElement('td');
|
|
1717
|
+
td.colSpan = 99;
|
|
1718
|
+
td.innerHTML = \`
|
|
1719
|
+
<div class="inline-archived-bubble">
|
|
1720
|
+
<span class="archived-round">R\${ac.round}</span>
|
|
1721
|
+
<span class="inline-line-ref">L\${ac.line}</span>
|
|
1722
|
+
<span>\${escapeHtml(ac.text)}\${ac.suggestion ? \`<div class="inline-suggestion">\${escapeHtml(ac.suggestion)}</div>\` : ''}</span>
|
|
1723
|
+
</div>
|
|
1724
|
+
\`;
|
|
1725
|
+
archTr.appendChild(td);
|
|
1726
|
+
archivedRows.push(archTr);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Insert all rows after tr
|
|
1730
|
+
let insertAfter = tr;
|
|
1731
|
+
for (const row of currentRows) {
|
|
1732
|
+
insertAfter.after(row);
|
|
1733
|
+
insertAfter = row;
|
|
1734
|
+
}
|
|
1735
|
+
for (const row of archivedRows) {
|
|
1736
|
+
insertAfter.after(row);
|
|
1737
|
+
insertAfter = row;
|
|
1201
1738
|
}
|
|
1202
1739
|
|
|
1203
|
-
//
|
|
1204
|
-
|
|
1740
|
+
// Click orange marker to toggle current bubbles
|
|
1741
|
+
const currentMarker = el.querySelector('.marker-current');
|
|
1742
|
+
if (currentMarker && currentRows.length > 0) {
|
|
1743
|
+
currentMarker.addEventListener('mousedown', (ev) => { ev.preventDefault(); ev.stopPropagation(); });
|
|
1744
|
+
currentMarker.addEventListener('mouseup', (ev) => {
|
|
1745
|
+
ev.stopPropagation();
|
|
1746
|
+
const visible = currentRows[0].style.display !== 'none';
|
|
1747
|
+
currentRows.forEach(r => r.style.display = visible ? 'none' : '');
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Click violet marker to toggle archived bubbles
|
|
1752
|
+
const archivedMarker = el.querySelector('.marker-archived');
|
|
1753
|
+
if (archivedMarker && archivedRows.length > 0) {
|
|
1754
|
+
archivedMarker.addEventListener('mousedown', (ev) => { ev.preventDefault(); ev.stopPropagation(); });
|
|
1755
|
+
archivedMarker.addEventListener('mouseup', (ev) => {
|
|
1756
|
+
ev.stopPropagation();
|
|
1757
|
+
const visible = archivedRows[0].style.display !== 'none';
|
|
1758
|
+
archivedRows.forEach(r => r.style.display = visible ? 'none' : '');
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
delete lineData[num];
|
|
1205
1763
|
});
|
|
1206
1764
|
}
|
|
1207
1765
|
|
|
@@ -1263,20 +1821,61 @@ function startServer(port) {
|
|
|
1263
1821
|
// Post-render: folds, inline comments, line click handlers
|
|
1264
1822
|
setTimeout(() => {
|
|
1265
1823
|
insertFoldIndicators(diffContainer, appState.currentDiff.diff, file.path);
|
|
1266
|
-
insertInlineComments(diffContainer, comments);
|
|
1824
|
+
insertInlineComments(diffContainer, comments, review.archivedComments || []);
|
|
1825
|
+
|
|
1826
|
+
// Line selection state
|
|
1827
|
+
let rangeStart = null;
|
|
1828
|
+
|
|
1829
|
+
function highlightRange(s, e) {
|
|
1830
|
+
diffContainer.querySelectorAll('tr.line-selected').forEach(r => r.classList.remove('line-selected'));
|
|
1831
|
+
if (s === null) return;
|
|
1832
|
+
diffContainer.querySelectorAll('.d2h-code-linenumber').forEach(numEl => {
|
|
1833
|
+
const ln = parseInt(numEl.querySelector('.line-num2')?.textContent?.trim());
|
|
1834
|
+
if (!isNaN(ln) && ln >= s && ln <= e) {
|
|
1835
|
+
const row = numEl.closest('tr');
|
|
1836
|
+
if (row) row.classList.add('line-selected');
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1267
1840
|
|
|
1841
|
+
// Click line numbers: click to select/deselect, shift+click to extend range
|
|
1842
|
+
// Use mousedown+mouseup to ignore drags
|
|
1268
1843
|
diffContainer.querySelectorAll('.d2h-code-linenumber').forEach(el => {
|
|
1269
|
-
|
|
1844
|
+
let mouseDownPos = null;
|
|
1845
|
+
el.addEventListener('mousedown', (ev) => {
|
|
1846
|
+
mouseDownPos = { x: ev.clientX, y: ev.clientY };
|
|
1847
|
+
ev.preventDefault(); // Prevent text selection starting from line numbers
|
|
1848
|
+
});
|
|
1849
|
+
el.addEventListener('mouseup', (ev) => {
|
|
1850
|
+
// Ignore if dragged more than 5px (user was selecting text)
|
|
1851
|
+
if (!mouseDownPos) return;
|
|
1852
|
+
const dx = Math.abs(ev.clientX - mouseDownPos.x);
|
|
1853
|
+
const dy = Math.abs(ev.clientY - mouseDownPos.y);
|
|
1854
|
+
mouseDownPos = null;
|
|
1855
|
+
if (dx > 5 || dy > 5) return;
|
|
1856
|
+
|
|
1270
1857
|
const lineNum = el.querySelector('.line-num2')?.textContent?.trim()
|
|
1271
|
-
|| el.querySelector('.line-num1')?.textContent?.trim()
|
|
1272
|
-
|| el.textContent?.trim();
|
|
1858
|
+
|| el.querySelector('.line-num1')?.textContent?.trim();
|
|
1273
1859
|
const num = parseInt(lineNum);
|
|
1274
|
-
if (
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1860
|
+
if (isNaN(num)) return;
|
|
1861
|
+
|
|
1862
|
+
const lineInput = document.getElementById('comment-line');
|
|
1863
|
+
const currentVal = lineInput?.value.trim() || '';
|
|
1864
|
+
|
|
1865
|
+
if (ev.shiftKey && rangeStart !== null) {
|
|
1866
|
+
const s = Math.min(rangeStart, num);
|
|
1867
|
+
const e = Math.max(rangeStart, num);
|
|
1868
|
+
if (lineInput) lineInput.value = s === e ? String(s) : \`\${s}-\${e}\`;
|
|
1869
|
+
highlightRange(s, e);
|
|
1870
|
+
} else if (currentVal && (rangeStart === num || currentVal.includes('-'))) {
|
|
1871
|
+
rangeStart = null;
|
|
1872
|
+
if (lineInput) lineInput.value = '';
|
|
1873
|
+
highlightRange(null, null);
|
|
1874
|
+
} else {
|
|
1875
|
+
rangeStart = num;
|
|
1876
|
+
if (lineInput) lineInput.value = String(num);
|
|
1877
|
+
highlightRange(num, num);
|
|
1878
|
+
document.getElementById('comment-text')?.focus();
|
|
1280
1879
|
}
|
|
1281
1880
|
});
|
|
1282
1881
|
});
|
|
@@ -1332,7 +1931,7 @@ function startServer(port) {
|
|
|
1332
1931
|
addComment.className = 'add-comment';
|
|
1333
1932
|
addComment.innerHTML = \`
|
|
1334
1933
|
<div class="add-comment-row">
|
|
1335
|
-
<input type="
|
|
1934
|
+
<input type="text" id="comment-line" placeholder="L# or L#-#" style="width:80px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px;font-family:'SF Mono','Fira Code',monospace;" title="Line number or range (e.g. 42 or 15-22). Leave empty for general file comment.">
|
|
1336
1935
|
<textarea id="comment-text" placeholder="Add a comment (line optional)..." rows="1"></textarea>
|
|
1337
1936
|
<button class="btn" id="btn-add-comment">Add</button>
|
|
1338
1937
|
</div>
|
|
@@ -1342,6 +1941,49 @@ function startServer(port) {
|
|
|
1342
1941
|
</div>
|
|
1343
1942
|
\`;
|
|
1344
1943
|
reviewSection.appendChild(addComment);
|
|
1944
|
+
|
|
1945
|
+
// Quick comment buttons
|
|
1946
|
+
const quickComments = document.createElement('div');
|
|
1947
|
+
quickComments.className = 'quick-comments';
|
|
1948
|
+
const presets = ['Naming', 'Missing test', 'Unnecessary change', 'Delete this file', 'Needs refactor', 'Wrong approach'];
|
|
1949
|
+
for (const preset of presets) {
|
|
1950
|
+
const btn = document.createElement('button');
|
|
1951
|
+
btn.className = 'quick-btn';
|
|
1952
|
+
btn.textContent = preset;
|
|
1953
|
+
btn.addEventListener('click', () => {
|
|
1954
|
+
const textArea = document.getElementById('comment-text');
|
|
1955
|
+
if (textArea) {
|
|
1956
|
+
textArea.value = preset;
|
|
1957
|
+
textArea.focus();
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
quickComments.appendChild(btn);
|
|
1961
|
+
}
|
|
1962
|
+
reviewSection.appendChild(quickComments);
|
|
1963
|
+
|
|
1964
|
+
// Archived comments from previous rounds
|
|
1965
|
+
const archived = review.archivedComments || [];
|
|
1966
|
+
if (archived.length > 0) {
|
|
1967
|
+
const archivedSection = document.createElement('div');
|
|
1968
|
+
archivedSection.className = 'archived-section';
|
|
1969
|
+
archivedSection.innerHTML = \`<div class="archived-header" id="toggle-archived">Previous round comments (\${archived.length}) \\u25B8</div>\`;
|
|
1970
|
+
const archivedList = document.createElement('div');
|
|
1971
|
+
archivedList.id = 'archived-list';
|
|
1972
|
+
archivedList.style.display = 'none';
|
|
1973
|
+
for (const ac of archived) {
|
|
1974
|
+
const item = document.createElement('div');
|
|
1975
|
+
item.className = 'archived-item';
|
|
1976
|
+
item.innerHTML = \`
|
|
1977
|
+
<span class="archived-round">R\${ac.round}</span>
|
|
1978
|
+
<span class="comment-line-ref">\${ac.line ? 'L' + ac.line : 'General'}</span>
|
|
1979
|
+
<span class="archived-text">\${escapeHtml(ac.text)}\${ac.suggestion ? \`<div class="comment-suggestion">\${escapeHtml(ac.suggestion)}</div>\` : ''}</span>
|
|
1980
|
+
\`;
|
|
1981
|
+
archivedList.appendChild(item);
|
|
1982
|
+
}
|
|
1983
|
+
archivedSection.appendChild(archivedList);
|
|
1984
|
+
reviewSection.appendChild(archivedSection);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1345
1987
|
container.appendChild(reviewSection);
|
|
1346
1988
|
|
|
1347
1989
|
// General comments section
|
|
@@ -1397,11 +2039,23 @@ function startServer(port) {
|
|
|
1397
2039
|
body.style.display = body.style.display === 'none' ? 'block' : 'none';
|
|
1398
2040
|
});
|
|
1399
2041
|
|
|
2042
|
+
// Archived comments toggle
|
|
2043
|
+
const toggleArchived = document.getElementById('toggle-archived');
|
|
2044
|
+
if (toggleArchived) {
|
|
2045
|
+
toggleArchived.addEventListener('click', () => {
|
|
2046
|
+
const list = document.getElementById('archived-list');
|
|
2047
|
+
const isHidden = list.style.display === 'none';
|
|
2048
|
+
list.style.display = isHidden ? 'block' : 'none';
|
|
2049
|
+
toggleArchived.textContent = \`Previous round comments (\${archived.length}) \${isHidden ? '\\u25BE' : '\\u25B8'}\`;
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1400
2053
|
function addFileComment() {
|
|
1401
2054
|
const text = document.getElementById('comment-text').value.trim();
|
|
1402
2055
|
if (!text) return;
|
|
1403
2056
|
|
|
1404
|
-
const
|
|
2057
|
+
const lineRaw = document.getElementById('comment-line').value.trim();
|
|
2058
|
+
const line = lineRaw || null; // Keep as string for ranges like "15-22"
|
|
1405
2059
|
const suggestion = document.getElementById('comment-suggestion')?.value.trim() || null;
|
|
1406
2060
|
|
|
1407
2061
|
const newComment = {
|
|
@@ -1557,11 +2211,12 @@ function startServer(port) {
|
|
|
1557
2211
|
// --- Keyboard shortcuts ---
|
|
1558
2212
|
document.addEventListener('keydown', (e) => {
|
|
1559
2213
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
2214
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1560
2215
|
|
|
1561
|
-
if (e.key === 'j'
|
|
2216
|
+
if (e.key === 'j') {
|
|
1562
2217
|
e.preventDefault();
|
|
1563
2218
|
navigateFile('next');
|
|
1564
|
-
} else if (e.key === 'k'
|
|
2219
|
+
} else if (e.key === 'k') {
|
|
1565
2220
|
e.preventDefault();
|
|
1566
2221
|
navigateFile('prev');
|
|
1567
2222
|
} else if (e.key === 'a') {
|
|
@@ -1595,9 +2250,69 @@ function startServer(port) {
|
|
|
1595
2250
|
}
|
|
1596
2251
|
}, 3000);
|
|
1597
2252
|
|
|
2253
|
+
// --- Shortcuts toggle ---
|
|
2254
|
+
document.getElementById('shortcuts-toggle').addEventListener('click', () => {
|
|
2255
|
+
const body = document.getElementById('shortcuts-body');
|
|
2256
|
+
const arrow = document.getElementById('shortcuts-arrow');
|
|
2257
|
+
const hidden = body.style.display === 'none';
|
|
2258
|
+
body.style.display = hidden ? 'block' : 'none';
|
|
2259
|
+
arrow.innerHTML = hidden ? '\\u25BE' : '\\u25B8';
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2262
|
+
// --- Sidebar resize ---
|
|
2263
|
+
(() => {
|
|
2264
|
+
const sidebar = document.getElementById('sidebar');
|
|
2265
|
+
const handle = document.getElementById('sidebar-resize');
|
|
2266
|
+
let dragging = false;
|
|
2267
|
+
|
|
2268
|
+
handle.addEventListener('mousedown', (e) => {
|
|
2269
|
+
e.preventDefault();
|
|
2270
|
+
dragging = true;
|
|
2271
|
+
handle.classList.add('dragging');
|
|
2272
|
+
document.body.style.cursor = 'col-resize';
|
|
2273
|
+
document.body.style.userSelect = 'none';
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
document.addEventListener('mousemove', (e) => {
|
|
2277
|
+
if (!dragging) return;
|
|
2278
|
+
const newWidth = Math.min(600, Math.max(180, e.clientX));
|
|
2279
|
+
sidebar.style.width = newWidth + 'px';
|
|
2280
|
+
});
|
|
2281
|
+
|
|
2282
|
+
document.addEventListener('mouseup', () => {
|
|
2283
|
+
if (!dragging) return;
|
|
2284
|
+
dragging = false;
|
|
2285
|
+
handle.classList.remove('dragging');
|
|
2286
|
+
document.body.style.cursor = '';
|
|
2287
|
+
document.body.style.userSelect = '';
|
|
2288
|
+
localStorage.setItem('diffback-sidebar-width', sidebar.style.width);
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
// Restore saved width
|
|
2292
|
+
const saved = localStorage.getItem('diffback-sidebar-width');
|
|
2293
|
+
if (saved) sidebar.style.width = saved;
|
|
2294
|
+
})();
|
|
2295
|
+
|
|
2296
|
+
// --- Theme switcher ---
|
|
2297
|
+
function setTheme(name) {
|
|
2298
|
+
document.documentElement.className = \`theme-\${name}\`;
|
|
2299
|
+
localStorage.setItem('diffback-theme', name);
|
|
2300
|
+
document.getElementById('theme-selector').value = name;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
document.getElementById('theme-selector').addEventListener('change', (e) => {
|
|
2304
|
+
setTheme(e.target.value);
|
|
2305
|
+
// Re-render diff to apply new colors
|
|
2306
|
+
if (appState.selectedFile) loadDiff(appState.selectedFile);
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
// Load saved theme
|
|
2310
|
+
const savedTheme = localStorage.getItem('diffback-theme') || 'solarized-dark';
|
|
2311
|
+
setTheme(savedTheme);
|
|
2312
|
+
|
|
1598
2313
|
// --- Init ---
|
|
1599
2314
|
loadFiles();
|
|
1600
|
-
|
|
2315
|
+
</script>
|
|
1601
2316
|
</body>
|
|
1602
2317
|
</html>
|
|
1603
2318
|
`);
|
|
@@ -1605,7 +2320,7 @@ function startServer(port) {
|
|
|
1605
2320
|
}
|
|
1606
2321
|
if (path === "/api/files" && req.method === "GET") {
|
|
1607
2322
|
const currentFiles = getChangedFiles();
|
|
1608
|
-
state = reconcileState(state, currentFiles);
|
|
2323
|
+
state = reconcileState(state, currentFiles, hashFile);
|
|
1609
2324
|
saveState(state);
|
|
1610
2325
|
const filesWithReview = currentFiles.map((f) => ({
|
|
1611
2326
|
...f,
|
|
@@ -1622,7 +2337,8 @@ function startServer(port) {
|
|
|
1622
2337
|
hasFeedback,
|
|
1623
2338
|
pending: currentFiles.length - reviewed - hasFeedback
|
|
1624
2339
|
},
|
|
1625
|
-
projectName
|
|
2340
|
+
projectName,
|
|
2341
|
+
round: state.round
|
|
1626
2342
|
});
|
|
1627
2343
|
return;
|
|
1628
2344
|
}
|
|
@@ -1664,10 +2380,12 @@ function startServer(port) {
|
|
|
1664
2380
|
if (path === "/api/review" && req.method === "POST") {
|
|
1665
2381
|
const body = JSON.parse(await parseBody(req));
|
|
1666
2382
|
const { path: filePath, status, comments } = body;
|
|
2383
|
+
const existing = state.files[filePath];
|
|
1667
2384
|
state.files[filePath] = {
|
|
1668
2385
|
status,
|
|
1669
2386
|
hash: hashFile(filePath),
|
|
1670
2387
|
comments: comments || [],
|
|
2388
|
+
archivedComments: existing?.archivedComments || [],
|
|
1671
2389
|
changedSinceReview: false
|
|
1672
2390
|
};
|
|
1673
2391
|
saveState(state);
|