glassbox 0.10.1 → 0.11.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +280 -43
- package/dist/client/app.global.js +33 -33
- package/dist/client/history.global.js +6 -6
- package/dist/client/styles.css +1 -1
- package/dist/svg-rasterize-worker.js +153 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ That's it. Data stays local. Works in any git repo.
|
|
|
39
39
|
|
|
40
40
|
<br>
|
|
41
41
|
|
|
42
|
-
<img src="assets/demo
|
|
42
|
+
<img src="assets/demo.svg" alt="Glassbox: triage by AI risk, annotate a diff, and let Claude Code apply the fix — the full review loop" width="720">
|
|
43
43
|
|
|
44
44
|
</div>
|
|
45
45
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
20641
|
-
await
|
|
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
|
|
20666
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
21897
|
-
if (
|
|
21898
|
-
|
|
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
|
|
21923
|
-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
23149
|
+
console.log(`[debug] Build timestamp: ${"2026-05-27T07:53:08.605Z"}`);
|
|
22913
23150
|
}
|
|
22914
23151
|
if (projectDir !== null) {
|
|
22915
23152
|
process.chdir(projectDir);
|