glassbox 0.1.2 → 0.1.4

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/dist/cli.js CHANGED
@@ -1,21 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/server.ts
4
- import { Hono as Hono3 } from "hono";
5
- import { serve } from "@hono/node-server";
6
- import { exec } from "child_process";
7
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
8
- import { join as join3, dirname } from "path";
9
- import { fileURLToPath } from "url";
10
-
11
- // src/routes/api.ts
12
- import { Hono } from "hono";
13
-
14
3
  // src/db/connection.ts
15
4
  import { PGlite } from "@electric-sql/pglite";
16
- import { join } from "path";
17
- import { homedir } from "os";
18
5
  import { mkdirSync } from "fs";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
19
8
  var dataDir = join(homedir(), ".glassbox", "data");
20
9
  mkdirSync(dataDir, { recursive: true });
21
10
  var db = null;
@@ -101,7 +90,7 @@ async function getReview(id) {
101
90
  }
102
91
  async function listReviews(repoPath) {
103
92
  const db2 = await getDb();
104
- if (repoPath) {
93
+ if (repoPath !== void 0 && repoPath !== "") {
105
94
  const result2 = await db2.query(
106
95
  "SELECT * FROM reviews WHERE repo_path = $1 ORDER BY created_at DESC",
107
96
  [repoPath]
@@ -266,157 +255,16 @@ async function getStaleCountsForReview(reviewId) {
266
255
  return counts;
267
256
  }
268
257
 
269
- // src/export/generate.ts
270
- import { mkdirSync as mkdirSync2, writeFileSync, readFileSync, unlinkSync, existsSync, appendFileSync } from "fs";
271
- import { join as join2 } from "path";
272
- import { homedir as homedir2 } from "os";
273
- import { execSync } from "child_process";
274
- var DISMISS_FILE = join2(homedir2(), ".glassbox", "gitignore-dismissed.json");
275
- var DISMISS_DAYS = 30;
276
- function loadDismissals() {
277
- try {
278
- return JSON.parse(readFileSync(DISMISS_FILE, "utf-8"));
279
- } catch {
280
- return {};
281
- }
282
- }
283
- function saveDismissals(data) {
284
- const dir = join2(homedir2(), ".glassbox");
285
- mkdirSync2(dir, { recursive: true });
286
- writeFileSync(DISMISS_FILE, JSON.stringify(data), "utf-8");
287
- }
288
- function isGlassboxGitignored(repoRoot) {
289
- try {
290
- execSync("git check-ignore -q .glassbox", { cwd: repoRoot, stdio: "pipe" });
291
- return true;
292
- } catch {
293
- return false;
294
- }
295
- }
296
- function shouldPromptGitignore(repoRoot) {
297
- if (isGlassboxGitignored(repoRoot)) return false;
298
- const dismissals = loadDismissals();
299
- const dismissed = dismissals[repoRoot];
300
- if (dismissed) {
301
- const daysSince = (Date.now() - dismissed) / (1e3 * 60 * 60 * 24);
302
- if (daysSince < DISMISS_DAYS) return false;
303
- }
304
- return true;
305
- }
306
- function addGlassboxToGitignore(repoRoot) {
307
- const gitignorePath = join2(repoRoot, ".gitignore");
308
- if (existsSync(gitignorePath)) {
309
- const content = readFileSync(gitignorePath, "utf-8");
310
- if (!content.endsWith("\n")) {
311
- appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
312
- } else {
313
- appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
314
- }
315
- } else {
316
- writeFileSync(gitignorePath, ".glassbox/\n", "utf-8");
317
- }
318
- }
319
- function dismissGitignorePrompt(repoRoot) {
320
- const dismissals = loadDismissals();
321
- dismissals[repoRoot] = Date.now();
322
- saveDismissals(dismissals);
323
- }
324
- function deleteReviewExport(reviewId, repoRoot) {
325
- const exportDir = join2(repoRoot, ".glassbox");
326
- const archivePath = join2(exportDir, `review-${reviewId}.md`);
327
- if (existsSync(archivePath)) unlinkSync(archivePath);
328
- }
329
- async function generateReviewExport(reviewId, repoRoot, isCurrent) {
330
- const review = await getReview(reviewId);
331
- if (!review) throw new Error("Review not found");
332
- const files = await getReviewFiles(reviewId);
333
- const annotations = await getAnnotationsForReview(reviewId);
334
- const exportDir = join2(repoRoot, ".glassbox");
335
- mkdirSync2(exportDir, { recursive: true });
336
- const byFile = {};
337
- for (const a of annotations) {
338
- if (!byFile[a.file_path]) byFile[a.file_path] = [];
339
- byFile[a.file_path].push(a);
340
- }
341
- const lines = [];
342
- lines.push("# Code Review");
343
- lines.push("");
344
- lines.push(`- **Repository**: ${review.repo_name}`);
345
- lines.push(`- **Review mode**: ${review.mode}${review.mode_args ? ` (${review.mode_args})` : ""}`);
346
- lines.push(`- **Review ID**: ${review.id}`);
347
- lines.push(`- **Date**: ${(/* @__PURE__ */ new Date()).toISOString()}`);
348
- lines.push(`- **Files reviewed**: ${files.filter((f) => f.status === "reviewed").length}/${files.length}`);
349
- lines.push(`- **Total annotations**: ${annotations.length}`);
350
- lines.push("");
351
- const categoryCounts = {};
352
- for (const a of annotations) {
353
- categoryCounts[a.category] = (categoryCounts[a.category] || 0) + 1;
354
- }
355
- if (Object.keys(categoryCounts).length > 0) {
356
- lines.push("## Annotation Summary");
357
- lines.push("");
358
- for (const [cat, count] of Object.entries(categoryCounts).sort((a, b) => b[1] - a[1])) {
359
- lines.push(`- **${cat}**: ${count}`);
360
- }
361
- lines.push("");
362
- }
363
- const rememberItems = annotations.filter((a) => a.category === "remember");
364
- if (rememberItems.length > 0) {
365
- lines.push("## Items to Remember");
366
- lines.push("");
367
- lines.push("> These annotations are flagged for long-term retention. AI tools should consider updating");
368
- lines.push("> project configuration (CLAUDE.md, .cursorrules, etc.) with these preferences/rules.");
369
- lines.push("");
370
- for (const item of rememberItems) {
371
- lines.push(`- **${item.file_path}:${item.line_number}** - ${item.content}`);
372
- }
373
- lines.push("");
374
- }
375
- lines.push("## File Annotations");
376
- lines.push("");
377
- for (const filePath of Object.keys(byFile).sort()) {
378
- const fileAnns = byFile[filePath];
379
- lines.push(`### ${filePath}`);
380
- lines.push("");
381
- for (const a of fileAnns) {
382
- lines.push(`- **Line ${a.line_number}** [${a.category}]: ${a.content}`);
383
- }
384
- lines.push("");
385
- }
386
- lines.push("---");
387
- lines.push("");
388
- lines.push("## Instructions for AI Tools");
389
- lines.push("");
390
- lines.push("When processing this code review:");
391
- lines.push("");
392
- lines.push("1. **bug** and **fix** annotations indicate code that needs to be changed. Apply the suggested fixes.");
393
- lines.push("2. **style** annotations indicate stylistic preferences. Apply them to the indicated lines and similar patterns nearby.");
394
- lines.push("3. **pattern-follow** annotations highlight good patterns. Continue using these patterns in new code.");
395
- lines.push("4. **pattern-avoid** annotations highlight anti-patterns. Refactor the indicated code and avoid the pattern elsewhere.");
396
- lines.push("5. **remember** annotations are rules/preferences to persist. Update the project's AI configuration file (e.g., CLAUDE.md) with these.");
397
- lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
398
- lines.push("");
399
- const content = lines.join("\n");
400
- const archivePath = join2(exportDir, `review-${review.id}.md`);
401
- writeFileSync(archivePath, content, "utf-8");
402
- if (isCurrent) {
403
- const latestPath = join2(exportDir, "latest-review.md");
404
- writeFileSync(latestPath, content, "utf-8");
405
- return latestPath;
406
- }
407
- return archivePath;
408
- }
409
-
410
258
  // src/git/diff.ts
411
- import { execSync as execSync2 } from "child_process";
412
- import { readFileSync as readFileSync2 } from "fs";
259
+ import { execSync } from "child_process";
260
+ import { readFileSync } from "fs";
413
261
  import { resolve } from "path";
414
262
  function git(args, cwd) {
415
263
  try {
416
- return execSync2(`git ${args}`, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
264
+ return execSync(`git ${args}`, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
417
265
  } catch (e) {
418
266
  const err = e;
419
- if (err.stdout) return err.stdout;
267
+ if (err.stdout !== void 0 && err.stdout !== "") return err.stdout;
420
268
  throw e;
421
269
  }
422
270
  }
@@ -425,7 +273,7 @@ function getRepoRoot(cwd) {
425
273
  }
426
274
  function getRepoName(cwd) {
427
275
  const root = getRepoRoot(cwd);
428
- return root.split("/").pop() || "unknown";
276
+ return root.split("/").pop() ?? "unknown";
429
277
  }
430
278
  function isGitRepo(cwd) {
431
279
  try {
@@ -488,7 +336,7 @@ function getAllFiles(repoRoot) {
488
336
  function createNewFileDiff(filePath, repoRoot) {
489
337
  let content;
490
338
  try {
491
- const buf = readFileSync2(resolve(repoRoot, filePath));
339
+ const buf = readFileSync(resolve(repoRoot, filePath));
492
340
  const checkLen = Math.min(buf.length, 8192);
493
341
  for (let i = 0; i < checkLen; i++) {
494
342
  if (buf[i] === 0) {
@@ -547,7 +395,7 @@ function parseDiff(raw2) {
547
395
  let status = "modified";
548
396
  if (header.includes("new file mode")) status = "added";
549
397
  else if (header.includes("deleted file mode")) status = "deleted";
550
- else if (oldPath) status = "renamed";
398
+ else if (oldPath !== null) status = "renamed";
551
399
  const isBinary = header.includes("Binary file");
552
400
  if (isBinary) {
553
401
  files.push({ filePath, oldPath, status, hunks: [], isBinary: true });
@@ -564,12 +412,13 @@ function parseHunks(raw2) {
564
412
  let match;
565
413
  const hunkStarts = [];
566
414
  while ((match = hunkRegex.exec(raw2)) !== null) {
415
+ const groups = match;
567
416
  hunkStarts.push({
568
417
  index: match.index + match[0].length,
569
418
  oldStart: parseInt(match[1], 10),
570
- oldCount: match[2] != null ? parseInt(match[2], 10) : 1,
419
+ oldCount: groups[2] !== void 0 ? parseInt(groups[2], 10) : 1,
571
420
  newStart: parseInt(match[3], 10),
572
- newCount: match[4] != null ? parseInt(match[4], 10) : 1
421
+ newCount: groups[4] !== void 0 ? parseInt(groups[4], 10) : 1
573
422
  });
574
423
  }
575
424
  for (let i = 0; i < hunkStarts.length; i++) {
@@ -608,7 +457,7 @@ function getFileContent(filePath, ref, cwd) {
608
457
  const repoRoot = getRepoRoot(cwd);
609
458
  try {
610
459
  if (ref === "working") {
611
- return execSync2(`cat "${resolve(repoRoot, filePath)}"`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
460
+ return execSync(`cat "${resolve(repoRoot, filePath)}"`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
612
461
  }
613
462
  return git(`show ${ref}:${filePath}`, repoRoot);
614
463
  } catch {
@@ -616,7 +465,7 @@ function getFileContent(filePath, ref, cwd) {
616
465
  }
617
466
  }
618
467
  function getHeadCommit(cwd) {
619
- return execSync2("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
468
+ return execSync("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
620
469
  }
621
470
  function getModeString(mode) {
622
471
  switch (mode.type) {
@@ -648,11 +497,269 @@ function getModeArgs(mode) {
648
497
  return mode.name;
649
498
  case "files":
650
499
  return mode.patterns.join(",");
651
- default:
500
+ case "uncommitted":
501
+ case "staged":
502
+ case "unstaged":
503
+ case "all":
652
504
  return void 0;
653
505
  }
654
506
  }
655
507
 
508
+ // src/review-update.ts
509
+ function findLineContent(diff, lineNumber, side) {
510
+ for (const hunk of diff.hunks) {
511
+ for (const line of hunk.lines) {
512
+ if (side === "old" && line.oldNum === lineNumber) return line.content;
513
+ if (side === "new" && line.newNum === lineNumber) return line.content;
514
+ }
515
+ }
516
+ return null;
517
+ }
518
+ function findMatchingLine(diff, content, origLineNum, side, radius = 10) {
519
+ let bestMatch = null;
520
+ let bestDistance = Infinity;
521
+ for (const hunk of diff.hunks) {
522
+ for (const line of hunk.lines) {
523
+ if (line.content !== content) continue;
524
+ const lineNum = side === "old" ? line.oldNum : line.newNum;
525
+ if (lineNum == null) continue;
526
+ const distance = Math.abs(lineNum - origLineNum);
527
+ if (distance <= radius && distance < bestDistance) {
528
+ bestDistance = distance;
529
+ bestMatch = { lineNumber: lineNum, side };
530
+ }
531
+ }
532
+ }
533
+ return bestMatch;
534
+ }
535
+ async function migrateAnnotations(annotations, oldDiff, newDiff) {
536
+ let staleCount = 0;
537
+ for (const annotation of annotations) {
538
+ const oldContent = findLineContent(oldDiff, annotation.line_number, annotation.side);
539
+ if (oldContent === null) {
540
+ if (!annotation.is_stale) {
541
+ await markAnnotationStale(annotation.id, null);
542
+ staleCount++;
543
+ }
544
+ continue;
545
+ }
546
+ const match = findMatchingLine(newDiff, oldContent, annotation.line_number, annotation.side);
547
+ if (match) {
548
+ if (match.lineNumber !== annotation.line_number || match.side !== annotation.side) {
549
+ await moveAnnotation(annotation.id, match.lineNumber, match.side);
550
+ } else if (annotation.is_stale) {
551
+ await markAnnotationCurrent(annotation.id);
552
+ }
553
+ } else {
554
+ if (!annotation.is_stale) {
555
+ await markAnnotationStale(annotation.id, oldContent);
556
+ staleCount++;
557
+ }
558
+ }
559
+ }
560
+ return staleCount;
561
+ }
562
+ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
563
+ const existingFiles = await getReviewFiles(reviewId);
564
+ const existingByPath = /* @__PURE__ */ new Map();
565
+ for (const f of existingFiles) {
566
+ existingByPath.set(f.file_path, f);
567
+ }
568
+ const newDiffsByPath = /* @__PURE__ */ new Map();
569
+ for (const d of newDiffs) {
570
+ newDiffsByPath.set(d.filePath, d);
571
+ }
572
+ let updated = 0;
573
+ let added = 0;
574
+ let stale = 0;
575
+ for (const [path, existingFile] of existingByPath) {
576
+ const newDiff = newDiffsByPath.get(path);
577
+ if (newDiff) {
578
+ const oldDiff = JSON.parse(existingFile.diff_data ?? "{}");
579
+ const annotations = await getAnnotationsForFile(existingFile.id);
580
+ if (annotations.length > 0) {
581
+ stale += await migrateAnnotations(annotations, oldDiff, newDiff);
582
+ }
583
+ await updateFileDiff(existingFile.id, JSON.stringify(newDiff));
584
+ updated++;
585
+ } else {
586
+ const annotations = await getAnnotationsForFile(existingFile.id);
587
+ if (annotations.length === 0) {
588
+ await deleteReviewFile(existingFile.id);
589
+ } else {
590
+ const oldDiff = JSON.parse(existingFile.diff_data ?? "{}");
591
+ for (const a of annotations) {
592
+ if (!a.is_stale) {
593
+ const content = findLineContent(oldDiff, a.line_number, a.side);
594
+ await markAnnotationStale(a.id, content);
595
+ stale++;
596
+ }
597
+ }
598
+ }
599
+ }
600
+ }
601
+ for (const [path, diff] of newDiffsByPath) {
602
+ if (!existingByPath.has(path)) {
603
+ await addReviewFile(reviewId, path, JSON.stringify(diff));
604
+ added++;
605
+ }
606
+ }
607
+ await updateReviewHead(reviewId, headCommit);
608
+ return { updated, added, stale };
609
+ }
610
+
611
+ // src/server.ts
612
+ import { serve } from "@hono/node-server";
613
+ import { exec } from "child_process";
614
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
615
+ import { Hono as Hono3 } from "hono";
616
+ import { dirname, join as join3 } from "path";
617
+ import { fileURLToPath } from "url";
618
+
619
+ // src/routes/api.ts
620
+ import { Hono } from "hono";
621
+
622
+ // src/export/generate.ts
623
+ import { execSync as execSync2 } from "child_process";
624
+ import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
625
+ import { homedir as homedir2 } from "os";
626
+ import { join as join2 } from "path";
627
+ var DISMISS_FILE = join2(homedir2(), ".glassbox", "gitignore-dismissed.json");
628
+ var DISMISS_DAYS = 30;
629
+ function loadDismissals() {
630
+ try {
631
+ return JSON.parse(readFileSync2(DISMISS_FILE, "utf-8"));
632
+ } catch {
633
+ return {};
634
+ }
635
+ }
636
+ function saveDismissals(data) {
637
+ const dir = join2(homedir2(), ".glassbox");
638
+ mkdirSync2(dir, { recursive: true });
639
+ writeFileSync(DISMISS_FILE, JSON.stringify(data), "utf-8");
640
+ }
641
+ function isGlassboxGitignored(repoRoot) {
642
+ try {
643
+ execSync2("git check-ignore -q .glassbox", { cwd: repoRoot, stdio: "pipe" });
644
+ return true;
645
+ } catch {
646
+ return false;
647
+ }
648
+ }
649
+ function shouldPromptGitignore(repoRoot) {
650
+ if (isGlassboxGitignored(repoRoot)) return false;
651
+ const dismissals = loadDismissals();
652
+ const dismissed = dismissals[repoRoot];
653
+ if (dismissed) {
654
+ const daysSince = (Date.now() - dismissed) / (1e3 * 60 * 60 * 24);
655
+ if (daysSince < DISMISS_DAYS) return false;
656
+ }
657
+ return true;
658
+ }
659
+ function addGlassboxToGitignore(repoRoot) {
660
+ const gitignorePath = join2(repoRoot, ".gitignore");
661
+ if (existsSync(gitignorePath)) {
662
+ const content = readFileSync2(gitignorePath, "utf-8");
663
+ if (!content.endsWith("\n")) {
664
+ appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
665
+ } else {
666
+ appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
667
+ }
668
+ } else {
669
+ writeFileSync(gitignorePath, ".glassbox/\n", "utf-8");
670
+ }
671
+ }
672
+ function dismissGitignorePrompt(repoRoot) {
673
+ const dismissals = loadDismissals();
674
+ dismissals[repoRoot] = Date.now();
675
+ saveDismissals(dismissals);
676
+ }
677
+ function deleteReviewExport(reviewId, repoRoot) {
678
+ const exportDir = join2(repoRoot, ".glassbox");
679
+ const archivePath = join2(exportDir, `review-${reviewId}.md`);
680
+ if (existsSync(archivePath)) unlinkSync(archivePath);
681
+ }
682
+ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
683
+ const review = await getReview(reviewId);
684
+ if (!review) throw new Error("Review not found");
685
+ const files = await getReviewFiles(reviewId);
686
+ const annotations = await getAnnotationsForReview(reviewId);
687
+ const exportDir = join2(repoRoot, ".glassbox");
688
+ mkdirSync2(exportDir, { recursive: true });
689
+ const byFile = {};
690
+ for (const a of annotations) {
691
+ if (!(a.file_path in byFile)) byFile[a.file_path] = [];
692
+ byFile[a.file_path].push(a);
693
+ }
694
+ const lines = [];
695
+ lines.push("# Code Review");
696
+ lines.push("");
697
+ lines.push(`- **Repository**: ${review.repo_name}`);
698
+ lines.push(`- **Review mode**: ${review.mode}${review.mode_args !== null && review.mode_args !== "" ? ` (${review.mode_args})` : ""}`);
699
+ lines.push(`- **Review ID**: ${review.id}`);
700
+ lines.push(`- **Date**: ${(/* @__PURE__ */ new Date()).toISOString()}`);
701
+ lines.push(`- **Files reviewed**: ${files.filter((f) => f.status === "reviewed").length}/${files.length}`);
702
+ lines.push(`- **Total annotations**: ${annotations.length}`);
703
+ lines.push("");
704
+ const categoryCounts = {};
705
+ for (const a of annotations) {
706
+ categoryCounts[a.category] = (categoryCounts[a.category] || 0) + 1;
707
+ }
708
+ if (Object.keys(categoryCounts).length > 0) {
709
+ lines.push("## Annotation Summary");
710
+ lines.push("");
711
+ for (const [cat, count] of Object.entries(categoryCounts).sort((a, b) => b[1] - a[1])) {
712
+ lines.push(`- **${cat}**: ${count}`);
713
+ }
714
+ lines.push("");
715
+ }
716
+ const rememberItems = annotations.filter((a) => a.category === "remember");
717
+ if (rememberItems.length > 0) {
718
+ lines.push("## Items to Remember");
719
+ lines.push("");
720
+ lines.push("> These annotations are flagged for long-term retention. AI tools should consider updating");
721
+ lines.push("> project configuration (CLAUDE.md, .cursorrules, etc.) with these preferences/rules.");
722
+ lines.push("");
723
+ for (const item of rememberItems) {
724
+ lines.push(`- **${item.file_path}:${item.line_number}** - ${item.content}`);
725
+ }
726
+ lines.push("");
727
+ }
728
+ lines.push("## File Annotations");
729
+ lines.push("");
730
+ for (const filePath of Object.keys(byFile).sort()) {
731
+ const fileAnns = byFile[filePath];
732
+ lines.push(`### ${filePath}`);
733
+ lines.push("");
734
+ for (const a of fileAnns) {
735
+ lines.push(`- **Line ${a.line_number}** [${a.category}]: ${a.content}`);
736
+ }
737
+ lines.push("");
738
+ }
739
+ lines.push("---");
740
+ lines.push("");
741
+ lines.push("## Instructions for AI Tools");
742
+ lines.push("");
743
+ lines.push("When processing this code review:");
744
+ lines.push("");
745
+ lines.push("1. **bug** and **fix** annotations indicate code that needs to be changed. Apply the suggested fixes.");
746
+ lines.push("2. **style** annotations indicate stylistic preferences. Apply them to the indicated lines and similar patterns nearby.");
747
+ lines.push("3. **pattern-follow** annotations highlight good patterns. Continue using these patterns in new code.");
748
+ lines.push("4. **pattern-avoid** annotations highlight anti-patterns. Refactor the indicated code and avoid the pattern elsewhere.");
749
+ lines.push("5. **remember** annotations are rules/preferences to persist. Update the project's AI configuration file (e.g., CLAUDE.md) with these.");
750
+ lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
751
+ lines.push("");
752
+ const content = lines.join("\n");
753
+ const archivePath = join2(exportDir, `review-${review.id}.md`);
754
+ writeFileSync(archivePath, content, "utf-8");
755
+ if (isCurrent) {
756
+ const latestPath = join2(exportDir, "latest-review.md");
757
+ writeFileSync(latestPath, content, "utf-8");
758
+ return latestPath;
759
+ }
760
+ return archivePath;
761
+ }
762
+
656
763
  // src/outline/parser.ts
657
764
  var BRACE_LANGS = /* @__PURE__ */ new Set([
658
765
  "javascript",
@@ -715,7 +822,7 @@ function langFromPath(filePath) {
715
822
  }
716
823
  function parseOutline(content, filePath) {
717
824
  const lang = langFromPath(filePath);
718
- if (!lang) return [];
825
+ if (lang === null) return [];
719
826
  if (BRACE_LANGS.has(lang)) return parseBraces(content, lang);
720
827
  if (INDENT_LANGS.has(lang)) return parseIndent(content, lang);
721
828
  return [];
@@ -809,7 +916,6 @@ function parseBraces(content, lang) {
809
916
  let braceDepth = 0;
810
917
  let inString = false;
811
918
  let stringChar = "";
812
- let inLineComment = false;
813
919
  let inBlockComment = false;
814
920
  let inTemplateLiteral = false;
815
921
  const { top: topFuncPatterns, method: methodFuncPatterns } = getFuncPatterns(lang);
@@ -848,7 +954,6 @@ function parseBraces(content, lang) {
848
954
  for (let j = 0; j < line.length; j++) {
849
955
  const ch = line[j];
850
956
  const next = line[j + 1];
851
- if (inLineComment) break;
852
957
  if (inBlockComment) {
853
958
  if (ch === "*" && next === "/") {
854
959
  inBlockComment = false;
@@ -877,7 +982,6 @@ function parseBraces(content, lang) {
877
982
  continue;
878
983
  }
879
984
  if (ch === "/" && next === "/") {
880
- inLineComment = true;
881
985
  break;
882
986
  }
883
987
  if (ch === "/" && next === "*") {
@@ -899,11 +1003,12 @@ function parseBraces(content, lang) {
899
1003
  braceDepth--;
900
1004
  while (stack.length > 0 && stack[stack.length - 1].depth >= braceDepth) {
901
1005
  const closed = stack.pop();
902
- closed.symbol.endLine = lineNum;
1006
+ if (closed !== void 0) {
1007
+ closed.symbol.endLine = lineNum;
1008
+ }
903
1009
  }
904
1010
  }
905
1011
  }
906
- inLineComment = false;
907
1012
  }
908
1013
  const lastLine = lines.length;
909
1014
  for (const item of stack) {
@@ -950,7 +1055,9 @@ function parseIndent(content, lang) {
950
1055
  function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
951
1056
  while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
952
1057
  const closed = stack.pop();
953
- closed.symbol.endLine = lineIdx;
1058
+ if (closed !== void 0) {
1059
+ closed.symbol.endLine = lineIdx;
1060
+ }
954
1061
  }
955
1062
  let endLine = lines.length;
956
1063
  for (let j = lineIdx + 1; j < lines.length; j++) {
@@ -974,7 +1081,7 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
974
1081
  // src/routes/api.ts
975
1082
  var apiRoutes = new Hono();
976
1083
  function resolveReviewId(c) {
977
- return c.req.query("reviewId") || c.get("reviewId");
1084
+ return c.req.query("reviewId") ?? c.get("reviewId");
978
1085
  }
979
1086
  apiRoutes.get("/reviews", async (c) => {
980
1087
  const repoRoot = c.get("repoRoot");
@@ -996,12 +1103,12 @@ apiRoutes.post("/review/complete", async (c) => {
996
1103
  const gitignorePrompt = shouldPromptGitignore(repoRoot);
997
1104
  return c.json({ status: "completed", exportPath, isCurrent, reviewId, gitignorePrompt });
998
1105
  });
999
- apiRoutes.post("/gitignore/add", async (c) => {
1106
+ apiRoutes.post("/gitignore/add", (c) => {
1000
1107
  const repoRoot = c.get("repoRoot");
1001
1108
  addGlassboxToGitignore(repoRoot);
1002
1109
  return c.json({ ok: true });
1003
1110
  });
1004
- apiRoutes.post("/gitignore/dismiss", async (c) => {
1111
+ apiRoutes.post("/gitignore/dismiss", (c) => {
1005
1112
  const repoRoot = c.get("repoRoot");
1006
1113
  dismissGitignorePrompt(repoRoot);
1007
1114
  return c.json({ ok: true });
@@ -1114,7 +1221,7 @@ apiRoutes.get("/outline/:fileId", async (c) => {
1114
1221
  const repoRoot = c.get("repoRoot");
1115
1222
  const file = await getReviewFile(c.req.param("fileId"));
1116
1223
  if (!file) return c.json({ error: "Not found" }, 404);
1117
- const diff = JSON.parse(file.diff_data || "{}");
1224
+ const diff = JSON.parse(file.diff_data ?? "{}");
1118
1225
  const isDeleted = diff.status === "deleted";
1119
1226
  let content = "";
1120
1227
  try {
@@ -1133,8 +1240,8 @@ apiRoutes.get("/context/:fileId", async (c) => {
1133
1240
  const repoRoot = c.get("repoRoot");
1134
1241
  const file = await getReviewFile(c.req.param("fileId"));
1135
1242
  if (!file) return c.json({ error: "Not found" }, 404);
1136
- const startLine = parseInt(c.req.query("start") || "1", 10);
1137
- const endLine = parseInt(c.req.query("end") || "20", 10);
1243
+ const startLine = parseInt(c.req.query("start") ?? "1", 10);
1244
+ const endLine = parseInt(c.req.query("end") ?? "20", 10);
1138
1245
  const content = getFileContent(file.file_path, "working", repoRoot);
1139
1246
  const allLines = content.split("\n");
1140
1247
  const clampedStart = Math.max(1, startLine);
@@ -1218,21 +1325,196 @@ function jsx(tag, props) {
1218
1325
  return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);
1219
1326
  }
1220
1327
 
1221
- // src/components/layout.tsx
1222
- function Layout({ title, reviewId, children }) {
1223
- return /* @__PURE__ */ jsx("html", { lang: "en", children: [
1224
- /* @__PURE__ */ jsx("head", { children: [
1225
- /* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
1226
- /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
1227
- /* @__PURE__ */ jsx("title", { children: title }),
1228
- /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "/static/styles.css" })
1328
+ // src/components/diffView.tsx
1329
+ function DiffView({ file, diff, annotations, mode }) {
1330
+ const annotationsByLine = {};
1331
+ for (const a of annotations) {
1332
+ const key = `${a.line_number}:${a.side}`;
1333
+ if (!(key in annotationsByLine)) annotationsByLine[key] = [];
1334
+ annotationsByLine[key].push(a);
1335
+ }
1336
+ return /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, children: [
1337
+ /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
1338
+ /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
1339
+ /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
1229
1340
  ] }),
1230
- /* @__PURE__ */ jsx("body", { "data-review-id": reviewId, children: [
1231
- children,
1232
- /* @__PURE__ */ jsx("script", { src: "/static/app.js" })
1233
- ] })
1341
+ diff.isBinary ? /* @__PURE__ */ jsx("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx(UnifiedDiff, { hunks: diff.hunks, annotationsByLine }) : /* @__PURE__ */ jsx(SplitDiff, { hunks: diff.hunks, annotationsByLine })
1342
+ ] });
1343
+ }
1344
+ function SplitDiff({ hunks, annotationsByLine }) {
1345
+ const lastHunk = hunks[hunks.length - 1];
1346
+ const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
1347
+ return /* @__PURE__ */ jsx("div", { className: "diff-table-split", children: [
1348
+ hunks.map((hunk, hunkIdx) => {
1349
+ const pairs = pairLines(hunk.lines);
1350
+ return /* @__PURE__ */ jsx("div", { className: "hunk-block", children: [
1351
+ /* @__PURE__ */ jsx(
1352
+ "div",
1353
+ {
1354
+ className: "hunk-separator",
1355
+ "data-hunk-idx": hunkIdx,
1356
+ "data-old-start": hunk.oldStart,
1357
+ "data-old-count": hunk.oldCount,
1358
+ "data-new-start": hunk.newStart,
1359
+ "data-new-count": hunk.newCount,
1360
+ children: [
1361
+ "@@ -",
1362
+ hunk.oldStart,
1363
+ ",",
1364
+ hunk.oldCount,
1365
+ " +",
1366
+ hunk.newStart,
1367
+ ",",
1368
+ hunk.newCount,
1369
+ " @@"
1370
+ ]
1371
+ }
1372
+ ),
1373
+ pairs.map((pair) => {
1374
+ const leftAnns = pair.left ? annotationsByLine[`${pair.left.oldNum}:old`] ?? [] : [];
1375
+ const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] ?? [] : [];
1376
+ const allAnns = [...leftAnns, ...rightAnns];
1377
+ return /* @__PURE__ */ jsx("div", { children: [
1378
+ /* @__PURE__ */ jsx("div", { className: "split-row", children: [
1379
+ /* @__PURE__ */ jsx(
1380
+ "div",
1381
+ {
1382
+ className: `diff-line split-left ${pair.left?.type || "empty"}`,
1383
+ "data-line": pair.left?.oldNum ?? "",
1384
+ "data-side": "old",
1385
+ "data-new-line": pair.left?.newNum ?? pair.right?.newNum ?? "",
1386
+ children: [
1387
+ /* @__PURE__ */ jsx("span", { className: "gutter", children: pair.left?.oldNum ?? "" }),
1388
+ /* @__PURE__ */ jsx("span", { className: "code", children: pair.left ? raw(escapeHtml(pair.left.content)) : "" })
1389
+ ]
1390
+ }
1391
+ ),
1392
+ /* @__PURE__ */ jsx(
1393
+ "div",
1394
+ {
1395
+ className: `diff-line split-right ${pair.right?.type || "empty"}`,
1396
+ "data-line": pair.right?.newNum ?? "",
1397
+ "data-side": "new",
1398
+ children: [
1399
+ /* @__PURE__ */ jsx("span", { className: "gutter", children: pair.right?.newNum ?? "" }),
1400
+ /* @__PURE__ */ jsx("span", { className: "code", children: pair.right ? raw(escapeHtml(pair.right.content)) : "" })
1401
+ ]
1402
+ }
1403
+ )
1404
+ ] }),
1405
+ allAnns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: allAnns }) : null
1406
+ ] });
1407
+ })
1408
+ ] });
1409
+ }),
1410
+ /* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
1411
+ ] });
1412
+ }
1413
+ function pairLines(lines) {
1414
+ const pairs = [];
1415
+ let i = 0;
1416
+ while (i < lines.length) {
1417
+ const line = lines[i];
1418
+ if (line.type === "context") {
1419
+ pairs.push({ left: line, right: line });
1420
+ i++;
1421
+ } else if (line.type === "remove") {
1422
+ const removes = [];
1423
+ while (i < lines.length && lines[i].type === "remove") {
1424
+ removes.push(lines[i]);
1425
+ i++;
1426
+ }
1427
+ const adds = [];
1428
+ while (i < lines.length && lines[i].type === "add") {
1429
+ adds.push(lines[i]);
1430
+ i++;
1431
+ }
1432
+ const max = Math.max(removes.length, adds.length);
1433
+ for (let j = 0; j < max; j++) {
1434
+ pairs.push({
1435
+ left: j < removes.length ? removes[j] : null,
1436
+ right: j < adds.length ? adds[j] : null
1437
+ });
1438
+ }
1439
+ } else {
1440
+ pairs.push({ left: null, right: line });
1441
+ i++;
1442
+ }
1443
+ }
1444
+ return pairs;
1445
+ }
1446
+ function UnifiedDiff({ hunks, annotationsByLine }) {
1447
+ const lastHunk = hunks[hunks.length - 1];
1448
+ const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
1449
+ return /* @__PURE__ */ jsx("div", { className: "diff-table-unified", children: [
1450
+ hunks.map((hunk, hunkIdx) => /* @__PURE__ */ jsx("div", { className: "hunk-block", children: [
1451
+ /* @__PURE__ */ jsx(
1452
+ "div",
1453
+ {
1454
+ className: "hunk-separator",
1455
+ "data-hunk-idx": hunkIdx,
1456
+ "data-old-start": hunk.oldStart,
1457
+ "data-old-count": hunk.oldCount,
1458
+ "data-new-start": hunk.newStart,
1459
+ "data-new-count": hunk.newCount,
1460
+ children: [
1461
+ "@@ -",
1462
+ hunk.oldStart,
1463
+ ",",
1464
+ hunk.oldCount,
1465
+ " +",
1466
+ hunk.newStart,
1467
+ ",",
1468
+ hunk.newCount,
1469
+ " @@"
1470
+ ]
1471
+ }
1472
+ ),
1473
+ hunk.lines.map((line) => {
1474
+ const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
1475
+ const side = line.type === "remove" ? "old" : "new";
1476
+ const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
1477
+ return /* @__PURE__ */ jsx("div", { children: [
1478
+ /* @__PURE__ */ jsx(
1479
+ "div",
1480
+ {
1481
+ className: `diff-line ${line.type}${anns.length ? " has-annotation" : ""}`,
1482
+ "data-line": lineNum,
1483
+ "data-side": side,
1484
+ children: [
1485
+ /* @__PURE__ */ jsx("span", { className: "gutter-old", children: line.oldNum ?? "" }),
1486
+ /* @__PURE__ */ jsx("span", { className: "gutter-new", children: line.newNum ?? "" }),
1487
+ /* @__PURE__ */ jsx("span", { className: "code", children: raw(escapeHtml(line.content)) })
1488
+ ]
1489
+ }
1490
+ ),
1491
+ anns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: anns }) : null
1492
+ ] });
1493
+ })
1494
+ ] })),
1495
+ /* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
1234
1496
  ] });
1235
1497
  }
1498
+ function AnnotationRows({ annotations }) {
1499
+ return /* @__PURE__ */ jsx("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx(
1500
+ "div",
1501
+ {
1502
+ className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
1503
+ "data-annotation-id": a.id,
1504
+ "data-is-stale": a.is_stale ? "true" : void 0,
1505
+ children: [
1506
+ /* @__PURE__ */ jsx("span", { className: "annotation-drag-handle", draggable: "true", title: "Drag to move", children: "\u283F" }),
1507
+ /* @__PURE__ */ jsx("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
1508
+ /* @__PURE__ */ jsx("span", { className: "annotation-text", children: a.content }),
1509
+ /* @__PURE__ */ jsx("div", { className: "annotation-actions", children: [
1510
+ a.is_stale ? /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
1511
+ /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon", "data-action": "edit", title: "Edit", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>') }),
1512
+ /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon btn-danger", "data-action": "delete", title: "Delete", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>') })
1513
+ ] })
1514
+ ]
1515
+ }
1516
+ )) });
1517
+ }
1236
1518
 
1237
1519
  // src/components/fileList.tsx
1238
1520
  function buildFileTree(files) {
@@ -1298,14 +1580,14 @@ function TreeView({ node, depth, annotationCounts, staleCounts }) {
1298
1580
  ] });
1299
1581
  }),
1300
1582
  node.files.map((f) => {
1301
- const diff = JSON.parse(f.diff_data || "{}");
1583
+ const diff = JSON.parse(f.diff_data ?? "{}");
1302
1584
  const count = annotationCounts[f.id] || 0;
1303
1585
  const stale = staleCounts[f.id] || 0;
1304
- const fileName = f.file_path.split("/").pop();
1586
+ const fileName = f.file_path.split("/").pop() ?? "";
1305
1587
  return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
1306
1588
  /* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
1307
1589
  /* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
1308
- /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status || ""}`, children: diff.status || "" }),
1590
+ /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status ?? ""}`, children: diff.status ?? "" }),
1309
1591
  stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
1310
1592
  count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
1311
1593
  ] });
@@ -1317,187 +1599,21 @@ function FileList({ files, annotationCounts, staleCounts }) {
1317
1599
  return /* @__PURE__ */ jsx("div", { className: "file-list", children: /* @__PURE__ */ jsx("div", { className: "file-list-items", children: /* @__PURE__ */ jsx(TreeView, { node: tree, depth: 0, annotationCounts, staleCounts }) }) });
1318
1600
  }
1319
1601
 
1320
- // src/components/diffView.tsx
1321
- function DiffView({ file, diff, annotations, mode }) {
1322
- const annotationsByLine = {};
1323
- for (const a of annotations) {
1324
- const key = `${a.line_number}:${a.side}`;
1325
- if (!annotationsByLine[key]) annotationsByLine[key] = [];
1326
- annotationsByLine[key].push(a);
1327
- }
1328
- return /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, children: [
1329
- /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
1330
- /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
1331
- /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
1602
+ // src/components/layout.tsx
1603
+ function Layout({ title, reviewId, children }) {
1604
+ return /* @__PURE__ */ jsx("html", { lang: "en", children: [
1605
+ /* @__PURE__ */ jsx("head", { children: [
1606
+ /* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
1607
+ /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
1608
+ /* @__PURE__ */ jsx("title", { children: title }),
1609
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "/static/styles.css" })
1332
1610
  ] }),
1333
- diff.isBinary ? /* @__PURE__ */ jsx("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx(UnifiedDiff, { hunks: diff.hunks, annotationsByLine }) : /* @__PURE__ */ jsx(SplitDiff, { hunks: diff.hunks, annotationsByLine })
1611
+ /* @__PURE__ */ jsx("body", { "data-review-id": reviewId, children: [
1612
+ children,
1613
+ /* @__PURE__ */ jsx("script", { src: "/static/app.js" })
1614
+ ] })
1334
1615
  ] });
1335
1616
  }
1336
- function SplitDiff({ hunks, annotationsByLine }) {
1337
- return /* @__PURE__ */ jsx("div", { className: "diff-table-split", children: hunks.map((hunk, hunkIdx) => {
1338
- const pairs = pairLines(hunk.lines);
1339
- return /* @__PURE__ */ jsx("div", { className: "hunk-block", children: [
1340
- /* @__PURE__ */ jsx(
1341
- "div",
1342
- {
1343
- className: "hunk-separator",
1344
- "data-hunk-idx": hunkIdx,
1345
- "data-old-start": hunk.oldStart,
1346
- "data-old-count": hunk.oldCount,
1347
- "data-new-start": hunk.newStart,
1348
- "data-new-count": hunk.newCount,
1349
- children: [
1350
- "@@ -",
1351
- hunk.oldStart,
1352
- ",",
1353
- hunk.oldCount,
1354
- " +",
1355
- hunk.newStart,
1356
- ",",
1357
- hunk.newCount,
1358
- " @@"
1359
- ]
1360
- }
1361
- ),
1362
- pairs.map((pair) => {
1363
- const leftAnns = pair.left ? annotationsByLine[`${pair.left.oldNum}:old`] || [] : [];
1364
- const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] || [] : [];
1365
- const allAnns = [...leftAnns, ...rightAnns];
1366
- return /* @__PURE__ */ jsx("div", { children: [
1367
- /* @__PURE__ */ jsx("div", { className: "split-row", children: [
1368
- /* @__PURE__ */ jsx(
1369
- "div",
1370
- {
1371
- className: `diff-line split-left ${pair.left?.type || "empty"}`,
1372
- "data-line": pair.left?.oldNum ?? "",
1373
- "data-side": "old",
1374
- children: [
1375
- /* @__PURE__ */ jsx("span", { className: "gutter", children: pair.left?.oldNum ?? "" }),
1376
- /* @__PURE__ */ jsx("span", { className: "code", children: pair.left ? raw(escapeHtml(pair.left.content)) : "" })
1377
- ]
1378
- }
1379
- ),
1380
- /* @__PURE__ */ jsx(
1381
- "div",
1382
- {
1383
- className: `diff-line split-right ${pair.right?.type || "empty"}`,
1384
- "data-line": pair.right?.newNum ?? "",
1385
- "data-side": "new",
1386
- children: [
1387
- /* @__PURE__ */ jsx("span", { className: "gutter", children: pair.right?.newNum ?? "" }),
1388
- /* @__PURE__ */ jsx("span", { className: "code", children: pair.right ? raw(escapeHtml(pair.right.content)) : "" })
1389
- ]
1390
- }
1391
- )
1392
- ] }),
1393
- allAnns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: allAnns }) : null
1394
- ] });
1395
- })
1396
- ] });
1397
- }) });
1398
- }
1399
- function pairLines(lines) {
1400
- const pairs = [];
1401
- let i = 0;
1402
- while (i < lines.length) {
1403
- const line = lines[i];
1404
- if (line.type === "context") {
1405
- pairs.push({ left: line, right: line });
1406
- i++;
1407
- } else if (line.type === "remove") {
1408
- const removes = [];
1409
- while (i < lines.length && lines[i].type === "remove") {
1410
- removes.push(lines[i]);
1411
- i++;
1412
- }
1413
- const adds = [];
1414
- while (i < lines.length && lines[i].type === "add") {
1415
- adds.push(lines[i]);
1416
- i++;
1417
- }
1418
- const max = Math.max(removes.length, adds.length);
1419
- for (let j = 0; j < max; j++) {
1420
- pairs.push({
1421
- left: j < removes.length ? removes[j] : null,
1422
- right: j < adds.length ? adds[j] : null
1423
- });
1424
- }
1425
- } else if (line.type === "add") {
1426
- pairs.push({ left: null, right: line });
1427
- i++;
1428
- } else {
1429
- i++;
1430
- }
1431
- }
1432
- return pairs;
1433
- }
1434
- function UnifiedDiff({ hunks, annotationsByLine }) {
1435
- return /* @__PURE__ */ jsx("div", { className: "diff-table-unified", children: hunks.map((hunk, hunkIdx) => /* @__PURE__ */ jsx("div", { className: "hunk-block", children: [
1436
- /* @__PURE__ */ jsx(
1437
- "div",
1438
- {
1439
- className: "hunk-separator",
1440
- "data-hunk-idx": hunkIdx,
1441
- "data-old-start": hunk.oldStart,
1442
- "data-old-count": hunk.oldCount,
1443
- "data-new-start": hunk.newStart,
1444
- "data-new-count": hunk.newCount,
1445
- children: [
1446
- "@@ -",
1447
- hunk.oldStart,
1448
- ",",
1449
- hunk.oldCount,
1450
- " +",
1451
- hunk.newStart,
1452
- ",",
1453
- hunk.newCount,
1454
- " @@"
1455
- ]
1456
- }
1457
- ),
1458
- hunk.lines.map((line) => {
1459
- const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
1460
- const side = line.type === "remove" ? "old" : "new";
1461
- const anns = annotationsByLine[`${lineNum}:${side}`] || [];
1462
- return /* @__PURE__ */ jsx("div", { children: [
1463
- /* @__PURE__ */ jsx(
1464
- "div",
1465
- {
1466
- className: `diff-line ${line.type}${anns.length ? " has-annotation" : ""}`,
1467
- "data-line": lineNum,
1468
- "data-side": side,
1469
- children: [
1470
- /* @__PURE__ */ jsx("span", { className: "gutter-old", children: line.oldNum ?? "" }),
1471
- /* @__PURE__ */ jsx("span", { className: "gutter-new", children: line.newNum ?? "" }),
1472
- /* @__PURE__ */ jsx("span", { className: "code", children: raw(escapeHtml(line.content)) })
1473
- ]
1474
- }
1475
- ),
1476
- anns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: anns }) : null
1477
- ] });
1478
- })
1479
- ] })) });
1480
- }
1481
- function AnnotationRows({ annotations }) {
1482
- return /* @__PURE__ */ jsx("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx(
1483
- "div",
1484
- {
1485
- className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
1486
- "data-annotation-id": a.id,
1487
- "data-is-stale": a.is_stale ? "true" : void 0,
1488
- children: [
1489
- /* @__PURE__ */ jsx("span", { className: "annotation-drag-handle", draggable: "true", title: "Drag to move", children: "\u283F" }),
1490
- /* @__PURE__ */ jsx("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
1491
- /* @__PURE__ */ jsx("span", { className: "annotation-text", children: a.content }),
1492
- /* @__PURE__ */ jsx("div", { className: "annotation-actions", children: [
1493
- a.is_stale ? /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
1494
- /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon", "data-action": "edit", title: "Edit", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>') }),
1495
- /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon btn-danger", "data-action": "delete", title: "Delete", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>') })
1496
- ] })
1497
- ]
1498
- }
1499
- )) });
1500
- }
1501
1617
 
1502
1618
  // src/components/reviewHistory.tsx
1503
1619
  function titleCase(s) {
@@ -1506,15 +1622,15 @@ function titleCase(s) {
1506
1622
  var trashIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
1507
1623
  function shortenArgs(args) {
1508
1624
  const shaPattern = /\b([0-9a-f]{7,40})\b/gi;
1509
- let hasLong = false;
1625
+ const result = { hasLong: false };
1510
1626
  const short = args.replace(shaPattern, (match) => {
1511
1627
  if (match.length > 8) {
1512
- hasLong = true;
1628
+ result.hasLong = true;
1513
1629
  return match.slice(0, 7);
1514
1630
  }
1515
1631
  return match;
1516
1632
  });
1517
- return { short, full: hasLong ? args : "" };
1633
+ return { short, full: result.hasLong ? args : "" };
1518
1634
  }
1519
1635
  function ReviewHistory({ reviews, currentReviewId }) {
1520
1636
  const hasOtherReviews = reviews.some((r) => r.id !== currentReviewId);
@@ -1525,9 +1641,9 @@ function ReviewHistory({ reviews, currentReviewId }) {
1525
1641
  const isCurrent = r.id === currentReviewId;
1526
1642
  const href = isCurrent ? "/" : `/review/${r.id}`;
1527
1643
  let argsDisplay = null;
1528
- if (r.mode_args) {
1644
+ if (r.mode_args !== null && r.mode_args !== "") {
1529
1645
  const { short, full } = shortenArgs(r.mode_args);
1530
- argsDisplay = full ? /* @__PURE__ */ jsx("span", { title: full, children: [
1646
+ argsDisplay = full !== "" ? /* @__PURE__ */ jsx("span", { title: full, children: [
1531
1647
  ": ",
1532
1648
  short
1533
1649
  ] }) : /* @__PURE__ */ jsx("span", { children: [
@@ -1660,7 +1776,7 @@ pageRoutes.get("/", async (c) => {
1660
1776
  /* @__PURE__ */ jsx("h2", { children: review.repo_name }),
1661
1777
  /* @__PURE__ */ jsx("span", { className: "review-mode", children: [
1662
1778
  review.mode,
1663
- review.mode_args ? `: ${review.mode_args}` : ""
1779
+ review.mode_args !== null && review.mode_args !== "" ? `: ${review.mode_args}` : ""
1664
1780
  ] })
1665
1781
  ] }),
1666
1782
  /* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
@@ -1701,7 +1817,7 @@ pageRoutes.get("/file/:fileId", async (c) => {
1701
1817
  const file = await getReviewFile(fileId);
1702
1818
  if (!file) return c.text("File not found", 404);
1703
1819
  const annotations = await getAnnotationsForFile(fileId);
1704
- const diff = JSON.parse(file.diff_data || "{}");
1820
+ const diff = JSON.parse(file.diff_data ?? "{}");
1705
1821
  const html = /* @__PURE__ */ jsx(DiffView, { file, diff, annotations, mode });
1706
1822
  return c.html(html.toString());
1707
1823
  });
@@ -1725,7 +1841,7 @@ pageRoutes.get("/review/:reviewId", async (c) => {
1725
1841
  /* @__PURE__ */ jsx("h2", { children: review.repo_name }),
1726
1842
  /* @__PURE__ */ jsx("span", { className: "review-mode", children: [
1727
1843
  review.mode,
1728
- review.mode_args ? `: ${review.mode_args}` : ""
1844
+ review.mode_args !== null && review.mode_args !== "" ? `: ${review.mode_args}` : ""
1729
1845
  ] })
1730
1846
  ] }),
1731
1847
  /* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
@@ -1773,7 +1889,9 @@ pageRoutes.get("/history", async (c) => {
1773
1889
  function tryServe(fetch, port) {
1774
1890
  return new Promise((resolve2, reject) => {
1775
1891
  const server = serve({ fetch, port });
1776
- server.on("listening", () => resolve2(port));
1892
+ server.on("listening", () => {
1893
+ resolve2(port);
1894
+ });
1777
1895
  server.on("error", (err) => {
1778
1896
  if (err.code === "EADDRINUSE") {
1779
1897
  reject(err);
@@ -1809,7 +1927,7 @@ async function startServer(port, reviewId, repoRoot) {
1809
1927
  actualPort = await tryServe(app.fetch, port + attempt);
1810
1928
  break;
1811
1929
  } catch (err) {
1812
- if (err.code === "EADDRINUSE" && attempt < 19) {
1930
+ if (err instanceof Error && err.code === "EADDRINUSE" && attempt < 19) {
1813
1931
  continue;
1814
1932
  }
1815
1933
  throw err;
@@ -1827,10 +1945,10 @@ async function startServer(port, reviewId, repoRoot) {
1827
1945
  }
1828
1946
 
1829
1947
  // src/update-check.ts
1830
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
1831
- import { join as join4, dirname as dirname2 } from "path";
1832
- import { homedir as homedir3 } from "os";
1948
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1833
1949
  import { get } from "https";
1950
+ import { homedir as homedir3 } from "os";
1951
+ import { dirname as dirname2, join as join4 } from "path";
1834
1952
  import { fileURLToPath as fileURLToPath2 } from "url";
1835
1953
  var DATA_DIR = join4(homedir3(), ".glassbox");
1836
1954
  var CHECK_FILE = join4(DATA_DIR, "last-update-check");
@@ -1859,7 +1977,7 @@ function saveCheckDate() {
1859
1977
  }
1860
1978
  function isFirstUseToday() {
1861
1979
  const last = getLastCheckDate();
1862
- if (!last) return true;
1980
+ if (last === null) return true;
1863
1981
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1864
1982
  return last !== today;
1865
1983
  }
@@ -1872,7 +1990,7 @@ function fetchLatestVersion() {
1872
1990
  }
1873
1991
  let data = "";
1874
1992
  res.on("data", (chunk) => {
1875
- data += chunk;
1993
+ data += chunk.toString();
1876
1994
  });
1877
1995
  res.on("end", () => {
1878
1996
  try {
@@ -1882,7 +2000,9 @@ function fetchLatestVersion() {
1882
2000
  }
1883
2001
  });
1884
2002
  });
1885
- req.on("error", () => resolve2(null));
2003
+ req.on("error", () => {
2004
+ resolve2(null);
2005
+ });
1886
2006
  req.on("timeout", () => {
1887
2007
  req.destroy();
1888
2008
  resolve2(null);
@@ -1916,7 +2036,7 @@ async function checkForUpdates(force) {
1916
2036
  const current = getCurrentVersion();
1917
2037
  const latest = await fetchLatestVersion();
1918
2038
  saveCheckDate();
1919
- if (!latest || compareVersions(current, latest) >= 0) return;
2039
+ if (latest === null || compareVersions(current, latest) >= 0) return;
1920
2040
  const cmd = detectUpgradeCommand();
1921
2041
  const updateLine = `Update available: ${current} \u2192 ${latest}`;
1922
2042
  const cmdLine = `Run: ${cmd}`;
@@ -1935,109 +2055,6 @@ async function checkForUpdates(force) {
1935
2055
  console.log("");
1936
2056
  }
1937
2057
 
1938
- // src/review-update.ts
1939
- function findLineContent(diff, lineNumber, side) {
1940
- for (const hunk of diff.hunks || []) {
1941
- for (const line of hunk.lines) {
1942
- if (side === "old" && line.oldNum === lineNumber) return line.content;
1943
- if (side === "new" && line.newNum === lineNumber) return line.content;
1944
- }
1945
- }
1946
- return null;
1947
- }
1948
- function findMatchingLine(diff, content, origLineNum, side, radius = 10) {
1949
- let bestMatch = null;
1950
- let bestDistance = Infinity;
1951
- for (const hunk of diff.hunks || []) {
1952
- for (const line of hunk.lines) {
1953
- if (line.content !== content) continue;
1954
- const lineNum = side === "old" ? line.oldNum : line.newNum;
1955
- if (lineNum == null) continue;
1956
- const distance = Math.abs(lineNum - origLineNum);
1957
- if (distance <= radius && distance < bestDistance) {
1958
- bestDistance = distance;
1959
- bestMatch = { lineNumber: lineNum, side };
1960
- }
1961
- }
1962
- }
1963
- return bestMatch;
1964
- }
1965
- async function migrateAnnotations(annotations, oldDiff, newDiff) {
1966
- let staleCount = 0;
1967
- for (const annotation of annotations) {
1968
- const oldContent = findLineContent(oldDiff, annotation.line_number, annotation.side);
1969
- if (!oldContent) {
1970
- if (!annotation.is_stale) {
1971
- await markAnnotationStale(annotation.id, null);
1972
- staleCount++;
1973
- }
1974
- continue;
1975
- }
1976
- const match = findMatchingLine(newDiff, oldContent, annotation.line_number, annotation.side);
1977
- if (match) {
1978
- if (match.lineNumber !== annotation.line_number || match.side !== annotation.side) {
1979
- await moveAnnotation(annotation.id, match.lineNumber, match.side);
1980
- } else if (annotation.is_stale) {
1981
- await markAnnotationCurrent(annotation.id);
1982
- }
1983
- } else {
1984
- if (!annotation.is_stale) {
1985
- await markAnnotationStale(annotation.id, oldContent);
1986
- staleCount++;
1987
- }
1988
- }
1989
- }
1990
- return staleCount;
1991
- }
1992
- async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1993
- const existingFiles = await getReviewFiles(reviewId);
1994
- const existingByPath = /* @__PURE__ */ new Map();
1995
- for (const f of existingFiles) {
1996
- existingByPath.set(f.file_path, f);
1997
- }
1998
- const newDiffsByPath = /* @__PURE__ */ new Map();
1999
- for (const d of newDiffs) {
2000
- newDiffsByPath.set(d.filePath, d);
2001
- }
2002
- let updated = 0;
2003
- let added = 0;
2004
- let stale = 0;
2005
- for (const [path, existingFile] of existingByPath) {
2006
- const newDiff = newDiffsByPath.get(path);
2007
- if (newDiff) {
2008
- const oldDiff = JSON.parse(existingFile.diff_data || "{}");
2009
- const annotations = await getAnnotationsForFile(existingFile.id);
2010
- if (annotations.length > 0) {
2011
- stale += await migrateAnnotations(annotations, oldDiff, newDiff);
2012
- }
2013
- await updateFileDiff(existingFile.id, JSON.stringify(newDiff));
2014
- updated++;
2015
- } else {
2016
- const annotations = await getAnnotationsForFile(existingFile.id);
2017
- if (annotations.length === 0) {
2018
- await deleteReviewFile(existingFile.id);
2019
- } else {
2020
- const oldDiff = JSON.parse(existingFile.diff_data || "{}");
2021
- for (const a of annotations) {
2022
- if (!a.is_stale) {
2023
- const content = findLineContent(oldDiff, a.line_number, a.side);
2024
- await markAnnotationStale(a.id, content);
2025
- stale++;
2026
- }
2027
- }
2028
- }
2029
- }
2030
- }
2031
- for (const [path, diff] of newDiffsByPath) {
2032
- if (!existingByPath.has(path)) {
2033
- await addReviewFile(reviewId, path, JSON.stringify(diff));
2034
- added++;
2035
- }
2036
- }
2037
- await updateReviewHead(reviewId, headCommit);
2038
- return { updated, added, stale };
2039
- }
2040
-
2041
2058
  // src/cli.ts
2042
2059
  function printUsage() {
2043
2060
  console.log(`
@@ -2084,6 +2101,7 @@ function parseArgs(argv) {
2084
2101
  case "-h":
2085
2102
  printUsage();
2086
2103
  process.exit(0);
2104
+ // falls through
2087
2105
  case "--uncommitted":
2088
2106
  mode = { type: "uncommitted" };
2089
2107
  break;
@@ -2141,7 +2159,7 @@ async function main() {
2141
2159
  }
2142
2160
  const { mode, port, resume, forceUpdateCheck, debug } = parsed;
2143
2161
  if (debug) {
2144
- console.log(`Build timestamp: ${"2026-03-06T01:38:17.060Z"}`);
2162
+ console.log(`Build timestamp: ${"2026-03-06T22:43:31.204Z"}`);
2145
2163
  }
2146
2164
  await checkForUpdates(forceUpdateCheck);
2147
2165
  const cwd = process.cwd();