diffback-review 1.2.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 +40 -13
- package/dist/cli.js +850 -158
- 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}`);
|
|
@@ -283,8 +321,9 @@ function startServer(port) {
|
|
|
283
321
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/base16/solarized-dark.min.css">
|
|
284
322
|
<script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
|
285
323
|
<style>
|
|
286
|
-
/* Solarized Dark */
|
|
287
324
|
:root {
|
|
325
|
+
--sidebar-width: 280px;
|
|
326
|
+
/* Theme colors - overridden by theme classes */
|
|
288
327
|
--bg: #002b36;
|
|
289
328
|
--bg-secondary: #073642;
|
|
290
329
|
--bg-tertiary: #0a4050;
|
|
@@ -300,8 +339,133 @@ function startServer(port) {
|
|
|
300
339
|
--red: #dc322f;
|
|
301
340
|
--magenta: #d33682;
|
|
302
341
|
--violet: #6c71c4;
|
|
303
|
-
|
|
304
|
-
|
|
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; }
|
|
305
469
|
|
|
306
470
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
307
471
|
|
|
@@ -319,8 +483,8 @@ function startServer(port) {
|
|
|
319
483
|
.toolbar {
|
|
320
484
|
display: flex;
|
|
321
485
|
align-items: center;
|
|
322
|
-
gap:
|
|
323
|
-
padding:
|
|
486
|
+
gap: 8px;
|
|
487
|
+
padding: 6px 12px;
|
|
324
488
|
background: var(--bg-secondary);
|
|
325
489
|
border-bottom: 1px solid var(--border);
|
|
326
490
|
flex-shrink: 0;
|
|
@@ -329,11 +493,22 @@ function startServer(port) {
|
|
|
329
493
|
font-weight: 600;
|
|
330
494
|
font-size: 14px;
|
|
331
495
|
color: var(--text-bright);
|
|
332
|
-
|
|
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;
|
|
333
507
|
}
|
|
334
508
|
.toolbar-stats {
|
|
335
509
|
font-size: 13px;
|
|
336
510
|
color: var(--text-muted);
|
|
511
|
+
white-space: nowrap;
|
|
337
512
|
}
|
|
338
513
|
.toolbar-stats .count { color: var(--cyan); font-weight: 600; }
|
|
339
514
|
.btn {
|
|
@@ -347,6 +522,17 @@ function startServer(port) {
|
|
|
347
522
|
font-weight: 500;
|
|
348
523
|
}
|
|
349
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
|
+
}
|
|
350
536
|
.btn-primary {
|
|
351
537
|
background: var(--accent);
|
|
352
538
|
color: var(--bg);
|
|
@@ -359,6 +545,7 @@ function startServer(port) {
|
|
|
359
545
|
color: var(--orange);
|
|
360
546
|
}
|
|
361
547
|
.btn-danger:hover { background: var(--orange); color: var(--bg); }
|
|
548
|
+
.btn { white-space: nowrap; }
|
|
362
549
|
|
|
363
550
|
/* Main layout */
|
|
364
551
|
.main {
|
|
@@ -370,14 +557,30 @@ function startServer(port) {
|
|
|
370
557
|
/* Sidebar */
|
|
371
558
|
.sidebar {
|
|
372
559
|
width: var(--sidebar-width);
|
|
560
|
+
min-width: 180px;
|
|
561
|
+
max-width: 600px;
|
|
373
562
|
border-right: 1px solid var(--border);
|
|
374
563
|
display: flex;
|
|
375
564
|
flex-direction: column;
|
|
376
565
|
flex-shrink: 0;
|
|
377
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);
|
|
378
581
|
}
|
|
379
582
|
.sidebar-header {
|
|
380
|
-
padding:
|
|
583
|
+
padding: 6px 12px;
|
|
381
584
|
font-size: 12px;
|
|
382
585
|
font-weight: 600;
|
|
383
586
|
color: var(--text-muted);
|
|
@@ -385,7 +588,29 @@ function startServer(port) {
|
|
|
385
588
|
letter-spacing: 0.5px;
|
|
386
589
|
border-bottom: 1px solid var(--border);
|
|
387
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;
|
|
388
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; }
|
|
389
614
|
.file-list {
|
|
390
615
|
flex: 1;
|
|
391
616
|
overflow-y: auto;
|
|
@@ -424,16 +649,35 @@ function startServer(port) {
|
|
|
424
649
|
font-size: 12px;
|
|
425
650
|
}
|
|
426
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); }
|
|
427
661
|
|
|
428
662
|
/* Shortcuts legend in sidebar */
|
|
429
663
|
.shortcuts-legend {
|
|
430
|
-
padding:
|
|
664
|
+
padding: 0;
|
|
431
665
|
border-top: 1px solid var(--border);
|
|
432
666
|
background: var(--bg-secondary);
|
|
433
667
|
font-size: 11px;
|
|
434
668
|
color: var(--text-muted);
|
|
435
669
|
flex-shrink: 0;
|
|
436
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
|
+
}
|
|
437
681
|
.shortcuts-legend div {
|
|
438
682
|
display: flex;
|
|
439
683
|
justify-content: space-between;
|
|
@@ -468,6 +712,7 @@ function startServer(port) {
|
|
|
468
712
|
border-bottom: 1px solid var(--border);
|
|
469
713
|
flex-shrink: 0;
|
|
470
714
|
z-index: 10;
|
|
715
|
+
overflow-x: auto;
|
|
471
716
|
}
|
|
472
717
|
.file-nav-btn {
|
|
473
718
|
background: none;
|
|
@@ -589,8 +834,7 @@ function startServer(port) {
|
|
|
589
834
|
gap: 8px;
|
|
590
835
|
align-items: flex-start;
|
|
591
836
|
}
|
|
592
|
-
.add-comment input[type="
|
|
593
|
-
width: 60px;
|
|
837
|
+
.add-comment input[type="text"] {
|
|
594
838
|
padding: 6px 8px;
|
|
595
839
|
background: var(--bg);
|
|
596
840
|
border: 1px solid var(--border);
|
|
@@ -624,6 +868,79 @@ function startServer(port) {
|
|
|
624
868
|
margin-top: 4px;
|
|
625
869
|
display: inline-block;
|
|
626
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
|
+
}
|
|
627
944
|
.suggestion-input {
|
|
628
945
|
margin-top: 4px;
|
|
629
946
|
}
|
|
@@ -720,11 +1037,13 @@ function startServer(port) {
|
|
|
720
1037
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
721
1038
|
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
|
722
1039
|
|
|
723
|
-
/* Line click hint */
|
|
724
|
-
.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; }
|
|
725
1042
|
.d2h-code-linenumber:hover {
|
|
726
1043
|
background: rgba(38, 139, 210, 0.25) !important;
|
|
727
1044
|
}
|
|
1045
|
+
/* Code content is freely selectable */
|
|
1046
|
+
.d2h-code-line-ctn { cursor: text; user-select: text; }
|
|
728
1047
|
|
|
729
1048
|
/* ---- diff2html Solarized Dark overrides ---- */
|
|
730
1049
|
.d2h-file-header, .d2h-file-wrapper {
|
|
@@ -749,51 +1068,51 @@ function startServer(port) {
|
|
|
749
1068
|
}
|
|
750
1069
|
/* Default text color for non-highlighted code */
|
|
751
1070
|
.d2h-code-line-ctn, .d2h-code-line-ctn * {
|
|
752
|
-
color:
|
|
1071
|
+
color: var(--text);
|
|
753
1072
|
}
|
|
754
|
-
/*
|
|
1073
|
+
/* Syntax highlighting via CSS variables (theme-aware) */
|
|
755
1074
|
.d2h-code-line-ctn .hljs-keyword,
|
|
756
1075
|
.d2h-code-line-ctn .hljs-selector-tag,
|
|
757
|
-
.d2h-code-line-ctn .hljs-deletion { color:
|
|
1076
|
+
.d2h-code-line-ctn .hljs-deletion { color: var(--hl-keyword) !important; }
|
|
758
1077
|
.d2h-code-line-ctn .hljs-string,
|
|
759
|
-
.d2h-code-line-ctn .hljs-addition { color:
|
|
1078
|
+
.d2h-code-line-ctn .hljs-addition { color: var(--hl-string) !important; }
|
|
760
1079
|
.d2h-code-line-ctn .hljs-built_in,
|
|
761
|
-
.d2h-code-line-ctn .hljs-type { color:
|
|
1080
|
+
.d2h-code-line-ctn .hljs-type { color: var(--hl-type) !important; }
|
|
762
1081
|
.d2h-code-line-ctn .hljs-function,
|
|
763
|
-
.d2h-code-line-ctn .hljs-title { color:
|
|
1082
|
+
.d2h-code-line-ctn .hljs-title { color: var(--hl-function) !important; }
|
|
764
1083
|
.d2h-code-line-ctn .hljs-number,
|
|
765
|
-
.d2h-code-line-ctn .hljs-literal { color:
|
|
1084
|
+
.d2h-code-line-ctn .hljs-literal { color: var(--hl-number) !important; }
|
|
766
1085
|
.d2h-code-line-ctn .hljs-comment,
|
|
767
|
-
.d2h-code-line-ctn .hljs-quote { color:
|
|
1086
|
+
.d2h-code-line-ctn .hljs-quote { color: var(--hl-comment) !important; font-style: italic; }
|
|
768
1087
|
.d2h-code-line-ctn .hljs-attr,
|
|
769
|
-
.d2h-code-line-ctn .hljs-attribute { color:
|
|
770
|
-
.d2h-code-line-ctn .hljs-params { color:
|
|
1088
|
+
.d2h-code-line-ctn .hljs-attribute { color: var(--hl-attr) !important; }
|
|
1089
|
+
.d2h-code-line-ctn .hljs-params { color: var(--text) !important; }
|
|
771
1090
|
.d2h-code-line-ctn .hljs-meta,
|
|
772
|
-
.d2h-code-line-ctn .hljs-meta .hljs-keyword { color:
|
|
1091
|
+
.d2h-code-line-ctn .hljs-meta .hljs-keyword { color: var(--hl-meta) !important; }
|
|
773
1092
|
.d2h-code-line-ctn .hljs-class .hljs-title,
|
|
774
|
-
.d2h-code-line-ctn .hljs-title.class_ { color:
|
|
1093
|
+
.d2h-code-line-ctn .hljs-title.class_ { color: var(--hl-type) !important; }
|
|
775
1094
|
.d2h-code-line-ctn .hljs-variable,
|
|
776
|
-
.d2h-code-line-ctn .hljs-template-variable { color:
|
|
777
|
-
.d2h-code-line-ctn .hljs-regexp { color:
|
|
778
|
-
.d2h-code-line-ctn .hljs-symbol { color:
|
|
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; }
|
|
779
1098
|
.d2h-code-line-ctn .hljs-selector-class,
|
|
780
|
-
.d2h-code-line-ctn .hljs-selector-id { color:
|
|
1099
|
+
.d2h-code-line-ctn .hljs-selector-id { color: var(--hl-function) !important; }
|
|
781
1100
|
/* Added lines */
|
|
782
1101
|
.d2h-ins, .d2h-ins .d2h-code-line-ctn {
|
|
783
|
-
background:
|
|
1102
|
+
background: var(--diff-ins-bg) !important;
|
|
784
1103
|
}
|
|
785
1104
|
.d2h-ins .d2h-code-line-ctn ins,
|
|
786
1105
|
.d2h-ins.d2h-change .d2h-code-line-ctn ins {
|
|
787
|
-
background:
|
|
1106
|
+
background: var(--diff-ins-highlight) !important;
|
|
788
1107
|
text-decoration: none !important;
|
|
789
1108
|
}
|
|
790
1109
|
/* Deleted lines */
|
|
791
1110
|
.d2h-del, .d2h-del .d2h-code-line-ctn {
|
|
792
|
-
background:
|
|
1111
|
+
background: var(--diff-del-bg) !important;
|
|
793
1112
|
}
|
|
794
1113
|
.d2h-del .d2h-code-line-ctn del,
|
|
795
1114
|
.d2h-del.d2h-change .d2h-code-line-ctn del {
|
|
796
|
-
background:
|
|
1115
|
+
background: var(--diff-del-highlight) !important;
|
|
797
1116
|
text-decoration: none !important;
|
|
798
1117
|
}
|
|
799
1118
|
/* Info/hunk headers - visually hidden but in DOM for fold indicator positioning */
|
|
@@ -807,17 +1126,17 @@ function startServer(port) {
|
|
|
807
1126
|
}
|
|
808
1127
|
/* Line numbers */
|
|
809
1128
|
.d2h-code-linenumber {
|
|
810
|
-
background:
|
|
811
|
-
color:
|
|
812
|
-
border-color:
|
|
1129
|
+
background: var(--bg-secondary) !important;
|
|
1130
|
+
color: var(--text-muted) !important;
|
|
1131
|
+
border-color: var(--border) !important;
|
|
813
1132
|
}
|
|
814
1133
|
.d2h-ins .d2h-code-linenumber {
|
|
815
|
-
background:
|
|
816
|
-
color:
|
|
1134
|
+
background: var(--diff-ins-linenumber) !important;
|
|
1135
|
+
color: var(--text-muted) !important;
|
|
817
1136
|
}
|
|
818
1137
|
.d2h-del .d2h-code-linenumber {
|
|
819
|
-
background:
|
|
820
|
-
color:
|
|
1138
|
+
background: var(--diff-del-linenumber) !important;
|
|
1139
|
+
color: var(--text-muted) !important;
|
|
821
1140
|
}
|
|
822
1141
|
/* Empty placeholder cells */
|
|
823
1142
|
.d2h-code-side-emptyplaceholder, .d2h-emptyplaceholder {
|
|
@@ -890,7 +1209,25 @@ function startServer(port) {
|
|
|
890
1209
|
width: 8px;
|
|
891
1210
|
height: 8px;
|
|
892
1211
|
border-radius: 50%;
|
|
893
|
-
|
|
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;
|
|
894
1231
|
}
|
|
895
1232
|
.inline-comment-row td {
|
|
896
1233
|
padding: 0 !important;
|
|
@@ -907,7 +1244,20 @@ function startServer(port) {
|
|
|
907
1244
|
font-size: 12px;
|
|
908
1245
|
color: var(--text-bright);
|
|
909
1246
|
line-height: 1.4;
|
|
1247
|
+
display: flex;
|
|
1248
|
+
align-items: flex-start;
|
|
1249
|
+
gap: 6px;
|
|
910
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;
|
|
1259
|
+
}
|
|
1260
|
+
.inline-comment-delete:hover { color: var(--orange); }
|
|
911
1261
|
.inline-comment-bubble .inline-line-ref {
|
|
912
1262
|
color: var(--cyan);
|
|
913
1263
|
font-family: "SF Mono", "Fira Code", monospace;
|
|
@@ -930,21 +1280,43 @@ function startServer(port) {
|
|
|
930
1280
|
<body>
|
|
931
1281
|
<div class="toolbar">
|
|
932
1282
|
<div class="toolbar-title" id="toolbar-title">diffback</div>
|
|
933
|
-
<div class="toolbar-
|
|
934
|
-
<
|
|
935
|
-
|
|
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>
|
|
936
1294
|
</div>
|
|
937
1295
|
|
|
938
1296
|
<div class="main">
|
|
939
|
-
<div class="sidebar">
|
|
940
|
-
<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>
|
|
941
1308
|
<div class="file-list" id="file-list"></div>
|
|
942
1309
|
<div class="shortcuts-legend">
|
|
943
|
-
<div
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
<div
|
|
947
|
-
|
|
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>
|
|
948
1320
|
</div>
|
|
949
1321
|
</div>
|
|
950
1322
|
|
|
@@ -953,8 +1325,7 @@ function startServer(port) {
|
|
|
953
1325
|
</div>
|
|
954
1326
|
</div>
|
|
955
1327
|
|
|
956
|
-
<script>
|
|
957
|
-
// --- State ---
|
|
1328
|
+
<script> // --- State ---
|
|
958
1329
|
let appState = {
|
|
959
1330
|
files: [],
|
|
960
1331
|
generalComments: [],
|
|
@@ -962,7 +1333,9 @@ function startServer(port) {
|
|
|
962
1333
|
projectName: '',
|
|
963
1334
|
selectedFile: null,
|
|
964
1335
|
currentDiff: null,
|
|
965
|
-
fileContents: {},
|
|
1336
|
+
fileContents: {},
|
|
1337
|
+
filter: 'all',
|
|
1338
|
+
round: 1,
|
|
966
1339
|
};
|
|
967
1340
|
|
|
968
1341
|
// --- API ---
|
|
@@ -981,6 +1354,7 @@ function startServer(port) {
|
|
|
981
1354
|
appState.generalComments = data.generalComments || [];
|
|
982
1355
|
appState.summary = data.summary;
|
|
983
1356
|
appState.projectName = data.projectName;
|
|
1357
|
+
appState.round = data.round || 1;
|
|
984
1358
|
renderToolbar();
|
|
985
1359
|
renderFileList();
|
|
986
1360
|
}
|
|
@@ -1020,8 +1394,19 @@ function startServer(port) {
|
|
|
1020
1394
|
}
|
|
1021
1395
|
|
|
1022
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
|
+
|
|
1023
1408
|
function navigateFile(direction) {
|
|
1024
|
-
const files =
|
|
1409
|
+
const files = getFilteredFiles();
|
|
1025
1410
|
if (files.length === 0) return;
|
|
1026
1411
|
const currentIdx = files.findIndex(f => f.path === appState.selectedFile);
|
|
1027
1412
|
let next;
|
|
@@ -1049,7 +1434,9 @@ function startServer(port) {
|
|
|
1049
1434
|
const container = document.getElementById('file-list');
|
|
1050
1435
|
container.innerHTML = '';
|
|
1051
1436
|
|
|
1052
|
-
|
|
1437
|
+
const filtered = getFilteredFiles();
|
|
1438
|
+
|
|
1439
|
+
for (const file of filtered) {
|
|
1053
1440
|
const el = document.createElement('div');
|
|
1054
1441
|
el.className = 'file-item' + (appState.selectedFile === file.path ? ' active' : '');
|
|
1055
1442
|
el.dataset.path = file.path;
|
|
@@ -1090,11 +1477,36 @@ function startServer(port) {
|
|
|
1090
1477
|
el.appendChild(statusIcon);
|
|
1091
1478
|
el.appendChild(reviewIcon);
|
|
1092
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
|
+
|
|
1093
1492
|
el.addEventListener('click', () => loadDiff(file.path));
|
|
1094
1493
|
container.appendChild(el);
|
|
1095
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
|
+
});
|
|
1096
1500
|
}
|
|
1097
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
|
+
|
|
1098
1510
|
// --- Parse hunk ranges from @@ header ---
|
|
1099
1511
|
function parseHunkHeader(headerText) {
|
|
1100
1512
|
// @@ -oldStart,oldCount +newStart,newCount @@
|
|
@@ -1183,51 +1595,171 @@ function startServer(port) {
|
|
|
1183
1595
|
}
|
|
1184
1596
|
|
|
1185
1597
|
// --- Insert inline comment markers & bubbles ---
|
|
1186
|
-
function
|
|
1187
|
-
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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);
|
|
1195
1633
|
}
|
|
1196
1634
|
|
|
1197
|
-
// Find all line number cells and match
|
|
1198
1635
|
diffContainer.querySelectorAll('.d2h-code-linenumber').forEach(el => {
|
|
1199
1636
|
const lineNum2 = el.querySelector('.line-num2')?.textContent?.trim();
|
|
1200
1637
|
const num = parseInt(lineNum2);
|
|
1201
|
-
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;
|
|
1202
1646
|
|
|
1203
|
-
// Add marker dot
|
|
1204
1647
|
el.style.position = 'relative';
|
|
1205
|
-
const marker = document.createElement('span');
|
|
1206
|
-
marker.className = 'comment-marker';
|
|
1207
|
-
el.appendChild(marker);
|
|
1208
1648
|
|
|
1209
|
-
//
|
|
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;
|
|
1210
1671
|
const tr = el.closest('tr');
|
|
1211
1672
|
if (!tr) return;
|
|
1212
1673
|
|
|
1213
|
-
|
|
1674
|
+
// Create current comment bubbles (visible by default)
|
|
1675
|
+
const currentRows = [];
|
|
1676
|
+
for (const comment of data.current) {
|
|
1214
1677
|
const commentTr = document.createElement('tr');
|
|
1215
|
-
commentTr.className = 'inline-comment-row';
|
|
1678
|
+
commentTr.className = 'inline-comment-row inline-current';
|
|
1216
1679
|
const td = document.createElement('td');
|
|
1217
1680
|
td.colSpan = 99;
|
|
1218
1681
|
td.innerHTML = \`
|
|
1219
1682
|
<div class="inline-comment-bubble">
|
|
1220
|
-
<
|
|
1221
|
-
|
|
1222
|
-
|
|
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>
|
|
1223
1689
|
</div>
|
|
1224
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
|
+
});
|
|
1225
1706
|
commentTr.appendChild(td);
|
|
1226
|
-
|
|
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;
|
|
1227
1738
|
}
|
|
1228
1739
|
|
|
1229
|
-
//
|
|
1230
|
-
|
|
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];
|
|
1231
1763
|
});
|
|
1232
1764
|
}
|
|
1233
1765
|
|
|
@@ -1289,20 +1821,61 @@ function startServer(port) {
|
|
|
1289
1821
|
// Post-render: folds, inline comments, line click handlers
|
|
1290
1822
|
setTimeout(() => {
|
|
1291
1823
|
insertFoldIndicators(diffContainer, appState.currentDiff.diff, file.path);
|
|
1292
|
-
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
|
+
}
|
|
1293
1840
|
|
|
1841
|
+
// Click line numbers: click to select/deselect, shift+click to extend range
|
|
1842
|
+
// Use mousedown+mouseup to ignore drags
|
|
1294
1843
|
diffContainer.querySelectorAll('.d2h-code-linenumber').forEach(el => {
|
|
1295
|
-
|
|
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
|
+
|
|
1296
1857
|
const lineNum = el.querySelector('.line-num2')?.textContent?.trim()
|
|
1297
|
-
|| el.querySelector('.line-num1')?.textContent?.trim()
|
|
1298
|
-
|| el.textContent?.trim();
|
|
1858
|
+
|| el.querySelector('.line-num1')?.textContent?.trim();
|
|
1299
1859
|
const num = parseInt(lineNum);
|
|
1300
|
-
if (
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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();
|
|
1306
1879
|
}
|
|
1307
1880
|
});
|
|
1308
1881
|
});
|
|
@@ -1358,7 +1931,7 @@ function startServer(port) {
|
|
|
1358
1931
|
addComment.className = 'add-comment';
|
|
1359
1932
|
addComment.innerHTML = \`
|
|
1360
1933
|
<div class="add-comment-row">
|
|
1361
|
-
<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.">
|
|
1362
1935
|
<textarea id="comment-text" placeholder="Add a comment (line optional)..." rows="1"></textarea>
|
|
1363
1936
|
<button class="btn" id="btn-add-comment">Add</button>
|
|
1364
1937
|
</div>
|
|
@@ -1368,6 +1941,49 @@ function startServer(port) {
|
|
|
1368
1941
|
</div>
|
|
1369
1942
|
\`;
|
|
1370
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
|
+
|
|
1371
1987
|
container.appendChild(reviewSection);
|
|
1372
1988
|
|
|
1373
1989
|
// General comments section
|
|
@@ -1423,11 +2039,23 @@ function startServer(port) {
|
|
|
1423
2039
|
body.style.display = body.style.display === 'none' ? 'block' : 'none';
|
|
1424
2040
|
});
|
|
1425
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
|
+
|
|
1426
2053
|
function addFileComment() {
|
|
1427
2054
|
const text = document.getElementById('comment-text').value.trim();
|
|
1428
2055
|
if (!text) return;
|
|
1429
2056
|
|
|
1430
|
-
const
|
|
2057
|
+
const lineRaw = document.getElementById('comment-line').value.trim();
|
|
2058
|
+
const line = lineRaw || null; // Keep as string for ranges like "15-22"
|
|
1431
2059
|
const suggestion = document.getElementById('comment-suggestion')?.value.trim() || null;
|
|
1432
2060
|
|
|
1433
2061
|
const newComment = {
|
|
@@ -1583,11 +2211,12 @@ function startServer(port) {
|
|
|
1583
2211
|
// --- Keyboard shortcuts ---
|
|
1584
2212
|
document.addEventListener('keydown', (e) => {
|
|
1585
2213
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
2214
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1586
2215
|
|
|
1587
|
-
if (e.key === 'j'
|
|
2216
|
+
if (e.key === 'j') {
|
|
1588
2217
|
e.preventDefault();
|
|
1589
2218
|
navigateFile('next');
|
|
1590
|
-
} else if (e.key === 'k'
|
|
2219
|
+
} else if (e.key === 'k') {
|
|
1591
2220
|
e.preventDefault();
|
|
1592
2221
|
navigateFile('prev');
|
|
1593
2222
|
} else if (e.key === 'a') {
|
|
@@ -1621,9 +2250,69 @@ function startServer(port) {
|
|
|
1621
2250
|
}
|
|
1622
2251
|
}, 3000);
|
|
1623
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
|
+
|
|
1624
2313
|
// --- Init ---
|
|
1625
2314
|
loadFiles();
|
|
1626
|
-
|
|
2315
|
+
</script>
|
|
1627
2316
|
</body>
|
|
1628
2317
|
</html>
|
|
1629
2318
|
`);
|
|
@@ -1631,7 +2320,7 @@ function startServer(port) {
|
|
|
1631
2320
|
}
|
|
1632
2321
|
if (path === "/api/files" && req.method === "GET") {
|
|
1633
2322
|
const currentFiles = getChangedFiles();
|
|
1634
|
-
state = reconcileState(state, currentFiles);
|
|
2323
|
+
state = reconcileState(state, currentFiles, hashFile);
|
|
1635
2324
|
saveState(state);
|
|
1636
2325
|
const filesWithReview = currentFiles.map((f) => ({
|
|
1637
2326
|
...f,
|
|
@@ -1648,7 +2337,8 @@ function startServer(port) {
|
|
|
1648
2337
|
hasFeedback,
|
|
1649
2338
|
pending: currentFiles.length - reviewed - hasFeedback
|
|
1650
2339
|
},
|
|
1651
|
-
projectName
|
|
2340
|
+
projectName,
|
|
2341
|
+
round: state.round
|
|
1652
2342
|
});
|
|
1653
2343
|
return;
|
|
1654
2344
|
}
|
|
@@ -1690,10 +2380,12 @@ function startServer(port) {
|
|
|
1690
2380
|
if (path === "/api/review" && req.method === "POST") {
|
|
1691
2381
|
const body = JSON.parse(await parseBody(req));
|
|
1692
2382
|
const { path: filePath, status, comments } = body;
|
|
2383
|
+
const existing = state.files[filePath];
|
|
1693
2384
|
state.files[filePath] = {
|
|
1694
2385
|
status,
|
|
1695
2386
|
hash: hashFile(filePath),
|
|
1696
2387
|
comments: comments || [],
|
|
2388
|
+
archivedComments: existing?.archivedComments || [],
|
|
1697
2389
|
changedSinceReview: false
|
|
1698
2390
|
};
|
|
1699
2391
|
saveState(state);
|