glassbox 0.3.6 → 0.4.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
@@ -88,7 +88,9 @@ var init_schema = __esm({
88
88
  sort_mode TEXT NOT NULL DEFAULT 'folder',
89
89
  risk_sort_dimension TEXT NOT NULL DEFAULT 'aggregate',
90
90
  show_risk_scores BOOLEAN NOT NULL DEFAULT FALSE,
91
- ignore_whitespace BOOLEAN NOT NULL DEFAULT FALSE
91
+ ignore_whitespace BOOLEAN NOT NULL DEFAULT FALSE,
92
+ svg_view_mode TEXT NOT NULL DEFAULT 'code',
93
+ last_image_mode TEXT NOT NULL DEFAULT 'metadata'
92
94
  );
93
95
  `;
94
96
  }
@@ -144,6 +146,8 @@ async function initSchema(db2) {
144
146
  await addColumnIfMissing(db2, "ai_analyses", "progress_completed", "INTEGER NOT NULL DEFAULT 0");
145
147
  await addColumnIfMissing(db2, "ai_analyses", "progress_total", "INTEGER NOT NULL DEFAULT 0");
146
148
  await addColumnIfMissing(db2, "user_preferences", "ignore_whitespace", "BOOLEAN NOT NULL DEFAULT FALSE");
149
+ await addColumnIfMissing(db2, "user_preferences", "svg_view_mode", "TEXT NOT NULL DEFAULT 'code'");
150
+ await addColumnIfMissing(db2, "user_preferences", "last_image_mode", "TEXT NOT NULL DEFAULT 'metadata'");
147
151
  await db2.exec(
148
152
  `UPDATE ai_analyses SET status = 'failed', error_message = 'Interrupted (server restarted)' WHERE status = 'running'`
149
153
  );
@@ -391,7 +395,7 @@ init_queries();
391
395
  init_connection();
392
396
  import { mkdirSync as mkdirSync7 } from "fs";
393
397
  import { tmpdir } from "os";
394
- import { join as join9, resolve as resolve3 } from "path";
398
+ import { join as join10, resolve as resolve4 } from "path";
395
399
 
396
400
  // src/debug.ts
397
401
  var debugEnabled = false;
@@ -800,7 +804,7 @@ async function getUserPreferences() {
800
804
  ["singleton"]
801
805
  );
802
806
  if (result.rows.length === 0) {
803
- return { sort_mode: "folder", risk_sort_dimension: "aggregate", show_risk_scores: false, ignore_whitespace: false };
807
+ return { sort_mode: "folder", risk_sort_dimension: "aggregate", show_risk_scores: false, ignore_whitespace: false, svg_view_mode: "code", last_image_mode: "metadata" };
804
808
  }
805
809
  return result.rows[0];
806
810
  }
@@ -809,14 +813,16 @@ async function saveUserPreferences(prefs) {
809
813
  const current = await getUserPreferences();
810
814
  const merged = { ...current, ...prefs };
811
815
  await db2.query(
812
- `INSERT INTO user_preferences (id, sort_mode, risk_sort_dimension, show_risk_scores, ignore_whitespace)
813
- VALUES ($1, $2, $3, $4, $5)
816
+ `INSERT INTO user_preferences (id, sort_mode, risk_sort_dimension, show_risk_scores, ignore_whitespace, svg_view_mode, last_image_mode)
817
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
814
818
  ON CONFLICT (id) DO UPDATE SET
815
819
  sort_mode = EXCLUDED.sort_mode,
816
820
  risk_sort_dimension = EXCLUDED.risk_sort_dimension,
817
821
  show_risk_scores = EXCLUDED.show_risk_scores,
818
- ignore_whitespace = EXCLUDED.ignore_whitespace`,
819
- ["singleton", merged.sort_mode, merged.risk_sort_dimension, merged.show_risk_scores, merged.ignore_whitespace]
822
+ ignore_whitespace = EXCLUDED.ignore_whitespace,
823
+ svg_view_mode = EXCLUDED.svg_view_mode,
824
+ last_image_mode = EXCLUDED.last_image_mode`,
825
+ ["singleton", merged.sort_mode, merged.risk_sort_dimension, merged.show_risk_scores, merged.ignore_whitespace, merged.svg_view_mode, merged.last_image_mode]
820
826
  );
821
827
  }
822
828
 
@@ -1717,9 +1723,9 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1717
1723
  // src/server.ts
1718
1724
  import { serve } from "@hono/node-server";
1719
1725
  import { exec } from "child_process";
1720
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
1726
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
1721
1727
  import { Hono as Hono4 } from "hono";
1722
- import { dirname, join as join6 } from "path";
1728
+ import { dirname, join as join7 } from "path";
1723
1729
  import { fileURLToPath } from "url";
1724
1730
 
1725
1731
  // src/routes/ai-api.ts
@@ -2439,8 +2445,8 @@ function isRetriable(err) {
2439
2445
  return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
2440
2446
  }
2441
2447
  function sleep(ms) {
2442
- return new Promise((resolve4) => {
2443
- setTimeout(resolve4, ms);
2448
+ return new Promise((resolve5) => {
2449
+ setTimeout(resolve5, ms);
2444
2450
  });
2445
2451
  }
2446
2452
 
@@ -2484,8 +2490,8 @@ function randomLines(count) {
2484
2490
  return lines.sort((a, b) => a.line - b.line);
2485
2491
  }
2486
2492
  function sleep2(ms) {
2487
- return new Promise((resolve4) => {
2488
- setTimeout(resolve4, ms);
2493
+ return new Promise((resolve5) => {
2494
+ setTimeout(resolve5, ms);
2489
2495
  });
2490
2496
  }
2491
2497
  async function mockRiskAnalysisBatch(files) {
@@ -2937,9 +2943,10 @@ aiApiRoutes.post("/preferences", async (c) => {
2937
2943
 
2938
2944
  // src/routes/api.ts
2939
2945
  init_queries();
2940
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2946
+ import { execSync as execSync5 } from "child_process";
2947
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2941
2948
  import { Hono as Hono2 } from "hono";
2942
- import { join as join5 } from "path";
2949
+ import { join as join6, resolve as resolve3 } from "path";
2943
2950
 
2944
2951
  // src/export/generate.ts
2945
2952
  init_queries();
@@ -3103,6 +3110,9 @@ function isImageFile(filePath) {
3103
3110
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3104
3111
  return IMAGE_EXTENSIONS.has(ext);
3105
3112
  }
3113
+ function isSvgFile(filePath) {
3114
+ return filePath.slice(filePath.lastIndexOf(".")).toLowerCase() === ".svg";
3115
+ }
3106
3116
  function getContentType(filePath) {
3107
3117
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3108
3118
  switch (ext) {
@@ -3397,6 +3407,139 @@ function parseWebp(data) {
3397
3407
  return { format: "webp", width, height, colorSpace: "srgb", channels: hasAlpha ? 4 : 3, depth: null, hasAlpha, density: null, exif: null };
3398
3408
  }
3399
3409
 
3410
+ // src/git/svg-rasterize.ts
3411
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3412
+ import { createRequire } from "module";
3413
+ import { join as join5 } from "path";
3414
+ var initialized = false;
3415
+ var ResvgClass;
3416
+ var fontBuffers = [];
3417
+ async function ensureInit() {
3418
+ if (initialized) return;
3419
+ const require2 = createRequire(import.meta.url);
3420
+ const resvgPath = require2.resolve("@resvg/resvg-wasm");
3421
+ const wasmPath = resvgPath.replace(/index\.(js|mjs)$/, "index_bg.wasm");
3422
+ const wasmBuffer = readFileSync6(wasmPath);
3423
+ const mod = await import("@resvg/resvg-wasm");
3424
+ await mod.initWasm(wasmBuffer);
3425
+ ResvgClass = mod.Resvg;
3426
+ fontBuffers = loadSystemFonts();
3427
+ initialized = true;
3428
+ }
3429
+ function loadSystemFonts() {
3430
+ const buffers = [];
3431
+ const candidates = getFontCandidates();
3432
+ for (const path of candidates) {
3433
+ if (!existsSync4(path)) continue;
3434
+ try {
3435
+ buffers.push(readFileSync6(path));
3436
+ } catch {
3437
+ }
3438
+ }
3439
+ return buffers;
3440
+ }
3441
+ function getFontCandidates() {
3442
+ switch (process.platform) {
3443
+ case "darwin": {
3444
+ const sys = "/System/Library/Fonts";
3445
+ const sup = "/System/Library/Fonts/Supplemental";
3446
+ return [
3447
+ // Core system fonts (serif, sans-serif, monospace)
3448
+ join5(sys, "Helvetica.ttc"),
3449
+ join5(sys, "Times.ttc"),
3450
+ join5(sys, "Courier.ttc"),
3451
+ join5(sys, "Menlo.ttc"),
3452
+ join5(sys, "SFPro.ttf"),
3453
+ join5(sys, "SFNS.ttf"),
3454
+ join5(sys, "SFNSMono.ttf"),
3455
+ // Supplemental (common named fonts in SVGs)
3456
+ join5(sup, "Arial.ttf"),
3457
+ join5(sup, "Arial Bold.ttf"),
3458
+ join5(sup, "Georgia.ttf"),
3459
+ join5(sup, "Verdana.ttf"),
3460
+ join5(sup, "Tahoma.ttf"),
3461
+ join5(sup, "Trebuchet MS.ttf"),
3462
+ join5(sup, "Impact.ttf"),
3463
+ join5(sup, "Comic Sans MS.ttf"),
3464
+ join5(sup, "Courier New.ttf"),
3465
+ join5(sup, "Times New Roman.ttf")
3466
+ ];
3467
+ }
3468
+ case "linux":
3469
+ return [
3470
+ // DejaVu (most common Linux fallback)
3471
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
3472
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
3473
+ "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
3474
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
3475
+ // Liberation (metric-compatible with Arial/Times/Courier)
3476
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
3477
+ "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
3478
+ "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
3479
+ // Noto (common on modern distros)
3480
+ "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
3481
+ ];
3482
+ case "win32": {
3483
+ const winFonts = join5(process.env.WINDIR ?? "C:\\Windows", "Fonts");
3484
+ return [
3485
+ join5(winFonts, "arial.ttf"),
3486
+ join5(winFonts, "arialbd.ttf"),
3487
+ join5(winFonts, "times.ttf"),
3488
+ join5(winFonts, "cour.ttf"),
3489
+ join5(winFonts, "verdana.ttf"),
3490
+ join5(winFonts, "tahoma.ttf"),
3491
+ join5(winFonts, "georgia.ttf"),
3492
+ join5(winFonts, "consola.ttf"),
3493
+ join5(winFonts, "segoeui.ttf")
3494
+ ];
3495
+ }
3496
+ default:
3497
+ return [];
3498
+ }
3499
+ }
3500
+ function parseSvgDimensions(svg) {
3501
+ const widthMatch = svg.match(/\bwidth\s*=\s*["']([^"']+)["']/);
3502
+ const heightMatch = svg.match(/\bheight\s*=\s*["']([^"']+)["']/);
3503
+ const viewBoxMatch = svg.match(/\bviewBox\s*=\s*["']([^"']+)["']/);
3504
+ let width = widthMatch ? parseFloat(widthMatch[1]) : NaN;
3505
+ let height = heightMatch ? parseFloat(heightMatch[1]) : NaN;
3506
+ if ((isNaN(width) || isNaN(height)) && viewBoxMatch) {
3507
+ const parts = viewBoxMatch[1].split(/[\s,]+/);
3508
+ if (parts.length >= 4) {
3509
+ if (isNaN(width)) width = parseFloat(parts[2]);
3510
+ if (isNaN(height)) height = parseFloat(parts[3]);
3511
+ }
3512
+ }
3513
+ if (isNaN(width)) width = 300;
3514
+ if (isNaN(height)) height = 150;
3515
+ return { width, height };
3516
+ }
3517
+ function svgUsesExternalFonts(svgData) {
3518
+ const svg = svgData.toString("utf-8");
3519
+ return /<text[\s>]/i.test(svg) || /font-family/i.test(svg) || /@font-face/i.test(svg);
3520
+ }
3521
+ async function rasterizeSvg(svgData) {
3522
+ await ensureInit();
3523
+ const svgString = svgData.toString("utf-8");
3524
+ const { width, height } = parseSvgDimensions(svgString);
3525
+ const maxDim = Math.max(width, height);
3526
+ const scale = Math.min(10, 8e3 / maxDim);
3527
+ const targetWidth = Math.round(width * scale);
3528
+ const resvg = new ResvgClass(svgString, {
3529
+ fitTo: { mode: "width", value: targetWidth },
3530
+ font: {
3531
+ loadSystemFonts: false,
3532
+ fontBuffers,
3533
+ defaultFontFamily: "Helvetica"
3534
+ }
3535
+ });
3536
+ const rendered = resvg.render();
3537
+ const png = Buffer.from(rendered.asPng());
3538
+ rendered.free();
3539
+ resvg.free();
3540
+ return png;
3541
+ }
3542
+
3400
3543
  // src/outline/parser.ts
3401
3544
  var BRACE_LANGS = /* @__PURE__ */ new Set([
3402
3545
  "javascript",
@@ -3826,6 +3969,23 @@ apiRoutes.patch("/files/:fileId/status", async (c) => {
3826
3969
  await updateFileStatus(c.req.param("fileId"), status);
3827
3970
  return c.json({ ok: true });
3828
3971
  });
3972
+ apiRoutes.post("/files/:fileId/reveal", async (c) => {
3973
+ const file = await getReviewFile(c.req.param("fileId"));
3974
+ if (!file) return c.json({ error: "Not found" }, 404);
3975
+ const repoRoot = c.get("repoRoot");
3976
+ const fullPath = resolve3(repoRoot, file.file_path);
3977
+ try {
3978
+ if (process.platform === "darwin") {
3979
+ execSync5(`open -R "${fullPath}"`);
3980
+ } else if (process.platform === "win32") {
3981
+ execSync5(`explorer /select,"${fullPath}"`);
3982
+ } else {
3983
+ execSync5(`xdg-open "${resolve3(fullPath, "..")}"`);
3984
+ }
3985
+ } catch {
3986
+ }
3987
+ return c.json({ ok: true });
3988
+ });
3829
3989
  function autoExport(c) {
3830
3990
  scheduleAutoExport(c.get("reviewId"), c.get("repoRoot"));
3831
3991
  }
@@ -3916,19 +4076,19 @@ apiRoutes.get("/context/:fileId", async (c) => {
3916
4076
  return c.json({ lines });
3917
4077
  });
3918
4078
  function readProjectSettings(repoRoot) {
3919
- const settingsPath = join5(repoRoot, ".glassbox", "settings.json");
4079
+ const settingsPath = join6(repoRoot, ".glassbox", "settings.json");
3920
4080
  try {
3921
- if (existsSync4(settingsPath)) {
3922
- return JSON.parse(readFileSync6(settingsPath, "utf-8"));
4081
+ if (existsSync5(settingsPath)) {
4082
+ return JSON.parse(readFileSync7(settingsPath, "utf-8"));
3923
4083
  }
3924
4084
  } catch {
3925
4085
  }
3926
4086
  return {};
3927
4087
  }
3928
4088
  function writeProjectSettings(repoRoot, settings) {
3929
- const dir = join5(repoRoot, ".glassbox");
4089
+ const dir = join6(repoRoot, ".glassbox");
3930
4090
  mkdirSync4(dir, { recursive: true });
3931
- writeFileSync4(join5(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
4091
+ writeFileSync4(join6(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
3932
4092
  }
3933
4093
  apiRoutes.get("/project-settings", (c) => {
3934
4094
  const repoRoot = c.get("repoRoot");
@@ -3978,6 +4138,16 @@ apiRoutes.get("/image/:fileId/:side", async (c) => {
3978
4138
  const oldPath = diff.oldPath ?? null;
3979
4139
  const image = side === "old" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : getNewImage(mode, file.file_path, repoRoot);
3980
4140
  if (!image) return c.text("Image not available", 404);
4141
+ if (isSvgFile(file.file_path)) {
4142
+ try {
4143
+ const png = await rasterizeSvg(image.data);
4144
+ return new Response(png, {
4145
+ headers: { "Content-Type": "image/png", "Cache-Control": "no-cache" }
4146
+ });
4147
+ } catch {
4148
+ return c.text("SVG rasterization failed", 500);
4149
+ }
4150
+ }
3981
4151
  const contentType = getContentType(file.file_path);
3982
4152
  return new Response(image.data, {
3983
4153
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
@@ -4057,7 +4227,7 @@ function jsx(tag, props) {
4057
4227
  }
4058
4228
 
4059
4229
  // src/components/imageDiff.tsx
4060
- function ImageDiff({ file, diff }) {
4230
+ function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
4061
4231
  const fileId = file.id;
4062
4232
  const isAdded = diff.status === "added";
4063
4233
  const isDeleted = diff.status === "deleted";
@@ -4072,8 +4242,11 @@ function ImageDiff({ file, diff }) {
4072
4242
  "data-file-path": file.file_path,
4073
4243
  "data-has-old": String(hasOld),
4074
4244
  "data-has-new": String(hasNew),
4245
+ ...baseWidth ? { "data-base-width": String(baseWidth) } : {},
4246
+ ...baseHeight ? { "data-base-height": String(baseHeight) } : {},
4075
4247
  children: [
4076
- /* @__PURE__ */ jsx("div", { className: "image-diff-panel image-diff-metadata active", "data-panel": "metadata", children: /* @__PURE__ */ jsx("div", { className: "image-metadata-loading", children: "Loading metadata..." }) }),
4248
+ fontWarning && /* @__PURE__ */ jsx("div", { className: "image-font-warning", children: "This SVG uses text that may render differently depending on locally installed fonts." }),
4249
+ /* @__PURE__ */ jsx("div", { className: "image-diff-panel image-diff-metadata", "data-panel": "metadata", children: /* @__PURE__ */ jsx("div", { className: "image-metadata-loading", children: "Loading metadata..." }) }),
4077
4250
  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: [
4078
4251
  /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
4079
4252
  /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-new image-blend", src: `/api/image/${fileId}/new`, alt: "New version" })
@@ -4101,6 +4274,7 @@ function ImageDiff({ file, diff }) {
4101
4274
  }
4102
4275
 
4103
4276
  // src/components/diffView.tsx
4277
+ var revealSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/><line x1="12" y1="11" x2="12" y2="17"/><polyline points="9 14 12 11 15 14"/></svg>';
4104
4278
  function DiffView({ file, diff, annotations, mode }) {
4105
4279
  const annotationsByLine = {};
4106
4280
  for (const a of annotations) {
@@ -4108,13 +4282,25 @@ function DiffView({ file, diff, annotations, mode }) {
4108
4282
  if (!(key in annotationsByLine)) annotationsByLine[key] = [];
4109
4283
  annotationsByLine[key].push(a);
4110
4284
  }
4111
- return /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, children: [
4112
- /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
4113
- /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
4114
- /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
4115
- ] }),
4116
- 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 })
4117
- ] });
4285
+ return /* @__PURE__ */ jsx(
4286
+ "div",
4287
+ {
4288
+ className: "diff-view",
4289
+ "data-file-id": file.id,
4290
+ "data-file-path": file.file_path,
4291
+ ...isSvgFile(diff.filePath) ? { "data-is-svg": "true" } : {},
4292
+ children: [
4293
+ /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
4294
+ /* @__PURE__ */ jsx("div", { className: "diff-header-file", children: [
4295
+ /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
4296
+ /* @__PURE__ */ jsx("button", { className: "reveal-btn", "data-file-id": file.id, title: "Reveal in file manager", children: raw(revealSvg) })
4297
+ ] }),
4298
+ /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
4299
+ ] }),
4300
+ 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 })
4301
+ ]
4302
+ }
4303
+ );
4118
4304
  }
4119
4305
  function getAnnotations(pair, annotationsByLine) {
4120
4306
  const leftAnns = pair.left ? annotationsByLine[`${pair.left.oldNum}:old`] ?? [] : [];
@@ -4637,6 +4823,7 @@ var zoomOutSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"
4637
4823
  var zoomInSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
4638
4824
  var actualSizeSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><text x="12" y="15.5" text-anchor="middle" font-size="9" font-weight="bold" fill="currentColor" stroke="none">1:1</text></svg>';
4639
4825
  var fitSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>';
4826
+ var revealSvgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/><line x1="12" y1="11" x2="12" y2="17"/><polyline points="9 14 12 11 15 14"/></svg>';
4640
4827
  var pageRoutes = new Hono3();
4641
4828
  pageRoutes.get("/", async (c) => {
4642
4829
  const reviewId = c.get("reviewId");
@@ -4684,6 +4871,10 @@ pageRoutes.get("/", async (c) => {
4684
4871
  ] }),
4685
4872
  /* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
4686
4873
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
4874
+ /* @__PURE__ */ jsx("div", { className: "diff-toolbar-svg-toggle", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
4875
+ /* @__PURE__ */ jsx("button", { className: "segment active", "data-svg-mode": "code", children: "Code" }),
4876
+ /* @__PURE__ */ jsx("button", { className: "segment", "data-svg-mode": "rendered", children: "Rendered" })
4877
+ ] }) }),
4687
4878
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar-text", children: [
4688
4879
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
4689
4880
  /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
@@ -4719,10 +4910,53 @@ pageRoutes.get("/file/:fileId", async (c) => {
4719
4910
  const fileId = c.req.param("fileId");
4720
4911
  const mode = c.req.query("mode") === "unified" ? "unified" : "split";
4721
4912
  const ignoreWhitespace = c.req.query("ignoreWhitespace") === "1";
4913
+ const view = c.req.query("view");
4722
4914
  const file = await getReviewFile(fileId);
4723
4915
  if (!file) return c.text("File not found", 404);
4916
+ const diff = JSON.parse(file.diff_data ?? "{}");
4917
+ if (view === "rendered" && isSvgFile(file.file_path)) {
4918
+ const repoRoot = c.get("repoRoot");
4919
+ const review = await getReview(file.review_id);
4920
+ let fontWarning = false;
4921
+ let svgBaseWidth = 300;
4922
+ let svgBaseHeight = 150;
4923
+ if (review) {
4924
+ const reviewMode = parseModeString(review.mode);
4925
+ const oldImg = diff.status !== "added" ? getOldImage(reviewMode, file.file_path, diff.oldPath ?? null, repoRoot) : null;
4926
+ const newImg = diff.status !== "deleted" ? getNewImage(reviewMode, file.file_path, repoRoot) : null;
4927
+ const svgData = newImg ?? oldImg;
4928
+ if (svgData) {
4929
+ const dims = parseSvgDimensions(svgData.data.toString("utf-8"));
4930
+ svgBaseWidth = dims.width;
4931
+ svgBaseHeight = dims.height;
4932
+ }
4933
+ if (oldImg && svgUsesExternalFonts(oldImg.data) || newImg && svgUsesExternalFonts(newImg.data)) {
4934
+ fontWarning = true;
4935
+ }
4936
+ }
4937
+ const html2 = /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, "data-is-svg": "true", children: [
4938
+ /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
4939
+ /* @__PURE__ */ jsx("div", { className: "diff-header-file", children: [
4940
+ /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
4941
+ /* @__PURE__ */ jsx("button", { className: "reveal-btn", "data-file-id": file.id, title: "Reveal in file manager", children: raw(revealSvgIcon) })
4942
+ ] }),
4943
+ /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
4944
+ ] }),
4945
+ /* @__PURE__ */ jsx(
4946
+ ImageDiff,
4947
+ {
4948
+ file,
4949
+ diff,
4950
+ fontWarning,
4951
+ baseWidth: svgBaseWidth,
4952
+ baseHeight: svgBaseHeight
4953
+ }
4954
+ )
4955
+ ] });
4956
+ return c.html(html2.toString());
4957
+ }
4724
4958
  const annotations = await getAnnotationsForFile(fileId);
4725
- let diff = JSON.parse(file.diff_data ?? "{}");
4959
+ let finalDiff = diff;
4726
4960
  if (ignoreWhitespace) {
4727
4961
  const repoRoot = c.get("repoRoot");
4728
4962
  const review = await getReview(file.review_id);
@@ -4730,11 +4964,11 @@ pageRoutes.get("/file/:fileId", async (c) => {
4730
4964
  const reviewMode = parseModeString(review.mode);
4731
4965
  const regenerated = getSingleFileDiff(reviewMode, file.file_path, repoRoot, "-w");
4732
4966
  if (regenerated) {
4733
- diff = regenerated;
4967
+ finalDiff = regenerated;
4734
4968
  }
4735
4969
  }
4736
4970
  }
4737
- const html = /* @__PURE__ */ jsx(DiffView, { file, diff, annotations, mode });
4971
+ const html = /* @__PURE__ */ jsx(DiffView, { file, diff: finalDiff, annotations, mode });
4738
4972
  return c.html(html.toString());
4739
4973
  });
4740
4974
  pageRoutes.get("/review/:reviewId", async (c) => {
@@ -4788,6 +5022,10 @@ pageRoutes.get("/review/:reviewId", async (c) => {
4788
5022
  ] }),
4789
5023
  /* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
4790
5024
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
5025
+ /* @__PURE__ */ jsx("div", { className: "diff-toolbar-svg-toggle", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
5026
+ /* @__PURE__ */ jsx("button", { className: "segment active", "data-svg-mode": "code", children: "Code" }),
5027
+ /* @__PURE__ */ jsx("button", { className: "segment", "data-svg-mode": "rendered", children: "Rendered" })
5028
+ ] }) }),
4791
5029
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar-text", children: [
4792
5030
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
4793
5031
  /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
@@ -4829,10 +5067,10 @@ pageRoutes.get("/history", async (c) => {
4829
5067
 
4830
5068
  // src/server.ts
4831
5069
  function tryServe(fetch2, port) {
4832
- return new Promise((resolve4, reject) => {
5070
+ return new Promise((resolve5, reject) => {
4833
5071
  const server = serve({ fetch: fetch2, port });
4834
5072
  server.on("listening", () => {
4835
- resolve4(port);
5073
+ resolve5(port);
4836
5074
  });
4837
5075
  server.on("error", (err) => {
4838
5076
  if (err.code === "EADDRINUSE") {
@@ -4852,13 +5090,13 @@ async function startServer(port, reviewId, repoRoot, options) {
4852
5090
  await next();
4853
5091
  });
4854
5092
  const selfDir = dirname(fileURLToPath(import.meta.url));
4855
- const distDir = existsSync5(join6(selfDir, "client", "styles.css")) ? join6(selfDir, "client") : join6(selfDir, "..", "dist", "client");
5093
+ const distDir = existsSync6(join7(selfDir, "client", "styles.css")) ? join7(selfDir, "client") : join7(selfDir, "..", "dist", "client");
4856
5094
  app.get("/static/styles.css", (c) => {
4857
- const css = readFileSync7(join6(distDir, "styles.css"), "utf-8");
5095
+ const css = readFileSync8(join7(distDir, "styles.css"), "utf-8");
4858
5096
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
4859
5097
  });
4860
5098
  app.get("/static/app.js", (c) => {
4861
- const js = readFileSync7(join6(distDir, "app.global.js"), "utf-8");
5099
+ const js = readFileSync8(join7(distDir, "app.global.js"), "utf-8");
4862
5100
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
4863
5101
  });
4864
5102
  app.route("/api", apiRoutes);
@@ -4894,8 +5132,8 @@ async function startServer(port, reviewId, repoRoot, options) {
4894
5132
  }
4895
5133
 
4896
5134
  // src/skills.ts
4897
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
4898
- import { join as join7 } from "path";
5135
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
5136
+ import { join as join8 } from "path";
4899
5137
  var SKILL_VERSION = 1;
4900
5138
  function versionHeader() {
4901
5139
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -4906,8 +5144,8 @@ function parseVersionHeader(content) {
4906
5144
  return parseInt(match[1], 10);
4907
5145
  }
4908
5146
  function updateFile(path, content) {
4909
- if (existsSync6(path)) {
4910
- const existing = readFileSync8(path, "utf-8");
5147
+ if (existsSync7(path)) {
5148
+ const existing = readFileSync9(path, "utf-8");
4911
5149
  const version = parseVersionHeader(existing);
4912
5150
  if (version !== null && version >= SKILL_VERSION) {
4913
5151
  return false;
@@ -4933,7 +5171,7 @@ function skillBody() {
4933
5171
  ].join("\n");
4934
5172
  }
4935
5173
  function ensureClaudeSkills(cwd) {
4936
- const dir = join7(cwd, ".claude", "skills", "glassbox");
5174
+ const dir = join8(cwd, ".claude", "skills", "glassbox");
4937
5175
  mkdirSync5(dir, { recursive: true });
4938
5176
  const content = [
4939
5177
  "---",
@@ -4946,10 +5184,10 @@ function ensureClaudeSkills(cwd) {
4946
5184
  skillBody(),
4947
5185
  ""
4948
5186
  ].join("\n");
4949
- return updateFile(join7(dir, "SKILL.md"), content);
5187
+ return updateFile(join8(dir, "SKILL.md"), content);
4950
5188
  }
4951
5189
  function ensureCursorRules(cwd) {
4952
- const rulesDir = join7(cwd, ".cursor", "rules");
5190
+ const rulesDir = join8(cwd, ".cursor", "rules");
4953
5191
  mkdirSync5(rulesDir, { recursive: true });
4954
5192
  const content = [
4955
5193
  "---",
@@ -4961,10 +5199,10 @@ function ensureCursorRules(cwd) {
4961
5199
  skillBody(),
4962
5200
  ""
4963
5201
  ].join("\n");
4964
- return updateFile(join7(rulesDir, "glassbox.mdc"), content);
5202
+ return updateFile(join8(rulesDir, "glassbox.mdc"), content);
4965
5203
  }
4966
5204
  function ensureCopilotPrompts(cwd) {
4967
- const promptsDir = join7(cwd, ".github", "prompts");
5205
+ const promptsDir = join8(cwd, ".github", "prompts");
4968
5206
  mkdirSync5(promptsDir, { recursive: true });
4969
5207
  const content = [
4970
5208
  "---",
@@ -4975,10 +5213,10 @@ function ensureCopilotPrompts(cwd) {
4975
5213
  skillBody(),
4976
5214
  ""
4977
5215
  ].join("\n");
4978
- return updateFile(join7(promptsDir, "glassbox.prompt.md"), content);
5216
+ return updateFile(join8(promptsDir, "glassbox.prompt.md"), content);
4979
5217
  }
4980
5218
  function ensureWindsurfRules(cwd) {
4981
- const rulesDir = join7(cwd, ".windsurf", "rules");
5219
+ const rulesDir = join8(cwd, ".windsurf", "rules");
4982
5220
  mkdirSync5(rulesDir, { recursive: true });
4983
5221
  const content = [
4984
5222
  "---",
@@ -4990,39 +5228,39 @@ function ensureWindsurfRules(cwd) {
4990
5228
  skillBody(),
4991
5229
  ""
4992
5230
  ].join("\n");
4993
- return updateFile(join7(rulesDir, "glassbox.md"), content);
5231
+ return updateFile(join8(rulesDir, "glassbox.md"), content);
4994
5232
  }
4995
5233
  function ensureSkills() {
4996
5234
  const cwd = process.cwd();
4997
5235
  const platforms = [];
4998
- if (existsSync6(join7(cwd, ".claude"))) {
5236
+ if (existsSync7(join8(cwd, ".claude"))) {
4999
5237
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
5000
5238
  }
5001
- if (existsSync6(join7(cwd, ".cursor"))) {
5239
+ if (existsSync7(join8(cwd, ".cursor"))) {
5002
5240
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
5003
5241
  }
5004
- if (existsSync6(join7(cwd, ".github", "prompts")) || existsSync6(join7(cwd, ".github", "copilot-instructions.md"))) {
5242
+ if (existsSync7(join8(cwd, ".github", "prompts")) || existsSync7(join8(cwd, ".github", "copilot-instructions.md"))) {
5005
5243
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
5006
5244
  }
5007
- if (existsSync6(join7(cwd, ".windsurf"))) {
5245
+ if (existsSync7(join8(cwd, ".windsurf"))) {
5008
5246
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
5009
5247
  }
5010
5248
  return platforms;
5011
5249
  }
5012
5250
 
5013
5251
  // src/update-check.ts
5014
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
5252
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
5015
5253
  import { get } from "https";
5016
5254
  import { homedir as homedir3 } from "os";
5017
- import { dirname as dirname2, join as join8 } from "path";
5255
+ import { dirname as dirname2, join as join9 } from "path";
5018
5256
  import { fileURLToPath as fileURLToPath2 } from "url";
5019
- var DATA_DIR = join8(homedir3(), ".glassbox");
5020
- var CHECK_FILE = join8(DATA_DIR, "last-update-check");
5257
+ var DATA_DIR = join9(homedir3(), ".glassbox");
5258
+ var CHECK_FILE = join9(DATA_DIR, "last-update-check");
5021
5259
  var PACKAGE_NAME = "glassbox";
5022
5260
  function getCurrentVersion() {
5023
5261
  try {
5024
5262
  const dir = dirname2(fileURLToPath2(import.meta.url));
5025
- const pkg = JSON.parse(readFileSync9(join8(dir, "..", "package.json"), "utf-8"));
5263
+ const pkg = JSON.parse(readFileSync10(join9(dir, "..", "package.json"), "utf-8"));
5026
5264
  return pkg.version;
5027
5265
  } catch {
5028
5266
  return "0.0.0";
@@ -5030,8 +5268,8 @@ function getCurrentVersion() {
5030
5268
  }
5031
5269
  function getLastCheckDate() {
5032
5270
  try {
5033
- if (existsSync7(CHECK_FILE)) {
5034
- return readFileSync9(CHECK_FILE, "utf-8").trim();
5271
+ if (existsSync8(CHECK_FILE)) {
5272
+ return readFileSync10(CHECK_FILE, "utf-8").trim();
5035
5273
  }
5036
5274
  } catch {
5037
5275
  }
@@ -5048,10 +5286,10 @@ function isFirstUseToday() {
5048
5286
  return last !== today;
5049
5287
  }
5050
5288
  function fetchLatestVersion() {
5051
- return new Promise((resolve4) => {
5289
+ return new Promise((resolve5) => {
5052
5290
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
5053
5291
  if (res.statusCode !== 200) {
5054
- resolve4(null);
5292
+ resolve5(null);
5055
5293
  return;
5056
5294
  }
5057
5295
  let data = "";
@@ -5060,18 +5298,18 @@ function fetchLatestVersion() {
5060
5298
  });
5061
5299
  res.on("end", () => {
5062
5300
  try {
5063
- resolve4(JSON.parse(data).version);
5301
+ resolve5(JSON.parse(data).version);
5064
5302
  } catch {
5065
- resolve4(null);
5303
+ resolve5(null);
5066
5304
  }
5067
5305
  });
5068
5306
  });
5069
5307
  req.on("error", () => {
5070
- resolve4(null);
5308
+ resolve5(null);
5071
5309
  });
5072
5310
  req.on("timeout", () => {
5073
5311
  req.destroy();
5074
- resolve4(null);
5312
+ resolve5(null);
5075
5313
  });
5076
5314
  });
5077
5315
  }
@@ -5209,7 +5447,7 @@ function parseArgs(argv) {
5209
5447
  port = parseInt(args[++i], 10);
5210
5448
  break;
5211
5449
  case "--data-dir":
5212
- dataDir = resolve3(args[++i]);
5450
+ dataDir = resolve4(args[++i]);
5213
5451
  break;
5214
5452
  case "--resume":
5215
5453
  resume = true;
@@ -5265,13 +5503,13 @@ async function main() {
5265
5503
  console.log("AI service test mode enabled \u2014 using mock AI responses");
5266
5504
  }
5267
5505
  if (debug) {
5268
- console.log(`[debug] Build timestamp: ${"2026-03-17T07:45:42.146Z"}`);
5506
+ console.log(`[debug] Build timestamp: ${"2026-03-19T04:05:06.487Z"}`);
5269
5507
  }
5270
5508
  if (projectDir) {
5271
5509
  process.chdir(projectDir);
5272
5510
  }
5273
5511
  if (dataDir === null) {
5274
- dataDir = join9(process.cwd(), ".glassbox");
5512
+ dataDir = join10(process.cwd(), ".glassbox");
5275
5513
  }
5276
5514
  if (demo !== null) {
5277
5515
  const scenario = DEMO_SCENARIOS.find((s) => s.id === demo);
@@ -5283,7 +5521,7 @@ async function main() {
5283
5521
  }
5284
5522
  process.exit(1);
5285
5523
  }
5286
- dataDir = join9(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
5524
+ dataDir = join10(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
5287
5525
  setDemoMode(demo);
5288
5526
  console.log(`
5289
5527
  DEMO MODE: ${scenario.label}