glassbox 0.16.0 → 0.17.0-beta.3

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
@@ -46,6 +46,8 @@ var init_schema = __esm({
46
46
  CREATE TABLE IF NOT EXISTS annotations (
47
47
  id TEXT PRIMARY KEY,
48
48
  review_file_id TEXT NOT NULL REFERENCES review_files(id) ON DELETE CASCADE,
49
+ -- 0 marks an image-level annotation (doc 23); line annotations use the
50
+ -- real 1-based line number.
49
51
  line_number INTEGER NOT NULL,
50
52
  side TEXT NOT NULL DEFAULT 'new',
51
53
  category TEXT NOT NULL DEFAULT 'note',
@@ -53,6 +55,9 @@ var init_schema = __esm({
53
55
  is_stale BOOLEAN NOT NULL DEFAULT FALSE,
54
56
  original_content TEXT,
55
57
  reply_to_note_id TEXT,
58
+ -- JSON {x,y,w,h} normalized fractions for an image-region annotation
59
+ -- (doc 23), or NULL for line annotations and general image comments.
60
+ region_data TEXT,
56
61
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
57
62
  updated_at TIMESTAMP NOT NULL DEFAULT NOW()
58
63
  );
@@ -148,6 +153,7 @@ async function initSchema(db2) {
148
153
  await addColumnIfMissing(db2, "annotations", "is_stale", "BOOLEAN NOT NULL DEFAULT FALSE");
149
154
  await addColumnIfMissing(db2, "annotations", "original_content", "TEXT");
150
155
  await addColumnIfMissing(db2, "annotations", "reply_to_note_id", "TEXT");
156
+ await addColumnIfMissing(db2, "annotations", "region_data", "TEXT");
151
157
  await addColumnIfMissing(db2, "ai_file_scores", "notes", "TEXT");
152
158
  await addColumnIfMissing(db2, "ai_analyses", "progress_completed", "INTEGER NOT NULL DEFAULT 0");
153
159
  await addColumnIfMissing(db2, "ai_analyses", "progress_total", "INTEGER NOT NULL DEFAULT 0");
@@ -15382,7 +15388,7 @@ function parseJsonColumn(schema, raw2) {
15382
15388
  const result = schema.safeParse(parsed);
15383
15389
  return result.success ? result.data : null;
15384
15390
  }
15385
- var TimestampSchema, ReviewSchema, ReviewFileSchema, AnnotationSchema, AnnotationWithFilePathSchema, AIAnalysisSchema, AIFileScoreSchema, UserPreferencesSchema, DimensionScoresSchema, FileScoreNotesSchema;
15391
+ var TimestampSchema, ReviewSchema, ReviewFileSchema, ImageRegionSchema, AnnotationSchema, AnnotationWithFilePathSchema, AIAnalysisSchema, AIFileScoreSchema, UserPreferencesSchema, DimensionScoresSchema, FileScoreNotesSchema;
15386
15392
  var init_schemas3 = __esm({
15387
15393
  "src/db/schemas.ts"() {
15388
15394
  "use strict";
@@ -15414,10 +15420,24 @@ var init_schemas3 = __esm({
15414
15420
  diff_data: external_exports.string().nullable(),
15415
15421
  created_at: TimestampSchema
15416
15422
  });
15423
+ ImageRegionSchema = external_exports.object({
15424
+ x: external_exports.number().min(0).max(1),
15425
+ y: external_exports.number().min(0).max(1),
15426
+ w: external_exports.number().min(0).max(1),
15427
+ h: external_exports.number().min(0).max(1),
15428
+ // Optional per-side scope (doc 23 §23.6 / §23.10): `old` = applies to the A
15429
+ // (old) image only, `new` = applies to the B (new) image only. Absent means
15430
+ // the region applies to both sides (the default), which is how every region
15431
+ // created before this field existed is interpreted.
15432
+ side: external_exports.enum(["old", "new"]).optional()
15433
+ });
15417
15434
  AnnotationSchema = external_exports.object({
15418
15435
  id: external_exports.string(),
15419
- review_file_id: external_exports.string(),
15436
+ // 0 marks an image-level annotation (doc 23): a general image comment, or —
15437
+ // when `region_data` is set — a comment anchored to a rectangle on the image.
15438
+ // Line-anchored text-diff annotations use the real 1-based line number.
15420
15439
  line_number: external_exports.number(),
15440
+ review_file_id: external_exports.string(),
15421
15441
  side: external_exports.string(),
15422
15442
  category: external_exports.string(),
15423
15443
  content: external_exports.string(),
@@ -15427,6 +15447,10 @@ var init_schemas3 = __esm({
15427
15447
  // threading), or null for a normal annotation. `.default(null)` tolerates
15428
15448
  // rows written before the column existed.
15429
15449
  reply_to_note_id: external_exports.string().nullable().default(null),
15450
+ // JSON-encoded {@link ImageRegion} for image-region annotations (doc 23), or
15451
+ // null for line annotations and general image comments. `.default(null)`
15452
+ // tolerates rows written before the column existed.
15453
+ region_data: external_exports.string().nullable().default(null),
15430
15454
  created_at: TimestampSchema,
15431
15455
  updated_at: TimestampSchema
15432
15456
  });
@@ -15501,6 +15525,7 @@ __export(queries_exports, {
15501
15525
  markAnnotationStale: () => markAnnotationStale,
15502
15526
  moveAnnotation: () => moveAnnotation,
15503
15527
  updateAnnotation: () => updateAnnotation,
15528
+ updateAnnotationRegion: () => updateAnnotationRegion,
15504
15529
  updateFileDiff: () => updateFileDiff,
15505
15530
  updateFileStatus: () => updateFileStatus,
15506
15531
  updateReviewHead: () => updateReviewHead,
@@ -15598,13 +15623,22 @@ async function deleteReviewFile(id) {
15598
15623
  await db2.query("DELETE FROM annotations WHERE review_file_id = $1", [id]);
15599
15624
  await db2.query("DELETE FROM review_files WHERE id = $1", [id]);
15600
15625
  }
15601
- async function addAnnotation(reviewFileId, lineNumber, side, category, content, replyToNoteId) {
15626
+ async function addAnnotation(reviewFileId, lineNumber, side, category, content, replyToNoteId, region) {
15602
15627
  const db2 = await getDb();
15603
15628
  const id = generateId();
15604
15629
  const result = await db2.query(
15605
- `INSERT INTO annotations (id, review_file_id, line_number, side, category, content, reply_to_note_id)
15606
- VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
15607
- [id, reviewFileId, lineNumber, side, category, content, replyToNoteId ?? null]
15630
+ `INSERT INTO annotations (id, review_file_id, line_number, side, category, content, reply_to_note_id, region_data)
15631
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
15632
+ [
15633
+ id,
15634
+ reviewFileId,
15635
+ lineNumber,
15636
+ side,
15637
+ category,
15638
+ content,
15639
+ replyToNoteId ?? null,
15640
+ region !== void 0 ? JSON.stringify(region) : null
15641
+ ]
15608
15642
  );
15609
15643
  const annotation = parseRow(AnnotationSchema, result.rows[0]);
15610
15644
  if (annotation === void 0) throw new Error("addAnnotation: INSERT did not return a row");
@@ -15640,6 +15674,13 @@ async function deleteAnnotation(id) {
15640
15674
  const db2 = await getDb();
15641
15675
  await db2.query("DELETE FROM annotations WHERE id = $1", [id]);
15642
15676
  }
15677
+ async function updateAnnotationRegion(id, region) {
15678
+ const db2 = await getDb();
15679
+ await db2.query(
15680
+ "UPDATE annotations SET region_data = $1, updated_at = NOW() WHERE id = $2",
15681
+ [JSON.stringify(region), id]
15682
+ );
15683
+ }
15643
15684
  async function moveAnnotation(id, lineNumber, side) {
15644
15685
  const db2 = await getDb();
15645
15686
  await db2.query(
@@ -15725,6 +15766,48 @@ var init_queries = __esm({
15725
15766
  }
15726
15767
  });
15727
15768
 
15769
+ // src/git/image-blobs.ts
15770
+ var image_blobs_exports = {};
15771
+ __export(image_blobs_exports, {
15772
+ clearImageBlobs: () => clearImageBlobs,
15773
+ readImageBlob: () => readImageBlob,
15774
+ writeImageBlob: () => writeImageBlob
15775
+ });
15776
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
15777
+ import { join as join3 } from "path";
15778
+ function blobDir(dataDir) {
15779
+ return join3(dataDir, "image-blobs");
15780
+ }
15781
+ function blobName(fileId, side) {
15782
+ return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
15783
+ }
15784
+ function writeImageBlob(dataDir, fileId, side, bytes) {
15785
+ if (bytes.length === 0) return;
15786
+ const dir = blobDir(dataDir);
15787
+ mkdirSync3(dir, { recursive: true });
15788
+ writeFileSync2(join3(dir, blobName(fileId, side)), bytes);
15789
+ }
15790
+ function readImageBlob(dataDir, fileId, side) {
15791
+ const path = join3(blobDir(dataDir), blobName(fileId, side));
15792
+ if (!existsSync2(path)) return null;
15793
+ try {
15794
+ return readFileSync2(path);
15795
+ } catch {
15796
+ return null;
15797
+ }
15798
+ }
15799
+ function clearImageBlobs(dataDir) {
15800
+ try {
15801
+ rmSync2(blobDir(dataDir), { recursive: true, force: true });
15802
+ } catch {
15803
+ }
15804
+ }
15805
+ var init_image_blobs = __esm({
15806
+ "src/git/image-blobs.ts"() {
15807
+ "use strict";
15808
+ }
15809
+ });
15810
+
15728
15811
  // src/review-notes/types.ts
15729
15812
  function isNoteKind(value) {
15730
15813
  return NOTE_KINDS.includes(value);
@@ -15848,18 +15931,18 @@ var init_view = __esm({
15848
15931
  // src/review-notes/store.ts
15849
15932
  import { spawnSync as spawnSync5 } from "child_process";
15850
15933
  import { createHash } from "crypto";
15851
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
15852
- import { dirname as dirname3, join as join6 } from "path";
15934
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync6, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
15935
+ import { dirname as dirname3, join as join7 } from "path";
15853
15936
  function sanitizeRel(file2) {
15854
15937
  const rel = file2.replace(/\\/g, "/").replace(/^\/+/, "").replace(/(^|\/)\.\.(?=\/|$)/g, "$1_");
15855
15938
  return rel === "" ? "file" : rel;
15856
15939
  }
15857
15940
  function shardPath(repoRoot, safeRel, index) {
15858
- return join6(repoRoot, NOTES_SUBDIR, `${safeRel}.${String(index).padStart(6, "0")}.sarif`);
15941
+ return join7(repoRoot, NOTES_SUBDIR, `${safeRel}.${String(index).padStart(6, "0")}.sarif`);
15859
15942
  }
15860
15943
  function listShardIndices(repoRoot, safeRel) {
15861
- const dir = join6(repoRoot, NOTES_SUBDIR, dirname3(safeRel));
15862
- if (!existsSync5(dir)) return [];
15944
+ const dir = join7(repoRoot, NOTES_SUBDIR, dirname3(safeRel));
15945
+ if (!existsSync6(dir)) return [];
15863
15946
  const base = safeRel.split("/").pop() ?? safeRel;
15864
15947
  const indices = [];
15865
15948
  for (const entry of readdirSync(dir)) {
@@ -15873,7 +15956,7 @@ function totalResults(log) {
15873
15956
  return log.runs.reduce((sum, run) => sum + run.results.length, 0);
15874
15957
  }
15875
15958
  function readLog(path) {
15876
- const raw2 = JSON.parse(readFileSync5(path, "utf-8"));
15959
+ const raw2 = JSON.parse(readFileSync6(path, "utf-8"));
15877
15960
  const parsed = SarifLogShapeSchema.safeParse(raw2);
15878
15961
  if (!parsed.success) {
15879
15962
  throw new Error(`existing notes shard is not a SARIF log we recognize, refusing to overwrite: ${path}`);
@@ -15899,7 +15982,7 @@ function gitValue(repoRoot, args) {
15899
15982
  }
15900
15983
  function anchorSnippet(repoRoot, safeRel, startLine, endLine) {
15901
15984
  try {
15902
- const lines = readFileSync5(join6(repoRoot, safeRel), "utf-8").split("\n");
15985
+ const lines = readFileSync6(join7(repoRoot, safeRel), "utf-8").split("\n");
15903
15986
  const slice = lines.slice(Math.max(0, startLine - 1), Math.max(startLine, endLine));
15904
15987
  if (slice.length === 0) return {};
15905
15988
  const snippet = slice.join("\n");
@@ -15937,7 +16020,7 @@ function writeReviewNote(repoRoot, input, opts = {}) {
15937
16020
  let index = indices.length > 0 ? indices[indices.length - 1] : 0;
15938
16021
  let path = shardPath(repoRoot, safeRel, index);
15939
16022
  let log;
15940
- if (existsSync5(path)) {
16023
+ if (existsSync6(path)) {
15941
16024
  log = readLog(path);
15942
16025
  if (totalResults(log) >= cap) {
15943
16026
  index += 1;
@@ -15948,12 +16031,12 @@ function writeReviewNote(repoRoot, input, opts = {}) {
15948
16031
  log = emptyLog(producer, { producerVersion: input.producerVersion, ...vcs });
15949
16032
  }
15950
16033
  findOrAddRun(log, producer, input.producerVersion, vcs).results.push(result);
15951
- mkdirSync4(dirname3(path), { recursive: true });
15952
- writeFileSync5(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
16034
+ mkdirSync5(dirname3(path), { recursive: true });
16035
+ writeFileSync6(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
15953
16036
  return { path, guid: guid3 };
15954
16037
  }
15955
16038
  function writeLog(path, log) {
15956
- writeFileSync5(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
16039
+ writeFileSync6(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
15957
16040
  }
15958
16041
  function hashArtifacts(repoRoot, artifacts) {
15959
16042
  const out = {};
@@ -15961,22 +16044,22 @@ function hashArtifacts(repoRoot, artifacts) {
15961
16044
  const safe = uri.replace(/\\/g, "/").replace(/^\/+/, "");
15962
16045
  if (/(^|\/)\.\.(\/|$)/.test(safe)) continue;
15963
16046
  try {
15964
- const abs = join6(repoRoot, safe);
16047
+ const abs = join7(repoRoot, safe);
15965
16048
  const stat = statSync2(abs);
15966
16049
  if (!stat.isFile() || stat.size > ARTIFACT_HASH_MAX_BYTES) continue;
15967
- out[uri] = createHash("sha256").update(readFileSync5(abs)).digest("hex");
16050
+ out[uri] = createHash("sha256").update(readFileSync6(abs)).digest("hex");
15968
16051
  } catch {
15969
16052
  }
15970
16053
  }
15971
16054
  return out;
15972
16055
  }
15973
16056
  function ensureArtifactLfsFilter(repoRoot) {
15974
- const path = join6(repoRoot, ".gitattributes");
16057
+ const path = join7(repoRoot, ".gitattributes");
15975
16058
  try {
15976
- const existing = existsSync5(path) ? readFileSync5(path, "utf-8") : "";
16059
+ const existing = existsSync6(path) ? readFileSync6(path, "utf-8") : "";
15977
16060
  if (existing.includes(".pr-notes/artifacts/**")) return;
15978
16061
  const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
15979
- writeFileSync5(path, `${existing}${prefix}${LFS_FILTER_LINE}
16062
+ writeFileSync6(path, `${existing}${prefix}${LFS_FILTER_LINE}
15980
16063
  `, "utf-8");
15981
16064
  } catch {
15982
16065
  }
@@ -15985,15 +16068,15 @@ function listShardPaths(repoRoot, safeRel) {
15985
16068
  return listShardIndices(repoRoot, safeRel).map((i) => shardPath(repoRoot, safeRel, i));
15986
16069
  }
15987
16070
  function allShardPaths(repoRoot) {
15988
- const root = join6(repoRoot, NOTES_SUBDIR);
15989
- if (!existsSync5(root)) return [];
16071
+ const root = join7(repoRoot, NOTES_SUBDIR);
16072
+ if (!existsSync6(root)) return [];
15990
16073
  const entries = readdirSync(root, { recursive: true });
15991
- return entries.filter((e) => SHARD_RE.test(e)).map((e) => join6(root, e));
16074
+ return entries.filter((e) => SHARD_RE.test(e)).map((e) => join7(root, e));
15992
16075
  }
15993
16076
  function persistOrDelete(path, log) {
15994
16077
  log.runs = log.runs.filter((run) => run.results.length > 0);
15995
16078
  if (log.runs.length === 0) {
15996
- if (existsSync5(path)) unlinkSync(path);
16079
+ if (existsSync6(path)) unlinkSync(path);
15997
16080
  } else {
15998
16081
  writeLog(path, log);
15999
16082
  }
@@ -16080,8 +16163,8 @@ function coalesceFile(repoRoot, file2) {
16080
16163
  return toRemove.length;
16081
16164
  }
16082
16165
  function coalesceAll(repoRoot) {
16083
- const root = join6(repoRoot, NOTES_SUBDIR);
16084
- if (!existsSync5(root)) return 0;
16166
+ const root = join7(repoRoot, NOTES_SUBDIR);
16167
+ if (!existsSync6(root)) return 0;
16085
16168
  const sources = /* @__PURE__ */ new Set();
16086
16169
  for (const entry of readdirSync(root, { recursive: true })) {
16087
16170
  const m = SHARD_RE.exec(entry);
@@ -16095,10 +16178,10 @@ function readArtifactText(repoRoot, uri) {
16095
16178
  const safe = uri.replace(/\\/g, "/").replace(/^\/+/, "");
16096
16179
  if (/(^|\/)\.\.(\/|$)/.test(safe)) return void 0;
16097
16180
  try {
16098
- const abs = join6(repoRoot, safe);
16181
+ const abs = join7(repoRoot, safe);
16099
16182
  const stat = statSync2(abs);
16100
16183
  if (!stat.isFile() || stat.size > ARTIFACT_MAX_BYTES) return void 0;
16101
- const buf = readFileSync5(abs);
16184
+ const buf = readFileSync6(abs);
16102
16185
  if (buf.includes(0)) return void 0;
16103
16186
  return buf.toString("utf-8");
16104
16187
  } catch {
@@ -16161,7 +16244,7 @@ var init_store = __esm({
16161
16244
  init_sarif();
16162
16245
  init_types();
16163
16246
  init_view();
16164
- NOTES_SUBDIR = join6(".pr-notes", "notes");
16247
+ NOTES_SUBDIR = join7(".pr-notes", "notes");
16165
16248
  SHARD_RE = /\.(\d{6})\.sarif$/;
16166
16249
  ARTIFACT_HASH_MAX_BYTES = 5e7;
16167
16250
  LFS_FILTER_LINE = ".pr-notes/artifacts/** filter=lfs diff=lfs merge=lfs -text";
@@ -16169,48 +16252,6 @@ var init_store = __esm({
16169
16252
  }
16170
16253
  });
16171
16254
 
16172
- // src/difftool/blob-store.ts
16173
- var blob_store_exports = {};
16174
- __export(blob_store_exports, {
16175
- clearDifftoolBlobs: () => clearDifftoolBlobs,
16176
- readDifftoolBlob: () => readDifftoolBlob,
16177
- writeDifftoolBlob: () => writeDifftoolBlob
16178
- });
16179
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
16180
- import { join as join8 } from "path";
16181
- function blobDir(dataDir) {
16182
- return join8(dataDir, "difftool-blobs");
16183
- }
16184
- function blobName(fileId, side) {
16185
- return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
16186
- }
16187
- function writeDifftoolBlob(dataDir, fileId, side, bytes) {
16188
- if (bytes.length === 0) return;
16189
- const dir = blobDir(dataDir);
16190
- mkdirSync6(dir, { recursive: true });
16191
- writeFileSync7(join8(dir, blobName(fileId, side)), bytes);
16192
- }
16193
- function readDifftoolBlob(dataDir, fileId, side) {
16194
- const path = join8(blobDir(dataDir), blobName(fileId, side));
16195
- if (!existsSync7(path)) return null;
16196
- try {
16197
- return readFileSync7(path);
16198
- } catch {
16199
- return null;
16200
- }
16201
- }
16202
- function clearDifftoolBlobs(dataDir) {
16203
- try {
16204
- rmSync4(blobDir(dataDir), { recursive: true, force: true });
16205
- } catch {
16206
- }
16207
- }
16208
- var init_blob_store = __esm({
16209
- "src/difftool/blob-store.ts"() {
16210
- "use strict";
16211
- }
16212
- });
16213
-
16214
16255
  // src/difftool/session.ts
16215
16256
  var session_exports = {};
16216
16257
  __export(session_exports, {
@@ -16715,17 +16756,17 @@ __export(difftool_discovery_exports, {
16715
16756
  tryAcquireStartingLock: () => tryAcquireStartingLock,
16716
16757
  writeDiscovery: () => writeDiscovery
16717
16758
  });
16718
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync18, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
16759
+ import { existsSync as existsSync13, mkdirSync as mkdirSync12, readFileSync as readFileSync17, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
16719
16760
  import { homedir as homedir4 } from "os";
16720
- import { join as join17 } from "path";
16761
+ import { join as join16 } from "path";
16721
16762
  function difftoolHome() {
16722
- return join17(homedir4(), ".glassbox");
16763
+ return join16(homedir4(), ".glassbox");
16723
16764
  }
16724
16765
  function discoveryPath(home = difftoolHome()) {
16725
- return join17(home, "difftool.lock");
16766
+ return join16(home, "difftool.lock");
16726
16767
  }
16727
16768
  function startingLockPath(home = difftoolHome()) {
16728
- return join17(home, "difftool-starting.lock");
16769
+ return join16(home, "difftool-starting.lock");
16729
16770
  }
16730
16771
  function parseDiscovery(raw2) {
16731
16772
  let parsed;
@@ -16739,9 +16780,9 @@ function parseDiscovery(raw2) {
16739
16780
  }
16740
16781
  function readDiscovery(home = difftoolHome()) {
16741
16782
  const path = discoveryPath(home);
16742
- if (!existsSync14(path)) return null;
16783
+ if (!existsSync13(path)) return null;
16743
16784
  try {
16744
- return parseDiscovery(readFileSync18(path, "utf-8"));
16785
+ return parseDiscovery(readFileSync17(path, "utf-8"));
16745
16786
  } catch {
16746
16787
  return null;
16747
16788
  }
@@ -16797,9 +16838,9 @@ var init_difftool_discovery = __esm({
16797
16838
  // src/cli.ts
16798
16839
  init_connection();
16799
16840
  init_queries();
16800
- import { existsSync as existsSync15, mkdirSync as mkdirSync13, realpathSync, statSync as statSync5 } from "fs";
16841
+ import { existsSync as existsSync14, mkdirSync as mkdirSync13, realpathSync, statSync as statSync5 } from "fs";
16801
16842
  import { tmpdir as tmpdir2 } from "os";
16802
- import { basename as basename2, join as join18, resolve as resolve10 } from "path";
16843
+ import { basename as basename2, join as join17, resolve as resolve10 } from "path";
16803
16844
 
16804
16845
  // src/debug.ts
16805
16846
  var debugEnabled = false;
@@ -17383,7 +17424,18 @@ async function saveUserPreferences(prefs) {
17383
17424
  }
17384
17425
 
17385
17426
  // src/demo.ts
17427
+ init_connection();
17386
17428
  init_queries();
17429
+ init_image_blobs();
17430
+ function seedSvgBlobs(fileId, diff) {
17431
+ if (!diff.filePath.toLowerCase().endsWith(".svg")) return;
17432
+ const dataDir = getDataDir();
17433
+ if (dataDir === null) return;
17434
+ const lines = diff.hunks.flatMap((h) => h.lines);
17435
+ const sideBytes = (keep) => Buffer.from(lines.filter((l) => keep(l.type)).map((l) => l.content).join("\n"), "utf8");
17436
+ writeImageBlob(dataDir, fileId, "old", sideBytes((t) => t !== "add"));
17437
+ writeImageBlob(dataDir, fileId, "new", sideBytes((t) => t !== "remove"));
17438
+ }
17387
17439
  function demoReviewNotes(filePath) {
17388
17440
  if (filePath !== "src/auth/session.ts") return [];
17389
17441
  return [
@@ -17655,6 +17707,26 @@ var DEMO_FILES = [
17655
17707
  status: "modified",
17656
17708
  isBinary: true,
17657
17709
  hunks: []
17710
+ },
17711
+ {
17712
+ // A real SVG change so the demo (and the e2e suite) exercises the SVG
17713
+ // *rendered* view and its vector zoom (GB-941). Modeled as a rename so both
17714
+ // sides resolve to real committed icons (old = red, new = blue, both 20x20
17715
+ // with explicit width/height so the <img> reports a natural size). Default
17716
+ // view is the code (text) diff; the Rendered toggle switches to ImageDiff.
17717
+ path: "tests/fixtures/diff/new/icon.svg",
17718
+ oldPath: "tests/fixtures/diff/old/icon.svg",
17719
+ status: "modified",
17720
+ hunks: [{
17721
+ oldStart: 1,
17722
+ oldCount: 1,
17723
+ newStart: 1,
17724
+ newCount: 1,
17725
+ lines: [
17726
+ { type: "remove", oldNum: 1, newNum: null, content: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><rect width="20" height="20" fill="red"/></svg>' },
17727
+ { type: "add", oldNum: null, newNum: 1, content: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><rect width="20" height="20" fill="blue"/></svg>' }
17728
+ ]
17729
+ }]
17658
17730
  }
17659
17731
  ];
17660
17732
  var GUIDED_NOTES = {
@@ -17823,6 +17895,7 @@ async function setupDemoReview(scenario) {
17823
17895
  };
17824
17896
  const rf = await addReviewFile(review.id, file2.path, JSON.stringify(diff));
17825
17897
  fileIdMap.set(file2.path, rf.id);
17898
+ seedSvgBlobs(rf.id, diff);
17826
17899
  }
17827
17900
  const { updateFileStatus: updateFileStatus2 } = await Promise.resolve().then(() => (init_queries(), queries_exports));
17828
17901
  const reviewedPaths = ["src/utils/password.ts", "src/db/redis.ts", "package.json"];
@@ -17925,9 +17998,9 @@ async function setupReviewNoteReply(fileIdMap) {
17925
17998
  }
17926
17999
 
17927
18000
  // src/git/diff.ts
17928
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
18001
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync3, rmSync as rmSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
17929
18002
  import { tmpdir } from "os";
17930
- import { basename, dirname, join as join3, resolve } from "path";
18003
+ import { basename, dirname, join as join4, resolve } from "path";
17931
18004
 
17932
18005
  // src/git/repo.ts
17933
18006
  import { spawnSync as spawnSync4 } from "child_process";
@@ -18082,16 +18155,16 @@ function getDirectComparisonFiles(mode, cwd) {
18082
18155
  function diffRawContent(displayPath, oldContent, newContent) {
18083
18156
  const rel = displayPath.replace(/\\/g, "/").replace(/^\/+/, "").replace(/(^|\/)\.\.(?=\/|$)/g, "$1_");
18084
18157
  const safeRel = rel === "" ? "file" : rel;
18085
- const work = mkdtempSync(join3(tmpdir(), "glassbox-difftool-append-"));
18086
- const rootA = join3(work, "a");
18087
- const rootB = join3(work, "b");
18088
- const oldAbs = join3(rootA, safeRel);
18089
- const newAbs = join3(rootB, safeRel);
18158
+ const work = mkdtempSync(join4(tmpdir(), "glassbox-difftool-append-"));
18159
+ const rootA = join4(work, "a");
18160
+ const rootB = join4(work, "b");
18161
+ const oldAbs = join4(rootA, safeRel);
18162
+ const newAbs = join4(rootB, safeRel);
18090
18163
  try {
18091
- mkdirSync3(dirname(oldAbs), { recursive: true });
18092
- mkdirSync3(dirname(newAbs), { recursive: true });
18093
- writeFileSync2(oldAbs, oldContent);
18094
- writeFileSync2(newAbs, newContent);
18164
+ mkdirSync4(dirname(oldAbs), { recursive: true });
18165
+ mkdirSync4(dirname(newAbs), { recursive: true });
18166
+ writeFileSync3(oldAbs, oldContent);
18167
+ writeFileSync3(newAbs, newContent);
18095
18168
  const rawDiff = gitOrEmpty(["diff", "--no-index", "-U3", toGitArg(oldAbs), toGitArg(newAbs)], work);
18096
18169
  const diffs = normalizeDiffPaths(parseDiff(rawDiff), rootA, rootB);
18097
18170
  if (diffs.length === 0) {
@@ -18104,7 +18177,7 @@ function diffRawContent(displayPath, oldContent, newContent) {
18104
18177
  return { ...diff, filePath: safeRel, oldPath: null, status };
18105
18178
  } finally {
18106
18179
  try {
18107
- rmSync2(work, { recursive: true, force: true });
18180
+ rmSync3(work, { recursive: true, force: true });
18108
18181
  } catch {
18109
18182
  }
18110
18183
  }
@@ -18139,7 +18212,7 @@ function getAllFiles(repoRoot) {
18139
18212
  function createNewFileDiff(filePath, repoRoot) {
18140
18213
  let content;
18141
18214
  try {
18142
- const buf = readFileSync2(resolve(repoRoot, filePath));
18215
+ const buf = readFileSync3(resolve(repoRoot, filePath));
18143
18216
  const checkLen = Math.min(buf.length, 8192);
18144
18217
  for (let i = 0; i < checkLen; i++) {
18145
18218
  if (buf[i] === 0) {
@@ -18259,7 +18332,7 @@ function getFileContent(filePath, ref, cwd) {
18259
18332
  const repoRoot = getRepoRoot(cwd);
18260
18333
  try {
18261
18334
  if (ref === "working") {
18262
- return readFileSync2(resolve(repoRoot, filePath), "utf-8");
18335
+ return readFileSync3(resolve(repoRoot, filePath), "utf-8");
18263
18336
  }
18264
18337
  return git(["show", `${ref}:${filePath}`], repoRoot);
18265
18338
  } catch {
@@ -18269,9 +18342,9 @@ function getFileContent(filePath, ref, cwd) {
18269
18342
  function getModeFileContent(mode, filePath, side, cwd) {
18270
18343
  if (mode.type === "diff") {
18271
18344
  const { rootA, rootB } = directComparisonRoots(mode);
18272
- const abs = join3(side === "old" ? rootA : rootB, filePath);
18345
+ const abs = join4(side === "old" ? rootA : rootB, filePath);
18273
18346
  try {
18274
- return readFileSync2(abs, "utf-8");
18347
+ return readFileSync3(abs, "utf-8");
18275
18348
  } catch {
18276
18349
  return "";
18277
18350
  }
@@ -18307,9 +18380,9 @@ function getSingleFileDiff(mode, filePath, repoRoot, extraFlags = "") {
18307
18380
  }
18308
18381
  if (mode.type === "diff") {
18309
18382
  const { rootA, rootB } = directComparisonRoots(mode);
18310
- const oldAbs = join3(rootA, filePath);
18311
- const newAbs = join3(rootB, filePath);
18312
- if (!existsSync2(oldAbs) || !existsSync2(newAbs)) return null;
18383
+ const oldAbs = join4(rootA, filePath);
18384
+ const newAbs = join4(rootB, filePath);
18385
+ if (!existsSync3(oldAbs) || !existsSync3(newAbs)) return null;
18313
18386
  const args2 = ["diff", "--no-index", "-U3"];
18314
18387
  if (extraFlags) args2.push(...extraFlags.split(" ").filter(Boolean));
18315
18388
  args2.push(toGitArg(oldAbs), toGitArg(newAbs));
@@ -18371,15 +18444,15 @@ function getModeArgs(mode) {
18371
18444
 
18372
18445
  // src/lock.ts
18373
18446
  init_zod();
18374
- import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
18375
- import { join as join4 } from "path";
18447
+ import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
18448
+ import { join as join5 } from "path";
18376
18449
  var lockPath = null;
18377
18450
  var LockFileSchema = external_exports.object({ pid: external_exports.number().int() });
18378
18451
  function acquireLock(dataDir) {
18379
- lockPath = join4(dataDir, "glassbox.lock");
18380
- if (existsSync3(lockPath)) {
18452
+ lockPath = join5(dataDir, "glassbox.lock");
18453
+ if (existsSync4(lockPath)) {
18381
18454
  try {
18382
- const raw2 = JSON.parse(readFileSync3(lockPath, "utf-8"));
18455
+ const raw2 = JSON.parse(readFileSync4(lockPath, "utf-8"));
18383
18456
  const contents = LockFileSchema.parse(raw2);
18384
18457
  const pid = contents.pid;
18385
18458
  try {
@@ -18392,13 +18465,13 @@ function acquireLock(dataDir) {
18392
18465
  process.exit(1);
18393
18466
  } catch {
18394
18467
  console.log(` Removing stale lock from PID ${pid}`);
18395
- rmSync3(lockPath, { force: true });
18468
+ rmSync4(lockPath, { force: true });
18396
18469
  }
18397
18470
  } catch {
18398
- rmSync3(lockPath, { force: true });
18471
+ rmSync4(lockPath, { force: true });
18399
18472
  }
18400
18473
  }
18401
- writeFileSync3(lockPath, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
18474
+ writeFileSync4(lockPath, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
18402
18475
  const cleanup = () => {
18403
18476
  releaseLock();
18404
18477
  };
@@ -18415,7 +18488,7 @@ function acquireLock(dataDir) {
18415
18488
  function releaseLock() {
18416
18489
  if (lockPath !== null) {
18417
18490
  try {
18418
- rmSync3(lockPath, { force: true });
18491
+ rmSync4(lockPath, { force: true });
18419
18492
  } catch {
18420
18493
  }
18421
18494
  lockPath = null;
@@ -18528,15 +18601,15 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
18528
18601
 
18529
18602
  // src/server.ts
18530
18603
  import { serve } from "@hono/node-server";
18531
- import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
18604
+ import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
18532
18605
  import { Hono as Hono19 } from "hono";
18533
- import { dirname as dirname4, join as join14 } from "path";
18606
+ import { dirname as dirname4, join as join13 } from "path";
18534
18607
  import { fileURLToPath as fileURLToPath2 } from "url";
18535
18608
 
18536
18609
  // src/channel-config.ts
18537
18610
  init_zod();
18538
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
18539
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
18611
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
18612
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
18540
18613
  import { fileURLToPath } from "url";
18541
18614
  var MCP_SERVER_KEY = "glassbox-channel";
18542
18615
  var McpConfigSchema = external_exports.object({
@@ -18546,11 +18619,11 @@ var HealthResponseSchema = external_exports.object({ ok: external_exports.boolea
18546
18619
  function getChannelServerPath() {
18547
18620
  const thisDir = dirname2(fileURLToPath(import.meta.url));
18548
18621
  const distPath = resolve2(thisDir, "channel.js");
18549
- if (existsSync4(distPath)) {
18622
+ if (existsSync5(distPath)) {
18550
18623
  return { command: process.execPath, args: [distPath] };
18551
18624
  }
18552
18625
  const srcPath = resolve2(thisDir, "channel.ts");
18553
- if (existsSync4(srcPath)) {
18626
+ if (existsSync5(srcPath)) {
18554
18627
  return { command: "npx", args: ["tsx", srcPath] };
18555
18628
  }
18556
18629
  return { command: process.execPath, args: [distPath] };
@@ -18560,12 +18633,12 @@ function projectRoot(dataDir) {
18560
18633
  }
18561
18634
  function registerChannel(dataDir) {
18562
18635
  const root = projectRoot(dataDir);
18563
- const mcpPath = join5(root, ".mcp.json");
18636
+ const mcpPath = join6(root, ".mcp.json");
18564
18637
  const { command, args } = getChannelServerPath();
18565
18638
  let config2 = {};
18566
- if (existsSync4(mcpPath)) {
18639
+ if (existsSync5(mcpPath)) {
18567
18640
  try {
18568
- const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
18641
+ const raw2 = JSON.parse(readFileSync5(mcpPath, "utf-8"));
18569
18642
  const parsed = McpConfigSchema.safeParse(raw2);
18570
18643
  if (parsed.success) config2 = parsed.data;
18571
18644
  } catch {
@@ -18576,14 +18649,14 @@ function registerChannel(dataDir) {
18576
18649
  command,
18577
18650
  args: [...args, "--data-dir", dataDir]
18578
18651
  };
18579
- writeFileSync4(mcpPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
18652
+ writeFileSync5(mcpPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
18580
18653
  }
18581
18654
  function unregisterChannel(dataDir) {
18582
18655
  const root = projectRoot(dataDir);
18583
- const mcpPath = join5(root, ".mcp.json");
18584
- if (!existsSync4(mcpPath)) return;
18656
+ const mcpPath = join6(root, ".mcp.json");
18657
+ if (!existsSync5(mcpPath)) return;
18585
18658
  try {
18586
- const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
18659
+ const raw2 = JSON.parse(readFileSync5(mcpPath, "utf-8"));
18587
18660
  const parsed = McpConfigSchema.safeParse(raw2);
18588
18661
  if (!parsed.success) return;
18589
18662
  const config2 = parsed.data;
@@ -18591,14 +18664,14 @@ function unregisterChannel(dataDir) {
18591
18664
  const servers = { ...config2.mcpServers };
18592
18665
  delete servers[MCP_SERVER_KEY];
18593
18666
  config2.mcpServers = servers;
18594
- writeFileSync4(mcpPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
18667
+ writeFileSync5(mcpPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
18595
18668
  }
18596
18669
  } catch {
18597
18670
  }
18598
18671
  }
18599
18672
  function getChannelPort(dataDir) {
18600
18673
  try {
18601
- const portStr = readFileSync4(join5(dataDir, "channel-port"), "utf-8").trim();
18674
+ const portStr = readFileSync5(join6(dataDir, "channel-port"), "utf-8").trim();
18602
18675
  const port = parseInt(portStr, 10);
18603
18676
  return isNaN(port) ? null : port;
18604
18677
  } catch {
@@ -19887,6 +19960,7 @@ __export(annotations_exports, {
19887
19960
  DeleteAnnotationReqSchema: () => DeleteAnnotationReqSchema,
19888
19961
  DeleteAnnotationRespSchema: () => DeleteAnnotationRespSchema,
19889
19962
  DeleteStaleAnnotationsRespSchema: () => DeleteStaleAnnotationsRespSchema,
19963
+ ImageRegionSchema: () => ImageRegionSchema,
19890
19964
  KeepAllStaleAnnotationsRespSchema: () => KeepAllStaleAnnotationsRespSchema,
19891
19965
  KeepAnnotationReqSchema: () => KeepAnnotationReqSchema,
19892
19966
  KeepAnnotationRespSchema: () => KeepAnnotationRespSchema,
@@ -19897,6 +19971,9 @@ __export(annotations_exports, {
19897
19971
  UpdateAnnotationBodySchema: () => UpdateAnnotationBodySchema,
19898
19972
  UpdateAnnotationReqSchema: () => UpdateAnnotationReqSchema,
19899
19973
  UpdateAnnotationRespSchema: () => UpdateAnnotationRespSchema,
19974
+ UpdateRegionBodySchema: () => UpdateRegionBodySchema,
19975
+ UpdateRegionReqSchema: () => UpdateRegionReqSchema,
19976
+ UpdateRegionRespSchema: () => UpdateRegionRespSchema,
19900
19977
  createAnnotation: () => createAnnotation,
19901
19978
  deleteAnnotation: () => deleteAnnotation2,
19902
19979
  deleteStaleAnnotations: () => deleteStaleAnnotations2,
@@ -19904,7 +19981,8 @@ __export(annotations_exports, {
19904
19981
  keepAnnotation: () => keepAnnotation,
19905
19982
  listAllAnnotations: () => listAllAnnotations,
19906
19983
  moveAnnotation: () => moveAnnotation2,
19907
- updateAnnotation: () => updateAnnotation2
19984
+ updateAnnotation: () => updateAnnotation2,
19985
+ updateAnnotationRegion: () => updateAnnotationRegion2
19908
19986
  });
19909
19987
  init_zod();
19910
19988
  init_schemas3();
@@ -19920,12 +19998,17 @@ var AnnotationCategorySchema = external_exports.enum([
19920
19998
  var AnnotationSideSchema = external_exports.enum(["old", "new"]);
19921
19999
  var CreateAnnotationReqSchema = external_exports.object({
19922
20000
  reviewFileId: external_exports.string().min(1),
19923
- lineNumber: external_exports.number().int().min(1),
20001
+ // `0` creates an image-level annotation (doc 23): a general image comment, or
20002
+ // — when `region` is set — a comment anchored to a rectangle on the image.
20003
+ // Line-anchored annotations pass the real 1-based line number.
20004
+ lineNumber: external_exports.number().int().min(0),
19924
20005
  side: AnnotationSideSchema,
19925
20006
  category: AnnotationCategorySchema,
19926
20007
  content: external_exports.string().min(1),
19927
20008
  /** SARIF guid of the AI review note this annotation replies to (doc 20 threading). */
19928
- replyToNoteId: external_exports.string().optional()
20009
+ replyToNoteId: external_exports.string().optional(),
20010
+ /** Normalized rectangle for an image-region annotation (doc 23). */
20011
+ region: ImageRegionSchema.optional()
19929
20012
  });
19930
20013
  var CreateAnnotationRespSchema = AnnotationSchema;
19931
20014
  var UpdateAnnotationReqSchema = external_exports.object({
@@ -19944,6 +20027,12 @@ var MoveAnnotationReqSchema = external_exports.object({
19944
20027
  });
19945
20028
  var MoveAnnotationBodySchema = MoveAnnotationReqSchema.omit({ id: true });
19946
20029
  var MoveAnnotationRespSchema = OkResponseSchema;
20030
+ var UpdateRegionReqSchema = external_exports.object({
20031
+ id: external_exports.string(),
20032
+ region: ImageRegionSchema
20033
+ });
20034
+ var UpdateRegionBodySchema = UpdateRegionReqSchema.omit({ id: true });
20035
+ var UpdateRegionRespSchema = OkResponseSchema;
19947
20036
  var KeepAnnotationReqSchema = external_exports.object({ id: external_exports.string() });
19948
20037
  var KeepAnnotationRespSchema = OkResponseSchema;
19949
20038
  var DeleteStaleAnnotationsRespSchema = OkResponseSchema;
@@ -19965,6 +20054,10 @@ async function moveAnnotation2(req) {
19965
20054
  const { id, ...body } = req;
19966
20055
  return apiCall(MoveAnnotationRespSchema, `/annotations/${id}/move`, { method: "PATCH", body });
19967
20056
  }
20057
+ async function updateAnnotationRegion2(req) {
20058
+ const { id, ...body } = req;
20059
+ return apiCall(UpdateRegionRespSchema, `/annotations/${id}/region`, { method: "PATCH", body });
20060
+ }
19968
20061
  async function keepAnnotation(req) {
19969
20062
  return apiCall(KeepAnnotationRespSchema, `/annotations/${req.id}/keep`, { method: "POST" });
19970
20063
  }
@@ -21666,25 +21759,26 @@ init_queries();
21666
21759
  // src/export/generate.ts
21667
21760
  init_zod();
21668
21761
  init_queries();
21762
+ init_schemas3();
21669
21763
  import { spawnSync as spawnSync6 } from "child_process";
21670
- import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
21764
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
21671
21765
  import { homedir as homedir2 } from "os";
21672
- import { join as join7 } from "path";
21673
- var DISMISS_FILE = join7(homedir2(), ".glassbox", "gitignore-dismissed.json");
21766
+ import { join as join8 } from "path";
21767
+ var DISMISS_FILE = join8(homedir2(), ".glassbox", "gitignore-dismissed.json");
21674
21768
  var DISMISS_DAYS = 30;
21675
21769
  var DismissalsSchema = external_exports.record(external_exports.string(), external_exports.number());
21676
21770
  function loadDismissals() {
21677
21771
  try {
21678
- const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync6(DISMISS_FILE, "utf-8")));
21772
+ const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync7(DISMISS_FILE, "utf-8")));
21679
21773
  return parsed.success ? parsed.data : {};
21680
21774
  } catch {
21681
21775
  return {};
21682
21776
  }
21683
21777
  }
21684
21778
  function saveDismissals(data) {
21685
- const dir = join7(homedir2(), ".glassbox");
21686
- mkdirSync5(dir, { recursive: true });
21687
- writeFileSync6(DISMISS_FILE, JSON.stringify(data), "utf-8");
21779
+ const dir = join8(homedir2(), ".glassbox");
21780
+ mkdirSync6(dir, { recursive: true });
21781
+ writeFileSync7(DISMISS_FILE, JSON.stringify(data), "utf-8");
21688
21782
  }
21689
21783
  function isGlassboxGitignored(repoRoot) {
21690
21784
  const result = spawnSync6("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
@@ -21702,16 +21796,16 @@ function shouldPromptGitignore(repoRoot) {
21702
21796
  return true;
21703
21797
  }
21704
21798
  function addGlassboxToGitignore(repoRoot) {
21705
- const gitignorePath = join7(repoRoot, ".gitignore");
21706
- if (existsSync6(gitignorePath)) {
21707
- const content = readFileSync6(gitignorePath, "utf-8");
21799
+ const gitignorePath = join8(repoRoot, ".gitignore");
21800
+ if (existsSync7(gitignorePath)) {
21801
+ const content = readFileSync7(gitignorePath, "utf-8");
21708
21802
  if (!content.endsWith("\n")) {
21709
21803
  appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
21710
21804
  } else {
21711
21805
  appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
21712
21806
  }
21713
21807
  } else {
21714
- writeFileSync6(gitignorePath, ".glassbox/\n", "utf-8");
21808
+ writeFileSync7(gitignorePath, ".glassbox/\n", "utf-8");
21715
21809
  }
21716
21810
  }
21717
21811
  function dismissGitignorePrompt2(repoRoot) {
@@ -21720,17 +21814,30 @@ function dismissGitignorePrompt2(repoRoot) {
21720
21814
  saveDismissals(dismissals);
21721
21815
  }
21722
21816
  function deleteReviewExport(reviewId, repoRoot) {
21723
- const exportDir = join7(repoRoot, ".glassbox");
21724
- const archivePath = join7(exportDir, `review-${reviewId}.md`);
21725
- if (existsSync6(archivePath)) unlinkSync2(archivePath);
21817
+ const exportDir = join8(repoRoot, ".glassbox");
21818
+ const archivePath = join8(exportDir, `review-${reviewId}.md`);
21819
+ if (existsSync7(archivePath)) unlinkSync2(archivePath);
21820
+ }
21821
+ function annotationAnchorLabel(a) {
21822
+ if (a.line_number !== 0) return `**Line ${a.line_number}**`;
21823
+ if (a.region_data !== null) {
21824
+ const parsed = ImageRegionSchema.safeParse(JSON.parse(a.region_data));
21825
+ if (parsed.success) {
21826
+ const { x, y, w, h, side } = parsed.data;
21827
+ const pct = (n) => `${Math.round(n * 100)}%`;
21828
+ const scope = side === "old" ? ", A image only" : side === "new" ? ", B image only" : "";
21829
+ return `**Image region (${pct(x)}, ${pct(y)}, ${pct(w)}\xD7${pct(h)}${scope})**`;
21830
+ }
21831
+ }
21832
+ return "**Image comment**";
21726
21833
  }
21727
21834
  async function generateReviewExport(reviewId, repoRoot, isCurrent) {
21728
21835
  const review = await getReview(reviewId);
21729
21836
  if (!review) throw new Error("Review not found");
21730
21837
  const files = await getReviewFiles(reviewId);
21731
21838
  const annotations = await getAnnotationsForReview(reviewId);
21732
- const exportDir = join7(repoRoot, ".glassbox");
21733
- mkdirSync5(exportDir, { recursive: true });
21839
+ const exportDir = join8(repoRoot, ".glassbox");
21840
+ mkdirSync6(exportDir, { recursive: true });
21734
21841
  const byFile = {};
21735
21842
  for (const a of annotations) {
21736
21843
  if (!(a.file_path in byFile)) byFile[a.file_path] = [];
@@ -21766,7 +21873,8 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
21766
21873
  lines.push("> project configuration (CLAUDE.md, .cursorrules, etc.) with these preferences/rules.");
21767
21874
  lines.push("");
21768
21875
  for (const item of rememberItems) {
21769
- lines.push(`- **${item.file_path}:${item.line_number}** - ${item.content}`);
21876
+ const anchor = item.line_number === 0 ? `${item.file_path} (image)` : `${item.file_path}:${item.line_number}`;
21877
+ lines.push(`- **${anchor}** - ${item.content}`);
21770
21878
  }
21771
21879
  lines.push("");
21772
21880
  }
@@ -21777,7 +21885,7 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
21777
21885
  lines.push(`### ${filePath}`);
21778
21886
  lines.push("");
21779
21887
  for (const a of fileAnns) {
21780
- lines.push(`- **Line ${a.line_number}** [${a.category}]: ${a.content}`);
21888
+ lines.push(`- ${annotationAnchorLabel(a)} [${a.category}]: ${a.content}`);
21781
21889
  }
21782
21890
  lines.push("");
21783
21891
  }
@@ -21797,11 +21905,11 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
21797
21905
  lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
21798
21906
  lines.push("");
21799
21907
  const content = lines.join("\n");
21800
- const archivePath = join7(exportDir, `review-${review.id}.md`);
21801
- writeFileSync6(archivePath, content, "utf-8");
21908
+ const archivePath = join8(exportDir, `review-${review.id}.md`);
21909
+ writeFileSync7(archivePath, content, "utf-8");
21802
21910
  if (isCurrent) {
21803
- const latestPath = join7(exportDir, "latest-review.md");
21804
- writeFileSync6(latestPath, content, "utf-8");
21911
+ const latestPath = join8(exportDir, "latest-review.md");
21912
+ writeFileSync7(latestPath, content, "utf-8");
21805
21913
  return latestPath;
21806
21914
  }
21807
21915
  return archivePath;
@@ -21833,7 +21941,8 @@ annotationsRoutes.post("/annotations", async (c) => {
21833
21941
  body.side,
21834
21942
  body.category,
21835
21943
  body.content,
21836
- body.replyToNoteId
21944
+ body.replyToNoteId,
21945
+ body.region
21837
21946
  );
21838
21947
  autoExport(c);
21839
21948
  return c.json(annotation, 201);
@@ -21854,6 +21963,15 @@ annotationsRoutes.delete("/annotations/:id", async (c) => {
21854
21963
  autoExport(c);
21855
21964
  return c.json({ ok: true });
21856
21965
  });
21966
+ annotationsRoutes.patch("/annotations/:id/region", async (c) => {
21967
+ const id = requirePathParam(c, "id");
21968
+ if (!id.ok) return id.response;
21969
+ const parsed = await parseBody(c, UpdateRegionBodySchema);
21970
+ if (!parsed.ok) return parsed.response;
21971
+ await updateAnnotationRegion(id.data, parsed.data.region);
21972
+ autoExport(c);
21973
+ return c.json({ ok: true });
21974
+ });
21857
21975
  annotationsRoutes.patch("/annotations/:id/move", async (c) => {
21858
21976
  const id = requirePathParam(c, "id");
21859
21977
  if (!id.ok) return id.response;
@@ -22026,7 +22144,6 @@ filesRoutes.post("/files/:fileId/open", async (c) => {
22026
22144
  // src/routes/api/image.ts
22027
22145
  init_connection();
22028
22146
  init_queries();
22029
- init_blob_store();
22030
22147
  import { Hono as Hono7 } from "hono";
22031
22148
 
22032
22149
  // src/git/image.ts
@@ -22355,255 +22472,22 @@ function getNewImage(mode, filePath, repoRoot) {
22355
22472
  return { data, size: data.length };
22356
22473
  }
22357
22474
 
22358
- // src/git/svg-rasterize.ts
22359
- import { Worker } from "worker_threads";
22360
-
22361
- // src/git/svg-rasterize-render.ts
22362
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
22363
- import { createRequire } from "module";
22364
- import { join as join10 } from "path";
22365
- var initialized = false;
22366
- var ResvgClass;
22367
- var fontBuffers = [];
22368
- async function ensureRenderInit() {
22369
- if (initialized) return;
22370
- const require2 = createRequire(import.meta.url);
22371
- const resvgPath = require2.resolve("@resvg/resvg-wasm");
22372
- const wasmPath = resvgPath.replace(/index\.(js|mjs)$/, "index_bg.wasm");
22373
- const wasmBuffer = readFileSync9(wasmPath);
22374
- const mod = await import("@resvg/resvg-wasm");
22375
- await mod.initWasm(wasmBuffer);
22376
- ResvgClass = mod.Resvg;
22377
- fontBuffers = loadSystemFonts();
22378
- initialized = true;
22379
- }
22380
- function loadSystemFonts() {
22381
- const buffers = [];
22382
- const candidates = getFontCandidates();
22383
- for (const path of candidates) {
22384
- if (!existsSync8(path)) continue;
22385
- try {
22386
- buffers.push(readFileSync9(path));
22387
- } catch {
22388
- }
22389
- }
22390
- return buffers;
22391
- }
22392
- function getFontCandidates() {
22393
- const os = process.platform;
22394
- if (os === "darwin") {
22395
- const sys = "/System/Library/Fonts";
22396
- const sup = "/System/Library/Fonts/Supplemental";
22397
- return [
22398
- // Core system fonts (serif, sans-serif, monospace)
22399
- join10(sys, "Helvetica.ttc"),
22400
- join10(sys, "Times.ttc"),
22401
- join10(sys, "Courier.ttc"),
22402
- join10(sys, "Menlo.ttc"),
22403
- join10(sys, "SFPro.ttf"),
22404
- join10(sys, "SFNS.ttf"),
22405
- join10(sys, "SFNSMono.ttf"),
22406
- // Supplemental (common named fonts in SVGs)
22407
- join10(sup, "Arial.ttf"),
22408
- join10(sup, "Arial Bold.ttf"),
22409
- join10(sup, "Georgia.ttf"),
22410
- join10(sup, "Verdana.ttf"),
22411
- join10(sup, "Tahoma.ttf"),
22412
- join10(sup, "Trebuchet MS.ttf"),
22413
- join10(sup, "Impact.ttf"),
22414
- join10(sup, "Comic Sans MS.ttf"),
22415
- join10(sup, "Courier New.ttf"),
22416
- join10(sup, "Times New Roman.ttf")
22417
- ];
22418
- }
22419
- if (os === "linux") {
22420
- return [
22421
- // DejaVu (most common Linux fallback)
22422
- "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
22423
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
22424
- "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
22425
- "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
22426
- // Liberation (metric-compatible with Arial/Times/Courier)
22427
- "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
22428
- "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
22429
- "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
22430
- // Noto (common on modern distros)
22431
- "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
22432
- ];
22433
- }
22434
- if (os === "win32") {
22435
- const winFonts = join10(process.env.WINDIR ?? "C:\\Windows", "Fonts");
22436
- return [
22437
- join10(winFonts, "arial.ttf"),
22438
- join10(winFonts, "arialbd.ttf"),
22439
- join10(winFonts, "times.ttf"),
22440
- join10(winFonts, "cour.ttf"),
22441
- join10(winFonts, "verdana.ttf"),
22442
- join10(winFonts, "tahoma.ttf"),
22443
- join10(winFonts, "georgia.ttf"),
22444
- join10(winFonts, "consola.ttf"),
22445
- join10(winFonts, "segoeui.ttf")
22446
- ];
22447
- }
22448
- return [];
22449
- }
22450
- function parseSvgDimensions(svg) {
22451
- const widthMatch = svg.match(/\bwidth\s*=\s*["']([^"']+)["']/);
22452
- const heightMatch = svg.match(/\bheight\s*=\s*["']([^"']+)["']/);
22453
- const viewBoxMatch = svg.match(/\bviewBox\s*=\s*["']([^"']+)["']/);
22454
- let width = widthMatch ? parseFloat(widthMatch[1]) : NaN;
22455
- let height = heightMatch ? parseFloat(heightMatch[1]) : NaN;
22456
- if ((isNaN(width) || isNaN(height)) && viewBoxMatch) {
22457
- const parts = viewBoxMatch[1].split(/[\s,]+/);
22458
- if (parts.length >= 4) {
22459
- if (isNaN(width)) width = parseFloat(parts[2]);
22460
- if (isNaN(height)) height = parseFloat(parts[3]);
22461
- }
22462
- }
22463
- if (isNaN(width)) width = 300;
22464
- if (isNaN(height)) height = 150;
22465
- return { width, height };
22466
- }
22467
- function svgUsesExternalFonts(svgData) {
22468
- const svg = svgData.toString("utf-8");
22469
- return /<text[\s>]/i.test(svg) || /font-family/i.test(svg) || /@font-face/i.test(svg);
22470
- }
22471
- var MAX_RENDER_DIM = 4e3;
22472
- async function renderSvgToPng(svgString) {
22473
- await ensureRenderInit();
22474
- const { width, height } = parseSvgDimensions(svgString);
22475
- const maxDim = Math.max(width, height);
22476
- const scale = Math.min(10, MAX_RENDER_DIM / maxDim);
22477
- const targetWidth = Math.round(width * scale);
22478
- const resvg = new ResvgClass(svgString, {
22479
- fitTo: { mode: "width", value: targetWidth },
22480
- font: {
22481
- loadSystemFonts: false,
22482
- fontBuffers,
22483
- defaultFontFamily: "Helvetica"
22484
- }
22485
- });
22486
- const rendered = resvg.render();
22487
- const png = Buffer.from(rendered.asPng());
22488
- rendered.free();
22489
- resvg.free();
22490
- return png;
22491
- }
22492
-
22493
- // src/git/svg-rasterize.ts
22494
- var RENDER_TIMEOUT_MS = 15e3;
22495
- var worker = null;
22496
- var workerReady = false;
22497
- var workerDisabled = false;
22498
- var nextJobId = 0;
22499
- var pending = /* @__PURE__ */ new Map();
22500
- function resolveWorkerUrl() {
22501
- 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);
22502
- }
22503
- function clearJobTimer(job) {
22504
- if (job.timer !== void 0) clearTimeout(job.timer);
22505
- }
22506
- function fallbackAllPending() {
22507
- for (const [id, job] of pending) {
22508
- pending.delete(id);
22509
- clearJobTimer(job);
22510
- renderSvgToPng(job.svg).then(job.resolve, job.reject);
22511
- }
22512
- }
22513
- function rejectAllPending(err) {
22514
- for (const [id, job] of pending) {
22515
- pending.delete(id);
22516
- clearJobTimer(job);
22517
- job.reject(err);
22518
- }
22519
- }
22520
- function disposeWorker() {
22521
- if (worker) {
22522
- worker.removeAllListeners();
22523
- void worker.terminate();
22524
- }
22525
- worker = null;
22526
- workerReady = false;
22527
- }
22528
- function onJobTimeout(id) {
22529
- const job = pending.get(id);
22530
- if (!job) return;
22531
- pending.delete(id);
22532
- clearJobTimer(job);
22533
- const requeue = [...pending.values()];
22534
- pending.clear();
22535
- for (const r of requeue) clearJobTimer(r);
22536
- disposeWorker();
22537
- job.reject(new Error(`SVG rasterization timed out after ${RENDER_TIMEOUT_MS} ms`));
22538
- for (const r of requeue) submit(r);
22539
- }
22540
- function getWorker() {
22541
- if (workerDisabled) return null;
22542
- if (worker) return worker;
22543
- let spawned;
22544
- try {
22545
- spawned = new Worker(resolveWorkerUrl());
22546
- } catch {
22547
- workerDisabled = true;
22548
- return null;
22549
- }
22550
- spawned.on("message", (msg) => {
22551
- if (msg.type === "ready") {
22552
- workerReady = true;
22553
- return;
22554
- }
22555
- const job = pending.get(msg.id);
22556
- if (!job) return;
22557
- pending.delete(msg.id);
22558
- clearJobTimer(job);
22559
- if (msg.type === "result") job.resolve(Buffer.from(msg.png));
22560
- else job.reject(new Error(msg.message));
22561
- });
22562
- const handleFailure = (err) => {
22563
- const startupFailure = !workerReady;
22564
- disposeWorker();
22565
- if (startupFailure) {
22566
- workerDisabled = true;
22567
- fallbackAllPending();
22568
- } else {
22569
- rejectAllPending(err);
22570
- }
22571
- };
22572
- spawned.on("error", handleFailure);
22573
- spawned.on("exit", (code) => {
22574
- if (code !== 0) handleFailure(new Error(`SVG rasterization worker exited with code ${code}`));
22575
- });
22576
- worker = spawned;
22577
- return spawned;
22578
- }
22579
- function submit(job) {
22580
- const activeWorker = getWorker();
22581
- if (!activeWorker) {
22582
- renderSvgToPng(job.svg).then(job.resolve, job.reject);
22583
- return;
22584
- }
22585
- const id = nextJobId++;
22586
- job.timer = setTimeout(() => {
22587
- onJobTimeout(id);
22588
- }, RENDER_TIMEOUT_MS);
22589
- pending.set(id, job);
22590
- activeWorker.postMessage({ id, svg: job.svg });
22591
- }
22592
- async function rasterizeSvg(svgData) {
22593
- const svg = svgData.toString("utf-8");
22594
- return new Promise((resolve11, reject) => {
22595
- submit({ svg, resolve: resolve11, reject });
22596
- });
22597
- }
22598
-
22599
22475
  // src/routes/api/image.ts
22476
+ init_image_blobs();
22600
22477
  var imageRoutes = new Hono7();
22601
- function difftoolImageSide(fileId, side) {
22478
+ function blobImageSide(fileId, side) {
22602
22479
  const dataDir = getDataDir();
22603
22480
  if (dataDir === null) return null;
22604
- const bytes = readDifftoolBlob(dataDir, fileId, side);
22481
+ const bytes = readImageBlob(dataDir, fileId, side);
22605
22482
  return bytes !== null ? { data: bytes, size: bytes.length } : null;
22606
22483
  }
22484
+ function resolveImageSide(fileId, side, status, mode, filePath, oldPath, repoRoot, isDifftool) {
22485
+ if (side === "old" && status === "added") return null;
22486
+ if (side === "new" && status === "deleted") return null;
22487
+ if (isDifftool) return blobImageSide(fileId, side);
22488
+ const fromGit = side === "old" ? getOldImage(mode, filePath, oldPath, repoRoot) : getNewImage(mode, filePath, repoRoot);
22489
+ return fromGit ?? blobImageSide(fileId, side);
22490
+ }
22607
22491
  imageRoutes.get("/image/:fileId/metadata", async (c) => {
22608
22492
  const fileIdParam = requirePathParam(c, "fileId");
22609
22493
  if (!fileIdParam.ok) return fileIdParam.response;
@@ -22617,8 +22501,8 @@ imageRoutes.get("/image/:fileId/metadata", async (c) => {
22617
22501
  const oldPath = diff?.oldPath ?? null;
22618
22502
  const status = diff?.status ?? "modified";
22619
22503
  const isDifftool = review.mode === "difftool";
22620
- const oldImage = status === "added" ? null : isDifftool ? difftoolImageSide(fileIdParam.data, "old") : getOldImage(mode, file2.file_path, oldPath, repoRoot);
22621
- const newImage = status === "deleted" ? null : isDifftool ? difftoolImageSide(fileIdParam.data, "new") : getNewImage(mode, file2.file_path, repoRoot);
22504
+ const oldImage = resolveImageSide(fileIdParam.data, "old", status, mode, file2.file_path, oldPath, repoRoot, isDifftool);
22505
+ const newImage = resolveImageSide(fileIdParam.data, "new", status, mode, file2.file_path, oldPath, repoRoot, isDifftool);
22622
22506
  const oldMeta = oldImage !== null ? extractMetadata(oldImage.data, oldPath ?? file2.file_path) : null;
22623
22507
  const newMeta = newImage !== null ? extractMetadata(newImage.data, file2.file_path) : null;
22624
22508
  return c.json({
@@ -22639,19 +22523,19 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
22639
22523
  const mode = parseModeString(review.mode);
22640
22524
  const diff = parseDiffData(file2.diff_data);
22641
22525
  const oldPath = diff?.oldPath ?? null;
22642
- const image = review.mode === "difftool" ? difftoolImageSide(fileIdParam.data, side) : side === "old" ? getOldImage(mode, file2.file_path, oldPath, repoRoot) : getNewImage(mode, file2.file_path, repoRoot);
22526
+ const status = diff?.status ?? "modified";
22527
+ const image = resolveImageSide(
22528
+ fileIdParam.data,
22529
+ side,
22530
+ status,
22531
+ mode,
22532
+ file2.file_path,
22533
+ oldPath,
22534
+ repoRoot,
22535
+ review.mode === "difftool"
22536
+ );
22643
22537
  if (!image) return c.text("Image not available", 404);
22644
22538
  const sidePath = side === "old" ? oldPath ?? file2.file_path : file2.file_path;
22645
- if (isSvgFile(sidePath)) {
22646
- try {
22647
- const png = await rasterizeSvg(image.data);
22648
- return new Response(new Uint8Array(png), {
22649
- headers: { "Content-Type": "image/png", "Cache-Control": "no-cache" }
22650
- });
22651
- } catch {
22652
- return c.text("SVG rasterization failed", 500);
22653
- }
22654
- }
22655
22539
  const contentType = getContentType(sidePath);
22656
22540
  return new Response(new Uint8Array(image.data), {
22657
22541
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
@@ -22661,7 +22545,7 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
22661
22545
  // src/routes/api/outline.ts
22662
22546
  init_queries();
22663
22547
  import { spawnSync as spawnSync8 } from "child_process";
22664
- import { readFileSync as readFileSync10 } from "fs";
22548
+ import { readFileSync as readFileSync9 } from "fs";
22665
22549
  import { Hono as Hono8 } from "hono";
22666
22550
  import { resolve as resolve6 } from "path";
22667
22551
 
@@ -23039,7 +22923,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
23039
22923
  }
23040
22924
  let content = "";
23041
22925
  try {
23042
- content = readFileSync10(resolve6(repoRoot, filePath), "utf-8");
22926
+ content = readFileSync9(resolve6(repoRoot, filePath), "utf-8");
23043
22927
  } catch {
23044
22928
  continue;
23045
22929
  }
@@ -23075,15 +22959,15 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
23075
22959
  }
23076
22960
 
23077
22961
  // src/routes/api/project-settings.ts
23078
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
22962
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
23079
22963
  import { Hono as Hono9 } from "hono";
23080
- import { join as join11 } from "path";
22964
+ import { join as join10 } from "path";
23081
22965
  var projectSettingsRoutes = new Hono9();
23082
22966
  function readProjectSettings(repoRoot) {
23083
- const settingsPath = join11(repoRoot, ".glassbox", "settings.json");
22967
+ const settingsPath = join10(repoRoot, ".glassbox", "settings.json");
23084
22968
  try {
23085
- if (existsSync9(settingsPath)) {
23086
- const raw2 = JSON.parse(readFileSync11(settingsPath, "utf-8"));
22969
+ if (existsSync8(settingsPath)) {
22970
+ const raw2 = JSON.parse(readFileSync10(settingsPath, "utf-8"));
23087
22971
  const parsed = ProjectSettingsSchema.safeParse(raw2);
23088
22972
  if (parsed.success) return parsed.data;
23089
22973
  }
@@ -23092,9 +22976,9 @@ function readProjectSettings(repoRoot) {
23092
22976
  return {};
23093
22977
  }
23094
22978
  function writeProjectSettings(repoRoot, settings) {
23095
- const dir = join11(repoRoot, ".glassbox");
22979
+ const dir = join10(repoRoot, ".glassbox");
23096
22980
  mkdirSync7(dir, { recursive: true });
23097
- writeFileSync8(join11(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
22981
+ writeFileSync8(join10(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
23098
22982
  }
23099
22983
  projectSettingsRoutes.get("/project-settings", (c) => {
23100
22984
  const repoRoot = c.get("repoRoot");
@@ -23112,7 +22996,7 @@ projectSettingsRoutes.patch("/project-settings", async (c) => {
23112
22996
 
23113
22997
  // src/routes/api/review-notes.ts
23114
22998
  init_store();
23115
- import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
22999
+ import { readFileSync as readFileSync11, statSync as statSync3 } from "fs";
23116
23000
  import { Hono as Hono10 } from "hono";
23117
23001
  import { extname, relative, resolve as resolve7 } from "path";
23118
23002
  var reviewNotesRoutes = new Hono10();
@@ -23139,7 +23023,7 @@ reviewNotesRoutes.get("/review-notes/artifact", (c) => {
23139
23023
  try {
23140
23024
  const stat = statSync3(abs);
23141
23025
  if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
23142
- const body = readFileSync12(abs);
23026
+ const body = readFileSync11(abs);
23143
23027
  return c.body(body, 200, { "Content-Type": contentType });
23144
23028
  } catch {
23145
23029
  return c.text("Not found", 404);
@@ -23316,13 +23200,13 @@ apiRoutes.route("/", systemRoutes);
23316
23200
  import { spawnSync as spawnSync9 } from "child_process";
23317
23201
  import { mkdirSync as mkdirSync8 } from "fs";
23318
23202
  import { Hono as Hono15 } from "hono";
23319
- import { join as join12 } from "path";
23203
+ import { join as join11 } from "path";
23320
23204
  var channelApiRoutes = new Hono15();
23321
23205
  channelApiRoutes.get("/status", async (c) => {
23322
23206
  const config2 = readGlobalConfig();
23323
23207
  const enabled = config2.channelEnabled === true;
23324
23208
  const repoRoot = c.get("repoRoot");
23325
- const dataDir = join12(repoRoot, ".glassbox");
23209
+ const dataDir = join11(repoRoot, ".glassbox");
23326
23210
  const connected = enabled ? await isChannelAlive(dataDir) : false;
23327
23211
  return c.json({ enabled, connected });
23328
23212
  });
@@ -23331,7 +23215,7 @@ channelApiRoutes.post("/enable", (c) => {
23331
23215
  config2.channelEnabled = true;
23332
23216
  });
23333
23217
  const repoRoot = c.get("repoRoot");
23334
- const dataDir = join12(repoRoot, ".glassbox");
23218
+ const dataDir = join11(repoRoot, ".glassbox");
23335
23219
  mkdirSync8(dataDir, { recursive: true });
23336
23220
  registerChannel(dataDir);
23337
23221
  return c.json({ ok: true });
@@ -23341,7 +23225,7 @@ channelApiRoutes.post("/disable", (c) => {
23341
23225
  config2.channelEnabled = false;
23342
23226
  });
23343
23227
  const repoRoot = c.get("repoRoot");
23344
- const dataDir = join12(repoRoot, ".glassbox");
23228
+ const dataDir = join11(repoRoot, ".glassbox");
23345
23229
  unregisterChannel(dataDir);
23346
23230
  return c.json({ ok: true });
23347
23231
  });
@@ -23349,7 +23233,7 @@ channelApiRoutes.post("/trigger", async (c) => {
23349
23233
  const parsed = await parseBody(c, TriggerChannelReqSchema);
23350
23234
  if (!parsed.ok) return parsed.response;
23351
23235
  const repoRoot = c.get("repoRoot");
23352
- const dataDir = join12(repoRoot, ".glassbox");
23236
+ const dataDir = join11(repoRoot, ".glassbox");
23353
23237
  const sent = await triggerChannel(dataDir, parsed.data.message);
23354
23238
  if (!sent) {
23355
23239
  return c.json({ error: "Channel not connected" }, 503);
@@ -23380,9 +23264,9 @@ channelApiRoutes.get("/claude-check", (c) => {
23380
23264
  import { Hono as Hono16 } from "hono";
23381
23265
  init_connection();
23382
23266
  init_queries();
23383
- init_blob_store();
23384
23267
  init_session();
23385
23268
  init_difftool();
23269
+ init_image_blobs();
23386
23270
  var difftoolApiRoutes = new Hono16();
23387
23271
  difftoolApiRoutes.get("/status", (c) => {
23388
23272
  return c.json(getDifftoolStatus("global"));
@@ -23423,8 +23307,8 @@ difftoolApiRoutes.post("/append", async (c) => {
23423
23307
  if (diff.isBinary || isSvgFile(diff.filePath)) {
23424
23308
  const dataDir = getDataDir();
23425
23309
  if (dataDir !== null) {
23426
- writeDifftoolBlob(dataDir, fileId, "old", oldContent);
23427
- writeDifftoolBlob(dataDir, fileId, "new", newContent);
23310
+ writeImageBlob(dataDir, fileId, "old", oldContent);
23311
+ writeImageBlob(dataDir, fileId, "new", newContent);
23428
23312
  }
23429
23313
  }
23430
23314
  return c.json({ ok: true, fileId });
@@ -23460,7 +23344,7 @@ difftoolApiRoutes.post("/end", (c) => {
23460
23344
  });
23461
23345
 
23462
23346
  // src/routes/pages.tsx
23463
- import { readFileSync as readFileSync14 } from "fs";
23347
+ import { readFileSync as readFileSync13 } from "fs";
23464
23348
  import { Hono as Hono17 } from "hono";
23465
23349
  import { resolve as resolve8 } from "path";
23466
23350
 
@@ -23645,25 +23529,31 @@ function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
23645
23529
  /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-metadata", "data-panel": "metadata", children: /* @__PURE__ */ jsx2("div", { className: "image-metadata-loading", children: "Loading metadata..." }) }),
23646
23530
  hasComparison && /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-visual", "data-panel": "difference", children: /* @__PURE__ */ jsx2("div", { className: "image-visual-canvas", "data-zoomable": "true", children: /* @__PURE__ */ jsxs2("div", { className: "image-zoom-wrap", children: [
23647
23531
  /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
23648
- /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-new image-blend", src: `/api/image/${fileId}/new`, alt: "New version" })
23532
+ /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-new image-blend", src: `/api/image/${fileId}/new`, alt: "New version" }),
23533
+ /* @__PURE__ */ jsx2("div", { className: "region-overlay", "data-region-overlay": true })
23649
23534
  ] }) }) }),
23650
23535
  hasComparison && /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-visual", "data-panel": "slice", children: /* @__PURE__ */ jsxs2("div", { className: "image-visual-canvas", "data-zoomable": "true", children: [
23651
23536
  /* @__PURE__ */ jsxs2("div", { className: "image-zoom-wrap", children: [
23652
23537
  /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
23653
- /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-new image-slice-clipped", src: `/api/image/${fileId}/new`, alt: "New version" })
23538
+ /* @__PURE__ */ jsx2("img", { className: "image-layer image-layer-new image-slice-clipped", src: `/api/image/${fileId}/new`, alt: "New version" }),
23539
+ /* @__PURE__ */ jsx2("div", { className: "region-overlay", "data-region-overlay": true })
23654
23540
  ] }),
23655
23541
  /* @__PURE__ */ jsx2("div", { className: "slice-line" }),
23656
23542
  /* @__PURE__ */ jsx2("div", { className: "slice-handle slice-handle-a" }),
23657
23543
  /* @__PURE__ */ jsx2("div", { className: "slice-handle slice-handle-b" })
23658
23544
  ] }) }),
23659
- !hasComparison && /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-visual", "data-panel": "image", children: /* @__PURE__ */ jsx2("div", { className: "image-visual-canvas", "data-zoomable": "true", children: /* @__PURE__ */ jsx2("div", { className: "image-zoom-wrap", children: /* @__PURE__ */ jsx2(
23660
- "img",
23661
- {
23662
- className: "image-layer image-layer-old",
23663
- src: `/api/image/${fileId}/${isAdded ? "new" : "old"}`,
23664
- alt: isAdded ? "New image" : "Deleted image"
23665
- }
23666
- ) }) }) })
23545
+ !hasComparison && /* @__PURE__ */ jsx2("div", { className: "image-diff-panel image-diff-visual", "data-panel": "image", children: /* @__PURE__ */ jsx2("div", { className: "image-visual-canvas", "data-zoomable": "true", children: /* @__PURE__ */ jsxs2("div", { className: "image-zoom-wrap", children: [
23546
+ /* @__PURE__ */ jsx2(
23547
+ "img",
23548
+ {
23549
+ className: "image-layer image-layer-old",
23550
+ src: `/api/image/${fileId}/${isAdded ? "new" : "old"}`,
23551
+ alt: isAdded ? "New image" : "Deleted image"
23552
+ }
23553
+ ),
23554
+ /* @__PURE__ */ jsx2("div", { className: "region-overlay", "data-region-overlay": true })
23555
+ ] }) }) }),
23556
+ /* @__PURE__ */ jsx2("div", { className: "image-feedback", "data-image-feedback": true })
23667
23557
  ]
23668
23558
  }
23669
23559
  );
@@ -24087,9 +23977,9 @@ function AnnotationRows({ annotations }) {
24087
23977
  }
24088
23978
 
24089
23979
  // src/themes/config.ts
24090
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "fs";
24091
- import { join as join13 } from "path";
24092
- var THEMES_DIR = join13(GLOBAL_CONFIG_DIR, "themes");
23980
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync12, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "fs";
23981
+ import { join as join12 } from "path";
23982
+ var THEMES_DIR = join12(GLOBAL_CONFIG_DIR, "themes");
24093
23983
  function getActiveThemeId() {
24094
23984
  const config2 = readGlobalConfig();
24095
23985
  const theme = config2.theme;
@@ -24103,13 +23993,13 @@ function setActiveThemeId(id) {
24103
23993
  });
24104
23994
  }
24105
23995
  function loadCustomThemes() {
24106
- if (!existsSync10(THEMES_DIR)) return [];
23996
+ if (!existsSync9(THEMES_DIR)) return [];
24107
23997
  const themes = [];
24108
23998
  try {
24109
23999
  const files = readdirSync2(THEMES_DIR).filter((f) => f.endsWith(".json"));
24110
24000
  for (const file2 of files) {
24111
24001
  try {
24112
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(join13(THEMES_DIR, file2), "utf-8")));
24002
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync12(join12(THEMES_DIR, file2), "utf-8")));
24113
24003
  if (!parsed.success) continue;
24114
24004
  const d = parsed.data;
24115
24005
  themes.push({ id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" });
@@ -24122,20 +24012,20 @@ function loadCustomThemes() {
24122
24012
  }
24123
24013
  function saveCustomTheme(theme) {
24124
24014
  mkdirSync9(THEMES_DIR, { recursive: true });
24125
- const filePath = join13(THEMES_DIR, `${theme.id}.json`);
24015
+ const filePath = join12(THEMES_DIR, `${theme.id}.json`);
24126
24016
  writeFileSync9(filePath, JSON.stringify(theme, null, 2), "utf-8");
24127
24017
  }
24128
24018
  function deleteCustomTheme(id) {
24129
- const filePath = join13(THEMES_DIR, `${id}.json`);
24130
- if (existsSync10(filePath)) {
24019
+ const filePath = join12(THEMES_DIR, `${id}.json`);
24020
+ if (existsSync9(filePath)) {
24131
24021
  unlinkSync3(filePath);
24132
24022
  }
24133
24023
  }
24134
24024
  function getCustomTheme(id) {
24135
- const filePath = join13(THEMES_DIR, `${id}.json`);
24136
- if (!existsSync10(filePath)) return void 0;
24025
+ const filePath = join12(THEMES_DIR, `${id}.json`);
24026
+ if (!existsSync9(filePath)) return void 0;
24137
24027
  try {
24138
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(filePath, "utf-8")));
24028
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync12(filePath, "utf-8")));
24139
24029
  if (!parsed.success) return void 0;
24140
24030
  const d = parsed.data;
24141
24031
  return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
@@ -24435,6 +24325,29 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
24435
24325
  // src/routes/pages.tsx
24436
24326
  init_queries();
24437
24327
 
24328
+ // src/git/svg-meta.ts
24329
+ function parseSvgDimensions(svg) {
24330
+ const widthMatch = svg.match(/\bwidth\s*=\s*["']([^"']+)["']/);
24331
+ const heightMatch = svg.match(/\bheight\s*=\s*["']([^"']+)["']/);
24332
+ const viewBoxMatch = svg.match(/\bviewBox\s*=\s*["']([^"']+)["']/);
24333
+ let width = widthMatch ? parseFloat(widthMatch[1]) : NaN;
24334
+ let height = heightMatch ? parseFloat(heightMatch[1]) : NaN;
24335
+ if ((isNaN(width) || isNaN(height)) && viewBoxMatch) {
24336
+ const parts = viewBoxMatch[1].split(/[\s,]+/);
24337
+ if (parts.length >= 4) {
24338
+ if (isNaN(width)) width = parseFloat(parts[2]);
24339
+ if (isNaN(height)) height = parseFloat(parts[3]);
24340
+ }
24341
+ }
24342
+ if (isNaN(width)) width = 300;
24343
+ if (isNaN(height)) height = 150;
24344
+ return { width, height };
24345
+ }
24346
+ function svgUsesExternalFonts(svgData) {
24347
+ const svg = svgData.toString("utf-8");
24348
+ return /<text[\s>]/i.test(svg) || /font-family/i.test(svg) || /@font-face/i.test(svg);
24349
+ }
24350
+
24438
24351
  // src/review-notes/reanchor.ts
24439
24352
  var MATCH_RADIUS = 50;
24440
24353
  function reanchorReviewNotes(notes, diff) {
@@ -24558,7 +24471,7 @@ pageRoutes.get("/file-raw", (c) => {
24558
24471
  const repoRoot = c.get("repoRoot");
24559
24472
  let content;
24560
24473
  try {
24561
- content = readFileSync14(resolve8(repoRoot, filePath), "utf-8");
24474
+ content = readFileSync13(resolve8(repoRoot, filePath), "utf-8");
24562
24475
  } catch {
24563
24476
  return c.text("File not found", 404);
24564
24477
  }
@@ -24748,17 +24661,17 @@ async function startServer(port, reviewId, repoRoot, options) {
24748
24661
  await next();
24749
24662
  });
24750
24663
  const selfDir = dirname4(fileURLToPath2(import.meta.url));
24751
- const distDir = existsSync11(join14(selfDir, "client", "styles.css")) ? join14(selfDir, "client") : join14(selfDir, "..", "dist", "client");
24664
+ const distDir = existsSync10(join13(selfDir, "client", "styles.css")) ? join13(selfDir, "client") : join13(selfDir, "..", "dist", "client");
24752
24665
  app.get("/static/styles.css", (c) => {
24753
- const css = readFileSync15(join14(distDir, "styles.css"), "utf-8");
24666
+ const css = readFileSync14(join13(distDir, "styles.css"), "utf-8");
24754
24667
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
24755
24668
  });
24756
24669
  app.get("/static/app.js", (c) => {
24757
- const js = readFileSync15(join14(distDir, "app.global.js"), "utf-8");
24670
+ const js = readFileSync14(join13(distDir, "app.global.js"), "utf-8");
24758
24671
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
24759
24672
  });
24760
24673
  app.get("/static/history.js", (c) => {
24761
- const js = readFileSync15(join14(distDir, "history.global.js"), "utf-8");
24674
+ const js = readFileSync14(join13(distDir, "history.global.js"), "utf-8");
24762
24675
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
24763
24676
  });
24764
24677
  app.get("/favicon.ico", (c) => c.body(null, 204));
@@ -24796,7 +24709,7 @@ async function startServer(port, reviewId, repoRoot, options) {
24796
24709
  try {
24797
24710
  const globalConfig2 = readGlobalConfig();
24798
24711
  if (globalConfig2.channelEnabled === true) {
24799
- const dataDir = join14(repoRoot, ".glassbox");
24712
+ const dataDir = join13(repoRoot, ".glassbox");
24800
24713
  registerChannel(dataDir);
24801
24714
  }
24802
24715
  } catch {
@@ -24811,8 +24724,8 @@ async function startServer(port, reviewId, repoRoot, options) {
24811
24724
  }
24812
24725
 
24813
24726
  // src/skills.ts
24814
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
24815
- import { join as join15 } from "path";
24727
+ import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
24728
+ import { join as join14 } from "path";
24816
24729
  var SKILL_VERSION = 1;
24817
24730
  function versionHeader() {
24818
24731
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -24823,8 +24736,8 @@ function parseVersionHeader(content) {
24823
24736
  return parseInt(match[1], 10);
24824
24737
  }
24825
24738
  function updateFile(path, content) {
24826
- if (existsSync12(path)) {
24827
- const existing = readFileSync16(path, "utf-8");
24739
+ if (existsSync11(path)) {
24740
+ const existing = readFileSync15(path, "utf-8");
24828
24741
  const version2 = parseVersionHeader(existing);
24829
24742
  if (version2 !== null && version2 >= SKILL_VERSION) {
24830
24743
  return false;
@@ -24850,7 +24763,7 @@ function skillBody() {
24850
24763
  ].join("\n");
24851
24764
  }
24852
24765
  function ensureClaudeSkills(cwd) {
24853
- const dir = join15(cwd, ".claude", "skills", "glassbox");
24766
+ const dir = join14(cwd, ".claude", "skills", "glassbox");
24854
24767
  mkdirSync10(dir, { recursive: true });
24855
24768
  const content = [
24856
24769
  "---",
@@ -24863,10 +24776,10 @@ function ensureClaudeSkills(cwd) {
24863
24776
  skillBody(),
24864
24777
  ""
24865
24778
  ].join("\n");
24866
- return updateFile(join15(dir, "SKILL.md"), content);
24779
+ return updateFile(join14(dir, "SKILL.md"), content);
24867
24780
  }
24868
24781
  function ensureCursorRules(cwd) {
24869
- const rulesDir = join15(cwd, ".cursor", "rules");
24782
+ const rulesDir = join14(cwd, ".cursor", "rules");
24870
24783
  mkdirSync10(rulesDir, { recursive: true });
24871
24784
  const content = [
24872
24785
  "---",
@@ -24878,10 +24791,10 @@ function ensureCursorRules(cwd) {
24878
24791
  skillBody(),
24879
24792
  ""
24880
24793
  ].join("\n");
24881
- return updateFile(join15(rulesDir, "glassbox.mdc"), content);
24794
+ return updateFile(join14(rulesDir, "glassbox.mdc"), content);
24882
24795
  }
24883
24796
  function ensureCopilotPrompts(cwd) {
24884
- const promptsDir = join15(cwd, ".github", "prompts");
24797
+ const promptsDir = join14(cwd, ".github", "prompts");
24885
24798
  mkdirSync10(promptsDir, { recursive: true });
24886
24799
  const content = [
24887
24800
  "---",
@@ -24892,10 +24805,10 @@ function ensureCopilotPrompts(cwd) {
24892
24805
  skillBody(),
24893
24806
  ""
24894
24807
  ].join("\n");
24895
- return updateFile(join15(promptsDir, "glassbox.prompt.md"), content);
24808
+ return updateFile(join14(promptsDir, "glassbox.prompt.md"), content);
24896
24809
  }
24897
24810
  function ensureWindsurfRules(cwd) {
24898
- const rulesDir = join15(cwd, ".windsurf", "rules");
24811
+ const rulesDir = join14(cwd, ".windsurf", "rules");
24899
24812
  mkdirSync10(rulesDir, { recursive: true });
24900
24813
  const content = [
24901
24814
  "---",
@@ -24907,21 +24820,21 @@ function ensureWindsurfRules(cwd) {
24907
24820
  skillBody(),
24908
24821
  ""
24909
24822
  ].join("\n");
24910
- return updateFile(join15(rulesDir, "glassbox.md"), content);
24823
+ return updateFile(join14(rulesDir, "glassbox.md"), content);
24911
24824
  }
24912
24825
  function ensureSkills() {
24913
24826
  const cwd = process.cwd();
24914
24827
  const platforms = [];
24915
- if (existsSync12(join15(cwd, ".claude"))) {
24828
+ if (existsSync11(join14(cwd, ".claude"))) {
24916
24829
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
24917
24830
  }
24918
- if (existsSync12(join15(cwd, ".cursor"))) {
24831
+ if (existsSync11(join14(cwd, ".cursor"))) {
24919
24832
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
24920
24833
  }
24921
- if (existsSync12(join15(cwd, ".github", "prompts")) || existsSync12(join15(cwd, ".github", "copilot-instructions.md"))) {
24834
+ if (existsSync11(join14(cwd, ".github", "prompts")) || existsSync11(join14(cwd, ".github", "copilot-instructions.md"))) {
24922
24835
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
24923
24836
  }
24924
- if (existsSync12(join15(cwd, ".windsurf"))) {
24837
+ if (existsSync11(join14(cwd, ".windsurf"))) {
24925
24838
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
24926
24839
  }
24927
24840
  return platforms;
@@ -24929,19 +24842,19 @@ function ensureSkills() {
24929
24842
 
24930
24843
  // src/update-check.ts
24931
24844
  init_zod();
24932
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
24845
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
24933
24846
  import { get } from "https";
24934
24847
  import { homedir as homedir3 } from "os";
24935
- import { dirname as dirname5, join as join16 } from "path";
24848
+ import { dirname as dirname5, join as join15 } from "path";
24936
24849
  import { fileURLToPath as fileURLToPath3 } from "url";
24937
- var DATA_DIR = join16(homedir3(), ".glassbox");
24938
- var CHECK_FILE = join16(DATA_DIR, "last-update-check");
24850
+ var DATA_DIR = join15(homedir3(), ".glassbox");
24851
+ var CHECK_FILE = join15(DATA_DIR, "last-update-check");
24939
24852
  var PACKAGE_NAME = "glassbox";
24940
24853
  var VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
24941
24854
  function getCurrentVersion() {
24942
24855
  try {
24943
24856
  const dir = dirname5(fileURLToPath3(import.meta.url));
24944
- const raw2 = JSON.parse(readFileSync17(join16(dir, "..", "package.json"), "utf-8"));
24857
+ const raw2 = JSON.parse(readFileSync16(join15(dir, "..", "package.json"), "utf-8"));
24945
24858
  return VersionPayloadSchema.parse(raw2).version;
24946
24859
  } catch {
24947
24860
  return "0.0.0";
@@ -24949,8 +24862,8 @@ function getCurrentVersion() {
24949
24862
  }
24950
24863
  function getLastCheckDate() {
24951
24864
  try {
24952
- if (existsSync13(CHECK_FILE)) {
24953
- return readFileSync17(CHECK_FILE, "utf-8").trim();
24865
+ if (existsSync12(CHECK_FILE)) {
24866
+ return readFileSync16(CHECK_FILE, "utf-8").trim();
24954
24867
  }
24955
24868
  } catch {
24956
24869
  }
@@ -25258,22 +25171,22 @@ async function main() {
25258
25171
  console.log("AI service test mode enabled \u2014 using mock AI responses");
25259
25172
  }
25260
25173
  if (debug) {
25261
- console.log(`[debug] Build timestamp: ${"2026-06-20T04:34:27.118Z"}`);
25174
+ console.log(`[debug] Build timestamp: ${"2026-06-22T06:43:57.425Z"}`);
25262
25175
  }
25263
25176
  if (projectDir !== null) {
25264
25177
  process.chdir(projectDir);
25265
25178
  }
25266
25179
  if (dataDir === null) {
25267
- dataDir = join18(process.cwd(), ".glassbox");
25180
+ dataDir = join17(process.cwd(), ".glassbox");
25268
25181
  }
25269
25182
  if (difftoolServe) {
25270
25183
  const { initDifftoolSession: initDifftoolSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
25271
25184
  const { writeDiscovery: writeDiscovery2, clearDiscovery: clearDiscovery2, releaseStartingLock: releaseStartingLock2 } = await Promise.resolve().then(() => (init_difftool_discovery(), difftool_discovery_exports));
25272
- const { clearDifftoolBlobs: clearDifftoolBlobs2 } = await Promise.resolve().then(() => (init_blob_store(), blob_store_exports));
25185
+ const { clearImageBlobs: clearImageBlobs2 } = await Promise.resolve().then(() => (init_image_blobs(), image_blobs_exports));
25273
25186
  mkdirSync13(dataDir, { recursive: true });
25274
25187
  setDataDir(dataDir);
25275
25188
  const sessionDataDir = dataDir;
25276
- clearDifftoolBlobs2(sessionDataDir);
25189
+ clearImageBlobs2(sessionDataDir);
25277
25190
  const repoRoot2 = process.cwd();
25278
25191
  const review2 = await createReview(repoRoot2, "git difftool", "difftool");
25279
25192
  const { port: actualPort, server } = await startServer(port, review2.id, repoRoot2, { noOpen, strictPort });
@@ -25285,7 +25198,7 @@ async function main() {
25285
25198
  server.close();
25286
25199
  } catch {
25287
25200
  }
25288
- clearDifftoolBlobs2(sessionDataDir);
25201
+ clearImageBlobs2(sessionDataDir);
25289
25202
  clearDiscovery2();
25290
25203
  releaseStartingLock2();
25291
25204
  process.exit(0);
@@ -25305,7 +25218,7 @@ async function main() {
25305
25218
  }
25306
25219
  process.exit(1);
25307
25220
  }
25308
- dataDir = join18(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
25221
+ dataDir = join17(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
25309
25222
  setDemoMode(demo);
25310
25223
  console.log(`
25311
25224
  DEMO MODE: ${scenario.label}
@@ -25333,7 +25246,7 @@ async function main() {
25333
25246
  if (mode.type === "diff") {
25334
25247
  const { pathA, pathB } = mode;
25335
25248
  for (const p of [pathA, pathB]) {
25336
- if (!existsSync15(p)) {
25249
+ if (!existsSync14(p)) {
25337
25250
  console.error(`Error: path does not exist: ${p}`);
25338
25251
  process.exit(1);
25339
25252
  }