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.
Files changed (3) hide show
  1. package/README.md +40 -13
  2. package/dist/cli.js +850 -158
  3. 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
- return JSON.parse(data);
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
- --sidebar-width: 280px;
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: 12px;
323
- padding: 8px 16px;
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
- flex: 1;
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: 8px 12px;
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: 8px 12px;
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="number"] {
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: #93a1a1;
1071
+ color: var(--text);
753
1072
  }
754
- /* Let highlight.js syntax colors take priority */
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: #859900 !important; }
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: #2aa198 !important; }
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: #b58900 !important; }
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: #268bd2 !important; }
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: #d33682 !important; }
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: #586e75 !important; font-style: italic; }
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: #b58900 !important; }
770
- .d2h-code-line-ctn .hljs-params { color: #93a1a1 !important; }
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: #cb4b16 !important; }
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: #b58900 !important; }
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: #268bd2 !important; }
777
- .d2h-code-line-ctn .hljs-regexp { color: #dc322f !important; }
778
- .d2h-code-line-ctn .hljs-symbol { color: #6c71c4 !important; }
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: #268bd2 !important; }
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: #0a3d0a !important;
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: #1a5c1a !important;
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: #3d0a0a !important;
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: #5c1a1a !important;
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: #073642 !important;
811
- color: #657b83 !important;
812
- border-color: #586e75 !important;
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: #082e08 !important;
816
- color: #839496 !important;
1134
+ background: var(--diff-ins-linenumber) !important;
1135
+ color: var(--text-muted) !important;
817
1136
  }
818
1137
  .d2h-del .d2h-code-linenumber {
819
- background: #2e0808 !important;
820
- color: #839496 !important;
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
- background: var(--yellow);
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-stats" id="toolbar-stats"></div>
934
- <button class="btn btn-primary" id="btn-generate" title="Generate feedback prompt">Generate Feedback</button>
935
- <button class="btn btn-danger" id="btn-finish" title="Finish review and clean up state">Finish Review</button>
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-header">Files</div>
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><span>Prev/Next file</span> <span><kbd>k</kbd> <kbd>j</kbd></span></div>
944
- <div><span>Viewed file</span> <kbd>a</kbd></div>
945
- <div><span>Add comment</span> <kbd>c</kbd></div>
946
- <div><span>Generate feedback</span> <kbd>g</kbd></div>
947
- <div><span>Send comment</span> <kbd>Cmd+Enter</kbd></div>
1310
+ <div class="shortcuts-toggle" id="shortcuts-toggle">
1311
+ <span>Shortcuts</span> <span id="shortcuts-arrow">&blacktriangleright;</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: {}, // cache for expanded fold content
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 = appState.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
- for (const file of appState.files) {
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 insertInlineComments(diffContainer, comments) {
1187
- const lineComments = comments.filter(c => c.line !== null);
1188
- if (lineComments.length === 0) return;
1189
-
1190
- // Group comments by line number
1191
- const byLine = {};
1192
- for (const c of lineComments) {
1193
- if (!byLine[c.line]) byLine[c.line] = [];
1194
- byLine[c.line].push(c);
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) || !byLine[num]) return;
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
- // Insert comment bubble row after this line's <tr>
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
- for (const comment of byLine[num]) {
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
- <span class="inline-line-ref">L\${comment.line}</span>
1221
- \${escapeHtml(comment.text)}
1222
- \${comment.suggestion ? \`<div class="inline-suggestion">\${escapeHtml(comment.suggestion)}</div>\` : ''}
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">&times;</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
- tr.after(commentTr);
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
- // Remove from map so we don't double-insert
1230
- delete byLine[num];
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
- el.addEventListener('click', () => {
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 (!isNaN(num)) {
1301
- const lineInput = document.getElementById('comment-line');
1302
- if (lineInput) {
1303
- lineInput.value = num;
1304
- document.getElementById('comment-text')?.focus();
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="number" id="comment-line" placeholder="Line" min="1" title="Line number (leave empty for general file comment)">
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 line = parseInt(document.getElementById('comment-line').value) || null;
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' || e.key === 'ArrowDown') {
2216
+ if (e.key === 'j') {
1588
2217
  e.preventDefault();
1589
2218
  navigateFile('next');
1590
- } else if (e.key === 'k' || e.key === 'ArrowUp') {
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
- </script>
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);