glassbox 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -391,7 +391,7 @@ init_queries();
391
391
  init_connection();
392
392
  import { mkdirSync as mkdirSync7 } from "fs";
393
393
  import { tmpdir } from "os";
394
- import { join as join9, resolve as resolve3 } from "path";
394
+ import { join as join10, resolve as resolve4 } from "path";
395
395
 
396
396
  // src/debug.ts
397
397
  var debugEnabled = false;
@@ -1717,9 +1717,9 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1717
1717
  // src/server.ts
1718
1718
  import { serve } from "@hono/node-server";
1719
1719
  import { exec } from "child_process";
1720
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
1720
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
1721
1721
  import { Hono as Hono4 } from "hono";
1722
- import { dirname, join as join6 } from "path";
1722
+ import { dirname, join as join7 } from "path";
1723
1723
  import { fileURLToPath } from "url";
1724
1724
 
1725
1725
  // src/routes/ai-api.ts
@@ -2439,8 +2439,8 @@ function isRetriable(err) {
2439
2439
  return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
2440
2440
  }
2441
2441
  function sleep(ms) {
2442
- return new Promise((resolve4) => {
2443
- setTimeout(resolve4, ms);
2442
+ return new Promise((resolve5) => {
2443
+ setTimeout(resolve5, ms);
2444
2444
  });
2445
2445
  }
2446
2446
 
@@ -2484,8 +2484,8 @@ function randomLines(count) {
2484
2484
  return lines.sort((a, b) => a.line - b.line);
2485
2485
  }
2486
2486
  function sleep2(ms) {
2487
- return new Promise((resolve4) => {
2488
- setTimeout(resolve4, ms);
2487
+ return new Promise((resolve5) => {
2488
+ setTimeout(resolve5, ms);
2489
2489
  });
2490
2490
  }
2491
2491
  async function mockRiskAnalysisBatch(files) {
@@ -2937,9 +2937,10 @@ aiApiRoutes.post("/preferences", async (c) => {
2937
2937
 
2938
2938
  // src/routes/api.ts
2939
2939
  init_queries();
2940
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2940
+ import { execSync as execSync5 } from "child_process";
2941
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2941
2942
  import { Hono as Hono2 } from "hono";
2942
- import { join as join5 } from "path";
2943
+ import { join as join6, resolve as resolve3 } from "path";
2943
2944
 
2944
2945
  // src/export/generate.ts
2945
2946
  init_queries();
@@ -3103,6 +3104,9 @@ function isImageFile(filePath) {
3103
3104
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3104
3105
  return IMAGE_EXTENSIONS.has(ext);
3105
3106
  }
3107
+ function isSvgFile(filePath) {
3108
+ return filePath.slice(filePath.lastIndexOf(".")).toLowerCase() === ".svg";
3109
+ }
3106
3110
  function getContentType(filePath) {
3107
3111
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3108
3112
  switch (ext) {
@@ -3397,6 +3401,139 @@ function parseWebp(data) {
3397
3401
  return { format: "webp", width, height, colorSpace: "srgb", channels: hasAlpha ? 4 : 3, depth: null, hasAlpha, density: null, exif: null };
3398
3402
  }
3399
3403
 
3404
+ // src/git/svg-rasterize.ts
3405
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3406
+ import { createRequire } from "module";
3407
+ import { join as join5 } from "path";
3408
+ var initialized = false;
3409
+ var ResvgClass;
3410
+ var fontBuffers = [];
3411
+ async function ensureInit() {
3412
+ if (initialized) return;
3413
+ const require2 = createRequire(import.meta.url);
3414
+ const resvgPath = require2.resolve("@resvg/resvg-wasm");
3415
+ const wasmPath = resvgPath.replace(/index\.(js|mjs)$/, "index_bg.wasm");
3416
+ const wasmBuffer = readFileSync6(wasmPath);
3417
+ const mod = await import("@resvg/resvg-wasm");
3418
+ await mod.initWasm(wasmBuffer);
3419
+ ResvgClass = mod.Resvg;
3420
+ fontBuffers = loadSystemFonts();
3421
+ initialized = true;
3422
+ }
3423
+ function loadSystemFonts() {
3424
+ const buffers = [];
3425
+ const candidates = getFontCandidates();
3426
+ for (const path of candidates) {
3427
+ if (!existsSync4(path)) continue;
3428
+ try {
3429
+ buffers.push(readFileSync6(path));
3430
+ } catch {
3431
+ }
3432
+ }
3433
+ return buffers;
3434
+ }
3435
+ function getFontCandidates() {
3436
+ switch (process.platform) {
3437
+ case "darwin": {
3438
+ const sys = "/System/Library/Fonts";
3439
+ const sup = "/System/Library/Fonts/Supplemental";
3440
+ return [
3441
+ // Core system fonts (serif, sans-serif, monospace)
3442
+ join5(sys, "Helvetica.ttc"),
3443
+ join5(sys, "Times.ttc"),
3444
+ join5(sys, "Courier.ttc"),
3445
+ join5(sys, "Menlo.ttc"),
3446
+ join5(sys, "SFPro.ttf"),
3447
+ join5(sys, "SFNS.ttf"),
3448
+ join5(sys, "SFNSMono.ttf"),
3449
+ // Supplemental (common named fonts in SVGs)
3450
+ join5(sup, "Arial.ttf"),
3451
+ join5(sup, "Arial Bold.ttf"),
3452
+ join5(sup, "Georgia.ttf"),
3453
+ join5(sup, "Verdana.ttf"),
3454
+ join5(sup, "Tahoma.ttf"),
3455
+ join5(sup, "Trebuchet MS.ttf"),
3456
+ join5(sup, "Impact.ttf"),
3457
+ join5(sup, "Comic Sans MS.ttf"),
3458
+ join5(sup, "Courier New.ttf"),
3459
+ join5(sup, "Times New Roman.ttf")
3460
+ ];
3461
+ }
3462
+ case "linux":
3463
+ return [
3464
+ // DejaVu (most common Linux fallback)
3465
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
3466
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
3467
+ "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
3468
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
3469
+ // Liberation (metric-compatible with Arial/Times/Courier)
3470
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
3471
+ "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
3472
+ "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
3473
+ // Noto (common on modern distros)
3474
+ "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
3475
+ ];
3476
+ case "win32": {
3477
+ const winFonts = join5(process.env.WINDIR ?? "C:\\Windows", "Fonts");
3478
+ return [
3479
+ join5(winFonts, "arial.ttf"),
3480
+ join5(winFonts, "arialbd.ttf"),
3481
+ join5(winFonts, "times.ttf"),
3482
+ join5(winFonts, "cour.ttf"),
3483
+ join5(winFonts, "verdana.ttf"),
3484
+ join5(winFonts, "tahoma.ttf"),
3485
+ join5(winFonts, "georgia.ttf"),
3486
+ join5(winFonts, "consola.ttf"),
3487
+ join5(winFonts, "segoeui.ttf")
3488
+ ];
3489
+ }
3490
+ default:
3491
+ return [];
3492
+ }
3493
+ }
3494
+ function parseSvgDimensions(svg) {
3495
+ const widthMatch = svg.match(/\bwidth\s*=\s*["']([^"']+)["']/);
3496
+ const heightMatch = svg.match(/\bheight\s*=\s*["']([^"']+)["']/);
3497
+ const viewBoxMatch = svg.match(/\bviewBox\s*=\s*["']([^"']+)["']/);
3498
+ let width = widthMatch ? parseFloat(widthMatch[1]) : NaN;
3499
+ let height = heightMatch ? parseFloat(heightMatch[1]) : NaN;
3500
+ if ((isNaN(width) || isNaN(height)) && viewBoxMatch) {
3501
+ const parts = viewBoxMatch[1].split(/[\s,]+/);
3502
+ if (parts.length >= 4) {
3503
+ if (isNaN(width)) width = parseFloat(parts[2]);
3504
+ if (isNaN(height)) height = parseFloat(parts[3]);
3505
+ }
3506
+ }
3507
+ if (isNaN(width)) width = 300;
3508
+ if (isNaN(height)) height = 150;
3509
+ return { width, height };
3510
+ }
3511
+ function svgUsesExternalFonts(svgData) {
3512
+ const svg = svgData.toString("utf-8");
3513
+ return /<text[\s>]/i.test(svg) || /font-family/i.test(svg) || /@font-face/i.test(svg);
3514
+ }
3515
+ async function rasterizeSvg(svgData) {
3516
+ await ensureInit();
3517
+ const svgString = svgData.toString("utf-8");
3518
+ const { width, height } = parseSvgDimensions(svgString);
3519
+ const maxDim = Math.max(width, height);
3520
+ const scale = Math.min(10, 8e3 / maxDim);
3521
+ const targetWidth = Math.round(width * scale);
3522
+ const resvg = new ResvgClass(svgString, {
3523
+ fitTo: { mode: "width", value: targetWidth },
3524
+ font: {
3525
+ loadSystemFonts: false,
3526
+ fontBuffers,
3527
+ defaultFontFamily: "Helvetica"
3528
+ }
3529
+ });
3530
+ const rendered = resvg.render();
3531
+ const png = Buffer.from(rendered.asPng());
3532
+ rendered.free();
3533
+ resvg.free();
3534
+ return png;
3535
+ }
3536
+
3400
3537
  // src/outline/parser.ts
3401
3538
  var BRACE_LANGS = /* @__PURE__ */ new Set([
3402
3539
  "javascript",
@@ -3826,6 +3963,23 @@ apiRoutes.patch("/files/:fileId/status", async (c) => {
3826
3963
  await updateFileStatus(c.req.param("fileId"), status);
3827
3964
  return c.json({ ok: true });
3828
3965
  });
3966
+ apiRoutes.post("/files/:fileId/reveal", async (c) => {
3967
+ const file = await getReviewFile(c.req.param("fileId"));
3968
+ if (!file) return c.json({ error: "Not found" }, 404);
3969
+ const repoRoot = c.get("repoRoot");
3970
+ const fullPath = resolve3(repoRoot, file.file_path);
3971
+ try {
3972
+ if (process.platform === "darwin") {
3973
+ execSync5(`open -R "${fullPath}"`);
3974
+ } else if (process.platform === "win32") {
3975
+ execSync5(`explorer /select,"${fullPath}"`);
3976
+ } else {
3977
+ execSync5(`xdg-open "${resolve3(fullPath, "..")}"`);
3978
+ }
3979
+ } catch {
3980
+ }
3981
+ return c.json({ ok: true });
3982
+ });
3829
3983
  function autoExport(c) {
3830
3984
  scheduleAutoExport(c.get("reviewId"), c.get("repoRoot"));
3831
3985
  }
@@ -3916,19 +4070,19 @@ apiRoutes.get("/context/:fileId", async (c) => {
3916
4070
  return c.json({ lines });
3917
4071
  });
3918
4072
  function readProjectSettings(repoRoot) {
3919
- const settingsPath = join5(repoRoot, ".glassbox", "settings.json");
4073
+ const settingsPath = join6(repoRoot, ".glassbox", "settings.json");
3920
4074
  try {
3921
- if (existsSync4(settingsPath)) {
3922
- return JSON.parse(readFileSync6(settingsPath, "utf-8"));
4075
+ if (existsSync5(settingsPath)) {
4076
+ return JSON.parse(readFileSync7(settingsPath, "utf-8"));
3923
4077
  }
3924
4078
  } catch {
3925
4079
  }
3926
4080
  return {};
3927
4081
  }
3928
4082
  function writeProjectSettings(repoRoot, settings) {
3929
- const dir = join5(repoRoot, ".glassbox");
4083
+ const dir = join6(repoRoot, ".glassbox");
3930
4084
  mkdirSync4(dir, { recursive: true });
3931
- writeFileSync4(join5(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
4085
+ writeFileSync4(join6(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
3932
4086
  }
3933
4087
  apiRoutes.get("/project-settings", (c) => {
3934
4088
  const repoRoot = c.get("repoRoot");
@@ -3978,6 +4132,16 @@ apiRoutes.get("/image/:fileId/:side", async (c) => {
3978
4132
  const oldPath = diff.oldPath ?? null;
3979
4133
  const image = side === "old" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : getNewImage(mode, file.file_path, repoRoot);
3980
4134
  if (!image) return c.text("Image not available", 404);
4135
+ if (isSvgFile(file.file_path)) {
4136
+ try {
4137
+ const png = await rasterizeSvg(image.data);
4138
+ return new Response(png, {
4139
+ headers: { "Content-Type": "image/png", "Cache-Control": "no-cache" }
4140
+ });
4141
+ } catch {
4142
+ return c.text("SVG rasterization failed", 500);
4143
+ }
4144
+ }
3981
4145
  const contentType = getContentType(file.file_path);
3982
4146
  return new Response(image.data, {
3983
4147
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
@@ -4057,7 +4221,7 @@ function jsx(tag, props) {
4057
4221
  }
4058
4222
 
4059
4223
  // src/components/imageDiff.tsx
4060
- function ImageDiff({ file, diff }) {
4224
+ function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
4061
4225
  const fileId = file.id;
4062
4226
  const isAdded = diff.status === "added";
4063
4227
  const isDeleted = diff.status === "deleted";
@@ -4072,7 +4236,10 @@ function ImageDiff({ file, diff }) {
4072
4236
  "data-file-path": file.file_path,
4073
4237
  "data-has-old": String(hasOld),
4074
4238
  "data-has-new": String(hasNew),
4239
+ ...baseWidth ? { "data-base-width": String(baseWidth) } : {},
4240
+ ...baseHeight ? { "data-base-height": String(baseHeight) } : {},
4075
4241
  children: [
4242
+ fontWarning && /* @__PURE__ */ jsx("div", { className: "image-font-warning", children: "This SVG uses text that may render differently depending on locally installed fonts." }),
4076
4243
  /* @__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..." }) }),
4077
4244
  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
4245
  /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
@@ -4101,6 +4268,7 @@ function ImageDiff({ file, diff }) {
4101
4268
  }
4102
4269
 
4103
4270
  // src/components/diffView.tsx
4271
+ 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
4272
  function DiffView({ file, diff, annotations, mode }) {
4105
4273
  const annotationsByLine = {};
4106
4274
  for (const a of annotations) {
@@ -4108,13 +4276,25 @@ function DiffView({ file, diff, annotations, mode }) {
4108
4276
  if (!(key in annotationsByLine)) annotationsByLine[key] = [];
4109
4277
  annotationsByLine[key].push(a);
4110
4278
  }
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
- ] });
4279
+ return /* @__PURE__ */ jsx(
4280
+ "div",
4281
+ {
4282
+ className: "diff-view",
4283
+ "data-file-id": file.id,
4284
+ "data-file-path": file.file_path,
4285
+ ...isSvgFile(diff.filePath) ? { "data-is-svg": "true" } : {},
4286
+ children: [
4287
+ /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
4288
+ /* @__PURE__ */ jsx("div", { className: "diff-header-file", children: [
4289
+ /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
4290
+ /* @__PURE__ */ jsx("button", { className: "reveal-btn", "data-file-id": file.id, title: "Reveal in file manager", children: raw(revealSvg) })
4291
+ ] }),
4292
+ /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
4293
+ ] }),
4294
+ 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 })
4295
+ ]
4296
+ }
4297
+ );
4118
4298
  }
4119
4299
  function getAnnotations(pair, annotationsByLine) {
4120
4300
  const leftAnns = pair.left ? annotationsByLine[`${pair.left.oldNum}:old`] ?? [] : [];
@@ -4637,6 +4817,7 @@ var zoomOutSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"
4637
4817
  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
4818
  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
4819
  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>';
4820
+ 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
4821
  var pageRoutes = new Hono3();
4641
4822
  pageRoutes.get("/", async (c) => {
4642
4823
  const reviewId = c.get("reviewId");
@@ -4684,6 +4865,10 @@ pageRoutes.get("/", async (c) => {
4684
4865
  ] }),
4685
4866
  /* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
4686
4867
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
4868
+ /* @__PURE__ */ jsx("div", { className: "diff-toolbar-svg-toggle", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
4869
+ /* @__PURE__ */ jsx("button", { className: "segment active", "data-svg-mode": "code", children: "Code" }),
4870
+ /* @__PURE__ */ jsx("button", { className: "segment", "data-svg-mode": "rendered", children: "Rendered" })
4871
+ ] }) }),
4687
4872
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar-text", children: [
4688
4873
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
4689
4874
  /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
@@ -4719,10 +4904,53 @@ pageRoutes.get("/file/:fileId", async (c) => {
4719
4904
  const fileId = c.req.param("fileId");
4720
4905
  const mode = c.req.query("mode") === "unified" ? "unified" : "split";
4721
4906
  const ignoreWhitespace = c.req.query("ignoreWhitespace") === "1";
4907
+ const view = c.req.query("view");
4722
4908
  const file = await getReviewFile(fileId);
4723
4909
  if (!file) return c.text("File not found", 404);
4910
+ const diff = JSON.parse(file.diff_data ?? "{}");
4911
+ if (view === "rendered" && isSvgFile(file.file_path)) {
4912
+ const repoRoot = c.get("repoRoot");
4913
+ const review = await getReview(file.review_id);
4914
+ let fontWarning = false;
4915
+ let svgBaseWidth = 300;
4916
+ let svgBaseHeight = 150;
4917
+ if (review) {
4918
+ const reviewMode = parseModeString(review.mode);
4919
+ const oldImg = diff.status !== "added" ? getOldImage(reviewMode, file.file_path, diff.oldPath ?? null, repoRoot) : null;
4920
+ const newImg = diff.status !== "deleted" ? getNewImage(reviewMode, file.file_path, repoRoot) : null;
4921
+ const svgData = newImg ?? oldImg;
4922
+ if (svgData) {
4923
+ const dims = parseSvgDimensions(svgData.data.toString("utf-8"));
4924
+ svgBaseWidth = dims.width;
4925
+ svgBaseHeight = dims.height;
4926
+ }
4927
+ if (oldImg && svgUsesExternalFonts(oldImg.data) || newImg && svgUsesExternalFonts(newImg.data)) {
4928
+ fontWarning = true;
4929
+ }
4930
+ }
4931
+ const html2 = /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, "data-is-svg": "true", children: [
4932
+ /* @__PURE__ */ jsx("div", { className: "diff-header", children: [
4933
+ /* @__PURE__ */ jsx("div", { className: "diff-header-file", children: [
4934
+ /* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
4935
+ /* @__PURE__ */ jsx("button", { className: "reveal-btn", "data-file-id": file.id, title: "Reveal in file manager", children: raw(revealSvgIcon) })
4936
+ ] }),
4937
+ /* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
4938
+ ] }),
4939
+ /* @__PURE__ */ jsx(
4940
+ ImageDiff,
4941
+ {
4942
+ file,
4943
+ diff,
4944
+ fontWarning,
4945
+ baseWidth: svgBaseWidth,
4946
+ baseHeight: svgBaseHeight
4947
+ }
4948
+ )
4949
+ ] });
4950
+ return c.html(html2.toString());
4951
+ }
4724
4952
  const annotations = await getAnnotationsForFile(fileId);
4725
- let diff = JSON.parse(file.diff_data ?? "{}");
4953
+ let finalDiff = diff;
4726
4954
  if (ignoreWhitespace) {
4727
4955
  const repoRoot = c.get("repoRoot");
4728
4956
  const review = await getReview(file.review_id);
@@ -4730,11 +4958,11 @@ pageRoutes.get("/file/:fileId", async (c) => {
4730
4958
  const reviewMode = parseModeString(review.mode);
4731
4959
  const regenerated = getSingleFileDiff(reviewMode, file.file_path, repoRoot, "-w");
4732
4960
  if (regenerated) {
4733
- diff = regenerated;
4961
+ finalDiff = regenerated;
4734
4962
  }
4735
4963
  }
4736
4964
  }
4737
- const html = /* @__PURE__ */ jsx(DiffView, { file, diff, annotations, mode });
4965
+ const html = /* @__PURE__ */ jsx(DiffView, { file, diff: finalDiff, annotations, mode });
4738
4966
  return c.html(html.toString());
4739
4967
  });
4740
4968
  pageRoutes.get("/review/:reviewId", async (c) => {
@@ -4788,6 +5016,10 @@ pageRoutes.get("/review/:reviewId", async (c) => {
4788
5016
  ] }),
4789
5017
  /* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
4790
5018
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
5019
+ /* @__PURE__ */ jsx("div", { className: "diff-toolbar-svg-toggle", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
5020
+ /* @__PURE__ */ jsx("button", { className: "segment active", "data-svg-mode": "code", children: "Code" }),
5021
+ /* @__PURE__ */ jsx("button", { className: "segment", "data-svg-mode": "rendered", children: "Rendered" })
5022
+ ] }) }),
4791
5023
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar-text", children: [
4792
5024
  /* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
4793
5025
  /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
@@ -4829,10 +5061,10 @@ pageRoutes.get("/history", async (c) => {
4829
5061
 
4830
5062
  // src/server.ts
4831
5063
  function tryServe(fetch2, port) {
4832
- return new Promise((resolve4, reject) => {
5064
+ return new Promise((resolve5, reject) => {
4833
5065
  const server = serve({ fetch: fetch2, port });
4834
5066
  server.on("listening", () => {
4835
- resolve4(port);
5067
+ resolve5(port);
4836
5068
  });
4837
5069
  server.on("error", (err) => {
4838
5070
  if (err.code === "EADDRINUSE") {
@@ -4852,13 +5084,13 @@ async function startServer(port, reviewId, repoRoot, options) {
4852
5084
  await next();
4853
5085
  });
4854
5086
  const selfDir = dirname(fileURLToPath(import.meta.url));
4855
- const distDir = existsSync5(join6(selfDir, "client", "styles.css")) ? join6(selfDir, "client") : join6(selfDir, "..", "dist", "client");
5087
+ const distDir = existsSync6(join7(selfDir, "client", "styles.css")) ? join7(selfDir, "client") : join7(selfDir, "..", "dist", "client");
4856
5088
  app.get("/static/styles.css", (c) => {
4857
- const css = readFileSync7(join6(distDir, "styles.css"), "utf-8");
5089
+ const css = readFileSync8(join7(distDir, "styles.css"), "utf-8");
4858
5090
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
4859
5091
  });
4860
5092
  app.get("/static/app.js", (c) => {
4861
- const js = readFileSync7(join6(distDir, "app.global.js"), "utf-8");
5093
+ const js = readFileSync8(join7(distDir, "app.global.js"), "utf-8");
4862
5094
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
4863
5095
  });
4864
5096
  app.route("/api", apiRoutes);
@@ -4894,8 +5126,8 @@ async function startServer(port, reviewId, repoRoot, options) {
4894
5126
  }
4895
5127
 
4896
5128
  // 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";
5129
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
5130
+ import { join as join8 } from "path";
4899
5131
  var SKILL_VERSION = 1;
4900
5132
  function versionHeader() {
4901
5133
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -4906,8 +5138,8 @@ function parseVersionHeader(content) {
4906
5138
  return parseInt(match[1], 10);
4907
5139
  }
4908
5140
  function updateFile(path, content) {
4909
- if (existsSync6(path)) {
4910
- const existing = readFileSync8(path, "utf-8");
5141
+ if (existsSync7(path)) {
5142
+ const existing = readFileSync9(path, "utf-8");
4911
5143
  const version = parseVersionHeader(existing);
4912
5144
  if (version !== null && version >= SKILL_VERSION) {
4913
5145
  return false;
@@ -4933,7 +5165,7 @@ function skillBody() {
4933
5165
  ].join("\n");
4934
5166
  }
4935
5167
  function ensureClaudeSkills(cwd) {
4936
- const dir = join7(cwd, ".claude", "skills", "glassbox");
5168
+ const dir = join8(cwd, ".claude", "skills", "glassbox");
4937
5169
  mkdirSync5(dir, { recursive: true });
4938
5170
  const content = [
4939
5171
  "---",
@@ -4946,10 +5178,10 @@ function ensureClaudeSkills(cwd) {
4946
5178
  skillBody(),
4947
5179
  ""
4948
5180
  ].join("\n");
4949
- return updateFile(join7(dir, "SKILL.md"), content);
5181
+ return updateFile(join8(dir, "SKILL.md"), content);
4950
5182
  }
4951
5183
  function ensureCursorRules(cwd) {
4952
- const rulesDir = join7(cwd, ".cursor", "rules");
5184
+ const rulesDir = join8(cwd, ".cursor", "rules");
4953
5185
  mkdirSync5(rulesDir, { recursive: true });
4954
5186
  const content = [
4955
5187
  "---",
@@ -4961,10 +5193,10 @@ function ensureCursorRules(cwd) {
4961
5193
  skillBody(),
4962
5194
  ""
4963
5195
  ].join("\n");
4964
- return updateFile(join7(rulesDir, "glassbox.mdc"), content);
5196
+ return updateFile(join8(rulesDir, "glassbox.mdc"), content);
4965
5197
  }
4966
5198
  function ensureCopilotPrompts(cwd) {
4967
- const promptsDir = join7(cwd, ".github", "prompts");
5199
+ const promptsDir = join8(cwd, ".github", "prompts");
4968
5200
  mkdirSync5(promptsDir, { recursive: true });
4969
5201
  const content = [
4970
5202
  "---",
@@ -4975,10 +5207,10 @@ function ensureCopilotPrompts(cwd) {
4975
5207
  skillBody(),
4976
5208
  ""
4977
5209
  ].join("\n");
4978
- return updateFile(join7(promptsDir, "glassbox.prompt.md"), content);
5210
+ return updateFile(join8(promptsDir, "glassbox.prompt.md"), content);
4979
5211
  }
4980
5212
  function ensureWindsurfRules(cwd) {
4981
- const rulesDir = join7(cwd, ".windsurf", "rules");
5213
+ const rulesDir = join8(cwd, ".windsurf", "rules");
4982
5214
  mkdirSync5(rulesDir, { recursive: true });
4983
5215
  const content = [
4984
5216
  "---",
@@ -4990,39 +5222,39 @@ function ensureWindsurfRules(cwd) {
4990
5222
  skillBody(),
4991
5223
  ""
4992
5224
  ].join("\n");
4993
- return updateFile(join7(rulesDir, "glassbox.md"), content);
5225
+ return updateFile(join8(rulesDir, "glassbox.md"), content);
4994
5226
  }
4995
5227
  function ensureSkills() {
4996
5228
  const cwd = process.cwd();
4997
5229
  const platforms = [];
4998
- if (existsSync6(join7(cwd, ".claude"))) {
5230
+ if (existsSync7(join8(cwd, ".claude"))) {
4999
5231
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
5000
5232
  }
5001
- if (existsSync6(join7(cwd, ".cursor"))) {
5233
+ if (existsSync7(join8(cwd, ".cursor"))) {
5002
5234
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
5003
5235
  }
5004
- if (existsSync6(join7(cwd, ".github", "prompts")) || existsSync6(join7(cwd, ".github", "copilot-instructions.md"))) {
5236
+ if (existsSync7(join8(cwd, ".github", "prompts")) || existsSync7(join8(cwd, ".github", "copilot-instructions.md"))) {
5005
5237
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
5006
5238
  }
5007
- if (existsSync6(join7(cwd, ".windsurf"))) {
5239
+ if (existsSync7(join8(cwd, ".windsurf"))) {
5008
5240
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
5009
5241
  }
5010
5242
  return platforms;
5011
5243
  }
5012
5244
 
5013
5245
  // src/update-check.ts
5014
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
5246
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
5015
5247
  import { get } from "https";
5016
5248
  import { homedir as homedir3 } from "os";
5017
- import { dirname as dirname2, join as join8 } from "path";
5249
+ import { dirname as dirname2, join as join9 } from "path";
5018
5250
  import { fileURLToPath as fileURLToPath2 } from "url";
5019
- var DATA_DIR = join8(homedir3(), ".glassbox");
5020
- var CHECK_FILE = join8(DATA_DIR, "last-update-check");
5251
+ var DATA_DIR = join9(homedir3(), ".glassbox");
5252
+ var CHECK_FILE = join9(DATA_DIR, "last-update-check");
5021
5253
  var PACKAGE_NAME = "glassbox";
5022
5254
  function getCurrentVersion() {
5023
5255
  try {
5024
5256
  const dir = dirname2(fileURLToPath2(import.meta.url));
5025
- const pkg = JSON.parse(readFileSync9(join8(dir, "..", "package.json"), "utf-8"));
5257
+ const pkg = JSON.parse(readFileSync10(join9(dir, "..", "package.json"), "utf-8"));
5026
5258
  return pkg.version;
5027
5259
  } catch {
5028
5260
  return "0.0.0";
@@ -5030,8 +5262,8 @@ function getCurrentVersion() {
5030
5262
  }
5031
5263
  function getLastCheckDate() {
5032
5264
  try {
5033
- if (existsSync7(CHECK_FILE)) {
5034
- return readFileSync9(CHECK_FILE, "utf-8").trim();
5265
+ if (existsSync8(CHECK_FILE)) {
5266
+ return readFileSync10(CHECK_FILE, "utf-8").trim();
5035
5267
  }
5036
5268
  } catch {
5037
5269
  }
@@ -5048,10 +5280,10 @@ function isFirstUseToday() {
5048
5280
  return last !== today;
5049
5281
  }
5050
5282
  function fetchLatestVersion() {
5051
- return new Promise((resolve4) => {
5283
+ return new Promise((resolve5) => {
5052
5284
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
5053
5285
  if (res.statusCode !== 200) {
5054
- resolve4(null);
5286
+ resolve5(null);
5055
5287
  return;
5056
5288
  }
5057
5289
  let data = "";
@@ -5060,18 +5292,18 @@ function fetchLatestVersion() {
5060
5292
  });
5061
5293
  res.on("end", () => {
5062
5294
  try {
5063
- resolve4(JSON.parse(data).version);
5295
+ resolve5(JSON.parse(data).version);
5064
5296
  } catch {
5065
- resolve4(null);
5297
+ resolve5(null);
5066
5298
  }
5067
5299
  });
5068
5300
  });
5069
5301
  req.on("error", () => {
5070
- resolve4(null);
5302
+ resolve5(null);
5071
5303
  });
5072
5304
  req.on("timeout", () => {
5073
5305
  req.destroy();
5074
- resolve4(null);
5306
+ resolve5(null);
5075
5307
  });
5076
5308
  });
5077
5309
  }
@@ -5209,7 +5441,7 @@ function parseArgs(argv) {
5209
5441
  port = parseInt(args[++i], 10);
5210
5442
  break;
5211
5443
  case "--data-dir":
5212
- dataDir = resolve3(args[++i]);
5444
+ dataDir = resolve4(args[++i]);
5213
5445
  break;
5214
5446
  case "--resume":
5215
5447
  resume = true;
@@ -5265,13 +5497,13 @@ async function main() {
5265
5497
  console.log("AI service test mode enabled \u2014 using mock AI responses");
5266
5498
  }
5267
5499
  if (debug) {
5268
- console.log(`[debug] Build timestamp: ${"2026-03-17T07:45:42.146Z"}`);
5500
+ console.log(`[debug] Build timestamp: ${"2026-03-19T03:31:50.984Z"}`);
5269
5501
  }
5270
5502
  if (projectDir) {
5271
5503
  process.chdir(projectDir);
5272
5504
  }
5273
5505
  if (dataDir === null) {
5274
- dataDir = join9(process.cwd(), ".glassbox");
5506
+ dataDir = join10(process.cwd(), ".glassbox");
5275
5507
  }
5276
5508
  if (demo !== null) {
5277
5509
  const scenario = DEMO_SCENARIOS.find((s) => s.id === demo);
@@ -5283,7 +5515,7 @@ async function main() {
5283
5515
  }
5284
5516
  process.exit(1);
5285
5517
  }
5286
- dataDir = join9(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
5518
+ dataDir = join10(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
5287
5519
  setDemoMode(demo);
5288
5520
  console.log(`
5289
5521
  DEMO MODE: ${scenario.label}