glassbox 0.8.2 → 0.8.3-beta.1

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
@@ -97,11 +97,6 @@ var init_schema = __esm({
97
97
  });
98
98
 
99
99
  // src/db/connection.ts
100
- var connection_exports = {};
101
- __export(connection_exports, {
102
- getDb: () => getDb,
103
- setDataDir: () => setDataDir
104
- });
105
100
  import { PGlite } from "@electric-sql/pglite";
106
101
  import { mkdirSync, rmSync } from "fs";
107
102
  import { join } from "path";
@@ -171,6 +166,16 @@ var init_connection = __esm({
171
166
  }
172
167
  });
173
168
 
169
+ // src/db/ids.ts
170
+ function generateId() {
171
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
172
+ }
173
+ var init_ids = __esm({
174
+ "src/db/ids.ts"() {
175
+ "use strict";
176
+ }
177
+ });
178
+
174
179
  // src/db/queries.ts
175
180
  var queries_exports = {};
176
181
  __export(queries_exports, {
@@ -181,6 +186,7 @@ __export(queries_exports, {
181
186
  deleteReview: () => deleteReview,
182
187
  deleteReviewFile: () => deleteReviewFile,
183
188
  deleteStaleAnnotations: () => deleteStaleAnnotations,
189
+ getAnnotationCountsForReview: () => getAnnotationCountsForReview,
184
190
  getAnnotationsForFile: () => getAnnotationsForFile,
185
191
  getAnnotationsForReview: () => getAnnotationsForReview,
186
192
  getLatestInProgressReview: () => getLatestInProgressReview,
@@ -199,9 +205,6 @@ __export(queries_exports, {
199
205
  updateReviewHead: () => updateReviewHead,
200
206
  updateReviewStatus: () => updateReviewStatus
201
207
  });
202
- function generateId() {
203
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
204
- }
205
208
  async function createReview(repoPath, repoName, mode, modeArgs, headCommit) {
206
209
  const db2 = await getDb();
207
210
  const id = generateId();
@@ -383,10 +386,26 @@ async function getStaleCountsForReview(reviewId) {
383
386
  }
384
387
  return counts;
385
388
  }
389
+ async function getAnnotationCountsForReview(reviewId) {
390
+ const db2 = await getDb();
391
+ const result = await db2.query(
392
+ `SELECT a.review_file_id, COUNT(*)::text as count FROM annotations a
393
+ JOIN review_files rf ON a.review_file_id = rf.id
394
+ WHERE rf.review_id = $1
395
+ GROUP BY a.review_file_id`,
396
+ [reviewId]
397
+ );
398
+ const counts = {};
399
+ for (const row of result.rows) {
400
+ counts[row.review_file_id] = parseInt(row.count, 10);
401
+ }
402
+ return counts;
403
+ }
386
404
  var init_queries = __esm({
387
405
  "src/db/queries.ts"() {
388
406
  "use strict";
389
407
  init_connection();
408
+ init_ids();
390
409
  }
391
410
  });
392
411
 
@@ -395,7 +414,7 @@ init_connection();
395
414
  init_queries();
396
415
  import { mkdirSync as mkdirSync9, realpathSync } from "fs";
397
416
  import { tmpdir } from "os";
398
- import { join as join13, resolve as resolve7 } from "path";
417
+ import { join as join13, resolve as resolve8 } from "path";
399
418
 
400
419
  // src/debug.ts
401
420
  var debugEnabled = false;
@@ -691,12 +710,10 @@ function saveGuidedReviewConfig(settings) {
691
710
 
692
711
  // src/db/ai-queries.ts
693
712
  init_connection();
694
- function generateId2() {
695
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
696
- }
713
+ init_ids();
697
714
  async function createAnalysis(reviewId, analysisType) {
698
715
  const db2 = await getDb();
699
- const id = generateId2();
716
+ const id = generateId();
700
717
  const result = await db2.query(
701
718
  `INSERT INTO ai_analyses (id, review_id, analysis_type, status)
702
719
  VALUES ($1, $2, $3, 'running') RETURNING *`,
@@ -737,7 +754,7 @@ async function appendFileScores(analysisId, scores) {
737
754
  const existingPaths = new Set(existing.rows.map((r) => r.file_path));
738
755
  for (const score of scores) {
739
756
  if (existingPaths.has(score.filePath)) continue;
740
- const id = generateId2();
757
+ const id = generateId();
741
758
  await db2.query(
742
759
  `INSERT INTO ai_file_scores (id, analysis_id, review_file_id, file_path, sort_order, aggregate_score, rationale, dimension_scores, notes)
743
760
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
@@ -1751,7 +1768,6 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1751
1768
 
1752
1769
  // src/server.ts
1753
1770
  import { serve } from "@hono/node-server";
1754
- import { exec } from "child_process";
1755
1771
  import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
1756
1772
  import { Hono as Hono16 } from "hono";
1757
1773
  import { dirname as dirname2, join as join10 } from "path";
@@ -2480,7 +2496,7 @@ async function runBatches(batches, totalFiles, processBatch, onBatchComplete, on
2480
2496
  while (nextIndex < batches.length || running.size > 0) {
2481
2497
  while (nextIndex < batches.length && running.size < concurrency) {
2482
2498
  if (shouldCancel !== void 0 && shouldCancel()) {
2483
- debugLog(`${tag}Batch runner cancelled \u2014 skipping batch ${String(nextIndex)} and ${String(batches.length - nextIndex - 1)} remaining`);
2499
+ debugLog(`${tag}Batch runner canceled \u2014 skipping batch ${String(nextIndex)} and ${String(batches.length - nextIndex - 1)} remaining`);
2484
2500
  nextIndex = batches.length;
2485
2501
  break;
2486
2502
  }
@@ -2509,8 +2525,8 @@ function isRetriable(err) {
2509
2525
  return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
2510
2526
  }
2511
2527
  function sleep(ms) {
2512
- return new Promise((resolve8) => {
2513
- setTimeout(resolve8, ms);
2528
+ return new Promise((resolve9) => {
2529
+ setTimeout(resolve9, ms);
2514
2530
  });
2515
2531
  }
2516
2532
 
@@ -2554,8 +2570,8 @@ function randomLines(count) {
2554
2570
  return lines.sort((a, b) => a.line - b.line);
2555
2571
  }
2556
2572
  function sleep2(ms) {
2557
- return new Promise((resolve8) => {
2558
- setTimeout(resolve8, ms);
2573
+ return new Promise((resolve9) => {
2574
+ setTimeout(resolve9, ms);
2559
2575
  });
2560
2576
  }
2561
2577
  async function mockRiskAnalysisBatch(files) {
@@ -2623,6 +2639,7 @@ async function mockGuidedAnalysisBatch(files) {
2623
2639
  }
2624
2640
 
2625
2641
  // src/routes/ai-analysis.ts
2642
+ init_connection();
2626
2643
  init_queries();
2627
2644
 
2628
2645
  // src/utils/resolveReviewId.ts
@@ -2648,7 +2665,7 @@ var VALID_RISK_DIMENSIONS = ["aggregate", "security", "correctness", "error-hand
2648
2665
  var VALID_SVG_VIEW_MODES = ["code", "rendered"];
2649
2666
  var VALID_IMAGE_MODES = ["metadata", "side-by-side", "difference", "slice"];
2650
2667
  var VALID_ANALYSIS_TYPES = ["risk", "narrative", "guided"];
2651
- var cancelledAnalyses = /* @__PURE__ */ new Set();
2668
+ var canceledAnalyses = /* @__PURE__ */ new Set();
2652
2669
  aiAnalysisRoutes.post("/analyze", async (c) => {
2653
2670
  const reviewId = resolveReviewId(c);
2654
2671
  const repoRoot = c.get("repoRoot");
@@ -2671,21 +2688,21 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
2671
2688
  return c.json({ error: "No files in review" }, 400);
2672
2689
  }
2673
2690
  if (invalidateCache) {
2674
- debugLog("POST /analyze: invalidateCache=true, cancelling all running analyses");
2691
+ debugLog("POST /analyze: invalidateCache=true, canceling all running analyses");
2675
2692
  for (const type of ["risk", "narrative", "guided"]) {
2676
2693
  const running = await getLatestAnalysis(reviewId, type);
2677
2694
  if (running !== void 0 && running.status === "running") {
2678
- debugLog(`POST /analyze: cancelling ${type} analysis id=${running.id}`);
2679
- cancelledAnalyses.add(running.id);
2680
- await updateAnalysisStatus(running.id, "failed", "Cancelled");
2695
+ debugLog(`POST /analyze: canceling ${type} analysis id=${running.id}`);
2696
+ canceledAnalyses.add(running.id);
2697
+ await updateAnalysisStatus(running.id, "failed", "Canceled");
2681
2698
  }
2682
2699
  }
2683
2700
  } else if (analysisType === "risk" || analysisType === "narrative") {
2684
2701
  const otherType = analysisType === "risk" ? "narrative" : "risk";
2685
2702
  const otherRunning = await getLatestAnalysis(reviewId, otherType);
2686
2703
  if (otherRunning !== void 0 && otherRunning.status === "running") {
2687
- debugLog(`POST /analyze: cancelling ${otherType} analysis id=${otherRunning.id} (switching to ${analysisType})`);
2688
- cancelledAnalyses.add(otherRunning.id);
2704
+ debugLog(`POST /analyze: canceling ${otherType} analysis id=${otherRunning.id} (switching to ${analysisType})`);
2705
+ canceledAnalyses.add(otherRunning.id);
2689
2706
  }
2690
2707
  }
2691
2708
  if (!invalidateCache) {
@@ -2727,76 +2744,46 @@ async function executeAnalysis(input) {
2727
2744
  debugLog(`Context window: ${String(contextWindow)} tokens`);
2728
2745
  const { batches, binaryFiles } = planBatches(files, contextWindow);
2729
2746
  const fileIdMap = new Map(files.map((f) => [f.file_path, f.id]));
2730
- const totalAnalyzable = batches.reduce((sum, b) => sum + b.files.length, 0);
2731
- debugLog(`Analysis plan: ${String(totalAnalyzable)} analyzable + ${String(binaryFiles.length)} binary = ${String(totalAnalyzable + binaryFiles.length)} total files in ${String(batches.length)} batch(es)`);
2732
- const prevScores = invalidateCache ? [] : await getPreviousScores(reviewId, analysisType, analysisId);
2733
- const binaryPathSet = new Set(binaryFiles.map((f) => f.file_path));
2734
- const unchangedPaths = /* @__PURE__ */ new Set();
2735
- const cachedScores = prevScores.filter((s) => {
2736
- if (fileIdMap.has(s.file_path) && !binaryPathSet.has(s.file_path)) {
2737
- unchangedPaths.add(s.file_path);
2738
- return true;
2739
- }
2740
- return false;
2747
+ debugLog(`Analysis plan: ${String(batches.reduce((s, b) => s + b.files.length, 0))} analyzable + ${String(binaryFiles.length)} binary in ${String(batches.length)} batch(es)`);
2748
+ const { cachedCount, filteredBatches } = await applyCachedScores({
2749
+ analysisId,
2750
+ analysisType,
2751
+ reviewId,
2752
+ fileIdMap,
2753
+ batches,
2754
+ binaryFiles,
2755
+ invalidateCache
2741
2756
  });
2742
- debugLog(`Cache: ${String(cachedScores.length)} scores from previous analysis, ${String(totalAnalyzable - cachedScores.length)} files need processing`);
2743
- if (cachedScores.length > 0) {
2744
- const cachedForInsert = cachedScores.map((s) => ({
2745
- reviewFileId: fileIdMap.get(s.file_path) ?? s.review_file_id,
2746
- filePath: s.file_path,
2747
- sortOrder: s.sort_order,
2748
- aggregateScore: s.aggregate_score,
2749
- rationale: s.rationale,
2750
- dimensionScores: s.dimension_scores !== null ? JSON.parse(s.dimension_scores) : null,
2751
- notes: s.notes !== null ? JSON.parse(s.notes) : null
2752
- }));
2753
- await appendFileScores(analysisId, cachedForInsert);
2754
- }
2755
- const filteredBatches = batches.map((batch) => {
2756
- const remaining = batch.files.filter((f) => !unchangedPaths.has(f.file_path));
2757
- return { files: remaining, estimatedTokens: batch.estimatedTokens };
2758
- }).filter((batch) => batch.files.length > 0);
2759
2757
  const filteredAnalyzable = filteredBatches.reduce((sum, b) => sum + b.files.length, 0);
2760
- const totalForProgress = filteredAnalyzable + binaryFiles.length + cachedScores.length;
2758
+ const totalForProgress = filteredAnalyzable + binaryFiles.length + cachedCount;
2761
2759
  debugLog(`After cache: ${String(filteredAnalyzable)} files to analyze in ${String(filteredBatches.length)} batch(es)`);
2762
- await updateAnalysisProgress(analysisId, cachedScores.length, totalForProgress);
2763
- if (binaryFiles.length > 0) {
2764
- debugLog(`Saving ${String(binaryFiles.length)} binary files with score 0`);
2765
- const binaryScoreEntries = binaryFiles.map((f, idx) => ({
2766
- reviewFileId: fileIdMap.get(f.file_path) ?? "",
2767
- filePath: f.file_path,
2768
- sortOrder: 99999 + idx,
2769
- // Will be re-sorted later
2770
- aggregateScore: analysisType === "risk" ? 0 : null,
2771
- rationale: "Binary file \u2014 not analyzed",
2772
- dimensionScores: analysisType === "risk" ? { security: 0, correctness: 0, "error-handling": 0, maintainability: 0, architecture: 0, performance: 0 } : null,
2773
- notes: null
2774
- }));
2775
- await appendFileScores(analysisId, binaryScoreEntries);
2776
- await updateAnalysisProgress(analysisId, cachedScores.length + binaryFiles.length, totalForProgress);
2777
- }
2760
+ await updateAnalysisProgress(analysisId, cachedCount, totalForProgress);
2761
+ await saveBinaryFiles({ analysisId, analysisType, fileIdMap, binaryFiles, cachedCount, totalForProgress });
2778
2762
  if (filteredBatches.length === 0) {
2779
2763
  debugLog("No batches to process (all files cached or binary), marking completed");
2780
2764
  await updateAnalysisStatus(analysisId, "completed");
2781
2765
  return;
2782
2766
  }
2783
- const shouldCancel = () => cancelledAnalyses.has(analysisId);
2784
- const progressOffset = cachedScores.length + binaryFiles.length;
2785
- const runArgs = [analysisId, filteredBatches, files.length, totalForProgress, progressOffset];
2786
- if (analysisType === "risk") {
2787
- await runBatchedAnalysis(...runArgs, riskAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId), shouldCancel);
2788
- } else if (analysisType === "narrative") {
2789
- await runBatchedAnalysis(...runArgs, narrativeAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId), shouldCancel);
2790
- } else {
2791
- await runBatchedAnalysis(...runArgs, guidedAnalysisConfig(config, repoRoot, fileIdMap, guidedReview), shouldCancel);
2792
- }
2793
- if (cancelledAnalyses.has(analysisId)) {
2794
- cancelledAnalyses.delete(analysisId);
2795
- debugLog(`Analysis ${analysisId} was cancelled (user switched modes)`);
2796
- await updateAnalysisStatus(analysisId, "failed", "Cancelled");
2767
+ const progressOffset = cachedCount + binaryFiles.length;
2768
+ await dispatchByType({
2769
+ analysisType,
2770
+ analysisId,
2771
+ filteredBatches,
2772
+ files,
2773
+ totalForProgress,
2774
+ progressOffset,
2775
+ config,
2776
+ repoRoot,
2777
+ fileIdMap,
2778
+ guidedReview
2779
+ });
2780
+ if (canceledAnalyses.has(analysisId)) {
2781
+ canceledAnalyses.delete(analysisId);
2782
+ debugLog(`Analysis ${analysisId} was canceled (user switched modes)`);
2783
+ await updateAnalysisStatus(analysisId, "failed", "Canceled");
2797
2784
  return;
2798
2785
  }
2799
- cancelledAnalyses.delete(analysisId);
2786
+ canceledAnalyses.delete(analysisId);
2800
2787
  debugLog(`Analysis ${analysisId} completed successfully`);
2801
2788
  await updateAnalysisStatus(analysisId, "completed");
2802
2789
  } catch (err) {
@@ -2806,6 +2793,66 @@ async function executeAnalysis(input) {
2806
2793
  await updateAnalysisStatus(analysisId, "failed", message);
2807
2794
  }
2808
2795
  }
2796
+ async function applyCachedScores(args) {
2797
+ const { analysisId, analysisType, reviewId, fileIdMap, batches, binaryFiles, invalidateCache } = args;
2798
+ const prevScores = invalidateCache ? [] : await getPreviousScores(reviewId, analysisType, analysisId);
2799
+ const binaryPathSet = new Set(binaryFiles.map((f) => f.file_path));
2800
+ const unchangedPaths = /* @__PURE__ */ new Set();
2801
+ const cachedScores = prevScores.filter((s) => {
2802
+ if (fileIdMap.has(s.file_path) && !binaryPathSet.has(s.file_path)) {
2803
+ unchangedPaths.add(s.file_path);
2804
+ return true;
2805
+ }
2806
+ return false;
2807
+ });
2808
+ debugLog(`Cache: ${String(cachedScores.length)} scores carried forward from previous analysis`);
2809
+ if (cachedScores.length > 0) {
2810
+ const cachedForInsert = cachedScores.map((s) => ({
2811
+ reviewFileId: fileIdMap.get(s.file_path) ?? s.review_file_id,
2812
+ filePath: s.file_path,
2813
+ sortOrder: s.sort_order,
2814
+ aggregateScore: s.aggregate_score,
2815
+ rationale: s.rationale,
2816
+ dimensionScores: safeJsonParse(s.dimension_scores),
2817
+ notes: safeJsonParse(s.notes)
2818
+ }));
2819
+ await appendFileScores(analysisId, cachedForInsert);
2820
+ }
2821
+ const filteredBatches = batches.map((batch) => ({
2822
+ files: batch.files.filter((f) => !unchangedPaths.has(f.file_path)),
2823
+ estimatedTokens: batch.estimatedTokens
2824
+ })).filter((batch) => batch.files.length > 0);
2825
+ return { cachedCount: cachedScores.length, filteredBatches };
2826
+ }
2827
+ async function saveBinaryFiles(args) {
2828
+ const { analysisId, analysisType, fileIdMap, binaryFiles, cachedCount, totalForProgress } = args;
2829
+ if (binaryFiles.length === 0) return;
2830
+ debugLog(`Saving ${String(binaryFiles.length)} binary files with score 0`);
2831
+ const binaryScoreEntries = binaryFiles.map((f, idx) => ({
2832
+ reviewFileId: fileIdMap.get(f.file_path) ?? "",
2833
+ filePath: f.file_path,
2834
+ sortOrder: 99999 + idx,
2835
+ // re-sorted by `updateSortOrders` later
2836
+ aggregateScore: analysisType === "risk" ? 0 : null,
2837
+ rationale: "Binary file \u2014 not analyzed",
2838
+ dimensionScores: analysisType === "risk" ? { security: 0, correctness: 0, "error-handling": 0, maintainability: 0, architecture: 0, performance: 0 } : null,
2839
+ notes: null
2840
+ }));
2841
+ await appendFileScores(analysisId, binaryScoreEntries);
2842
+ await updateAnalysisProgress(analysisId, cachedCount + binaryFiles.length, totalForProgress);
2843
+ }
2844
+ async function dispatchByType(args) {
2845
+ const { analysisType, analysisId, filteredBatches, files, totalForProgress, progressOffset, config, repoRoot, fileIdMap, guidedReview } = args;
2846
+ const shouldCancel = () => canceledAnalyses.has(analysisId);
2847
+ const runArgs = [analysisId, filteredBatches, files.length, totalForProgress, progressOffset];
2848
+ if (analysisType === "risk") {
2849
+ await runBatchedAnalysis(...runArgs, riskAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId), shouldCancel);
2850
+ } else if (analysisType === "narrative") {
2851
+ await runBatchedAnalysis(...runArgs, narrativeAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId), shouldCancel);
2852
+ } else {
2853
+ await runBatchedAnalysis(...runArgs, guidedAnalysisConfig(config, repoRoot, fileIdMap, guidedReview), shouldCancel);
2854
+ }
2855
+ }
2809
2856
  async function runBatchedAnalysis(analysisId, batches, totalFiles, progressTotal, progressOffset, cfg, shouldCancel) {
2810
2857
  const allResults = await runBatches(
2811
2858
  batches,
@@ -2828,8 +2875,7 @@ async function runBatchedAnalysis(analysisId, batches, totalFiles, progressTotal
2828
2875
  if (cfg.finalize) await cfg.finalize(allResults, batches.length);
2829
2876
  }
2830
2877
  async function updateSortOrders(analysisId, entries) {
2831
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
2832
- const db2 = await getDb2();
2878
+ const db2 = await getDb();
2833
2879
  for (const [filePath, sortOrder] of entries) {
2834
2880
  await db2.query(
2835
2881
  "UPDATE ai_file_scores SET sort_order = $1 WHERE analysis_id = $2 AND file_path = $3",
@@ -2939,11 +2985,19 @@ aiAnalysisRoutes.get("/analysis/:type", async (c) => {
2939
2985
  sortOrder: s.sort_order,
2940
2986
  aggregateScore: s.aggregate_score,
2941
2987
  rationale: s.rationale,
2942
- dimensionScores: s.dimension_scores !== null ? JSON.parse(s.dimension_scores) : null,
2943
- notes: s.notes !== null ? JSON.parse(s.notes) : null
2988
+ dimensionScores: safeJsonParse(s.dimension_scores),
2989
+ notes: safeJsonParse(s.notes)
2944
2990
  }))
2945
2991
  });
2946
2992
  });
2993
+ function safeJsonParse(raw) {
2994
+ if (raw === null || raw === void 0) return null;
2995
+ try {
2996
+ return JSON.parse(raw);
2997
+ } catch {
2998
+ return null;
2999
+ }
3000
+ }
2947
3001
  aiAnalysisRoutes.get("/analysis/:type/status", async (c) => {
2948
3002
  const reviewId = resolveReviewId(c);
2949
3003
  const analysisType = c.req.param("type");
@@ -2987,7 +3041,11 @@ aiAnalysisRoutes.get("/preferences", async (c) => {
2987
3041
  return c.json(prefs);
2988
3042
  });
2989
3043
  aiAnalysisRoutes.post("/preferences", async (c) => {
2990
- const body = await c.req.json();
3044
+ const raw = await c.req.json();
3045
+ if (typeof raw !== "object" || raw === null) {
3046
+ return c.json({ error: "body must be a JSON object" }, 400);
3047
+ }
3048
+ const body = raw;
2991
3049
  if (body.sort_mode !== void 0) {
2992
3050
  const v = checkEnum(body.sort_mode, "sort_mode", VALID_SORT_MODES);
2993
3051
  if ("error" in v) return c.json({ error: v.error }, 400);
@@ -3010,7 +3068,14 @@ aiAnalysisRoutes.post("/preferences", async (c) => {
3010
3068
  const v = checkEnum(body.last_image_mode, "last_image_mode", VALID_IMAGE_MODES);
3011
3069
  if ("error" in v) return c.json({ error: v.error }, 400);
3012
3070
  }
3013
- await saveUserPreferences(body);
3071
+ const allowed = {};
3072
+ if (body.sort_mode !== void 0) allowed.sort_mode = body.sort_mode;
3073
+ if (body.risk_sort_dimension !== void 0) allowed.risk_sort_dimension = body.risk_sort_dimension;
3074
+ if (body.show_risk_scores !== void 0) allowed.show_risk_scores = body.show_risk_scores;
3075
+ if (body.ignore_whitespace !== void 0) allowed.ignore_whitespace = body.ignore_whitespace;
3076
+ if (body.svg_view_mode !== void 0) allowed.svg_view_mode = body.svg_view_mode;
3077
+ if (body.last_image_mode !== void 0) allowed.last_image_mode = body.last_image_mode;
3078
+ await saveUserPreferences(allowed);
3014
3079
  return c.json({ ok: true });
3015
3080
  });
3016
3081
 
@@ -3352,6 +3417,9 @@ contextRoutes.get("/context/:fileId", async (c) => {
3352
3417
  if (!file) return c.json({ error: "Not found" }, 404);
3353
3418
  const startLine = parseInt(c.req.query("start") ?? "1", 10);
3354
3419
  const endLine = parseInt(c.req.query("end") ?? "20", 10);
3420
+ if (!Number.isFinite(startLine) || !Number.isFinite(endLine)) {
3421
+ return c.json({ error: "start and end must be integers" }, 400);
3422
+ }
3355
3423
  const content = getFileContent(file.file_path, "working", repoRoot);
3356
3424
  const allLines = content.split("\n");
3357
3425
  const clampedStart = Math.max(1, startLine);
@@ -3365,20 +3433,42 @@ contextRoutes.get("/context/:fileId", async (c) => {
3365
3433
 
3366
3434
  // src/routes/api/files.ts
3367
3435
  init_queries();
3368
- import { execFileSync } from "child_process";
3369
3436
  import { Hono as Hono6 } from "hono";
3437
+ import { resolve as resolve4 } from "path";
3438
+
3439
+ // src/utils/openOS.ts
3440
+ import { execFileSync } from "child_process";
3370
3441
  import { resolve as resolve3 } from "path";
3442
+ function openOS(target, mode) {
3443
+ if (mode === "reveal") {
3444
+ if (process.platform === "darwin") {
3445
+ execFileSync("open", ["-R", target]);
3446
+ } else if (process.platform === "win32") {
3447
+ execFileSync("explorer", ["/select," + target]);
3448
+ } else {
3449
+ execFileSync("xdg-open", [resolve3(target, "..")]);
3450
+ }
3451
+ return;
3452
+ }
3453
+ if (process.platform === "darwin") {
3454
+ execFileSync("open", [target]);
3455
+ } else if (process.platform === "win32") {
3456
+ execFileSync("cmd", ["/c", "start", "", target]);
3457
+ } else {
3458
+ execFileSync("xdg-open", [target]);
3459
+ }
3460
+ }
3461
+
3462
+ // src/routes/api/files.ts
3371
3463
  var filesRoutes = new Hono6();
3372
3464
  var VALID_FILE_STATUSES = ["pending", "reviewed"];
3373
3465
  filesRoutes.get("/files", async (c) => {
3374
3466
  const reviewId = resolveReviewId(c);
3375
- const files = await getReviewFiles(reviewId);
3376
- const annotationCounts = {};
3377
- for (const file of files) {
3378
- const annotations = await getAnnotationsForFile(file.id);
3379
- annotationCounts[file.id] = annotations.length;
3380
- }
3381
- const staleCounts = await getStaleCountsForReview(reviewId);
3467
+ const [files, annotationCounts, staleCounts] = await Promise.all([
3468
+ getReviewFiles(reviewId),
3469
+ getAnnotationCountsForReview(reviewId),
3470
+ getStaleCountsForReview(reviewId)
3471
+ ]);
3382
3472
  return c.json({ files, annotationCounts, staleCounts });
3383
3473
  });
3384
3474
  filesRoutes.get("/files/:fileId", async (c) => {
@@ -3398,15 +3488,9 @@ filesRoutes.post("/files/:fileId/reveal", async (c) => {
3398
3488
  const file = await getReviewFile(c.req.param("fileId"));
3399
3489
  if (!file) return c.json({ error: "Not found" }, 404);
3400
3490
  const repoRoot = c.get("repoRoot");
3401
- const fullPath = resolve3(repoRoot, file.file_path);
3491
+ const fullPath = resolve4(repoRoot, file.file_path);
3402
3492
  try {
3403
- if (process.platform === "darwin") {
3404
- execFileSync("open", ["-R", fullPath]);
3405
- } else if (process.platform === "win32") {
3406
- execFileSync("explorer", ["/select," + fullPath]);
3407
- } else {
3408
- execFileSync("xdg-open", [resolve3(fullPath, "..")]);
3409
- }
3493
+ openOS(fullPath, "reveal");
3410
3494
  } catch {
3411
3495
  }
3412
3496
  return c.json({ ok: true });
@@ -3419,7 +3503,7 @@ import { Hono as Hono7 } from "hono";
3419
3503
  // src/git/image.ts
3420
3504
  import { spawnSync as spawnSync6 } from "child_process";
3421
3505
  import { readFileSync as readFileSync6 } from "fs";
3422
- import { resolve as resolve4 } from "path";
3506
+ import { resolve as resolve5 } from "path";
3423
3507
 
3424
3508
  // src/git/image-metadata.ts
3425
3509
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
@@ -3689,7 +3773,7 @@ function gitShowFile(ref, filePath, repoRoot) {
3689
3773
  }
3690
3774
  function readWorkingFile(filePath, repoRoot) {
3691
3775
  try {
3692
- return readFileSync6(resolve4(repoRoot, filePath));
3776
+ return readFileSync6(resolve5(repoRoot, filePath));
3693
3777
  } catch {
3694
3778
  return null;
3695
3779
  }
@@ -3913,7 +3997,7 @@ init_queries();
3913
3997
  import { spawnSync as spawnSync7 } from "child_process";
3914
3998
  import { readFileSync as readFileSync8 } from "fs";
3915
3999
  import { Hono as Hono8 } from "hono";
3916
- import { resolve as resolve5 } from "path";
4000
+ import { resolve as resolve6 } from "path";
3917
4001
 
3918
4002
  // src/outline/parser.ts
3919
4003
  var BRACE_LANGS = /* @__PURE__ */ new Set([
@@ -4282,7 +4366,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
4282
4366
  if (!/\.(js|mjs|cjs|jsx|ts|tsx|mts|cts|java|go|rs|c|h|cpp|cc|cxx|hpp|cs|swift|php|kt|kts|scala|dart|groovy|py|rb)$/i.test(ext)) continue;
4283
4367
  let content = "";
4284
4368
  try {
4285
- content = readFileSync8(resolve5(repoRoot, filePath), "utf-8");
4369
+ content = readFileSync8(resolve6(repoRoot, filePath), "utf-8");
4286
4370
  } catch {
4287
4371
  continue;
4288
4372
  }
@@ -4342,7 +4426,11 @@ projectSettingsRoutes.get("/project-settings", (c) => {
4342
4426
  });
4343
4427
  projectSettingsRoutes.patch("/project-settings", async (c) => {
4344
4428
  const repoRoot = c.get("repoRoot");
4345
- const body = await c.req.json();
4429
+ const raw = await c.req.json();
4430
+ if (typeof raw !== "object" || raw === null) {
4431
+ return c.json({ error: "body must be a JSON object" }, 400);
4432
+ }
4433
+ const body = raw;
4346
4434
  if (body.appName !== void 0 && typeof body.appName !== "string") {
4347
4435
  return c.json({ error: "appName must be a string" }, 400);
4348
4436
  }
@@ -4459,13 +4547,21 @@ sharePromptRoutes.post("/share-prompt/dismiss", (c) => {
4459
4547
  return c.json({ ok: true });
4460
4548
  });
4461
4549
  sharePromptRoutes.post("/share-prompt/tick", async (c) => {
4462
- const body = await c.req.json();
4550
+ const raw = await c.req.json();
4551
+ if (typeof raw !== "object" || raw === null) {
4552
+ return c.json({ error: "body must be a JSON object" }, 400);
4553
+ }
4554
+ const body = raw;
4555
+ if (typeof body.sessionMs !== "number" || !Number.isFinite(body.sessionMs)) {
4556
+ return c.json({ error: "sessionMs must be a finite number" }, 400);
4557
+ }
4558
+ const sessionMs = body.sessionMs;
4463
4559
  let totalOpenMs = 0;
4464
4560
  updateGlobalConfig((config) => {
4465
4561
  if (config.sharePrompt === void 0) config.sharePrompt = {};
4466
4562
  const sp = config.sharePrompt;
4467
4563
  const current = typeof sp.totalOpenMs === "number" ? sp.totalOpenMs : 0;
4468
- const next = current + (body.sessionMs > 0 ? body.sessionMs : 0);
4564
+ const next = current + (sessionMs > 0 ? sessionMs : 0);
4469
4565
  sp.totalOpenMs = next;
4470
4566
  totalOpenMs = next;
4471
4567
  });
@@ -4552,199 +4648,21 @@ channelApiRoutes.get("/claude-check", (c) => {
4552
4648
  // src/routes/pages.tsx
4553
4649
  import { readFileSync as readFileSync11 } from "fs";
4554
4650
  import { Hono as Hono14 } from "hono";
4555
- import { resolve as resolve6 } from "path";
4556
-
4557
- // src/utils/escapeHtml.ts
4558
- function escapeHtml(str) {
4559
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4560
- }
4561
- function escapeAttr(str) {
4562
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4563
- }
4564
-
4565
- // src/jsx-runtime.ts
4566
- var SafeHtml = class {
4567
- __html;
4568
- constructor(html) {
4569
- this.__html = html;
4570
- }
4571
- toString() {
4572
- return this.__html;
4573
- }
4574
- };
4575
- var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
4576
- function renderChildren(children) {
4577
- if (children == null || typeof children === "boolean") return "";
4578
- if (children instanceof SafeHtml) return children.__html;
4579
- if (typeof children === "string") return escapeHtml(children);
4580
- if (typeof children === "number") return String(children);
4581
- if (Array.isArray(children)) return children.map(renderChildren).join("");
4582
- return "";
4583
- }
4584
- var ATTR_ALIASES = {
4585
- // HTML attributes
4586
- className: "class",
4587
- htmlFor: "for",
4588
- httpEquiv: "http-equiv",
4589
- acceptCharset: "accept-charset",
4590
- accessKey: "accesskey",
4591
- autoCapitalize: "autocapitalize",
4592
- autoComplete: "autocomplete",
4593
- autoFocus: "autofocus",
4594
- autoPlay: "autoplay",
4595
- colSpan: "colspan",
4596
- contentEditable: "contenteditable",
4597
- crossOrigin: "crossorigin",
4598
- dateTime: "datetime",
4599
- defaultChecked: "checked",
4600
- defaultValue: "value",
4601
- encType: "enctype",
4602
- formAction: "formaction",
4603
- formEncType: "formenctype",
4604
- formMethod: "formmethod",
4605
- formNoValidate: "formnovalidate",
4606
- formTarget: "formtarget",
4607
- hrefLang: "hreflang",
4608
- inputMode: "inputmode",
4609
- maxLength: "maxlength",
4610
- minLength: "minlength",
4611
- noModule: "nomodule",
4612
- noValidate: "novalidate",
4613
- readOnly: "readonly",
4614
- referrerPolicy: "referrerpolicy",
4615
- rowSpan: "rowspan",
4616
- spellCheck: "spellcheck",
4617
- srcDoc: "srcdoc",
4618
- srcLang: "srclang",
4619
- srcSet: "srcset",
4620
- tabIndex: "tabindex",
4621
- useMap: "usemap",
4622
- // SVG presentation attributes (camelCase → kebab-case)
4623
- strokeWidth: "stroke-width",
4624
- strokeLinecap: "stroke-linecap",
4625
- strokeLinejoin: "stroke-linejoin",
4626
- strokeDasharray: "stroke-dasharray",
4627
- strokeDashoffset: "stroke-dashoffset",
4628
- strokeMiterlimit: "stroke-miterlimit",
4629
- strokeOpacity: "stroke-opacity",
4630
- fillOpacity: "fill-opacity",
4631
- fillRule: "fill-rule",
4632
- clipPath: "clip-path",
4633
- clipRule: "clip-rule",
4634
- colorInterpolation: "color-interpolation",
4635
- colorInterpolationFilters: "color-interpolation-filters",
4636
- floodColor: "flood-color",
4637
- floodOpacity: "flood-opacity",
4638
- lightingColor: "lighting-color",
4639
- stopColor: "stop-color",
4640
- stopOpacity: "stop-opacity",
4641
- shapeRendering: "shape-rendering",
4642
- imageRendering: "image-rendering",
4643
- textRendering: "text-rendering",
4644
- pointerEvents: "pointer-events",
4645
- vectorEffect: "vector-effect",
4646
- paintOrder: "paint-order",
4647
- // SVG text/font attributes
4648
- fontFamily: "font-family",
4649
- fontSize: "font-size",
4650
- fontStyle: "font-style",
4651
- fontVariant: "font-variant",
4652
- fontWeight: "font-weight",
4653
- fontStretch: "font-stretch",
4654
- textAnchor: "text-anchor",
4655
- textDecoration: "text-decoration",
4656
- dominantBaseline: "dominant-baseline",
4657
- alignmentBaseline: "alignment-baseline",
4658
- baselineShift: "baseline-shift",
4659
- letterSpacing: "letter-spacing",
4660
- wordSpacing: "word-spacing",
4661
- writingMode: "writing-mode",
4662
- glyphOrientationHorizontal: "glyph-orientation-horizontal",
4663
- glyphOrientationVertical: "glyph-orientation-vertical",
4664
- // SVG marker/gradient/filter attributes
4665
- markerStart: "marker-start",
4666
- markerMid: "marker-mid",
4667
- markerEnd: "marker-end",
4668
- gradientUnits: "gradientUnits",
4669
- gradientTransform: "gradientTransform",
4670
- spreadMethod: "spreadMethod",
4671
- patternUnits: "patternUnits",
4672
- patternContentUnits: "patternContentUnits",
4673
- patternTransform: "patternTransform",
4674
- maskUnits: "maskUnits",
4675
- maskContentUnits: "maskContentUnits",
4676
- filterUnits: "filterUnits",
4677
- primitiveUnits: "primitiveUnits",
4678
- clipPathUnits: "clipPathUnits",
4679
- // SVG xlink (legacy but still used)
4680
- xlinkHref: "xlink:href",
4681
- xlinkShow: "xlink:show",
4682
- xlinkActuate: "xlink:actuate",
4683
- xlinkType: "xlink:type",
4684
- xlinkRole: "xlink:role",
4685
- xlinkTitle: "xlink:title",
4686
- xlinkArcrole: "xlink:arcrole",
4687
- xmlBase: "xml:base",
4688
- xmlLang: "xml:lang",
4689
- xmlSpace: "xml:space",
4690
- xmlns: "xmlns",
4691
- xmlnsXlink: "xmlns:xlink",
4692
- // SVG filter primitive attributes
4693
- stdDeviation: "stdDeviation",
4694
- baseFrequency: "baseFrequency",
4695
- numOctaves: "numOctaves",
4696
- kernelMatrix: "kernelMatrix",
4697
- surfaceScale: "surfaceScale",
4698
- specularConstant: "specularConstant",
4699
- specularExponent: "specularExponent",
4700
- diffuseConstant: "diffuseConstant",
4701
- pointsAtX: "pointsAtX",
4702
- pointsAtY: "pointsAtY",
4703
- pointsAtZ: "pointsAtZ",
4704
- limitingConeAngle: "limitingConeAngle",
4705
- tableValues: "tableValues"
4706
- // viewBox, preserveAspectRatio stay as-is (already correct casing)
4707
- };
4708
- function renderAttr(key, value) {
4709
- const name = ATTR_ALIASES[key] ?? key;
4710
- if (value == null || value === false) return "";
4711
- if (value === true) return ` ${name}`;
4712
- let strValue;
4713
- if (value instanceof SafeHtml) {
4714
- strValue = value.__html;
4715
- } else if (typeof value === "number") {
4716
- strValue = String(value);
4717
- } else if (typeof value === "string") {
4718
- strValue = escapeAttr(value);
4719
- } else {
4720
- strValue = "";
4721
- }
4722
- return ` ${name}="${strValue}"`;
4723
- }
4724
- function jsx(tag, props) {
4725
- if (typeof tag === "function") return tag(props);
4726
- const { children, ...attrs } = props;
4727
- const attrStr = Object.entries(attrs).map(([k, v]) => renderAttr(k, v)).join("");
4728
- if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
4729
- const childStr = children != null ? renderChildren(children) : "";
4730
- return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);
4731
- }
4732
- function Fragment({ children }) {
4733
- return new SafeHtml(children != null ? renderChildren(children) : "");
4734
- }
4651
+ import { resolve as resolve7 } from "path";
4735
4652
 
4736
4653
  // src/icons.tsx
4654
+ import { jsx, jsxs } from "kerfjs/jsx-runtime";
4737
4655
  var S14 = { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" };
4738
4656
  var S12 = { ...S14, width: "12", height: "12" };
4739
4657
  var S16 = { ...S14, width: "16", height: "16" };
4740
4658
  function IconEdit() {
4741
- return /* @__PURE__ */ jsx("svg", { ...S14, children: [
4659
+ return /* @__PURE__ */ jsxs("svg", { ...S14, children: [
4742
4660
  /* @__PURE__ */ jsx("path", { d: "M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }),
4743
4661
  /* @__PURE__ */ jsx("path", { d: "m15 5 4 4" })
4744
4662
  ] });
4745
4663
  }
4746
4664
  function IconTrash() {
4747
- return /* @__PURE__ */ jsx("svg", { ...S14, children: [
4665
+ return /* @__PURE__ */ jsxs("svg", { ...S14, children: [
4748
4666
  /* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
4749
4667
  /* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
4750
4668
  /* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }),
@@ -4753,7 +4671,7 @@ function IconTrash() {
4753
4671
  ] });
4754
4672
  }
4755
4673
  function IconTrash16() {
4756
- return /* @__PURE__ */ jsx("svg", { ...S16, children: [
4674
+ return /* @__PURE__ */ jsxs("svg", { ...S16, children: [
4757
4675
  /* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
4758
4676
  /* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
4759
4677
  /* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }),
@@ -4762,7 +4680,7 @@ function IconTrash16() {
4762
4680
  ] });
4763
4681
  }
4764
4682
  function IconReveal() {
4765
- return /* @__PURE__ */ jsx("svg", { ...S12, children: [
4683
+ return /* @__PURE__ */ jsxs("svg", { ...S12, children: [
4766
4684
  /* @__PURE__ */ jsx("path", { d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" }),
4767
4685
  /* @__PURE__ */ jsx("line", { x1: "12", y1: "11", x2: "12", y2: "17" }),
4768
4686
  /* @__PURE__ */ jsx("polyline", { points: "9 14 12 11 15 14" })
@@ -4772,13 +4690,13 @@ function IconZoomOut() {
4772
4690
  return /* @__PURE__ */ jsx("svg", { ...S14, children: /* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" }) });
4773
4691
  }
4774
4692
  function IconZoomIn() {
4775
- return /* @__PURE__ */ jsx("svg", { ...S14, children: [
4693
+ return /* @__PURE__ */ jsxs("svg", { ...S14, children: [
4776
4694
  /* @__PURE__ */ jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
4777
4695
  /* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
4778
4696
  ] });
4779
4697
  }
4780
4698
  function IconFit() {
4781
- return /* @__PURE__ */ jsx("svg", { ...S14, children: [
4699
+ return /* @__PURE__ */ jsxs("svg", { ...S14, children: [
4782
4700
  /* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
4783
4701
  /* @__PURE__ */ jsx("path", { d: "M9 21H3v-6" }),
4784
4702
  /* @__PURE__ */ jsx("path", { d: "M21 3l-7 7" }),
@@ -4786,7 +4704,7 @@ function IconFit() {
4786
4704
  ] });
4787
4705
  }
4788
4706
  function IconActualSize() {
4789
- return /* @__PURE__ */ jsx("svg", { ...S14, children: [
4707
+ return /* @__PURE__ */ jsxs("svg", { ...S14, children: [
4790
4708
  /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
4791
4709
  /* @__PURE__ */ jsx("text", { x: "12", y: "15.5", textAnchor: "middle", fontSize: "9", fontWeight: "bold", fill: "currentColor", stroke: "none", children: "1:1" })
4792
4710
  ] });
@@ -4851,6 +4769,7 @@ function buildSegments(str, commonPositions) {
4851
4769
  }
4852
4770
 
4853
4771
  // src/components/imageDiff.tsx
4772
+ import { jsx as jsx2, jsxs as jsxs2 } from "kerfjs/jsx-runtime";
4854
4773
  function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
4855
4774
  const fileId = file.id;
4856
4775
  const isAdded = diff.status === "added";
@@ -4858,7 +4777,7 @@ function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
4858
4777
  const hasOld = !isAdded;
4859
4778
  const hasNew = !isDeleted;
4860
4779
  const hasComparison = hasOld && hasNew;
4861
- return /* @__PURE__ */ jsx(
4780
+ return /* @__PURE__ */ jsxs2(
4862
4781
  "div",
4863
4782
  {
4864
4783
  className: "image-diff",
@@ -4869,22 +4788,22 @@ function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
4869
4788
  ...baseWidth !== void 0 ? { "data-base-width": String(baseWidth) } : {},
4870
4789
  ...baseHeight !== void 0 ? { "data-base-height": String(baseHeight) } : {},
4871
4790
  children: [
4872
- fontWarning === true && /* @__PURE__ */ jsx("div", { className: "image-font-warning", children: "This SVG uses text that may render differently depending on locally installed fonts." }),
4873
- /* @__PURE__ */ jsx("div", { className: "image-diff-panel image-diff-metadata", "data-panel": "metadata", children: /* @__PURE__ */ jsx("div", { className: "image-metadata-loading", children: "Loading metadata..." }) }),
4874
- hasComparison && /* @__PURE__ */ jsx("div", { className: "image-diff-panel image-diff-visual", "data-panel": "difference", children: /* @__PURE__ */ jsx("div", { className: "image-visual-canvas", "data-zoomable": "true", children: /* @__PURE__ */ jsx("div", { className: "image-zoom-wrap", children: [
4875
- /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
4876
- /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-new image-blend", src: `/api/image/${fileId}/new`, alt: "New version" })
4791
+ fontWarning === true && /* @__PURE__ */ jsx2("div", { className: "image-font-warning", children: "This SVG uses text that may render differently depending on locally installed fonts." }),
4792
+ /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-metadata", "data-panel": "metadata", children: /* @__PURE__ */ jsx2("div", { className: "image-metadata-loading", children: "Loading metadata..." }) }),
4793
+ hasComparison && /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-visual", "data-panel": "difference", children: /* @__PURE__ */ jsx2("div", { className: "image-visual-canvas", "data-zoomable": "true", children: /* @__PURE__ */ jsxs2("div", { className: "image-zoom-wrap", children: [
4794
+ /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
4795
+ /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-new image-blend", src: `/api/image/${fileId}/new`, alt: "New version" })
4877
4796
  ] }) }) }),
4878
- hasComparison && /* @__PURE__ */ jsx("div", { className: "image-diff-panel image-diff-visual", "data-panel": "slice", children: /* @__PURE__ */ jsx("div", { className: "image-visual-canvas", "data-zoomable": "true", children: [
4879
- /* @__PURE__ */ jsx("div", { className: "image-zoom-wrap", children: [
4880
- /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
4881
- /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-new image-slice-clipped", src: `/api/image/${fileId}/new`, alt: "New version" })
4797
+ hasComparison && /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-visual", "data-panel": "slice", children: /* @__PURE__ */ jsxs2("div", { className: "image-visual-canvas", "data-zoomable": "true", children: [
4798
+ /* @__PURE__ */ jsxs2("div", { className: "image-zoom-wrap", children: [
4799
+ /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
4800
+ /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-new image-slice-clipped", src: `/api/image/${fileId}/new`, alt: "New version" })
4882
4801
  ] }),
4883
- /* @__PURE__ */ jsx("div", { className: "slice-line" }),
4884
- /* @__PURE__ */ jsx("div", { className: "slice-handle slice-handle-a" }),
4885
- /* @__PURE__ */ jsx("div", { className: "slice-handle slice-handle-b" })
4802
+ /* @__PURE__ */ jsx2("div", { className: "slice-line" }),
4803
+ /* @__PURE__ */ jsx2("div", { className: "slice-handle slice-handle-a" }),
4804
+ /* @__PURE__ */ jsx2("div", { className: "slice-handle slice-handle-b" })
4886
4805
  ] }) }),
4887
- !hasComparison && /* @__PURE__ */ jsx("div", { className: "image-diff-panel image-diff-visual", "data-panel": "image", children: /* @__PURE__ */ jsx("div", { className: "image-visual-canvas", "data-zoomable": "true", children: /* @__PURE__ */ jsx("div", { className: "image-zoom-wrap", children: /* @__PURE__ */ jsx(
4806
+ !hasComparison && /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-visual", "data-panel": "image", children: /* @__PURE__ */ jsx2("div", { className: "image-visual-canvas", "data-zoomable": "true", children: /* @__PURE__ */ jsx2("div", { className: "image-zoom-wrap", children: /* @__PURE__ */ jsx2(
4888
4807
  "img",
4889
4808
  {
4890
4809
  className: "image-layer image-layer-old",
@@ -4898,6 +4817,7 @@ function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
4898
4817
  }
4899
4818
 
4900
4819
  // src/components/diffView.tsx
4820
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "kerfjs/jsx-runtime";
4901
4821
  function DiffView({ file, diff, annotations, mode }) {
4902
4822
  const annotationsByLine = {};
4903
4823
  for (const a of annotations) {
@@ -4905,7 +4825,7 @@ function DiffView({ file, diff, annotations, mode }) {
4905
4825
  if (!(key in annotationsByLine)) annotationsByLine[key] = [];
4906
4826
  annotationsByLine[key].push(a);
4907
4827
  }
4908
- return /* @__PURE__ */ jsx(
4828
+ return /* @__PURE__ */ jsxs3(
4909
4829
  "div",
4910
4830
  {
4911
4831
  className: "diff-view",
@@ -4913,14 +4833,14 @@ function DiffView({ file, diff, annotations, mode }) {
4913
4833
  "data-file-path": file.file_path,
4914
4834
  ...isSvgFile(diff.filePath) ? { "data-is-svg": "true" } : {},
4915
4835
  children: [
4916
- /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
4917
- /* @__PURE__ */ jsx("div", { className: "diff-header-file", children: [
4918
- /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
4919
- /* @__PURE__ */ jsx("button", { className: "reveal-btn", "data-file-id": file.id, title: "Reveal in file manager", children: /* @__PURE__ */ jsx(IconReveal, {}) })
4836
+ /* @__PURE__ */ jsxs3("div", { className: "diff-header", children: [
4837
+ /* @__PURE__ */ jsxs3("div", { className: "diff-header-file", children: [
4838
+ /* @__PURE__ */ jsx3("span", { className: "file-path", children: diff.filePath }),
4839
+ /* @__PURE__ */ jsx3("button", { className: "reveal-btn", "data-file-id": file.id, title: "Reveal in file manager", children: /* @__PURE__ */ jsx3(IconReveal, {}) })
4920
4840
  ] }),
4921
- /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
4841
+ /* @__PURE__ */ jsx3("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx3("span", { className: `file-status ${diff.status}`, children: diff.status }) })
4922
4842
  ] }),
4923
- diff.isBinary && isImageFile(diff.filePath) ? /* @__PURE__ */ jsx(ImageDiff, { file, diff }) : 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 })
4843
+ diff.isBinary && isImageFile(diff.filePath) ? /* @__PURE__ */ jsx3(ImageDiff, { file, diff }) : diff.isBinary ? /* @__PURE__ */ jsx3("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx3(UnifiedDiff, { hunks: diff.hunks, annotationsByLine }) : /* @__PURE__ */ jsx3(SplitDiff, { hunks: diff.hunks, annotationsByLine })
4924
4844
  ]
4925
4845
  }
4926
4846
  );
@@ -4964,11 +4884,11 @@ function SplitDiff({ hunks, annotationsByLine }) {
4964
4884
  }
4965
4885
  }
4966
4886
  if (run.length > 0) groups.push({ type: "columns", items: run });
4967
- return /* @__PURE__ */ jsx("div", { className: "diff-table-split", children: groups.map((group) => {
4887
+ return /* @__PURE__ */ jsx3("div", { className: "diff-table-split", children: groups.map((group) => {
4968
4888
  if (group.type === "annotated") {
4969
- return /* @__PURE__ */ jsx("div", { children: [
4970
- /* @__PURE__ */ jsx("div", { className: "split-row", children: [
4971
- /* @__PURE__ */ jsx(
4889
+ return /* @__PURE__ */ jsxs3("div", { children: [
4890
+ /* @__PURE__ */ jsxs3("div", { className: "split-row", children: [
4891
+ /* @__PURE__ */ jsxs3(
4972
4892
  "div",
4973
4893
  {
4974
4894
  className: `diff-line split-left ${group.pair.left?.type || "empty"}`,
@@ -4976,124 +4896,97 @@ function SplitDiff({ hunks, annotationsByLine }) {
4976
4896
  "data-side": "old",
4977
4897
  "data-new-line": group.pair.left?.newNum ?? group.pair.right?.newNum ?? "",
4978
4898
  children: [
4979
- /* @__PURE__ */ jsx("span", { className: "gutter", "data-line-number": group.pair.left?.oldNum ?? "" }),
4980
- /* @__PURE__ */ jsx("span", { className: "code", children: renderPairContent(group.pair, "left") })
4899
+ /* @__PURE__ */ jsx3("span", { className: "gutter", "data-line-number": group.pair.left?.oldNum ?? "" }),
4900
+ /* @__PURE__ */ jsx3("span", { className: "code", children: renderPairContent(group.pair, "left") })
4981
4901
  ]
4982
4902
  }
4983
4903
  ),
4984
- /* @__PURE__ */ jsx(
4904
+ /* @__PURE__ */ jsxs3(
4985
4905
  "div",
4986
4906
  {
4987
4907
  className: `diff-line split-right ${group.pair.right?.type || "empty"}`,
4988
4908
  "data-line": group.pair.right?.newNum ?? "",
4989
4909
  "data-side": "new",
4990
4910
  children: [
4991
- /* @__PURE__ */ jsx("span", { className: "gutter", "data-line-number": group.pair.right?.newNum ?? "" }),
4992
- /* @__PURE__ */ jsx("span", { className: "code", children: renderPairContent(group.pair, "right") })
4911
+ /* @__PURE__ */ jsx3("span", { className: "gutter", "data-line-number": group.pair.right?.newNum ?? "" }),
4912
+ /* @__PURE__ */ jsx3("span", { className: "code", children: renderPairContent(group.pair, "right") })
4993
4913
  ]
4994
4914
  }
4995
4915
  )
4996
4916
  ] }),
4997
- /* @__PURE__ */ jsx(AnnotationRows, { annotations: group.annotations })
4917
+ /* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations })
4998
4918
  ] });
4999
4919
  }
5000
- return /* @__PURE__ */ jsx("div", { className: "split-columns", children: [
5001
- /* @__PURE__ */ jsx("div", { className: "split-col split-col-left", children: group.items.map((item) => {
5002
- if (item.kind === "separator") {
5003
- const { hunk, hunkIdx, gapStart, gapEnd } = item;
5004
- return /* @__PURE__ */ jsx(
5005
- "div",
5006
- {
5007
- className: "hunk-separator",
5008
- "data-hunk-idx": hunkIdx,
5009
- "data-old-start": hunk.oldStart,
5010
- "data-old-count": hunk.oldCount,
5011
- "data-new-start": hunk.newStart,
5012
- "data-new-count": hunk.newCount,
5013
- "data-gap-start": gapStart,
5014
- "data-gap-end": gapEnd,
5015
- children: [
5016
- "@@ -",
5017
- hunk.oldStart,
5018
- ",",
5019
- hunk.oldCount,
5020
- " +",
5021
- hunk.newStart,
5022
- ",",
5023
- hunk.newCount,
5024
- " @@"
5025
- ]
5026
- }
5027
- );
5028
- }
5029
- if (item.kind === "tail") {
5030
- return /* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": item.start, children: "\u2195 Show remaining lines" });
5031
- }
5032
- const { pair } = item;
5033
- return /* @__PURE__ */ jsx(
5034
- "div",
5035
- {
5036
- className: `diff-line split-left ${pair.left?.type || "empty"}`,
5037
- "data-line": pair.left?.oldNum ?? "",
5038
- "data-side": "old",
5039
- "data-new-line": pair.left?.newNum ?? pair.right?.newNum ?? "",
5040
- children: [
5041
- /* @__PURE__ */ jsx("span", { className: "gutter", "data-line-number": pair.left?.oldNum ?? "" }),
5042
- /* @__PURE__ */ jsx("span", { className: "code", children: renderPairContent(pair, "left") })
5043
- ]
5044
- }
5045
- );
5046
- }) }),
5047
- /* @__PURE__ */ jsx("div", { className: "split-col split-col-right", children: group.items.map((item) => {
5048
- if (item.kind === "separator") {
5049
- const { hunk, hunkIdx, gapStart, gapEnd } = item;
5050
- return /* @__PURE__ */ jsx(
5051
- "div",
5052
- {
5053
- className: "hunk-separator",
5054
- "data-hunk-idx": hunkIdx,
5055
- "data-old-start": hunk.oldStart,
5056
- "data-old-count": hunk.oldCount,
5057
- "data-new-start": hunk.newStart,
5058
- "data-new-count": hunk.newCount,
5059
- "data-gap-start": gapStart,
5060
- "data-gap-end": gapEnd,
5061
- children: [
5062
- "@@ -",
5063
- hunk.oldStart,
5064
- ",",
5065
- hunk.oldCount,
5066
- " +",
5067
- hunk.newStart,
5068
- ",",
5069
- hunk.newCount,
5070
- " @@"
5071
- ]
5072
- }
5073
- );
4920
+ return /* @__PURE__ */ jsxs3("div", { className: "split-columns", children: [
4921
+ /* @__PURE__ */ jsx3("div", { className: "split-col split-col-left", children: renderSplitColumn(group.items, "left") }),
4922
+ /* @__PURE__ */ jsx3("div", { className: "split-col split-col-right", children: renderSplitColumn(group.items, "right") })
4923
+ ] });
4924
+ }) });
4925
+ }
4926
+ function renderSplitColumn(items, side) {
4927
+ return /* @__PURE__ */ jsx3(Fragment, { children: items.map((item) => {
4928
+ if (item.kind === "separator") {
4929
+ const { hunk, hunkIdx, gapStart, gapEnd } = item;
4930
+ return /* @__PURE__ */ jsxs3(
4931
+ "div",
4932
+ {
4933
+ className: "hunk-separator",
4934
+ "data-hunk-idx": hunkIdx,
4935
+ "data-old-start": hunk.oldStart,
4936
+ "data-old-count": hunk.oldCount,
4937
+ "data-new-start": hunk.newStart,
4938
+ "data-new-count": hunk.newCount,
4939
+ "data-gap-start": gapStart,
4940
+ "data-gap-end": gapEnd,
4941
+ children: [
4942
+ "@@ -",
4943
+ hunk.oldStart,
4944
+ ",",
4945
+ hunk.oldCount,
4946
+ " +",
4947
+ hunk.newStart,
4948
+ ",",
4949
+ hunk.newCount,
4950
+ " @@"
4951
+ ]
5074
4952
  }
5075
- if (item.kind === "tail") {
5076
- return /* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": item.start, children: "\u2195 Show remaining lines" });
4953
+ );
4954
+ }
4955
+ if (item.kind === "tail") {
4956
+ return /* @__PURE__ */ jsx3("div", { className: "hunk-separator hunk-expander-tail", "data-start": item.start, children: "\u2195 Show remaining lines" });
4957
+ }
4958
+ const { pair } = item;
4959
+ if (side === "left") {
4960
+ return /* @__PURE__ */ jsxs3(
4961
+ "div",
4962
+ {
4963
+ className: `diff-line split-left ${pair.left?.type || "empty"}`,
4964
+ "data-line": pair.left?.oldNum ?? "",
4965
+ "data-side": "old",
4966
+ "data-new-line": pair.left?.newNum ?? pair.right?.newNum ?? "",
4967
+ children: [
4968
+ /* @__PURE__ */ jsx3("span", { className: "gutter", "data-line-number": pair.left?.oldNum ?? "" }),
4969
+ /* @__PURE__ */ jsx3("span", { className: "code", children: renderPairContent(pair, "left") })
4970
+ ]
5077
4971
  }
5078
- const { pair } = item;
5079
- return /* @__PURE__ */ jsx(
5080
- "div",
5081
- {
5082
- className: `diff-line split-right ${pair.right?.type || "empty"}`,
5083
- "data-line": pair.right?.newNum ?? "",
5084
- "data-side": "new",
5085
- children: [
5086
- /* @__PURE__ */ jsx("span", { className: "gutter", "data-line-number": pair.right?.newNum ?? "" }),
5087
- /* @__PURE__ */ jsx("span", { className: "code", children: renderPairContent(pair, "right") })
5088
- ]
5089
- }
5090
- );
5091
- }) })
5092
- ] });
4972
+ );
4973
+ }
4974
+ return /* @__PURE__ */ jsxs3(
4975
+ "div",
4976
+ {
4977
+ className: `diff-line split-right ${pair.right?.type || "empty"}`,
4978
+ "data-line": pair.right?.newNum ?? "",
4979
+ "data-side": "new",
4980
+ children: [
4981
+ /* @__PURE__ */ jsx3("span", { className: "gutter", "data-line-number": pair.right?.newNum ?? "" }),
4982
+ /* @__PURE__ */ jsx3("span", { className: "code", children: renderPairContent(pair, "right") })
4983
+ ]
4984
+ }
4985
+ );
5093
4986
  }) });
5094
4987
  }
5095
4988
  function renderSegments(segments) {
5096
- return /* @__PURE__ */ jsx(Fragment, { children: segments.map((s) => s.changed ? /* @__PURE__ */ jsx("span", { className: "char-change", children: s.text }) : /* @__PURE__ */ jsx(Fragment, { children: s.text })) });
4989
+ return /* @__PURE__ */ jsx3(Fragment, { children: segments.map((s) => s.changed ? /* @__PURE__ */ jsx3("span", { className: "char-change", children: s.text }) : /* @__PURE__ */ jsx3(Fragment, { children: s.text })) });
5097
4990
  }
5098
4991
  function renderPairContent(pair, side) {
5099
4992
  const line = side === "left" ? pair.left : pair.right;
@@ -5171,11 +5064,11 @@ function buildUnifiedCharDiffs(lines) {
5171
5064
  function UnifiedDiff({ hunks, annotationsByLine }) {
5172
5065
  const lastHunk = hunks[hunks.length - 1];
5173
5066
  const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
5174
- return /* @__PURE__ */ jsx("div", { className: "diff-table-unified", children: [
5067
+ return /* @__PURE__ */ jsxs3("div", { className: "diff-table-unified", children: [
5175
5068
  hunks.map((hunk, hunkIdx) => {
5176
5069
  const charDiffs = buildUnifiedCharDiffs(hunk.lines);
5177
- return /* @__PURE__ */ jsx("div", { className: "hunk-block", children: [
5178
- /* @__PURE__ */ jsx(
5070
+ return /* @__PURE__ */ jsxs3("div", { className: "hunk-block", children: [
5071
+ /* @__PURE__ */ jsxs3(
5179
5072
  "div",
5180
5073
  {
5181
5074
  className: "hunk-separator",
@@ -5202,43 +5095,44 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
5202
5095
  const side = line.type === "remove" ? "old" : "new";
5203
5096
  const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
5204
5097
  const segments = charDiffs.get(line);
5205
- return /* @__PURE__ */ jsx("div", { children: [
5206
- /* @__PURE__ */ jsx(
5098
+ return /* @__PURE__ */ jsxs3("div", { children: [
5099
+ /* @__PURE__ */ jsxs3(
5207
5100
  "div",
5208
5101
  {
5209
5102
  className: `diff-line ${line.type}${anns.length ? " has-annotation" : ""}`,
5210
5103
  "data-line": lineNum,
5211
5104
  "data-side": side,
5212
5105
  children: [
5213
- /* @__PURE__ */ jsx("span", { className: "gutter-old", "data-line-number": line.oldNum ?? "" }),
5214
- /* @__PURE__ */ jsx("span", { className: "gutter-new", "data-line-number": line.newNum ?? "" }),
5215
- /* @__PURE__ */ jsx("span", { className: "code", children: segments ? renderSegments(segments) : line.content })
5106
+ /* @__PURE__ */ jsx3("span", { className: "gutter-old", "data-line-number": line.oldNum ?? "" }),
5107
+ /* @__PURE__ */ jsx3("span", { className: "gutter-new", "data-line-number": line.newNum ?? "" }),
5108
+ /* @__PURE__ */ jsx3("span", { className: "code", children: segments ? renderSegments(segments) : line.content })
5216
5109
  ]
5217
5110
  }
5218
5111
  ),
5219
- anns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: anns }) : null
5112
+ anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null
5220
5113
  ] });
5221
5114
  })
5222
5115
  ] });
5223
5116
  }),
5224
- /* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
5117
+ /* @__PURE__ */ jsx3("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
5225
5118
  ] });
5226
5119
  }
5227
5120
  function AnnotationRows({ annotations }) {
5228
- return /* @__PURE__ */ jsx("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx(
5121
+ return /* @__PURE__ */ jsx3("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsxs3(
5229
5122
  "div",
5230
5123
  {
5231
5124
  className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
5125
+ "data-key": a.id,
5232
5126
  "data-annotation-id": a.id,
5233
5127
  "data-is-stale": a.is_stale ? "true" : void 0,
5234
5128
  children: [
5235
- /* @__PURE__ */ jsx("span", { className: "annotation-drag-handle", draggable: "true", title: "Drag to move", children: "\u283F" }),
5236
- /* @__PURE__ */ jsx("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
5237
- /* @__PURE__ */ jsx("span", { className: "annotation-text", children: a.content }),
5238
- /* @__PURE__ */ jsx("div", { className: "annotation-actions", children: [
5239
- a.is_stale ? /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
5240
- /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon", "data-action": "edit", title: "Edit", children: /* @__PURE__ */ jsx(IconEdit, {}) }),
5241
- /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon btn-danger", "data-action": "delete", title: "Delete", children: /* @__PURE__ */ jsx(IconTrash, {}) })
5129
+ /* @__PURE__ */ jsx3("span", { className: "annotation-drag-handle", draggable: true, title: "Drag to move", children: "\u283F" }),
5130
+ /* @__PURE__ */ jsx3("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
5131
+ /* @__PURE__ */ jsx3("span", { className: "annotation-text", children: a.content }),
5132
+ /* @__PURE__ */ jsxs3("div", { className: "annotation-actions", children: [
5133
+ a.is_stale ? /* @__PURE__ */ jsx3("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
5134
+ /* @__PURE__ */ jsx3("button", { className: "btn btn-xs btn-icon", "data-action": "edit", title: "Edit", children: /* @__PURE__ */ jsx3(IconEdit, {}) }),
5135
+ /* @__PURE__ */ jsx3("button", { className: "btn btn-xs btn-icon btn-danger", "data-action": "delete", title: "Delete", children: /* @__PURE__ */ jsx3(IconTrash, {}) })
5242
5136
  ] })
5243
5137
  ]
5244
5138
  }
@@ -5788,25 +5682,27 @@ function generateThemeId() {
5788
5682
  }
5789
5683
 
5790
5684
  // src/components/layout.tsx
5685
+ import { jsx as jsx4, jsxs as jsxs4 } from "kerfjs/jsx-runtime";
5791
5686
  function Layout({ title, reviewId, children }) {
5792
5687
  const themeId = getActiveThemeId();
5793
5688
  const themeColors = getActiveThemeColors();
5794
5689
  const themeStyle = themeToInlineStyle(themeColors);
5795
- return /* @__PURE__ */ jsx("html", { lang: "en", style: themeStyle, "data-theme": themeId, children: [
5796
- /* @__PURE__ */ jsx("head", { children: [
5797
- /* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
5798
- /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
5799
- /* @__PURE__ */ jsx("title", { children: title }),
5800
- /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "/static/styles.css" })
5690
+ return /* @__PURE__ */ jsxs4("html", { lang: "en", style: themeStyle, "data-theme": themeId, children: [
5691
+ /* @__PURE__ */ jsxs4("head", { children: [
5692
+ /* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
5693
+ /* @__PURE__ */ jsx4("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
5694
+ /* @__PURE__ */ jsx4("title", { children: title }),
5695
+ /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: "/static/styles.css" })
5801
5696
  ] }),
5802
- /* @__PURE__ */ jsx("body", { "data-review-id": reviewId, children: [
5697
+ /* @__PURE__ */ jsxs4("body", { "data-review-id": reviewId, children: [
5803
5698
  children,
5804
- /* @__PURE__ */ jsx("script", { src: "/static/app.js" })
5699
+ /* @__PURE__ */ jsx4("script", { src: "/static/app.js" })
5805
5700
  ] })
5806
5701
  ] });
5807
5702
  }
5808
5703
 
5809
5704
  // src/components/reviewHistory.tsx
5705
+ import { jsx as jsx5, jsxs as jsxs5 } from "kerfjs/jsx-runtime";
5810
5706
  function titleCase(s) {
5811
5707
  return s.replace(/[_-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
5812
5708
  }
@@ -5825,53 +5721,59 @@ function shortenArgs(args) {
5825
5721
  function ReviewHistory({ reviews, currentReviewId }) {
5826
5722
  const hasOtherReviews = reviews.some((r) => r.id !== currentReviewId);
5827
5723
  const hasCompletedOthers = reviews.some((r) => r.id !== currentReviewId && r.status === "completed");
5828
- return /* @__PURE__ */ jsx("div", { className: "history-page", children: [
5829
- /* @__PURE__ */ jsx("h1", { children: "Review History" }),
5830
- reviews.length === 0 ? /* @__PURE__ */ jsx("p", { style: "color:var(--text-dim)", children: "No previous reviews found." }) : /* @__PURE__ */ jsx("div", { children: reviews.map((r) => {
5724
+ return /* @__PURE__ */ jsxs5("div", { className: "history-page", children: [
5725
+ /* @__PURE__ */ jsx5("h1", { children: "Review History" }),
5726
+ reviews.length === 0 ? /* @__PURE__ */ jsx5("p", { style: "color:var(--text-dim)", children: "No previous reviews found." }) : /* @__PURE__ */ jsx5("div", { children: reviews.map((r) => {
5831
5727
  const isCurrent = r.id === currentReviewId;
5832
5728
  const href = isCurrent ? "/" : `/review/${r.id}`;
5833
5729
  let argsDisplay = null;
5834
5730
  if (r.mode_args !== null && r.mode_args !== "") {
5835
5731
  const { short, full } = shortenArgs(r.mode_args);
5836
- argsDisplay = full !== "" ? /* @__PURE__ */ jsx("span", { title: full, children: [
5732
+ argsDisplay = full !== "" ? /* @__PURE__ */ jsxs5("span", { title: full, children: [
5837
5733
  ": ",
5838
5734
  short
5839
- ] }) : /* @__PURE__ */ jsx("span", { children: [
5735
+ ] }) : /* @__PURE__ */ jsxs5("span", { children: [
5840
5736
  ": ",
5841
5737
  short
5842
5738
  ] });
5843
5739
  }
5844
- return /* @__PURE__ */ jsx("div", { children: [
5845
- /* @__PURE__ */ jsx("a", { href, className: "history-item-link", children: /* @__PURE__ */ jsx("div", { className: "history-item", "data-review-id": r.id, children: [
5846
- /* @__PURE__ */ jsx("h3", { children: [
5740
+ return /* @__PURE__ */ jsxs5("div", { children: [
5741
+ /* @__PURE__ */ jsx5("a", { href, className: "history-item-link", children: /* @__PURE__ */ jsxs5("div", { className: "history-item", "data-review-id": r.id, children: [
5742
+ /* @__PURE__ */ jsxs5("h3", { children: [
5847
5743
  r.repo_name,
5848
5744
  " - ",
5849
5745
  titleCase(r.mode),
5850
5746
  argsDisplay,
5851
- isCurrent ? /* @__PURE__ */ jsx("span", { className: "status-badge in_progress", style: "margin-left:8px", children: "Current" }) : null,
5852
- /* @__PURE__ */ jsx("span", { className: `status-badge ${r.status}`, style: "margin-left:8px", children: titleCase(r.status) })
5747
+ isCurrent ? /* @__PURE__ */ jsx5("span", { className: "status-badge in_progress", style: "margin-left:8px", children: "Current" }) : null,
5748
+ /* @__PURE__ */ jsx5("span", { className: `status-badge ${r.status}`, style: "margin-left:8px", children: titleCase(r.status) })
5853
5749
  ] }),
5854
- /* @__PURE__ */ jsx("div", { className: "meta", children: [
5750
+ /* @__PURE__ */ jsxs5("div", { className: "meta", children: [
5855
5751
  "ID: ",
5856
5752
  r.id,
5857
5753
  " | Created: ",
5858
- r.created_at
5754
+ /* The `Review` type says `created_at: string`, but PGLite
5755
+ actually returns a `Date` from `timestamp` columns at
5756
+ runtime, which the kerf JSX runtime rejects. The
5757
+ explicit String() coercion is intentional. */
5758
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
5759
+ String(r.created_at)
5859
5760
  ] }),
5860
- !isCurrent ? /* @__PURE__ */ jsx("button", { className: "delete-review-btn", "data-delete-id": r.id, title: "Delete review", children: /* @__PURE__ */ jsx(IconTrash16, {}) }) : null
5761
+ !isCurrent ? /* @__PURE__ */ jsx5("button", { className: "delete-review-btn", "data-delete-id": r.id, title: "Delete review", children: /* @__PURE__ */ jsx5(IconTrash16, {}) }) : null
5861
5762
  ] }) }),
5862
- isCurrent && hasOtherReviews ? /* @__PURE__ */ jsx("div", { className: "bulk-actions", children: [
5863
- /* @__PURE__ */ jsx("span", { children: "Bulk actions:" }),
5864
- hasCompletedOthers ? /* @__PURE__ */ jsx("button", { className: "btn btn-sm btn-danger", id: "delete-completed-btn", children: "Delete Completed" }) : null,
5865
- /* @__PURE__ */ jsx("button", { className: "btn btn-sm btn-danger", id: "delete-all-btn", children: "Delete All" })
5763
+ isCurrent && hasOtherReviews ? /* @__PURE__ */ jsxs5("div", { className: "bulk-actions", children: [
5764
+ /* @__PURE__ */ jsx5("span", { children: "Bulk actions:" }),
5765
+ hasCompletedOthers ? /* @__PURE__ */ jsx5("button", { className: "btn btn-sm btn-danger", id: "delete-completed-btn", children: "Delete Completed" }) : null,
5766
+ /* @__PURE__ */ jsx5("button", { className: "btn btn-sm btn-danger", id: "delete-all-btn", children: "Delete All" })
5866
5767
  ] }) : null
5867
5768
  ] });
5868
5769
  }) }),
5869
- /* @__PURE__ */ jsx("a", { href: "/", className: "btn btn-link", style: "margin-top:16px;display:inline-block", children: "Back to current review" }),
5870
- /* @__PURE__ */ jsx("script", { src: "/static/history.js" })
5770
+ /* @__PURE__ */ jsx5("a", { href: "/", className: "btn btn-link", style: "margin-top:16px;display:inline-block", children: "Back to current review" }),
5771
+ /* @__PURE__ */ jsx5("script", { src: "/static/history.js" })
5871
5772
  ] });
5872
5773
  }
5873
5774
 
5874
5775
  // src/components/fileList.tsx
5776
+ import { jsx as jsx6, jsxs as jsxs6 } from "kerfjs/jsx-runtime";
5875
5777
  function buildFileTree(files) {
5876
5778
  const root = { name: "", children: [], files: [] };
5877
5779
  for (const f of files) {
@@ -5917,21 +5819,21 @@ function hasStale(node, staleCounts) {
5917
5819
  }
5918
5820
  function TreeView({ node, depth, annotationCounts, staleCounts }) {
5919
5821
  const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
5920
- return /* @__PURE__ */ jsx("div", { children: [
5822
+ return /* @__PURE__ */ jsxs6("div", { children: [
5921
5823
  sortedChildren.map((child) => {
5922
5824
  const total = countFiles(child);
5923
5825
  const isCollapsible = total > 1;
5924
5826
  const stale = hasStale(child, staleCounts);
5925
- return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
5926
- /* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
5927
- isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
5928
- /* @__PURE__ */ jsx("span", { className: "folder-name", children: [
5827
+ return /* @__PURE__ */ jsxs6("div", { className: "folder-group", children: [
5828
+ /* @__PURE__ */ jsxs6("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
5829
+ isCollapsible ? /* @__PURE__ */ jsx6("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx6("span", { className: "folder-arrow-spacer" }),
5830
+ /* @__PURE__ */ jsxs6("span", { className: "folder-name", children: [
5929
5831
  child.name,
5930
5832
  "/"
5931
5833
  ] }),
5932
- stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
5834
+ stale ? /* @__PURE__ */ jsx6("span", { className: "stale-dot" }) : null
5933
5835
  ] }),
5934
- /* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
5836
+ /* @__PURE__ */ jsx6("div", { className: "folder-content", children: /* @__PURE__ */ jsx6(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
5935
5837
  ] });
5936
5838
  }),
5937
5839
  node.files.map((f) => {
@@ -5939,89 +5841,90 @@ function TreeView({ node, depth, annotationCounts, staleCounts }) {
5939
5841
  const count = annotationCounts[f.id] || 0;
5940
5842
  const stale = staleCounts[f.id] || 0;
5941
5843
  const fileName = f.file_path.split("/").pop() ?? "";
5942
- return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
5943
- /* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
5944
- /* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
5945
- /* @__PURE__ */ jsx("span", { className: `file-status ${diff?.status ?? ""}`, children: diff?.status ?? "" }),
5946
- stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
5947
- count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
5844
+ return /* @__PURE__ */ jsxs6("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
5845
+ /* @__PURE__ */ jsx6("span", { className: `status-dot ${f.status}` }),
5846
+ /* @__PURE__ */ jsx6("span", { className: "file-name", title: f.file_path, children: fileName }),
5847
+ /* @__PURE__ */ jsx6("span", { className: `file-status ${diff?.status ?? ""}`, children: diff?.status ?? "" }),
5848
+ stale > 0 ? /* @__PURE__ */ jsx6("span", { className: "stale-dot" }) : null,
5849
+ count > 0 ? /* @__PURE__ */ jsx6("span", { className: "annotation-count", children: count }) : null
5948
5850
  ] });
5949
5851
  })
5950
5852
  ] });
5951
5853
  }
5952
5854
  function FileList({ files, annotationCounts, staleCounts }) {
5953
5855
  const tree = buildFileTree(files);
5954
- 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 }) }) });
5856
+ return /* @__PURE__ */ jsx6("div", { className: "file-list", children: /* @__PURE__ */ jsx6("div", { className: "file-list-items", children: /* @__PURE__ */ jsx6(TreeView, { node: tree, depth: 0, annotationCounts, staleCounts }) }) });
5955
5857
  }
5956
5858
 
5957
5859
  // src/components/reviewShell.tsx
5860
+ import { jsx as jsx7, jsxs as jsxs7 } from "kerfjs/jsx-runtime";
5958
5861
  function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, footer }) {
5959
- return /* @__PURE__ */ jsx("div", { className: "review-app", "data-review-id": reviewId, children: [
5960
- /* @__PURE__ */ jsx("div", { id: "update-banner", className: "update-banner", style: "display:none", children: [
5961
- /* @__PURE__ */ jsx("span", { id: "update-banner-label", children: "Update available" }),
5962
- /* @__PURE__ */ jsx("div", { className: "update-banner-actions", children: [
5963
- /* @__PURE__ */ jsx("button", { id: "update-install-btn", className: "btn btn-sm btn-accent", children: "Install Update" }),
5964
- /* @__PURE__ */ jsx("button", { id: "update-banner-dismiss", className: "btn btn-sm", children: "Later" })
5862
+ return /* @__PURE__ */ jsxs7("div", { className: "review-app", "data-review-id": reviewId, children: [
5863
+ /* @__PURE__ */ jsxs7("div", { id: "update-banner", className: "update-banner", style: "display:none", children: [
5864
+ /* @__PURE__ */ jsx7("span", { id: "update-banner-label", children: "Update available" }),
5865
+ /* @__PURE__ */ jsxs7("div", { className: "update-banner-actions", children: [
5866
+ /* @__PURE__ */ jsx7("button", { id: "update-install-btn", className: "btn btn-sm btn-accent", children: "Install Update" }),
5867
+ /* @__PURE__ */ jsx7("button", { id: "update-banner-dismiss", className: "btn btn-sm", children: "Later" })
5965
5868
  ] })
5966
5869
  ] }),
5967
- /* @__PURE__ */ jsx("div", { className: "review-body", children: [
5968
- /* @__PURE__ */ jsx("aside", { className: "sidebar", children: [
5969
- /* @__PURE__ */ jsx("div", { className: "sidebar-header", children: [
5970
- /* @__PURE__ */ jsx("h2", { children: review.repo_name }),
5971
- /* @__PURE__ */ jsx("span", { className: "review-mode", children: [
5870
+ /* @__PURE__ */ jsxs7("div", { className: "review-body", children: [
5871
+ /* @__PURE__ */ jsxs7("aside", { className: "sidebar", children: [
5872
+ /* @__PURE__ */ jsxs7("div", { className: "sidebar-header", children: [
5873
+ /* @__PURE__ */ jsx7("h2", { children: review.repo_name }),
5874
+ /* @__PURE__ */ jsxs7("span", { className: "review-mode", children: [
5972
5875
  review.mode,
5973
5876
  review.mode_args !== null && review.mode_args !== "" ? `: ${review.mode_args}` : ""
5974
5877
  ] })
5975
5878
  ] }),
5976
- /* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
5977
- /* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts }),
5978
- /* @__PURE__ */ jsx("div", { className: "sidebar-share", id: "sidebar-share" }),
5979
- /* @__PURE__ */ jsx("div", { className: "sidebar-footer", children: footer })
5879
+ /* @__PURE__ */ jsx7("div", { className: "file-filter", children: /* @__PURE__ */ jsx7("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
5880
+ /* @__PURE__ */ jsx7(FileList, { files, annotationCounts, staleCounts }),
5881
+ /* @__PURE__ */ jsx7("div", { className: "sidebar-share", id: "sidebar-share" }),
5882
+ /* @__PURE__ */ jsx7("div", { className: "sidebar-footer", children: footer })
5980
5883
  ] }),
5981
- /* @__PURE__ */ jsx("div", { className: "sidebar-resize", id: "sidebar-resize" }),
5982
- /* @__PURE__ */ jsx("main", { className: "main-content", children: [
5983
- /* @__PURE__ */ jsx("div", { className: "welcome-message", children: [
5984
- /* @__PURE__ */ jsx("h3", { children: "Select a file to begin reviewing" }),
5985
- /* @__PURE__ */ jsx("p", { children: [
5884
+ /* @__PURE__ */ jsx7("div", { className: "sidebar-resize", id: "sidebar-resize" }),
5885
+ /* @__PURE__ */ jsxs7("main", { className: "main-content", children: [
5886
+ /* @__PURE__ */ jsxs7("div", { className: "welcome-message", children: [
5887
+ /* @__PURE__ */ jsx7("h3", { children: "Select a file to begin reviewing" }),
5888
+ /* @__PURE__ */ jsxs7("p", { children: [
5986
5889
  files.length,
5987
5890
  " file(s) to review"
5988
5891
  ] }),
5989
- /* @__PURE__ */ jsx("p", { className: "progress-summary", id: "progress-summary" })
5892
+ /* @__PURE__ */ jsx7("p", { className: "progress-summary", id: "progress-summary" })
5990
5893
  ] }),
5991
- /* @__PURE__ */ jsx("div", { className: "diff-nav-bar", id: "diff-nav-bar", style: "display:none", children: [
5992
- /* @__PURE__ */ jsx("button", { className: "nav-btn disabled", id: "nav-back-btn", disabled: true, title: "Back", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) }) }),
5993
- /* @__PURE__ */ jsx("button", { className: "nav-btn disabled", id: "nav-forward-btn", disabled: true, title: "Forward", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }) }) }),
5994
- /* @__PURE__ */ jsx("span", { className: "nav-file-path", id: "nav-file-path" })
5894
+ /* @__PURE__ */ jsxs7("div", { className: "diff-nav-bar", id: "diff-nav-bar", style: "display:none", children: [
5895
+ /* @__PURE__ */ jsx7("button", { className: "nav-btn disabled", id: "nav-back-btn", disabled: true, title: "Back", children: /* @__PURE__ */ jsx7("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx7("path", { d: "m15 18-6-6 6-6" }) }) }),
5896
+ /* @__PURE__ */ jsx7("button", { className: "nav-btn disabled", id: "nav-forward-btn", disabled: true, title: "Forward", children: /* @__PURE__ */ jsx7("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx7("path", { d: "m9 18 6-6-6-6" }) }) }),
5897
+ /* @__PURE__ */ jsx7("span", { className: "nav-file-path", id: "nav-file-path" })
5995
5898
  ] }),
5996
- /* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
5997
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
5998
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-svg-toggle", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
5999
- /* @__PURE__ */ jsx("button", { className: "segment active", "data-svg-mode": "code", children: "Code" }),
6000
- /* @__PURE__ */ jsx("button", { className: "segment", "data-svg-mode": "rendered", children: "Rendered" })
5899
+ /* @__PURE__ */ jsx7("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
5900
+ /* @__PURE__ */ jsxs7("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
5901
+ /* @__PURE__ */ jsx7("div", { className: "diff-toolbar-svg-toggle", style: "display:none", children: /* @__PURE__ */ jsxs7("div", { className: "segmented-control", children: [
5902
+ /* @__PURE__ */ jsx7("button", { className: "segment active", "data-svg-mode": "code", children: "Code" }),
5903
+ /* @__PURE__ */ jsx7("button", { className: "segment", "data-svg-mode": "rendered", children: "Rendered" })
6001
5904
  ] }) }),
6002
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-text", children: [
6003
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
6004
- /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
6005
- /* @__PURE__ */ jsx("button", { className: "segment active", "data-diff-mode": "split", children: "Split" }),
6006
- /* @__PURE__ */ jsx("button", { className: "segment", "data-diff-mode": "unified", children: "Unified" })
5905
+ /* @__PURE__ */ jsxs7("div", { className: "diff-toolbar-text", children: [
5906
+ /* @__PURE__ */ jsxs7("div", { className: "diff-toolbar-left", children: [
5907
+ /* @__PURE__ */ jsxs7("div", { className: "segmented-control", children: [
5908
+ /* @__PURE__ */ jsx7("button", { className: "segment active", "data-diff-mode": "split", children: "Split" }),
5909
+ /* @__PURE__ */ jsx7("button", { className: "segment", "data-diff-mode": "unified", children: "Unified" })
6007
5910
  ] }),
6008
- /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "wrap-toggle", children: "Wrap" }),
6009
- /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "whitespace-toggle", children: "Ignore Whitespace" })
5911
+ /* @__PURE__ */ jsx7("button", { className: "toolbar-btn", id: "wrap-toggle", children: "Wrap" }),
5912
+ /* @__PURE__ */ jsx7("button", { className: "toolbar-btn", id: "whitespace-toggle", children: "Ignore Whitespace" })
6010
5913
  ] }),
6011
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-right", children: /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "language-btn", children: "Plain Text" }) })
5914
+ /* @__PURE__ */ jsx7("div", { className: "diff-toolbar-right", children: /* @__PURE__ */ jsx7("button", { className: "toolbar-btn", id: "language-btn", children: "Plain Text" }) })
6012
5915
  ] }),
6013
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-image", style: "display:none", children: [
6014
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
6015
- /* @__PURE__ */ jsx("button", { className: "segment active", "data-image-mode": "metadata", children: "Metadata" }),
6016
- /* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "difference", children: "Difference" }),
6017
- /* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "slice", children: "Slice" }),
6018
- /* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "image", style: "display:none", children: "Image" })
5916
+ /* @__PURE__ */ jsxs7("div", { className: "diff-toolbar-image", style: "display:none", children: [
5917
+ /* @__PURE__ */ jsx7("div", { className: "diff-toolbar-left", children: /* @__PURE__ */ jsxs7("div", { className: "segmented-control", children: [
5918
+ /* @__PURE__ */ jsx7("button", { className: "segment active", "data-image-mode": "metadata", children: "Metadata" }),
5919
+ /* @__PURE__ */ jsx7("button", { className: "segment", "data-image-mode": "difference", children: "Difference" }),
5920
+ /* @__PURE__ */ jsx7("button", { className: "segment", "data-image-mode": "slice", children: "Slice" }),
5921
+ /* @__PURE__ */ jsx7("button", { className: "segment", "data-image-mode": "image", style: "display:none", children: "Image" })
6019
5922
  ] }) }),
6020
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-right", children: [
6021
- /* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "out", title: "Zoom out", children: /* @__PURE__ */ jsx(IconZoomOut, {}) }),
6022
- /* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "fit", title: "Fit to view", children: /* @__PURE__ */ jsx(IconFit, {}) }),
6023
- /* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "actual", title: "Actual size (1:1)", children: /* @__PURE__ */ jsx(IconActualSize, {}) }),
6024
- /* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "in", title: "Zoom in", children: /* @__PURE__ */ jsx(IconZoomIn, {}) })
5923
+ /* @__PURE__ */ jsxs7("div", { className: "diff-toolbar-right", children: [
5924
+ /* @__PURE__ */ jsx7("button", { className: "image-zoom-btn", "data-zoom-action": "out", title: "Zoom out", children: /* @__PURE__ */ jsx7(IconZoomOut, {}) }),
5925
+ /* @__PURE__ */ jsx7("button", { className: "image-zoom-btn", "data-zoom-action": "fit", title: "Fit to view", children: /* @__PURE__ */ jsx7(IconFit, {}) }),
5926
+ /* @__PURE__ */ jsx7("button", { className: "image-zoom-btn", "data-zoom-action": "actual", title: "Actual size (1:1)", children: /* @__PURE__ */ jsx7(IconActualSize, {}) }),
5927
+ /* @__PURE__ */ jsx7("button", { className: "image-zoom-btn", "data-zoom-action": "in", title: "Zoom in", children: /* @__PURE__ */ jsx7(IconZoomIn, {}) })
6025
5928
  ] })
6026
5929
  ] })
6027
5930
  ] })
@@ -6032,22 +5935,21 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
6032
5935
 
6033
5936
  // src/routes/pages.tsx
6034
5937
  init_queries();
5938
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "kerfjs/jsx-runtime";
6035
5939
  var pageRoutes = new Hono14();
6036
5940
  pageRoutes.get("/", async (c) => {
6037
5941
  const reviewId = c.get("reviewId");
6038
5942
  const review = await getReview(reviewId);
6039
5943
  if (!review) return c.text("Review not found", 404);
6040
- const files = await getReviewFiles(reviewId);
6041
- const annotationCounts = {};
6042
- for (const f of files) {
6043
- const anns = await getAnnotationsForFile(f.id);
6044
- annotationCounts[f.id] = anns.length;
6045
- }
6046
- const footer = /* @__PURE__ */ jsx(Fragment, { children: [
6047
- /* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
6048
- /* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" })
5944
+ const [files, annotationCounts] = await Promise.all([
5945
+ getReviewFiles(reviewId),
5946
+ getAnnotationCountsForReview(reviewId)
5947
+ ]);
5948
+ const footer = /* @__PURE__ */ jsxs8(Fragment2, { children: [
5949
+ /* @__PURE__ */ jsx8("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
5950
+ /* @__PURE__ */ jsx8("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" })
6049
5951
  ] });
6050
- const html = /* @__PURE__ */ jsx(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx(ReviewShell, { reviewId, review, files, annotationCounts, staleCounts: {}, footer }) });
5952
+ const html = /* @__PURE__ */ jsx8(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx8(ReviewShell, { reviewId, review, files, annotationCounts, staleCounts: {}, footer }) });
6051
5953
  return c.html(html.toString());
6052
5954
  });
6053
5955
  pageRoutes.get("/file/:fileId", async (c) => {
@@ -6078,15 +5980,15 @@ pageRoutes.get("/file/:fileId", async (c) => {
6078
5980
  fontWarning = true;
6079
5981
  }
6080
5982
  }
6081
- const html2 = /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, "data-is-svg": "true", children: [
6082
- /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
6083
- /* @__PURE__ */ jsx("div", { className: "diff-header-file", children: [
6084
- /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
6085
- /* @__PURE__ */ jsx("button", { className: "reveal-btn", "data-file-id": file.id, title: "Reveal in file manager", children: /* @__PURE__ */ jsx(IconReveal, {}) })
5983
+ const html2 = /* @__PURE__ */ jsxs8("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, "data-is-svg": "true", children: [
5984
+ /* @__PURE__ */ jsxs8("div", { className: "diff-header", children: [
5985
+ /* @__PURE__ */ jsxs8("div", { className: "diff-header-file", children: [
5986
+ /* @__PURE__ */ jsx8("span", { className: "file-path", children: diff.filePath }),
5987
+ /* @__PURE__ */ jsx8("button", { className: "reveal-btn", "data-file-id": file.id, title: "Reveal in file manager", children: /* @__PURE__ */ jsx8(IconReveal, {}) })
6086
5988
  ] }),
6087
- /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
5989
+ /* @__PURE__ */ jsx8("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx8("span", { className: `file-status ${diff.status}`, children: diff.status }) })
6088
5990
  ] }),
6089
- /* @__PURE__ */ jsx(
5991
+ /* @__PURE__ */ jsx8(
6090
5992
  ImageDiff,
6091
5993
  {
6092
5994
  file,
@@ -6112,7 +6014,7 @@ pageRoutes.get("/file/:fileId", async (c) => {
6112
6014
  }
6113
6015
  }
6114
6016
  }
6115
- const html = /* @__PURE__ */ jsx(DiffView, { file, diff: finalDiff, annotations, mode });
6017
+ const html = /* @__PURE__ */ jsx8(DiffView, { file, diff: finalDiff, annotations, mode });
6116
6018
  return c.html(html.toString());
6117
6019
  });
6118
6020
  pageRoutes.get("/file-raw", (c) => {
@@ -6121,7 +6023,7 @@ pageRoutes.get("/file-raw", (c) => {
6121
6023
  const repoRoot = c.get("repoRoot");
6122
6024
  let content;
6123
6025
  try {
6124
- content = readFileSync11(resolve6(repoRoot, filePath), "utf-8");
6026
+ content = readFileSync11(resolve7(repoRoot, filePath), "utf-8");
6125
6027
  } catch {
6126
6028
  return c.text("File not found", 404);
6127
6029
  }
@@ -6145,7 +6047,7 @@ pageRoutes.get("/file-raw", (c) => {
6145
6047
  }]
6146
6048
  };
6147
6049
  const fakeFile = { id: "", review_id: "", file_path: filePath, status: "reviewed", diff_data: null, created_at: "" };
6148
- const html = /* @__PURE__ */ jsx(DiffView, { file: fakeFile, diff, annotations: [], mode: "unified" });
6050
+ const html = /* @__PURE__ */ jsx8(DiffView, { file: fakeFile, diff, annotations: [], mode: "unified" });
6149
6051
  return c.html(html.toString());
6150
6052
  });
6151
6053
  pageRoutes.get("/review/:reviewId", async (c) => {
@@ -6156,25 +6058,23 @@ pageRoutes.get("/review/:reviewId", async (c) => {
6156
6058
  }
6157
6059
  const review = await getReview(reviewId);
6158
6060
  if (!review) return c.text("Review not found", 404);
6159
- const files = await getReviewFiles(reviewId);
6160
- const annotationCounts = {};
6161
- for (const f of files) {
6162
- const anns = await getAnnotationsForFile(f.id);
6163
- annotationCounts[f.id] = anns.length;
6164
- }
6165
- const footer = /* @__PURE__ */ jsx(Fragment, { children: [
6166
- review.status === "completed" ? /* @__PURE__ */ jsx("button", { className: "btn btn-primary", id: "reopen-review", children: "Reopen Review" }) : /* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
6167
- /* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" }),
6168
- /* @__PURE__ */ jsx("a", { href: "/", className: "btn btn-sm btn-link", children: "Back to current review" })
6061
+ const [files, annotationCounts] = await Promise.all([
6062
+ getReviewFiles(reviewId),
6063
+ getAnnotationCountsForReview(reviewId)
6064
+ ]);
6065
+ const footer = /* @__PURE__ */ jsxs8(Fragment2, { children: [
6066
+ review.status === "completed" ? /* @__PURE__ */ jsx8("button", { className: "btn btn-primary", id: "reopen-review", children: "Reopen Review" }) : /* @__PURE__ */ jsx8("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
6067
+ /* @__PURE__ */ jsx8("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" }),
6068
+ /* @__PURE__ */ jsx8("a", { href: "/", className: "btn btn-sm btn-link", children: "Back to current review" })
6169
6069
  ] });
6170
- const html = /* @__PURE__ */ jsx(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx(ReviewShell, { reviewId, review, files, annotationCounts, staleCounts: {}, footer }) });
6070
+ const html = /* @__PURE__ */ jsx8(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx8(ReviewShell, { reviewId, review, files, annotationCounts, staleCounts: {}, footer }) });
6171
6071
  return c.html(html.toString());
6172
6072
  });
6173
6073
  pageRoutes.get("/history", async (c) => {
6174
6074
  const repoRoot = c.get("repoRoot");
6175
6075
  const currentReviewId = c.get("reviewId");
6176
6076
  const reviews = await listReviews(repoRoot);
6177
- const html = /* @__PURE__ */ jsx(Layout, { title: "Review History", reviewId: "", children: /* @__PURE__ */ jsx(ReviewHistory, { reviews, currentReviewId }) });
6077
+ const html = /* @__PURE__ */ jsx8(Layout, { title: "Review History", reviewId: "", children: /* @__PURE__ */ jsx8(ReviewHistory, { reviews, currentReviewId }) });
6178
6078
  return c.html(html.toString());
6179
6079
  });
6180
6080
 
@@ -6314,10 +6214,10 @@ themeApiRoutes.delete("/:id", (c) => {
6314
6214
 
6315
6215
  // src/server.ts
6316
6216
  function tryServe(appFetch, port) {
6317
- return new Promise((resolve8, reject) => {
6217
+ return new Promise((resolve9, reject) => {
6318
6218
  const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
6319
6219
  server.on("listening", () => {
6320
- resolve8(port);
6220
+ resolve9(port);
6321
6221
  });
6322
6222
  server.on("error", (err) => {
6323
6223
  if (err.code === "EADDRINUSE") {
@@ -6387,8 +6287,10 @@ async function startServer(port, reviewId, repoRoot, options) {
6387
6287
  } catch {
6388
6288
  }
6389
6289
  if (options?.noOpen !== true) {
6390
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
6391
- exec(`${openCmd} ${url}`);
6290
+ try {
6291
+ openOS(url, "url");
6292
+ } catch {
6293
+ }
6392
6294
  }
6393
6295
  }
6394
6296
 
@@ -6547,10 +6449,10 @@ function isFirstUseToday() {
6547
6449
  return last !== today;
6548
6450
  }
6549
6451
  function fetchLatestVersion() {
6550
- return new Promise((resolve8) => {
6452
+ return new Promise((resolve9) => {
6551
6453
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
6552
6454
  if (res.statusCode !== 200) {
6553
- resolve8(null);
6455
+ resolve9(null);
6554
6456
  return;
6555
6457
  }
6556
6458
  let data = "";
@@ -6559,18 +6461,18 @@ function fetchLatestVersion() {
6559
6461
  });
6560
6462
  res.on("end", () => {
6561
6463
  try {
6562
- resolve8(JSON.parse(data).version);
6464
+ resolve9(JSON.parse(data).version);
6563
6465
  } catch {
6564
- resolve8(null);
6466
+ resolve9(null);
6565
6467
  }
6566
6468
  });
6567
6469
  });
6568
6470
  req.on("error", () => {
6569
- resolve8(null);
6471
+ resolve9(null);
6570
6472
  });
6571
6473
  req.on("timeout", () => {
6572
6474
  req.destroy();
6573
- resolve8(null);
6475
+ resolve9(null);
6574
6476
  });
6575
6477
  });
6576
6478
  }
@@ -6708,7 +6610,7 @@ function parseArgs(argv) {
6708
6610
  port = parseInt(args[++i], 10);
6709
6611
  break;
6710
6612
  case "--data-dir":
6711
- dataDir = resolve7(args[++i]);
6613
+ dataDir = resolve8(args[++i]);
6712
6614
  break;
6713
6615
  case "--resume":
6714
6616
  resume = true;
@@ -6764,7 +6666,7 @@ async function main() {
6764
6666
  console.log("AI service test mode enabled \u2014 using mock AI responses");
6765
6667
  }
6766
6668
  if (debug) {
6767
- console.log(`[debug] Build timestamp: ${"2026-05-03T02:47:39.743Z"}`);
6669
+ console.log(`[debug] Build timestamp: ${"2026-05-13T06:55:27.037Z"}`);
6768
6670
  }
6769
6671
  if (projectDir !== null) {
6770
6672
  process.chdir(projectDir);