glassbox 0.10.2-beta.1 → 0.11.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
@@ -16207,7 +16207,34 @@ var DEMO_SCENARIOS = [
16207
16207
  { id: 4, label: "Annotations with different categories" },
16208
16208
  { id: 5, label: "Settings dialog with guided review" }
16209
16209
  ];
16210
+ function buildLargeMinifiedSvg(nudge) {
16211
+ let body = "";
16212
+ for (let i = 0; i < 12e3; i++) {
16213
+ const hue = i * 37 % 360;
16214
+ body += `<path d="M${i % 512} ${i * 3 % 512}l4 0 0 4-4 0z" fill="hsl(${hue} 70% 50%)" opacity="0.${i % 90}"/>`;
16215
+ }
16216
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-build="${nudge}">${body}</svg>`;
16217
+ }
16210
16218
  var DEMO_FILES = [
16219
+ {
16220
+ // Regression guard for GB-821: a large minified SVG must not freeze the
16221
+ // UI when selected. The diff viewer renders this as a text diff by
16222
+ // default (svgViewMode = 'code'), so the server must truncate the giant
16223
+ // single line for display instead of emitting a multi-hundred-KB text
16224
+ // node whose layout/paint locks the main thread.
16225
+ path: "src/assets/icons.min.svg",
16226
+ status: "modified",
16227
+ hunks: [{
16228
+ oldStart: 1,
16229
+ oldCount: 1,
16230
+ newStart: 1,
16231
+ newCount: 1,
16232
+ lines: [
16233
+ { type: "remove", oldNum: 1, newNum: null, content: buildLargeMinifiedSvg("a") },
16234
+ { type: "add", oldNum: null, newNum: 1, content: buildLargeMinifiedSvg("b") }
16235
+ ]
16236
+ }]
16237
+ },
16211
16238
  {
16212
16239
  path: "src/auth/session.ts",
16213
16240
  status: "modified",
@@ -16415,6 +16442,22 @@ var DEMO_FILES = [
16415
16442
  { type: "context", oldNum: 12, newNum: 14, content: ' "typescript": "^5.3.0"' }
16416
16443
  ]
16417
16444
  }]
16445
+ },
16446
+ {
16447
+ // An image change, so the demo (and the e2e suite) exercises the image
16448
+ // comparison modes — metadata / difference / slice. Modeled as a rename so
16449
+ // the old and new sides resolve to two different real images in the repo
16450
+ // (demo mode resolves to "uncommitted", so the image route reads the old
16451
+ // side from git HEAD and the new side from the working tree), giving a
16452
+ // genuine visual diff. The `src-tauri/icons/` path is deliberate: it sorts
16453
+ // after `package.json` in the folder tree, so the auto-selected first file
16454
+ // stays a text diff. Regression guard for GB-823: the slice tool's bottom
16455
+ // handle must stay above the toolbar and be grabbable.
16456
+ path: "src-tauri/icons/128x128.png",
16457
+ oldPath: "src-tauri/icons/64x64.png",
16458
+ status: "modified",
16459
+ isBinary: true,
16460
+ hunks: []
16418
16461
  }
16419
16462
  ];
16420
16463
  var GUIDED_NOTES = {
@@ -16569,10 +16612,10 @@ async function setupDemoReview(scenario) {
16569
16612
  for (const file2 of DEMO_FILES) {
16570
16613
  const diff = {
16571
16614
  filePath: file2.path,
16572
- oldPath: null,
16615
+ oldPath: file2.oldPath ?? null,
16573
16616
  status: file2.status,
16574
16617
  hunks: file2.hunks,
16575
- isBinary: false
16618
+ isBinary: file2.isBinary ?? false
16576
16619
  };
16577
16620
  const rf = await addReviewFile(review.id, file2.path, JSON.stringify(diff));
16578
16621
  fileIdMap.set(file2.path, rf.id);
@@ -16847,13 +16890,12 @@ function parseDiff(raw) {
16847
16890
  if (headerEnd === -1 && !header.includes("Binary")) {
16848
16891
  const pathMatch2 = chunk.match(/^a\/(.+?) b\/(.+)/m);
16849
16892
  if (pathMatch2) {
16850
- const isBinary3 = header.includes("Binary");
16851
16893
  files.push({
16852
16894
  filePath: pathMatch2[2],
16853
16895
  oldPath: pathMatch2[1] !== pathMatch2[2] ? pathMatch2[1] : null,
16854
16896
  status: header.includes("new file") ? "added" : header.includes("deleted file") ? "deleted" : "modified",
16855
16897
  hunks: [],
16856
- isBinary: isBinary3
16898
+ isBinary: false
16857
16899
  });
16858
16900
  }
16859
16901
  continue;
@@ -17419,26 +17461,29 @@ async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
17419
17461
  }
17420
17462
 
17421
17463
  // src/ai/context-builder.ts
17464
+ function linePrefix(type) {
17465
+ return type === "add" ? "+" : type === "remove" ? "-" : " ";
17466
+ }
17422
17467
  function summarizeHunk(hunk, maxLines) {
17423
17468
  const lines = [];
17424
17469
  lines.push(`@@ -${String(hunk.oldStart)},${String(hunk.oldCount)} +${String(hunk.newStart)},${String(hunk.newCount)} @@`);
17425
17470
  const hunkLines = hunk.lines;
17426
17471
  if (hunkLines.length <= maxLines) {
17427
17472
  for (const line of hunkLines) {
17428
- const prefix = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
17473
+ const prefix = linePrefix(line.type);
17429
17474
  lines.push(prefix + line.content);
17430
17475
  }
17431
17476
  } else {
17432
17477
  const half = Math.floor(maxLines / 2);
17433
17478
  for (let i = 0; i < half; i++) {
17434
17479
  const line = hunkLines[i];
17435
- const prefix = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
17480
+ const prefix = linePrefix(line.type);
17436
17481
  lines.push(prefix + line.content);
17437
17482
  }
17438
17483
  lines.push(`... (${String(hunkLines.length - maxLines)} lines omitted) ...`);
17439
17484
  for (let i = hunkLines.length - half; i < hunkLines.length; i++) {
17440
17485
  const line = hunkLines[i];
17441
- const prefix = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
17486
+ const prefix = linePrefix(line.type);
17442
17487
  lines.push(prefix + line.content);
17443
17488
  }
17444
17489
  }
@@ -17451,7 +17496,7 @@ function buildDiffText(diff, charBudget) {
17451
17496
  for (const hunk of diff.hunks) {
17452
17497
  fullLines.push(`@@ -${String(hunk.oldStart)},${String(hunk.oldCount)} +${String(hunk.newStart)},${String(hunk.newCount)} @@`);
17453
17498
  for (const line of hunk.lines) {
17454
- const prefix = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
17499
+ const prefix = linePrefix(line.type);
17455
17500
  fullLines.push(prefix + line.content);
17456
17501
  }
17457
17502
  }
@@ -18839,6 +18884,7 @@ __export(themes_exports, {
18839
18884
  PartialThemeColorsSchema: () => PartialThemeColorsSchema,
18840
18885
  SetActiveThemeReqSchema: () => SetActiveThemeReqSchema,
18841
18886
  SetActiveThemeRespSchema: () => SetActiveThemeRespSchema,
18887
+ StoredCustomThemeSchema: () => StoredCustomThemeSchema,
18842
18888
  ThemeColorsSchema: () => ThemeColorsSchema,
18843
18889
  ThemeSummarySchema: () => ThemeSummarySchema,
18844
18890
  UpdateThemeBodySchema: () => UpdateThemeBodySchema,
@@ -19336,6 +19382,12 @@ function buildPartialThemeColorsSchema() {
19336
19382
  return external_exports.object(shape).partial();
19337
19383
  }
19338
19384
  var PartialThemeColorsSchema = buildPartialThemeColorsSchema();
19385
+ var StoredCustomThemeSchema = external_exports.object({
19386
+ id: external_exports.string().min(1),
19387
+ name: external_exports.string().min(1),
19388
+ baseTheme: external_exports.string().optional(),
19389
+ colors: external_exports.record(external_exports.string(), external_exports.string())
19390
+ });
19339
19391
  var ThemeSummarySchema = external_exports.object({
19340
19392
  id: external_exports.string(),
19341
19393
  name: external_exports.string(),
@@ -19452,6 +19504,13 @@ function parseQuery(c, schema) {
19452
19504
  }
19453
19505
  return { ok: true, data: result.data };
19454
19506
  }
19507
+ function requirePathParam(c, name) {
19508
+ const value = c.req.param(name);
19509
+ if (value === void 0 || value.trim() === "") {
19510
+ return { ok: false, response: c.json({ error: `Missing or empty path parameter: ${name}` }, 400) };
19511
+ }
19512
+ return { ok: true, data: value };
19513
+ }
19455
19514
  function errorResponse(c, message, status = 400) {
19456
19515
  return c.json({ error: message }, status);
19457
19516
  }
@@ -19469,6 +19528,13 @@ function parseAnalysisTimestamp(updatedAt) {
19469
19528
  }
19470
19529
  return /* @__PURE__ */ new Date(updatedAt + "Z");
19471
19530
  }
19531
+ function resolveFileId(fileIdMap, filePath) {
19532
+ const id = fileIdMap.get(filePath);
19533
+ if (id === void 0 || id === "") {
19534
+ throw new Error(`No review_file id for path: ${filePath}`);
19535
+ }
19536
+ return id;
19537
+ }
19472
19538
  var canceledAnalyses = /* @__PURE__ */ new Set();
19473
19539
  aiAnalysisRoutes.post("/analyze", async (c) => {
19474
19540
  const reviewId = resolveReviewId(c);
@@ -19632,7 +19698,7 @@ async function saveBinaryFiles(args) {
19632
19698
  if (binaryFiles.length === 0) return;
19633
19699
  debugLog(`Saving ${String(binaryFiles.length)} binary files with score 0`);
19634
19700
  const binaryScoreEntries = binaryFiles.map((f, idx) => ({
19635
- reviewFileId: fileIdMap.get(f.file_path) ?? "",
19701
+ reviewFileId: resolveFileId(fileIdMap, f.file_path),
19636
19702
  filePath: f.file_path,
19637
19703
  sortOrder: 99999 + idx,
19638
19704
  // re-sorted by `updateSortOrders` later
@@ -19701,7 +19767,7 @@ function riskAnalysisConfig(config2, repoRoot, fileIdMap, guidedReview, analysis
19701
19767
  r.aggregate = Math.max(r.aggregate, maxDimension);
19702
19768
  },
19703
19769
  mapResult: (r) => ({
19704
- reviewFileId: fileIdMap.get(r.filePath) ?? "",
19770
+ reviewFileId: resolveFileId(fileIdMap, r.filePath),
19705
19771
  filePath: r.filePath,
19706
19772
  sortOrder: 0,
19707
19773
  // Placeholder — final sort happens after all batches
@@ -19724,7 +19790,7 @@ function narrativeAnalysisConfig(config2, repoRoot, fileIdMap, guidedReview, ana
19724
19790
  () => mockNarrativeAnalysisBatch(files)
19725
19791
  ),
19726
19792
  mapResult: (r) => ({
19727
- reviewFileId: fileIdMap.get(r.filePath) ?? "",
19793
+ reviewFileId: resolveFileId(fileIdMap, r.filePath),
19728
19794
  filePath: r.filePath,
19729
19795
  sortOrder: r.position,
19730
19796
  // Batch-local position — will be re-sorted after merge
@@ -19749,7 +19815,7 @@ function guidedAnalysisConfig(config2, repoRoot, fileIdMap, guidedReview) {
19749
19815
  return runGuidedAnalysisBatch(files, config2, repoRoot, guidedReview);
19750
19816
  },
19751
19817
  mapResult: (r, idx) => ({
19752
- reviewFileId: fileIdMap.get(r.filePath) ?? "",
19818
+ reviewFileId: resolveFileId(fileIdMap, r.filePath),
19753
19819
  filePath: r.filePath,
19754
19820
  sortOrder: idx,
19755
19821
  aggregateScore: null,
@@ -19921,6 +19987,7 @@ import { Hono as Hono4 } from "hono";
19921
19987
  init_queries();
19922
19988
 
19923
19989
  // src/export/generate.ts
19990
+ init_zod();
19924
19991
  init_queries();
19925
19992
  import { spawnSync as spawnSync5 } from "child_process";
19926
19993
  import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
@@ -19928,9 +19995,11 @@ import { homedir as homedir2 } from "os";
19928
19995
  import { join as join5 } from "path";
19929
19996
  var DISMISS_FILE = join5(homedir2(), ".glassbox", "gitignore-dismissed.json");
19930
19997
  var DISMISS_DAYS = 30;
19998
+ var DismissalsSchema = external_exports.record(external_exports.string(), external_exports.number());
19931
19999
  function loadDismissals() {
19932
20000
  try {
19933
- return JSON.parse(readFileSync5(DISMISS_FILE, "utf-8"));
20001
+ const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync5(DISMISS_FILE, "utf-8")));
20002
+ return parsed.success ? parsed.data : {};
19934
20003
  } catch {
19935
20004
  return {};
19936
20005
  }
@@ -20089,26 +20158,34 @@ annotationsRoutes.post("/annotations", async (c) => {
20089
20158
  return c.json(annotation, 201);
20090
20159
  });
20091
20160
  annotationsRoutes.patch("/annotations/:id", async (c) => {
20161
+ const id = requirePathParam(c, "id");
20162
+ if (!id.ok) return id.response;
20092
20163
  const parsed = await parseBody(c, UpdateAnnotationBodySchema);
20093
20164
  if (!parsed.ok) return parsed.response;
20094
- await updateAnnotation(c.req.param("id"), parsed.data.content, parsed.data.category);
20165
+ await updateAnnotation(id.data, parsed.data.content, parsed.data.category);
20095
20166
  autoExport(c);
20096
20167
  return c.json({ ok: true });
20097
20168
  });
20098
20169
  annotationsRoutes.delete("/annotations/:id", async (c) => {
20099
- await deleteAnnotation(c.req.param("id"));
20170
+ const id = requirePathParam(c, "id");
20171
+ if (!id.ok) return id.response;
20172
+ await deleteAnnotation(id.data);
20100
20173
  autoExport(c);
20101
20174
  return c.json({ ok: true });
20102
20175
  });
20103
20176
  annotationsRoutes.patch("/annotations/:id/move", async (c) => {
20177
+ const id = requirePathParam(c, "id");
20178
+ if (!id.ok) return id.response;
20104
20179
  const parsed = await parseBody(c, MoveAnnotationBodySchema);
20105
20180
  if (!parsed.ok) return parsed.response;
20106
- await moveAnnotation(c.req.param("id"), parsed.data.lineNumber, parsed.data.side);
20181
+ await moveAnnotation(id.data, parsed.data.lineNumber, parsed.data.side);
20107
20182
  autoExport(c);
20108
20183
  return c.json({ ok: true });
20109
20184
  });
20110
20185
  annotationsRoutes.post("/annotations/:id/keep", async (c) => {
20111
- await markAnnotationCurrent(c.req.param("id"));
20186
+ const id = requirePathParam(c, "id");
20187
+ if (!id.ok) return id.response;
20188
+ await markAnnotationCurrent(id.data);
20112
20189
  autoExport(c);
20113
20190
  return c.json({ ok: true });
20114
20191
  });
@@ -20136,7 +20213,9 @@ init_queries();
20136
20213
  var contextRoutes = new Hono5();
20137
20214
  contextRoutes.get("/context/:fileId", async (c) => {
20138
20215
  const repoRoot = c.get("repoRoot");
20139
- const file2 = await getReviewFile(c.req.param("fileId"));
20216
+ const fileId = requirePathParam(c, "fileId");
20217
+ if (!fileId.ok) return fileId.response;
20218
+ const file2 = await getReviewFile(fileId.data);
20140
20219
  if (!file2) return c.json({ error: "Not found" }, 404);
20141
20220
  const parsed = parseQuery(c, GetContextLinesQuerySchema);
20142
20221
  if (!parsed.ok) return parsed.response;
@@ -20192,19 +20271,25 @@ filesRoutes.get("/files", async (c) => {
20192
20271
  return c.json({ files, annotationCounts, staleCounts });
20193
20272
  });
20194
20273
  filesRoutes.get("/files/:fileId", async (c) => {
20195
- const file2 = await getReviewFile(c.req.param("fileId"));
20274
+ const fileId = requirePathParam(c, "fileId");
20275
+ if (!fileId.ok) return fileId.response;
20276
+ const file2 = await getReviewFile(fileId.data);
20196
20277
  if (!file2) return c.json({ error: "Not found" }, 404);
20197
20278
  const annotations = await getAnnotationsForFile(file2.id);
20198
20279
  return c.json({ file: file2, annotations });
20199
20280
  });
20200
20281
  filesRoutes.patch("/files/:fileId/status", async (c) => {
20282
+ const fileId = requirePathParam(c, "fileId");
20283
+ if (!fileId.ok) return fileId.response;
20201
20284
  const parsed = await parseBody(c, SetFileStatusBodySchema);
20202
20285
  if (!parsed.ok) return parsed.response;
20203
- await updateFileStatus(c.req.param("fileId"), parsed.data.status);
20286
+ await updateFileStatus(fileId.data, parsed.data.status);
20204
20287
  return c.json({ ok: true });
20205
20288
  });
20206
20289
  filesRoutes.post("/files/:fileId/reveal", async (c) => {
20207
- const file2 = await getReviewFile(c.req.param("fileId"));
20290
+ const fileId = requirePathParam(c, "fileId");
20291
+ if (!fileId.ok) return fileId.response;
20292
+ const file2 = await getReviewFile(fileId.data);
20208
20293
  if (!file2) return c.json({ error: "Not found" }, 404);
20209
20294
  const repoRoot = c.get("repoRoot");
20210
20295
  const fullPath = resolve4(repoRoot, file2.file_path);
@@ -20528,13 +20613,16 @@ function getNewImage(mode, filePath, repoRoot) {
20528
20613
  }
20529
20614
 
20530
20615
  // src/git/svg-rasterize.ts
20616
+ import { Worker } from "worker_threads";
20617
+
20618
+ // src/git/svg-rasterize-render.ts
20531
20619
  import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
20532
20620
  import { createRequire } from "module";
20533
20621
  import { join as join6 } from "path";
20534
20622
  var initialized = false;
20535
20623
  var ResvgClass;
20536
20624
  var fontBuffers = [];
20537
- async function ensureInit() {
20625
+ async function ensureRenderInit() {
20538
20626
  if (initialized) return;
20539
20627
  const require2 = createRequire(import.meta.url);
20540
20628
  const resvgPath = require2.resolve("@resvg/resvg-wasm");
@@ -20637,9 +20725,8 @@ function svgUsesExternalFonts(svgData) {
20637
20725
  const svg = svgData.toString("utf-8");
20638
20726
  return /<text[\s>]/i.test(svg) || /font-family/i.test(svg) || /@font-face/i.test(svg);
20639
20727
  }
20640
- async function rasterizeSvg(svgData) {
20641
- await ensureInit();
20642
- const svgString = svgData.toString("utf-8");
20728
+ async function renderSvgToPng(svgString) {
20729
+ await ensureRenderInit();
20643
20730
  const { width, height } = parseSvgDimensions(svgString);
20644
20731
  const maxDim = Math.max(width, height);
20645
20732
  const scale = Math.min(10, 8e3 / maxDim);
@@ -20659,11 +20746,118 @@ async function rasterizeSvg(svgData) {
20659
20746
  return png;
20660
20747
  }
20661
20748
 
20749
+ // src/git/svg-rasterize.ts
20750
+ var RENDER_TIMEOUT_MS = 15e3;
20751
+ var worker = null;
20752
+ var workerReady = false;
20753
+ var workerDisabled = false;
20754
+ var nextJobId = 0;
20755
+ var pending = /* @__PURE__ */ new Map();
20756
+ function resolveWorkerUrl() {
20757
+ return import.meta.url.endsWith(".ts") ? new URL("./svg-rasterize-worker-boot.mjs", import.meta.url) : new URL("./svg-rasterize-worker.js", import.meta.url);
20758
+ }
20759
+ function clearJobTimer(job) {
20760
+ if (job.timer !== void 0) clearTimeout(job.timer);
20761
+ }
20762
+ function fallbackAllPending() {
20763
+ for (const [id, job] of pending) {
20764
+ pending.delete(id);
20765
+ clearJobTimer(job);
20766
+ renderSvgToPng(job.svg).then(job.resolve, job.reject);
20767
+ }
20768
+ }
20769
+ function rejectAllPending(err) {
20770
+ for (const [id, job] of pending) {
20771
+ pending.delete(id);
20772
+ clearJobTimer(job);
20773
+ job.reject(err);
20774
+ }
20775
+ }
20776
+ function disposeWorker() {
20777
+ if (worker) {
20778
+ worker.removeAllListeners();
20779
+ void worker.terminate();
20780
+ }
20781
+ worker = null;
20782
+ workerReady = false;
20783
+ }
20784
+ function onJobTimeout(id) {
20785
+ const job = pending.get(id);
20786
+ if (!job) return;
20787
+ pending.delete(id);
20788
+ clearJobTimer(job);
20789
+ const requeue = [...pending.values()];
20790
+ pending.clear();
20791
+ for (const r of requeue) clearJobTimer(r);
20792
+ disposeWorker();
20793
+ job.reject(new Error(`SVG rasterization timed out after ${RENDER_TIMEOUT_MS} ms`));
20794
+ for (const r of requeue) submit(r);
20795
+ }
20796
+ function getWorker() {
20797
+ if (workerDisabled) return null;
20798
+ if (worker) return worker;
20799
+ let spawned;
20800
+ try {
20801
+ spawned = new Worker(resolveWorkerUrl());
20802
+ } catch {
20803
+ workerDisabled = true;
20804
+ return null;
20805
+ }
20806
+ spawned.on("message", (msg) => {
20807
+ if (msg.type === "ready") {
20808
+ workerReady = true;
20809
+ return;
20810
+ }
20811
+ const job = pending.get(msg.id);
20812
+ if (!job) return;
20813
+ pending.delete(msg.id);
20814
+ clearJobTimer(job);
20815
+ if (msg.type === "result") job.resolve(Buffer.from(msg.png));
20816
+ else job.reject(new Error(msg.message));
20817
+ });
20818
+ const handleFailure = (err) => {
20819
+ const startupFailure = !workerReady;
20820
+ disposeWorker();
20821
+ if (startupFailure) {
20822
+ workerDisabled = true;
20823
+ fallbackAllPending();
20824
+ } else {
20825
+ rejectAllPending(err);
20826
+ }
20827
+ };
20828
+ spawned.on("error", handleFailure);
20829
+ spawned.on("exit", (code) => {
20830
+ if (code !== 0) handleFailure(new Error(`SVG rasterization worker exited with code ${code}`));
20831
+ });
20832
+ worker = spawned;
20833
+ return spawned;
20834
+ }
20835
+ function submit(job) {
20836
+ const activeWorker = getWorker();
20837
+ if (!activeWorker) {
20838
+ renderSvgToPng(job.svg).then(job.resolve, job.reject);
20839
+ return;
20840
+ }
20841
+ const id = nextJobId++;
20842
+ job.timer = setTimeout(() => {
20843
+ onJobTimeout(id);
20844
+ }, RENDER_TIMEOUT_MS);
20845
+ pending.set(id, job);
20846
+ activeWorker.postMessage({ id, svg: job.svg });
20847
+ }
20848
+ async function rasterizeSvg(svgData) {
20849
+ const svg = svgData.toString("utf-8");
20850
+ return new Promise((resolve9, reject) => {
20851
+ submit({ svg, resolve: resolve9, reject });
20852
+ });
20853
+ }
20854
+
20662
20855
  // src/routes/api/image.ts
20663
20856
  var imageRoutes = new Hono7();
20664
20857
  imageRoutes.get("/image/:fileId/metadata", async (c) => {
20665
- const fileId = c.req.param("fileId");
20666
- const file2 = await getReviewFile(fileId);
20858
+ const fileIdParam = requirePathParam(c, "fileId");
20859
+ if (!fileIdParam.ok) return fileIdParam.response;
20860
+ const file2 = await getReviewFile(fileIdParam.data);
20667
20861
  if (!file2) return c.json({ error: "Not found" }, 404);
20668
20862
  const repoRoot = c.get("repoRoot");
20669
20863
  const review = await getReview(file2.review_id);
@@ -20682,10 +20876,11 @@ imageRoutes.get("/image/:fileId/metadata", async (c) => {
20682
20876
  });
20683
20877
  });
20684
20878
  imageRoutes.get("/image/:fileId/:side", async (c) => {
20685
- const fileId = c.req.param("fileId");
20879
+ const fileIdParam = requirePathParam(c, "fileId");
20880
+ if (!fileIdParam.ok) return fileIdParam.response;
20686
20881
  const side = c.req.param("side");
20687
20882
  if (side !== "old" && side !== "new") return c.text("Invalid side", 400);
20688
- const file2 = await getReviewFile(fileId);
20883
+ const file2 = await getReviewFile(fileIdParam.data);
20689
20884
  if (!file2) return c.text("Not found", 404);
20690
20885
  const repoRoot = c.get("repoRoot");
20691
20886
  const review = await getReview(file2.review_id);
@@ -21040,7 +21235,9 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
21040
21235
  var outlineRoutes = new Hono8();
21041
21236
  outlineRoutes.get("/outline/:fileId", async (c) => {
21042
21237
  const repoRoot = c.get("repoRoot");
21043
- const file2 = await getReviewFile(c.req.param("fileId"));
21238
+ const fileId = requirePathParam(c, "fileId");
21239
+ if (!fileId.ok) return fileId.response;
21240
+ const file2 = await getReviewFile(fileId.data);
21044
21241
  if (!file2) return c.json({ error: "Not found" }, 404);
21045
21242
  const diff = parseDiffData(file2.diff_data);
21046
21243
  const isDeleted = diff?.status === "deleted";
@@ -21211,7 +21408,9 @@ reviewsRoutes.post("/review/refresh", async (c) => {
21211
21408
  });
21212
21409
  });
21213
21410
  reviewsRoutes.delete("/review/:id", async (c) => {
21214
- const reviewId = c.req.param("id");
21411
+ const idParam = requirePathParam(c, "id");
21412
+ if (!idParam.ok) return idParam.response;
21413
+ const reviewId = idParam.data;
21215
21414
  const currentReviewId2 = c.get("currentReviewId");
21216
21415
  if (reviewId === currentReviewId2) {
21217
21416
  return c.json({ error: "Cannot delete the current review" }, 400);
@@ -21499,6 +21698,17 @@ function buildSegments(str, commonPositions) {
21499
21698
  return segments;
21500
21699
  }
21501
21700
 
21701
+ // src/utils/lineTruncate.ts
21702
+ var MAX_DIFF_LINE_LENGTH = 5e3;
21703
+ function truncateDiffLine(content) {
21704
+ if (content.length <= MAX_DIFF_LINE_LENGTH) return null;
21705
+ return {
21706
+ text: content.slice(0, MAX_DIFF_LINE_LENGTH),
21707
+ hidden: content.length - MAX_DIFF_LINE_LENGTH,
21708
+ fullLength: content.length
21709
+ };
21710
+ }
21711
+
21502
21712
  // src/components/imageDiff.tsx
21503
21713
  import { jsx as jsx2, jsxs as jsxs2 } from "kerfjs/jsx-runtime";
21504
21714
  function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
@@ -21719,6 +21929,25 @@ function renderSplitColumn(items, side) {
21719
21929
  function renderSegments(segments) {
21720
21930
  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 })) });
21721
21931
  }
21932
+ function renderLineContent(content) {
21933
+ const t = truncateDiffLine(content);
21934
+ if (t === null) return content;
21935
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
21936
+ t.text,
21937
+ /* @__PURE__ */ jsxs3(
21938
+ "span",
21939
+ {
21940
+ className: "line-truncated",
21941
+ title: `Line too long to display \u2014 ${t.fullLength.toLocaleString("en-US")} characters total`,
21942
+ children: [
21943
+ "\u2026 \u27E8",
21944
+ t.hidden.toLocaleString("en-US"),
21945
+ " more characters hidden\u27E9"
21946
+ ]
21947
+ }
21948
+ )
21949
+ ] });
21950
+ }
21722
21951
  function renderPairContent(pair, side) {
21723
21952
  const line = side === "left" ? pair.left : pair.right;
21724
21953
  if (!line) return "";
@@ -21728,7 +21957,7 @@ function renderPairContent(pair, side) {
21728
21957
  return renderSegments(side === "left" ? diff.oldSegments : diff.newSegments);
21729
21958
  }
21730
21959
  }
21731
- return line.content;
21960
+ return renderLineContent(line.content);
21732
21961
  }
21733
21962
  function pairLines(lines) {
21734
21963
  const pairs = [];
@@ -21836,7 +22065,7 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
21836
22065
  children: [
21837
22066
  /* @__PURE__ */ jsx3("span", { className: "gutter-old", "data-line-number": line.oldNum ?? "" }),
21838
22067
  /* @__PURE__ */ jsx3("span", { className: "gutter-new", "data-line-number": line.newNum ?? "" }),
21839
- /* @__PURE__ */ jsx3("span", { className: "code", children: segments ? renderSegments(segments) : line.content })
22068
+ /* @__PURE__ */ jsx3("span", { className: "code", children: segments ? renderSegments(segments) : renderLineContent(line.content) })
21840
22069
  ]
21841
22070
  }
21842
22071
  ),
@@ -21893,10 +22122,10 @@ function loadCustomThemes() {
21893
22122
  const files = readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json"));
21894
22123
  for (const file2 of files) {
21895
22124
  try {
21896
- const data = JSON.parse(readFileSync10(join9(THEMES_DIR, file2), "utf-8"));
21897
- if (data.id !== void 0 && data.id !== "" && data.name !== void 0 && data.name !== "" && data.colors !== void 0) {
21898
- themes.push({ id: data.id, name: data.name, colors: data.colors, builtIn: false, baseTheme: data.baseTheme ?? "" });
21899
- }
22125
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync10(join9(THEMES_DIR, file2), "utf-8")));
22126
+ if (!parsed.success) continue;
22127
+ const d = parsed.data;
22128
+ themes.push({ id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" });
21900
22129
  } catch {
21901
22130
  }
21902
22131
  }
@@ -21919,8 +22148,10 @@ function getCustomTheme(id) {
21919
22148
  const filePath = join9(THEMES_DIR, `${id}.json`);
21920
22149
  if (!existsSync7(filePath)) return void 0;
21921
22150
  try {
21922
- const data = JSON.parse(readFileSync10(filePath, "utf-8"));
21923
- return { ...data, builtIn: false };
22151
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync10(filePath, "utf-8")));
22152
+ if (!parsed.success) return void 0;
22153
+ const d = parsed.data;
22154
+ return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
21924
22155
  } catch {
21925
22156
  return void 0;
21926
22157
  }
@@ -22393,7 +22624,9 @@ themeApiRoutes.post("/", async (c) => {
22393
22624
  return c.json(newTheme, 201);
22394
22625
  });
22395
22626
  themeApiRoutes.post("/:id/edit", async (c) => {
22396
- const id = c.req.param("id");
22627
+ const idParam = requirePathParam(c, "id");
22628
+ if (!idParam.ok) return idParam.response;
22629
+ const id = idParam.data;
22397
22630
  const parsed = await parseBody(c, EditThemeBodySchema);
22398
22631
  if (!parsed.ok) return parsed.response;
22399
22632
  const body = parsed.data;
@@ -22421,7 +22654,9 @@ themeApiRoutes.post("/:id/edit", async (c) => {
22421
22654
  return c.json({ theme: updated, copied: false });
22422
22655
  });
22423
22656
  themeApiRoutes.patch("/:id", async (c) => {
22424
- const id = c.req.param("id");
22657
+ const idParam = requirePathParam(c, "id");
22658
+ if (!idParam.ok) return idParam.response;
22659
+ const id = idParam.data;
22425
22660
  if (getBuiltInTheme(id)) {
22426
22661
  return c.json({ error: "Cannot edit built-in theme" }, 400);
22427
22662
  }
@@ -22441,7 +22676,9 @@ themeApiRoutes.patch("/:id", async (c) => {
22441
22676
  return c.json(updated);
22442
22677
  });
22443
22678
  themeApiRoutes.delete("/:id", (c) => {
22444
- const id = c.req.param("id");
22679
+ const idParam = requirePathParam(c, "id");
22680
+ if (!idParam.ok) return idParam.response;
22681
+ const id = idParam.data;
22445
22682
  if (getBuiltInTheme(id)) {
22446
22683
  return c.json({ error: "Cannot delete built-in theme" }, 400);
22447
22684
  }
@@ -22909,7 +23146,7 @@ async function main() {
22909
23146
  console.log("AI service test mode enabled \u2014 using mock AI responses");
22910
23147
  }
22911
23148
  if (debug) {
22912
- console.log(`[debug] Build timestamp: ${"2026-05-25T08:36:26.737Z"}`);
23149
+ console.log(`[debug] Build timestamp: ${"2026-05-27T07:54:11.326Z"}`);
22913
23150
  }
22914
23151
  if (projectDir !== null) {
22915
23152
  process.chdir(projectDir);