glassbox 0.8.1 → 0.8.2-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/dist/cli.js +968 -1039
- package/dist/client/app.global.js +8 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -395,7 +395,7 @@ init_connection();
|
|
|
395
395
|
init_queries();
|
|
396
396
|
import { mkdirSync as mkdirSync9, realpathSync } from "fs";
|
|
397
397
|
import { tmpdir } from "os";
|
|
398
|
-
import { join as join13, resolve as
|
|
398
|
+
import { join as join13, resolve as resolve7 } from "path";
|
|
399
399
|
|
|
400
400
|
// src/debug.ts
|
|
401
401
|
var debugEnabled = false;
|
|
@@ -425,10 +425,37 @@ function debugLog(...args) {
|
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
// src/
|
|
428
|
+
// src/global-config.ts
|
|
429
429
|
import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
430
430
|
import { homedir } from "os";
|
|
431
431
|
import { join as join2 } from "path";
|
|
432
|
+
var GLOBAL_CONFIG_DIR = join2(homedir(), ".glassbox");
|
|
433
|
+
var GLOBAL_CONFIG_PATH = join2(GLOBAL_CONFIG_DIR, "config.json");
|
|
434
|
+
function readGlobalConfig() {
|
|
435
|
+
try {
|
|
436
|
+
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
437
|
+
const parsed = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
438
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
439
|
+
return parsed;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
446
|
+
function writeGlobalConfig(config) {
|
|
447
|
+
mkdirSync2(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
448
|
+
writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
449
|
+
try {
|
|
450
|
+
chmodSync(GLOBAL_CONFIG_PATH, 384);
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function updateGlobalConfig(mutator) {
|
|
455
|
+
const cfg = readGlobalConfig();
|
|
456
|
+
const result = mutator(cfg);
|
|
457
|
+
writeGlobalConfig(result === void 0 ? cfg : result);
|
|
458
|
+
}
|
|
432
459
|
|
|
433
460
|
// src/ai/api-keys.ts
|
|
434
461
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -589,11 +616,12 @@ function saveAPIKey(platform, key, storage) {
|
|
|
589
616
|
if (storage === "keychain") {
|
|
590
617
|
saveKeyToKeychain(platform, key);
|
|
591
618
|
} else {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
619
|
+
updateGlobalConfig((raw) => {
|
|
620
|
+
const cfg = raw;
|
|
621
|
+
if (cfg.ai === void 0) cfg.ai = {};
|
|
622
|
+
if (cfg.ai.keys === void 0) cfg.ai.keys = {};
|
|
623
|
+
cfg.ai.keys[platform] = Buffer.from(key).toString("base64");
|
|
624
|
+
});
|
|
597
625
|
}
|
|
598
626
|
}
|
|
599
627
|
function deleteAPIKey(platform) {
|
|
@@ -610,11 +638,13 @@ function deleteAPIKey(platform) {
|
|
|
610
638
|
}
|
|
611
639
|
} catch {
|
|
612
640
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
641
|
+
if (readConfigFile().ai?.keys === void 0) return;
|
|
642
|
+
updateGlobalConfig((raw) => {
|
|
643
|
+
const cfg = raw;
|
|
644
|
+
if (cfg.ai?.keys !== void 0) {
|
|
645
|
+
cfg.ai.keys[platform] = "";
|
|
646
|
+
}
|
|
647
|
+
});
|
|
618
648
|
}
|
|
619
649
|
function detectAvailablePlatforms() {
|
|
620
650
|
const results = [];
|
|
@@ -628,24 +658,8 @@ function detectAvailablePlatforms() {
|
|
|
628
658
|
}
|
|
629
659
|
|
|
630
660
|
// src/ai/config.ts
|
|
631
|
-
var CONFIG_DIR = join2(homedir(), ".glassbox");
|
|
632
|
-
var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
|
|
633
661
|
function readConfigFile() {
|
|
634
|
-
|
|
635
|
-
if (existsSync(CONFIG_PATH)) {
|
|
636
|
-
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
637
|
-
}
|
|
638
|
-
} catch {
|
|
639
|
-
}
|
|
640
|
-
return {};
|
|
641
|
-
}
|
|
642
|
-
function writeConfigFile(config) {
|
|
643
|
-
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
644
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
645
|
-
try {
|
|
646
|
-
chmodSync(CONFIG_PATH, 384);
|
|
647
|
-
} catch {
|
|
648
|
-
}
|
|
662
|
+
return readGlobalConfig();
|
|
649
663
|
}
|
|
650
664
|
function loadAIConfig() {
|
|
651
665
|
const config = readConfigFile();
|
|
@@ -655,11 +669,12 @@ function loadAIConfig() {
|
|
|
655
669
|
return { platform, model, apiKey: key, keySource: source };
|
|
656
670
|
}
|
|
657
671
|
function saveAIConfigPreferences(platform, model) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
672
|
+
updateGlobalConfig((config) => {
|
|
673
|
+
const cfg = config;
|
|
674
|
+
if (cfg.ai === void 0) cfg.ai = {};
|
|
675
|
+
cfg.ai.platform = platform;
|
|
676
|
+
cfg.ai.model = model;
|
|
677
|
+
});
|
|
663
678
|
}
|
|
664
679
|
function loadGuidedReviewConfig() {
|
|
665
680
|
const config = readConfigFile();
|
|
@@ -669,9 +684,9 @@ function loadGuidedReviewConfig() {
|
|
|
669
684
|
};
|
|
670
685
|
}
|
|
671
686
|
function saveGuidedReviewConfig(settings) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
687
|
+
updateGlobalConfig((config) => {
|
|
688
|
+
config.guidedReview = { enabled: settings.enabled, topics: settings.topics };
|
|
689
|
+
});
|
|
675
690
|
}
|
|
676
691
|
|
|
677
692
|
// src/db/ai-queries.ts
|
|
@@ -1308,6 +1323,18 @@ function getHeadCommit(cwd) {
|
|
|
1308
1323
|
return spawnSync3("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).stdout.trim();
|
|
1309
1324
|
}
|
|
1310
1325
|
|
|
1326
|
+
// src/git/parseDiffData.ts
|
|
1327
|
+
function parseDiffData(raw) {
|
|
1328
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
1329
|
+
try {
|
|
1330
|
+
const parsed = JSON.parse(raw);
|
|
1331
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1332
|
+
return parsed;
|
|
1333
|
+
} catch {
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1311
1338
|
// src/git/diff.ts
|
|
1312
1339
|
function git2(args, cwd) {
|
|
1313
1340
|
const result = spawnSync4("git", args, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
|
|
@@ -1689,7 +1716,7 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
|
|
|
1689
1716
|
for (const [path, existingFile] of existingByPath) {
|
|
1690
1717
|
const newDiff = newDiffsByPath.get(path);
|
|
1691
1718
|
if (newDiff) {
|
|
1692
|
-
const oldDiff =
|
|
1719
|
+
const oldDiff = parseDiffData(existingFile.diff_data) ?? {};
|
|
1693
1720
|
const annotations = await getAnnotationsForFile(existingFile.id);
|
|
1694
1721
|
if (annotations.length > 0) {
|
|
1695
1722
|
stale += await migrateAnnotations(annotations, oldDiff, newDiff);
|
|
@@ -1701,7 +1728,7 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
|
|
|
1701
1728
|
if (annotations.length === 0) {
|
|
1702
1729
|
await deleteReviewFile(existingFile.id);
|
|
1703
1730
|
} else {
|
|
1704
|
-
const oldDiff =
|
|
1731
|
+
const oldDiff = parseDiffData(existingFile.diff_data) ?? {};
|
|
1705
1732
|
for (const a of annotations) {
|
|
1706
1733
|
if (!a.is_stale) {
|
|
1707
1734
|
const content = findLineContent(oldDiff, a.line_number, a.side);
|
|
@@ -1725,9 +1752,8 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
|
|
|
1725
1752
|
// src/server.ts
|
|
1726
1753
|
import { serve } from "@hono/node-server";
|
|
1727
1754
|
import { exec } from "child_process";
|
|
1728
|
-
import { existsSync as
|
|
1729
|
-
import { Hono as
|
|
1730
|
-
import { homedir as homedir6 } from "os";
|
|
1755
|
+
import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
|
|
1756
|
+
import { Hono as Hono16 } from "hono";
|
|
1731
1757
|
import { dirname as dirname2, join as join10 } from "path";
|
|
1732
1758
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1733
1759
|
|
|
@@ -1985,7 +2011,7 @@ function buildFileContexts(files, charBudget) {
|
|
|
1985
2011
|
const contexts = [];
|
|
1986
2012
|
const perFileBudget = Math.floor(charBudget / Math.max(files.length, 1));
|
|
1987
2013
|
for (const file of files) {
|
|
1988
|
-
const diff =
|
|
2014
|
+
const diff = parseDiffData(file.diff_data) ?? {};
|
|
1989
2015
|
let added = 0;
|
|
1990
2016
|
let removed = 0;
|
|
1991
2017
|
const hunks = diff.hunks;
|
|
@@ -2048,6 +2074,45 @@ function extractJSON(text) {
|
|
|
2048
2074
|
}
|
|
2049
2075
|
throw new Error(`Could not extract JSON from AI response: ${text.slice(0, 300)}`);
|
|
2050
2076
|
}
|
|
2077
|
+
async function runAnalysisBatch(files, config, repoRoot, options) {
|
|
2078
|
+
const contextWindow = getModelContextWindow(config.platform, config.model);
|
|
2079
|
+
const charBudget = Math.floor(contextWindow * 0.7 * 3);
|
|
2080
|
+
const contexts = buildFileContexts(files, charBudget);
|
|
2081
|
+
const validPaths = new Set(files.map((f) => f.file_path));
|
|
2082
|
+
const initialPrompt = [
|
|
2083
|
+
options.initialPromptHeader(files.length),
|
|
2084
|
+
"",
|
|
2085
|
+
formatContextsForPrompt(contexts)
|
|
2086
|
+
].join("\n");
|
|
2087
|
+
const messages = [{ role: "user", content: initialPrompt }];
|
|
2088
|
+
for (let round = 0; round < 3; round++) {
|
|
2089
|
+
const response = await sendAIRequest(config, options.systemPrompt, messages);
|
|
2090
|
+
const parsed = extractJSON(response.content);
|
|
2091
|
+
if (isNeedContext(parsed)) {
|
|
2092
|
+
const safePaths = parsed.needContext.filter((p) => validPaths.has(p));
|
|
2093
|
+
if (safePaths.length === 0) {
|
|
2094
|
+
throw new Error("AI requested context for files not in the review");
|
|
2095
|
+
}
|
|
2096
|
+
const fileContents = safePaths.map((path) => ({
|
|
2097
|
+
path,
|
|
2098
|
+
content: getFileContent(path, "working", repoRoot)
|
|
2099
|
+
}));
|
|
2100
|
+
messages.push({ role: "assistant", content: response.content });
|
|
2101
|
+
messages.push({
|
|
2102
|
+
role: "user",
|
|
2103
|
+
content: `Here is the full content of the requested files:
|
|
2104
|
+
|
|
2105
|
+
${formatAdditionalContext(fileContents)}`
|
|
2106
|
+
});
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
if (!Array.isArray(parsed)) {
|
|
2110
|
+
throw new Error(`Expected an array of ${options.resultLabel} from AI`);
|
|
2111
|
+
}
|
|
2112
|
+
return parsed;
|
|
2113
|
+
}
|
|
2114
|
+
throw new Error(`${options.analysisName} did not converge after 3 context rounds`);
|
|
2115
|
+
}
|
|
2051
2116
|
|
|
2052
2117
|
// src/ai/analyze-guided.ts
|
|
2053
2118
|
var TOPIC_DISPLAY = {
|
|
@@ -2132,45 +2197,13 @@ If you need full file content to explain accurately, output ONLY: {"needContext"
|
|
|
2132
2197
|
|
|
2133
2198
|
CRITICAL: Your entire response must be parseable by JSON.parse(). No prose, no markdown, no explanation.`;
|
|
2134
2199
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
`Provide educational walkthrough notes for these ${String(files.length)} changed files:`,
|
|
2143
|
-
"",
|
|
2144
|
-
formatContextsForPrompt(contexts)
|
|
2145
|
-
].join("\n");
|
|
2146
|
-
const messages = [{ role: "user", content: initialPrompt }];
|
|
2147
|
-
for (let round = 0; round < 3; round++) {
|
|
2148
|
-
const response = await sendAIRequest(config, systemPrompt, messages);
|
|
2149
|
-
const parsed = extractJSON(response.content);
|
|
2150
|
-
if (isNeedContext(parsed)) {
|
|
2151
|
-
const safePaths = parsed.needContext.filter((p) => validPaths.has(p));
|
|
2152
|
-
if (safePaths.length === 0) {
|
|
2153
|
-
throw new Error("AI requested context for files not in the review");
|
|
2154
|
-
}
|
|
2155
|
-
const fileContents = safePaths.map((path) => ({
|
|
2156
|
-
path,
|
|
2157
|
-
content: getFileContent(path, "working", repoRoot)
|
|
2158
|
-
}));
|
|
2159
|
-
messages.push({ role: "assistant", content: response.content });
|
|
2160
|
-
messages.push({
|
|
2161
|
-
role: "user",
|
|
2162
|
-
content: `Here is the full content of the requested files:
|
|
2163
|
-
|
|
2164
|
-
${formatAdditionalContext(fileContents)}`
|
|
2165
|
-
});
|
|
2166
|
-
continue;
|
|
2167
|
-
}
|
|
2168
|
-
if (!Array.isArray(parsed)) {
|
|
2169
|
-
throw new Error("Expected an array of guided review notes from AI");
|
|
2170
|
-
}
|
|
2171
|
-
return parsed;
|
|
2172
|
-
}
|
|
2173
|
-
throw new Error("Guided analysis did not converge after 3 context rounds");
|
|
2200
|
+
function runGuidedAnalysisBatch(files, config, repoRoot, guidedReview) {
|
|
2201
|
+
return runAnalysisBatch(files, config, repoRoot, {
|
|
2202
|
+
systemPrompt: buildSystemPrompt(guidedReview),
|
|
2203
|
+
initialPromptHeader: (n) => `Provide educational walkthrough notes for these ${String(n)} changed files:`,
|
|
2204
|
+
resultLabel: "guided review notes",
|
|
2205
|
+
analysisName: "Guided analysis"
|
|
2206
|
+
});
|
|
2174
2207
|
}
|
|
2175
2208
|
|
|
2176
2209
|
// src/ai/guided-review.ts
|
|
@@ -2260,45 +2293,14 @@ Every file in the diff must appear exactly once.
|
|
|
2260
2293
|
If you need full file content to determine dependencies, output ONLY: {"needContext":["path/to/file.ts"]}
|
|
2261
2294
|
|
|
2262
2295
|
CRITICAL: Your entire response must be parseable by JSON.parse(). No prose, no markdown, no explanation.`;
|
|
2263
|
-
|
|
2264
|
-
const contextWindow = getModelContextWindow(config.platform, config.model);
|
|
2265
|
-
const charBudget = Math.floor(contextWindow * 0.7 * 3);
|
|
2296
|
+
function runNarrativeAnalysisBatch(files, config, repoRoot, guidedReview) {
|
|
2266
2297
|
const systemPrompt = SYSTEM_PROMPT + (guidedReview !== void 0 ? buildGuidedReviewSuffix(guidedReview, "narrative") : "");
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
""
|
|
2272
|
-
|
|
2273
|
-
].join("\n");
|
|
2274
|
-
const messages = [{ role: "user", content: initialPrompt }];
|
|
2275
|
-
for (let round = 0; round < 3; round++) {
|
|
2276
|
-
const response = await sendAIRequest(config, systemPrompt, messages);
|
|
2277
|
-
const parsed = extractJSON(response.content);
|
|
2278
|
-
if (isNeedContext(parsed)) {
|
|
2279
|
-
const safePaths = parsed.needContext.filter((p) => validPaths.has(p));
|
|
2280
|
-
if (safePaths.length === 0) {
|
|
2281
|
-
throw new Error("AI requested context for files not in the review");
|
|
2282
|
-
}
|
|
2283
|
-
const fileContents = safePaths.map((path) => ({
|
|
2284
|
-
path,
|
|
2285
|
-
content: getFileContent(path, "working", repoRoot)
|
|
2286
|
-
}));
|
|
2287
|
-
messages.push({ role: "assistant", content: response.content });
|
|
2288
|
-
messages.push({
|
|
2289
|
-
role: "user",
|
|
2290
|
-
content: `Here is the full content of the requested files:
|
|
2291
|
-
|
|
2292
|
-
${formatAdditionalContext(fileContents)}`
|
|
2293
|
-
});
|
|
2294
|
-
continue;
|
|
2295
|
-
}
|
|
2296
|
-
if (!Array.isArray(parsed)) {
|
|
2297
|
-
throw new Error("Expected an array of narrative ordering from AI");
|
|
2298
|
-
}
|
|
2299
|
-
return parsed;
|
|
2300
|
-
}
|
|
2301
|
-
throw new Error("Narrative analysis did not converge after 3 context rounds");
|
|
2298
|
+
return runAnalysisBatch(files, config, repoRoot, {
|
|
2299
|
+
systemPrompt,
|
|
2300
|
+
initialPromptHeader: (n) => `Determine the best reading order for reviewing these ${String(n)} changed files:`,
|
|
2301
|
+
resultLabel: "narrative ordering",
|
|
2302
|
+
analysisName: "Narrative analysis"
|
|
2303
|
+
});
|
|
2302
2304
|
}
|
|
2303
2305
|
function mergeNarrativeOrders(batchResults, batchCount) {
|
|
2304
2306
|
if (batchCount <= 1) {
|
|
@@ -2359,45 +2361,14 @@ The aggregate should be the MAX of all individual dimension scores (if a file ha
|
|
|
2359
2361
|
If you need full file content to assess accurately, output ONLY: {"needContext":["path/to/file.ts"]}
|
|
2360
2362
|
|
|
2361
2363
|
CRITICAL: Your entire response must be parseable by JSON.parse(). No prose, no markdown, no explanation.`;
|
|
2362
|
-
|
|
2363
|
-
const contextWindow = getModelContextWindow(config.platform, config.model);
|
|
2364
|
-
const charBudget = Math.floor(contextWindow * 0.7 * 3);
|
|
2364
|
+
function runRiskAnalysisBatch(files, config, repoRoot, guidedReview) {
|
|
2365
2365
|
const systemPrompt = SYSTEM_PROMPT2 + (guidedReview !== void 0 ? buildGuidedReviewSuffix(guidedReview, "risk") : "");
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
""
|
|
2371
|
-
|
|
2372
|
-
].join("\n");
|
|
2373
|
-
const messages = [{ role: "user", content: initialPrompt }];
|
|
2374
|
-
for (let round = 0; round < 3; round++) {
|
|
2375
|
-
const response = await sendAIRequest(config, systemPrompt, messages);
|
|
2376
|
-
const parsed = extractJSON(response.content);
|
|
2377
|
-
if (isNeedContext(parsed)) {
|
|
2378
|
-
const safePaths = parsed.needContext.filter((p) => validPaths.has(p));
|
|
2379
|
-
if (safePaths.length === 0) {
|
|
2380
|
-
throw new Error("AI requested context for files not in the review");
|
|
2381
|
-
}
|
|
2382
|
-
const fileContents = safePaths.map((path) => ({
|
|
2383
|
-
path,
|
|
2384
|
-
content: getFileContent(path, "working", repoRoot)
|
|
2385
|
-
}));
|
|
2386
|
-
messages.push({ role: "assistant", content: response.content });
|
|
2387
|
-
messages.push({
|
|
2388
|
-
role: "user",
|
|
2389
|
-
content: `Here is the full content of the requested files:
|
|
2390
|
-
|
|
2391
|
-
${formatAdditionalContext(fileContents)}`
|
|
2392
|
-
});
|
|
2393
|
-
continue;
|
|
2394
|
-
}
|
|
2395
|
-
if (!Array.isArray(parsed)) {
|
|
2396
|
-
throw new Error("Expected an array of risk assessments from AI");
|
|
2397
|
-
}
|
|
2398
|
-
return parsed;
|
|
2399
|
-
}
|
|
2400
|
-
throw new Error("Risk analysis did not converge after 3 context rounds");
|
|
2366
|
+
return runAnalysisBatch(files, config, repoRoot, {
|
|
2367
|
+
systemPrompt,
|
|
2368
|
+
initialPromptHeader: (n) => `Analyze the following ${String(n)} file diffs for risk:`,
|
|
2369
|
+
resultLabel: "risk assessments",
|
|
2370
|
+
analysisName: "Risk analysis"
|
|
2371
|
+
});
|
|
2401
2372
|
}
|
|
2402
2373
|
|
|
2403
2374
|
// src/ai/batch-planner.ts
|
|
@@ -2538,8 +2509,8 @@ function isRetriable(err) {
|
|
|
2538
2509
|
return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
|
|
2539
2510
|
}
|
|
2540
2511
|
function sleep(ms) {
|
|
2541
|
-
return new Promise((
|
|
2542
|
-
setTimeout(
|
|
2512
|
+
return new Promise((resolve8) => {
|
|
2513
|
+
setTimeout(resolve8, ms);
|
|
2543
2514
|
});
|
|
2544
2515
|
}
|
|
2545
2516
|
|
|
@@ -2583,8 +2554,8 @@ function randomLines(count) {
|
|
|
2583
2554
|
return lines.sort((a, b) => a.line - b.line);
|
|
2584
2555
|
}
|
|
2585
2556
|
function sleep2(ms) {
|
|
2586
|
-
return new Promise((
|
|
2587
|
-
setTimeout(
|
|
2557
|
+
return new Promise((resolve8) => {
|
|
2558
|
+
setTimeout(resolve8, ms);
|
|
2588
2559
|
});
|
|
2589
2560
|
}
|
|
2590
2561
|
async function mockRiskAnalysisBatch(files) {
|
|
@@ -2653,6 +2624,24 @@ async function mockGuidedAnalysisBatch(files) {
|
|
|
2653
2624
|
|
|
2654
2625
|
// src/routes/ai-analysis.ts
|
|
2655
2626
|
init_queries();
|
|
2627
|
+
|
|
2628
|
+
// src/utils/resolveReviewId.ts
|
|
2629
|
+
function resolveReviewId(c) {
|
|
2630
|
+
return c.req.query("reviewId") ?? c.get("reviewId");
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/utils/validate.ts
|
|
2634
|
+
function checkEnum(value, name, allowed) {
|
|
2635
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
2636
|
+
return { error: `${name} must be one of: ${allowed.join(", ")}` };
|
|
2637
|
+
}
|
|
2638
|
+
return { ok: value };
|
|
2639
|
+
}
|
|
2640
|
+
function isNonEmptyString(value) {
|
|
2641
|
+
return typeof value === "string" && value.trim() !== "";
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// src/routes/ai-analysis.ts
|
|
2656
2645
|
var aiAnalysisRoutes = new Hono();
|
|
2657
2646
|
var VALID_SORT_MODES = ["folder", "risk", "narrative", "guided"];
|
|
2658
2647
|
var VALID_RISK_DIMENSIONS = ["aggregate", "security", "correctness", "error-handling", "maintainability", "architecture", "performance"];
|
|
@@ -2661,15 +2650,14 @@ var VALID_IMAGE_MODES = ["metadata", "side-by-side", "difference", "slice"];
|
|
|
2661
2650
|
var VALID_ANALYSIS_TYPES = ["risk", "narrative", "guided"];
|
|
2662
2651
|
var cancelledAnalyses = /* @__PURE__ */ new Set();
|
|
2663
2652
|
aiAnalysisRoutes.post("/analyze", async (c) => {
|
|
2664
|
-
const reviewId = c
|
|
2653
|
+
const reviewId = resolveReviewId(c);
|
|
2665
2654
|
const repoRoot = c.get("repoRoot");
|
|
2666
2655
|
const body = await c.req.json();
|
|
2667
2656
|
const analysisType = body.type;
|
|
2668
2657
|
const invalidateCache = body.invalidateCache === true;
|
|
2669
2658
|
debugLog(`POST /analyze: type=${analysisType}, reviewId=${reviewId}`);
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
}
|
|
2659
|
+
const typeCheck = checkEnum(analysisType, "type", VALID_ANALYSIS_TYPES);
|
|
2660
|
+
if ("error" in typeCheck) return c.json({ error: typeCheck.error }, 400);
|
|
2673
2661
|
const testMode = isAIServiceTest();
|
|
2674
2662
|
const config = loadAIConfig();
|
|
2675
2663
|
if (config.apiKey === null && !testMode) {
|
|
@@ -2719,113 +2707,115 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
|
|
|
2719
2707
|
const analysis = await createAnalysis(reviewId, analysisType);
|
|
2720
2708
|
debugLog(`POST /analyze: created new analysis id=${analysis.id}`);
|
|
2721
2709
|
const guidedReview = loadGuidedReviewConfig();
|
|
2722
|
-
void (
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
const binaryPathSet = new Set(binaryFiles.map((f) => f.file_path));
|
|
2733
|
-
const unchangedPaths = /* @__PURE__ */ new Set();
|
|
2734
|
-
const cachedScores = prevScores.filter((s) => {
|
|
2735
|
-
if (fileIdMap.has(s.file_path) && !binaryPathSet.has(s.file_path)) {
|
|
2736
|
-
unchangedPaths.add(s.file_path);
|
|
2737
|
-
return true;
|
|
2738
|
-
}
|
|
2739
|
-
return false;
|
|
2740
|
-
});
|
|
2741
|
-
debugLog(`Cache: ${String(cachedScores.length)} scores from previous analysis, ${String(totalAnalyzable - cachedScores.length)} files need processing`);
|
|
2742
|
-
if (cachedScores.length > 0) {
|
|
2743
|
-
const cachedForInsert = cachedScores.map((s) => ({
|
|
2744
|
-
reviewFileId: fileIdMap.get(s.file_path) ?? s.review_file_id,
|
|
2745
|
-
filePath: s.file_path,
|
|
2746
|
-
sortOrder: s.sort_order,
|
|
2747
|
-
aggregateScore: s.aggregate_score,
|
|
2748
|
-
rationale: s.rationale,
|
|
2749
|
-
dimensionScores: s.dimension_scores !== null ? JSON.parse(s.dimension_scores) : null,
|
|
2750
|
-
notes: s.notes !== null ? JSON.parse(s.notes) : null
|
|
2751
|
-
}));
|
|
2752
|
-
await appendFileScores(analysis.id, cachedForInsert);
|
|
2753
|
-
}
|
|
2754
|
-
const filteredBatches = batches.map((batch) => {
|
|
2755
|
-
const remaining = batch.files.filter((f) => !unchangedPaths.has(f.file_path));
|
|
2756
|
-
return { files: remaining, estimatedTokens: batch.estimatedTokens };
|
|
2757
|
-
}).filter((batch) => batch.files.length > 0);
|
|
2758
|
-
const filteredAnalyzable = filteredBatches.reduce((sum, b) => sum + b.files.length, 0);
|
|
2759
|
-
const totalForProgress = filteredAnalyzable + binaryFiles.length + cachedScores.length;
|
|
2760
|
-
debugLog(`After cache: ${String(filteredAnalyzable)} files to analyze in ${String(filteredBatches.length)} batch(es)`);
|
|
2761
|
-
await updateAnalysisProgress(analysis.id, cachedScores.length, totalForProgress);
|
|
2762
|
-
if (binaryFiles.length > 0) {
|
|
2763
|
-
debugLog(`Saving ${String(binaryFiles.length)} binary files with score 0`);
|
|
2764
|
-
const binaryScoreEntries = binaryFiles.map((f, idx) => ({
|
|
2765
|
-
reviewFileId: fileIdMap.get(f.file_path) ?? "",
|
|
2766
|
-
filePath: f.file_path,
|
|
2767
|
-
sortOrder: 99999 + idx,
|
|
2768
|
-
// Will be re-sorted later
|
|
2769
|
-
aggregateScore: analysisType === "risk" ? 0 : null,
|
|
2770
|
-
rationale: "Binary file \u2014 not analyzed",
|
|
2771
|
-
dimensionScores: analysisType === "risk" ? { security: 0, correctness: 0, "error-handling": 0, maintainability: 0, architecture: 0, performance: 0 } : null,
|
|
2772
|
-
notes: null
|
|
2773
|
-
}));
|
|
2774
|
-
await appendFileScores(analysis.id, binaryScoreEntries);
|
|
2775
|
-
await updateAnalysisProgress(analysis.id, cachedScores.length + binaryFiles.length, totalForProgress);
|
|
2776
|
-
}
|
|
2777
|
-
if (filteredBatches.length === 0) {
|
|
2778
|
-
debugLog("No batches to process (all files cached or binary), marking completed");
|
|
2779
|
-
await updateAnalysisStatus(analysis.id, "completed");
|
|
2780
|
-
return;
|
|
2781
|
-
}
|
|
2782
|
-
const shouldCancel = () => cancelledAnalyses.has(analysis.id);
|
|
2783
|
-
const progressOffset = cachedScores.length + binaryFiles.length;
|
|
2784
|
-
if (analysisType === "risk") {
|
|
2785
|
-
await runBatchedRiskAnalysis(analysis.id, filteredBatches, files, config, repoRoot, fileIdMap, totalForProgress, progressOffset, shouldCancel, guidedReview);
|
|
2786
|
-
} else if (analysisType === "narrative") {
|
|
2787
|
-
await runBatchedNarrativeAnalysis(analysis.id, filteredBatches, files, config, repoRoot, fileIdMap, totalForProgress, progressOffset, shouldCancel, guidedReview);
|
|
2788
|
-
} else {
|
|
2789
|
-
await runBatchedGuidedAnalysis(analysis.id, filteredBatches, files, config, repoRoot, fileIdMap, totalForProgress, progressOffset, shouldCancel, guidedReview);
|
|
2790
|
-
}
|
|
2791
|
-
if (cancelledAnalyses.has(analysis.id)) {
|
|
2792
|
-
cancelledAnalyses.delete(analysis.id);
|
|
2793
|
-
debugLog(`Analysis ${analysis.id} was cancelled (user switched modes)`);
|
|
2794
|
-
await updateAnalysisStatus(analysis.id, "failed", "Cancelled");
|
|
2795
|
-
return;
|
|
2796
|
-
}
|
|
2797
|
-
cancelledAnalyses.delete(analysis.id);
|
|
2798
|
-
debugLog(`Analysis ${analysis.id} completed successfully`);
|
|
2799
|
-
await updateAnalysisStatus(analysis.id, "completed");
|
|
2800
|
-
} catch (err) {
|
|
2801
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2802
|
-
console.error(`Analysis failed: ${message}`);
|
|
2803
|
-
debugLog(`Analysis ${analysis.id} failed: ${message}`);
|
|
2804
|
-
await updateAnalysisStatus(analysis.id, "failed", message);
|
|
2805
|
-
}
|
|
2806
|
-
})();
|
|
2710
|
+
void executeAnalysis({
|
|
2711
|
+
analysisId: analysis.id,
|
|
2712
|
+
analysisType: typeCheck.ok,
|
|
2713
|
+
reviewId,
|
|
2714
|
+
files,
|
|
2715
|
+
config,
|
|
2716
|
+
repoRoot,
|
|
2717
|
+
guidedReview,
|
|
2718
|
+
invalidateCache
|
|
2719
|
+
});
|
|
2807
2720
|
return c.json({ analysisId: analysis.id, status: "running" });
|
|
2808
2721
|
});
|
|
2809
|
-
async function
|
|
2722
|
+
async function executeAnalysis(input) {
|
|
2723
|
+
const { analysisId, analysisType, reviewId, files, config, repoRoot, guidedReview, invalidateCache } = input;
|
|
2724
|
+
try {
|
|
2725
|
+
debugLog("Background analysis starting...");
|
|
2726
|
+
const contextWindow = getModelContextWindow(config.platform, config.model);
|
|
2727
|
+
debugLog(`Context window: ${String(contextWindow)} tokens`);
|
|
2728
|
+
const { batches, binaryFiles } = planBatches(files, contextWindow);
|
|
2729
|
+
const fileIdMap = new Map(files.map((f) => [f.file_path, f.id]));
|
|
2730
|
+
const totalAnalyzable = batches.reduce((sum, b) => sum + b.files.length, 0);
|
|
2731
|
+
debugLog(`Analysis plan: ${String(totalAnalyzable)} analyzable + ${String(binaryFiles.length)} binary = ${String(totalAnalyzable + binaryFiles.length)} total files in ${String(batches.length)} batch(es)`);
|
|
2732
|
+
const prevScores = invalidateCache ? [] : await getPreviousScores(reviewId, analysisType, analysisId);
|
|
2733
|
+
const binaryPathSet = new Set(binaryFiles.map((f) => f.file_path));
|
|
2734
|
+
const unchangedPaths = /* @__PURE__ */ new Set();
|
|
2735
|
+
const cachedScores = prevScores.filter((s) => {
|
|
2736
|
+
if (fileIdMap.has(s.file_path) && !binaryPathSet.has(s.file_path)) {
|
|
2737
|
+
unchangedPaths.add(s.file_path);
|
|
2738
|
+
return true;
|
|
2739
|
+
}
|
|
2740
|
+
return false;
|
|
2741
|
+
});
|
|
2742
|
+
debugLog(`Cache: ${String(cachedScores.length)} scores from previous analysis, ${String(totalAnalyzable - cachedScores.length)} files need processing`);
|
|
2743
|
+
if (cachedScores.length > 0) {
|
|
2744
|
+
const cachedForInsert = cachedScores.map((s) => ({
|
|
2745
|
+
reviewFileId: fileIdMap.get(s.file_path) ?? s.review_file_id,
|
|
2746
|
+
filePath: s.file_path,
|
|
2747
|
+
sortOrder: s.sort_order,
|
|
2748
|
+
aggregateScore: s.aggregate_score,
|
|
2749
|
+
rationale: s.rationale,
|
|
2750
|
+
dimensionScores: s.dimension_scores !== null ? JSON.parse(s.dimension_scores) : null,
|
|
2751
|
+
notes: s.notes !== null ? JSON.parse(s.notes) : null
|
|
2752
|
+
}));
|
|
2753
|
+
await appendFileScores(analysisId, cachedForInsert);
|
|
2754
|
+
}
|
|
2755
|
+
const filteredBatches = batches.map((batch) => {
|
|
2756
|
+
const remaining = batch.files.filter((f) => !unchangedPaths.has(f.file_path));
|
|
2757
|
+
return { files: remaining, estimatedTokens: batch.estimatedTokens };
|
|
2758
|
+
}).filter((batch) => batch.files.length > 0);
|
|
2759
|
+
const filteredAnalyzable = filteredBatches.reduce((sum, b) => sum + b.files.length, 0);
|
|
2760
|
+
const totalForProgress = filteredAnalyzable + binaryFiles.length + cachedScores.length;
|
|
2761
|
+
debugLog(`After cache: ${String(filteredAnalyzable)} files to analyze in ${String(filteredBatches.length)} batch(es)`);
|
|
2762
|
+
await updateAnalysisProgress(analysisId, cachedScores.length, totalForProgress);
|
|
2763
|
+
if (binaryFiles.length > 0) {
|
|
2764
|
+
debugLog(`Saving ${String(binaryFiles.length)} binary files with score 0`);
|
|
2765
|
+
const binaryScoreEntries = binaryFiles.map((f, idx) => ({
|
|
2766
|
+
reviewFileId: fileIdMap.get(f.file_path) ?? "",
|
|
2767
|
+
filePath: f.file_path,
|
|
2768
|
+
sortOrder: 99999 + idx,
|
|
2769
|
+
// Will be re-sorted later
|
|
2770
|
+
aggregateScore: analysisType === "risk" ? 0 : null,
|
|
2771
|
+
rationale: "Binary file \u2014 not analyzed",
|
|
2772
|
+
dimensionScores: analysisType === "risk" ? { security: 0, correctness: 0, "error-handling": 0, maintainability: 0, architecture: 0, performance: 0 } : null,
|
|
2773
|
+
notes: null
|
|
2774
|
+
}));
|
|
2775
|
+
await appendFileScores(analysisId, binaryScoreEntries);
|
|
2776
|
+
await updateAnalysisProgress(analysisId, cachedScores.length + binaryFiles.length, totalForProgress);
|
|
2777
|
+
}
|
|
2778
|
+
if (filteredBatches.length === 0) {
|
|
2779
|
+
debugLog("No batches to process (all files cached or binary), marking completed");
|
|
2780
|
+
await updateAnalysisStatus(analysisId, "completed");
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
const shouldCancel = () => cancelledAnalyses.has(analysisId);
|
|
2784
|
+
const progressOffset = cachedScores.length + binaryFiles.length;
|
|
2785
|
+
const runArgs = [analysisId, filteredBatches, files.length, totalForProgress, progressOffset];
|
|
2786
|
+
if (analysisType === "risk") {
|
|
2787
|
+
await runBatchedAnalysis(...runArgs, riskAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId), shouldCancel);
|
|
2788
|
+
} else if (analysisType === "narrative") {
|
|
2789
|
+
await runBatchedAnalysis(...runArgs, narrativeAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId), shouldCancel);
|
|
2790
|
+
} else {
|
|
2791
|
+
await runBatchedAnalysis(...runArgs, guidedAnalysisConfig(config, repoRoot, fileIdMap, guidedReview), shouldCancel);
|
|
2792
|
+
}
|
|
2793
|
+
if (cancelledAnalyses.has(analysisId)) {
|
|
2794
|
+
cancelledAnalyses.delete(analysisId);
|
|
2795
|
+
debugLog(`Analysis ${analysisId} was cancelled (user switched modes)`);
|
|
2796
|
+
await updateAnalysisStatus(analysisId, "failed", "Cancelled");
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
cancelledAnalyses.delete(analysisId);
|
|
2800
|
+
debugLog(`Analysis ${analysisId} completed successfully`);
|
|
2801
|
+
await updateAnalysisStatus(analysisId, "completed");
|
|
2802
|
+
} catch (err) {
|
|
2803
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2804
|
+
console.error(`Analysis failed: ${message}`);
|
|
2805
|
+
debugLog(`Analysis ${analysisId} failed: ${message}`);
|
|
2806
|
+
await updateAnalysisStatus(analysisId, "failed", message);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
async function runBatchedAnalysis(analysisId, batches, totalFiles, progressTotal, progressOffset, cfg, shouldCancel) {
|
|
2810
2810
|
const allResults = await runBatches(
|
|
2811
2811
|
batches,
|
|
2812
|
-
|
|
2813
|
-
async (batch) =>
|
|
2812
|
+
totalFiles,
|
|
2813
|
+
async (batch) => cfg.runBatch(batch.files),
|
|
2814
2814
|
async (_batchIndex, results) => {
|
|
2815
|
-
|
|
2816
|
-
const
|
|
2817
|
-
r.aggregate = Math.max(r.aggregate, maxDimension);
|
|
2815
|
+
if (cfg.postProcessResult) {
|
|
2816
|
+
for (const r of results) cfg.postProcessResult(r);
|
|
2818
2817
|
}
|
|
2819
|
-
const scores = results.map((r) => (
|
|
2820
|
-
reviewFileId: fileIdMap.get(r.filePath) ?? "",
|
|
2821
|
-
filePath: r.filePath,
|
|
2822
|
-
sortOrder: 0,
|
|
2823
|
-
// Placeholder — final sort happens after all batches
|
|
2824
|
-
aggregateScore: r.aggregate,
|
|
2825
|
-
rationale: r.rationale,
|
|
2826
|
-
dimensionScores: r.scores,
|
|
2827
|
-
notes: r.notes ?? null
|
|
2828
|
-
}));
|
|
2818
|
+
const scores = results.map((r, idx) => cfg.mapResult(r, idx));
|
|
2829
2819
|
await appendFileScores(analysisId, scores);
|
|
2830
2820
|
},
|
|
2831
2821
|
async (progress) => {
|
|
@@ -2833,91 +2823,98 @@ async function runBatchedRiskAnalysis(analysisId, batches, allFiles, config, rep
|
|
|
2833
2823
|
},
|
|
2834
2824
|
1,
|
|
2835
2825
|
shouldCancel,
|
|
2836
|
-
|
|
2826
|
+
cfg.analysisType
|
|
2837
2827
|
);
|
|
2838
|
-
|
|
2839
|
-
|
|
2828
|
+
if (cfg.finalize) await cfg.finalize(allResults, batches.length);
|
|
2829
|
+
}
|
|
2830
|
+
async function updateSortOrders(analysisId, entries) {
|
|
2840
2831
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
|
|
2841
2832
|
const db2 = await getDb2();
|
|
2842
|
-
for (const [filePath, sortOrder] of
|
|
2833
|
+
for (const [filePath, sortOrder] of entries) {
|
|
2843
2834
|
await db2.query(
|
|
2844
2835
|
"UPDATE ai_file_scores SET sort_order = $1 WHERE analysis_id = $2 AND file_path = $3",
|
|
2845
2836
|
[sortOrder, analysisId, filePath]
|
|
2846
2837
|
);
|
|
2847
2838
|
}
|
|
2848
2839
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
dimensionScores: null,
|
|
2863
|
-
notes: r.notes ?? null
|
|
2864
|
-
}));
|
|
2865
|
-
await appendFileScores(analysisId, scores);
|
|
2866
|
-
},
|
|
2867
|
-
async (progress) => {
|
|
2868
|
-
await updateAnalysisProgress(analysisId, progressOffset + progress.completedFiles, progressTotal);
|
|
2840
|
+
function pickRunner(real, mock) {
|
|
2841
|
+
return isAIServiceTest() ? mock() : real();
|
|
2842
|
+
}
|
|
2843
|
+
function riskAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId) {
|
|
2844
|
+
return {
|
|
2845
|
+
analysisType: "risk",
|
|
2846
|
+
runBatch: (files) => pickRunner(
|
|
2847
|
+
() => runRiskAnalysisBatch(files, config, repoRoot, guidedReview),
|
|
2848
|
+
() => mockRiskAnalysisBatch(files)
|
|
2849
|
+
),
|
|
2850
|
+
postProcessResult: (r) => {
|
|
2851
|
+
const maxDimension = Math.max(...Object.values(r.scores));
|
|
2852
|
+
r.aggregate = Math.max(r.aggregate, maxDimension);
|
|
2869
2853
|
},
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
);
|
|
2854
|
+
mapResult: (r) => ({
|
|
2855
|
+
reviewFileId: fileIdMap.get(r.filePath) ?? "",
|
|
2856
|
+
filePath: r.filePath,
|
|
2857
|
+
sortOrder: 0,
|
|
2858
|
+
// Placeholder — final sort happens after all batches
|
|
2859
|
+
aggregateScore: r.aggregate,
|
|
2860
|
+
rationale: r.rationale,
|
|
2861
|
+
dimensionScores: r.scores,
|
|
2862
|
+
notes: r.notes ?? null
|
|
2863
|
+
}),
|
|
2864
|
+
finalize: async (allResults) => {
|
|
2865
|
+
const sorted = allResults.slice().sort((a, b) => b.aggregate - a.aggregate);
|
|
2866
|
+
await updateSortOrders(analysisId, sorted.map((r, idx) => [r.filePath, idx]));
|
|
2883
2867
|
}
|
|
2884
|
-
}
|
|
2868
|
+
};
|
|
2885
2869
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2870
|
+
function narrativeAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId) {
|
|
2871
|
+
return {
|
|
2872
|
+
analysisType: "narrative",
|
|
2873
|
+
runBatch: (files) => pickRunner(
|
|
2874
|
+
() => runNarrativeAnalysisBatch(files, config, repoRoot, guidedReview),
|
|
2875
|
+
() => mockNarrativeAnalysisBatch(files)
|
|
2876
|
+
),
|
|
2877
|
+
mapResult: (r) => ({
|
|
2878
|
+
reviewFileId: fileIdMap.get(r.filePath) ?? "",
|
|
2879
|
+
filePath: r.filePath,
|
|
2880
|
+
sortOrder: r.position,
|
|
2881
|
+
// Batch-local position — will be re-sorted after merge
|
|
2882
|
+
aggregateScore: null,
|
|
2883
|
+
rationale: r.rationale,
|
|
2884
|
+
dimensionScores: null,
|
|
2885
|
+
notes: r.notes ?? null
|
|
2886
|
+
}),
|
|
2887
|
+
finalize: async (allResults, batchCount) => {
|
|
2888
|
+
if (allResults.length === 0) return;
|
|
2889
|
+
const merged = mergeNarrativeOrders(allResults, batchCount);
|
|
2890
|
+
await updateSortOrders(analysisId, merged);
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
function guidedAnalysisConfig(config, repoRoot, fileIdMap, guidedReview) {
|
|
2895
|
+
return {
|
|
2896
|
+
analysisType: "guided",
|
|
2897
|
+
runBatch: (files) => {
|
|
2898
|
+
if (isAIServiceTest()) return mockGuidedAnalysisBatch(files);
|
|
2892
2899
|
if (guidedReview === void 0) throw new Error("Guided review config required");
|
|
2893
|
-
return runGuidedAnalysisBatch(
|
|
2894
|
-
},
|
|
2895
|
-
async (_batchIndex, results) => {
|
|
2896
|
-
const scores = results.map((r, idx) => ({
|
|
2897
|
-
reviewFileId: fileIdMap.get(r.filePath) ?? "",
|
|
2898
|
-
filePath: r.filePath,
|
|
2899
|
-
sortOrder: idx,
|
|
2900
|
-
aggregateScore: null,
|
|
2901
|
-
rationale: null,
|
|
2902
|
-
dimensionScores: null,
|
|
2903
|
-
notes: r.notes
|
|
2904
|
-
}));
|
|
2905
|
-
await appendFileScores(analysisId, scores);
|
|
2906
|
-
},
|
|
2907
|
-
async (progress) => {
|
|
2908
|
-
await updateAnalysisProgress(analysisId, progressOffset + progress.completedFiles, progressTotal);
|
|
2900
|
+
return runGuidedAnalysisBatch(files, config, repoRoot, guidedReview);
|
|
2909
2901
|
},
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2902
|
+
mapResult: (r, idx) => ({
|
|
2903
|
+
reviewFileId: fileIdMap.get(r.filePath) ?? "",
|
|
2904
|
+
filePath: r.filePath,
|
|
2905
|
+
sortOrder: idx,
|
|
2906
|
+
aggregateScore: null,
|
|
2907
|
+
rationale: null,
|
|
2908
|
+
dimensionScores: null,
|
|
2909
|
+
notes: r.notes
|
|
2910
|
+
})
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2915
2913
|
aiAnalysisRoutes.get("/analysis/:type", async (c) => {
|
|
2916
|
-
const reviewId = c
|
|
2914
|
+
const reviewId = resolveReviewId(c);
|
|
2917
2915
|
const analysisType = c.req.param("type");
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
}
|
|
2916
|
+
const typeCheck = checkEnum(analysisType, "type", VALID_ANALYSIS_TYPES);
|
|
2917
|
+
if ("error" in typeCheck) return c.json({ error: typeCheck.error }, 400);
|
|
2921
2918
|
const analysis = await getLatestAnalysis(reviewId, analysisType);
|
|
2922
2919
|
if (analysis === void 0) {
|
|
2923
2920
|
debugLog(`GET /analysis/${analysisType}: no analysis found`);
|
|
@@ -2948,11 +2945,10 @@ aiAnalysisRoutes.get("/analysis/:type", async (c) => {
|
|
|
2948
2945
|
});
|
|
2949
2946
|
});
|
|
2950
2947
|
aiAnalysisRoutes.get("/analysis/:type/status", async (c) => {
|
|
2951
|
-
const reviewId = c
|
|
2948
|
+
const reviewId = resolveReviewId(c);
|
|
2952
2949
|
const analysisType = c.req.param("type");
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
}
|
|
2950
|
+
const typeCheck = checkEnum(analysisType, "type", VALID_ANALYSIS_TYPES);
|
|
2951
|
+
if ("error" in typeCheck) return c.json({ error: typeCheck.error }, 400);
|
|
2956
2952
|
const analysis = await getLatestAnalysis(reviewId, analysisType);
|
|
2957
2953
|
if (analysis === void 0) {
|
|
2958
2954
|
debugLog(`GET /analysis/${analysisType}/status: no analysis found`);
|
|
@@ -2992,11 +2988,13 @@ aiAnalysisRoutes.get("/preferences", async (c) => {
|
|
|
2992
2988
|
});
|
|
2993
2989
|
aiAnalysisRoutes.post("/preferences", async (c) => {
|
|
2994
2990
|
const body = await c.req.json();
|
|
2995
|
-
if (body.sort_mode !== void 0
|
|
2996
|
-
|
|
2991
|
+
if (body.sort_mode !== void 0) {
|
|
2992
|
+
const v = checkEnum(body.sort_mode, "sort_mode", VALID_SORT_MODES);
|
|
2993
|
+
if ("error" in v) return c.json({ error: v.error }, 400);
|
|
2997
2994
|
}
|
|
2998
|
-
if (body.risk_sort_dimension !== void 0
|
|
2999
|
-
|
|
2995
|
+
if (body.risk_sort_dimension !== void 0) {
|
|
2996
|
+
const v = checkEnum(body.risk_sort_dimension, "risk_sort_dimension", VALID_RISK_DIMENSIONS);
|
|
2997
|
+
if ("error" in v) return c.json({ error: v.error }, 400);
|
|
3000
2998
|
}
|
|
3001
2999
|
if (body.show_risk_scores !== void 0 && typeof body.show_risk_scores !== "boolean") {
|
|
3002
3000
|
return c.json({ error: "show_risk_scores must be a boolean" }, 400);
|
|
@@ -3004,11 +3002,13 @@ aiAnalysisRoutes.post("/preferences", async (c) => {
|
|
|
3004
3002
|
if (body.ignore_whitespace !== void 0 && typeof body.ignore_whitespace !== "boolean") {
|
|
3005
3003
|
return c.json({ error: "ignore_whitespace must be a boolean" }, 400);
|
|
3006
3004
|
}
|
|
3007
|
-
if (body.svg_view_mode !== void 0
|
|
3008
|
-
|
|
3005
|
+
if (body.svg_view_mode !== void 0) {
|
|
3006
|
+
const v = checkEnum(body.svg_view_mode, "svg_view_mode", VALID_SVG_VIEW_MODES);
|
|
3007
|
+
if ("error" in v) return c.json({ error: v.error }, 400);
|
|
3009
3008
|
}
|
|
3010
|
-
if (body.last_image_mode !== void 0
|
|
3011
|
-
|
|
3009
|
+
if (body.last_image_mode !== void 0) {
|
|
3010
|
+
const v = checkEnum(body.last_image_mode, "last_image_mode", VALID_IMAGE_MODES);
|
|
3011
|
+
if ("error" in v) return c.json({ error: v.error }, 400);
|
|
3012
3012
|
}
|
|
3013
3013
|
await saveUserPreferences(body);
|
|
3014
3014
|
return c.json({ ok: true });
|
|
@@ -3031,10 +3031,9 @@ aiConfigRoutes.get("/config", (c) => {
|
|
|
3031
3031
|
});
|
|
3032
3032
|
aiConfigRoutes.post("/config", async (c) => {
|
|
3033
3033
|
const body = await c.req.json();
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
if (typeof body.model !== "string" || body.model.trim() === "") {
|
|
3034
|
+
const platformCheck = checkEnum(body.platform, "platform", VALID_PLATFORMS);
|
|
3035
|
+
if ("error" in platformCheck) return c.json({ error: platformCheck.error }, 400);
|
|
3036
|
+
if (!isNonEmptyString(body.model)) {
|
|
3038
3037
|
return c.json({ error: "model must be a non-empty string" }, 400);
|
|
3039
3038
|
}
|
|
3040
3039
|
if (body.guidedReview !== void 0) {
|
|
@@ -3050,7 +3049,7 @@ aiConfigRoutes.post("/config", async (c) => {
|
|
|
3050
3049
|
return c.json({ error: "guidedReview.topics must be an array of strings" }, 400);
|
|
3051
3050
|
}
|
|
3052
3051
|
}
|
|
3053
|
-
saveAIConfigPreferences(
|
|
3052
|
+
saveAIConfigPreferences(platformCheck.ok, body.model);
|
|
3054
3053
|
if (body.guidedReview !== void 0) {
|
|
3055
3054
|
saveGuidedReviewConfig(body.guidedReview);
|
|
3056
3055
|
}
|
|
@@ -3078,28 +3077,25 @@ aiConfigRoutes.get("/key-status", (c) => {
|
|
|
3078
3077
|
});
|
|
3079
3078
|
aiConfigRoutes.post("/key", async (c) => {
|
|
3080
3079
|
const body = await c.req.json();
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
if (typeof body.key !== "string" || body.key.trim() === "") {
|
|
3080
|
+
const platformCheck = checkEnum(body.platform, "platform", VALID_PLATFORMS);
|
|
3081
|
+
if ("error" in platformCheck) return c.json({ error: platformCheck.error }, 400);
|
|
3082
|
+
if (!isNonEmptyString(body.key)) {
|
|
3085
3083
|
return c.json({ error: "key must be a non-empty string" }, 400);
|
|
3086
3084
|
}
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
}
|
|
3085
|
+
const storageCheck = checkEnum(body.storage, "storage", VALID_KEY_STORAGES);
|
|
3086
|
+
if ("error" in storageCheck) return c.json({ error: storageCheck.error }, 400);
|
|
3090
3087
|
saveAPIKey(
|
|
3091
|
-
|
|
3088
|
+
platformCheck.ok,
|
|
3092
3089
|
body.key,
|
|
3093
|
-
|
|
3090
|
+
storageCheck.ok
|
|
3094
3091
|
);
|
|
3095
3092
|
return c.json({ ok: true });
|
|
3096
3093
|
});
|
|
3097
3094
|
aiConfigRoutes.delete("/key", (c) => {
|
|
3098
3095
|
const platform = c.req.query("platform") ?? "anthropic";
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
deleteAPIKey(platform);
|
|
3096
|
+
const platformCheck = checkEnum(platform, "platform", VALID_PLATFORMS);
|
|
3097
|
+
if ("error" in platformCheck) return c.json({ error: platformCheck.error }, 400);
|
|
3098
|
+
deleteAPIKey(platformCheck.ok);
|
|
3103
3099
|
return c.json({ ok: true });
|
|
3104
3100
|
});
|
|
3105
3101
|
|
|
@@ -3109,12 +3105,11 @@ aiApiRoutes.route("/", aiConfigRoutes);
|
|
|
3109
3105
|
aiApiRoutes.route("/", aiAnalysisRoutes);
|
|
3110
3106
|
|
|
3111
3107
|
// src/routes/api.ts
|
|
3108
|
+
import { Hono as Hono12 } from "hono";
|
|
3109
|
+
|
|
3110
|
+
// src/routes/api/annotations.ts
|
|
3112
3111
|
init_queries();
|
|
3113
|
-
import { execFileSync, spawnSync as spawnSync7 } from "child_process";
|
|
3114
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
3115
3112
|
import { Hono as Hono4 } from "hono";
|
|
3116
|
-
import { homedir as homedir3 } from "os";
|
|
3117
|
-
import { join as join7, resolve as resolve4 } from "path";
|
|
3118
3113
|
|
|
3119
3114
|
// src/export/generate.ts
|
|
3120
3115
|
init_queries();
|
|
@@ -3265,10 +3260,166 @@ function scheduleAutoExport(reviewId, repoRoot) {
|
|
|
3265
3260
|
}, DEBOUNCE_MS);
|
|
3266
3261
|
}
|
|
3267
3262
|
|
|
3263
|
+
// src/routes/api/annotations.ts
|
|
3264
|
+
var annotationsRoutes = new Hono4();
|
|
3265
|
+
var VALID_CATEGORIES = ["bug", "fix", "style", "pattern-follow", "pattern-avoid", "note", "remember"];
|
|
3266
|
+
var VALID_SIDES = ["old", "new"];
|
|
3267
|
+
function autoExport(c) {
|
|
3268
|
+
scheduleAutoExport(c.get("reviewId"), c.get("repoRoot"));
|
|
3269
|
+
}
|
|
3270
|
+
annotationsRoutes.post("/annotations", async (c) => {
|
|
3271
|
+
const body = await c.req.json();
|
|
3272
|
+
if (!isNonEmptyString(body.reviewFileId)) {
|
|
3273
|
+
return c.json({ error: "reviewFileId must be a non-empty string" }, 400);
|
|
3274
|
+
}
|
|
3275
|
+
if (typeof body.lineNumber !== "number" || !Number.isInteger(body.lineNumber) || body.lineNumber < 1) {
|
|
3276
|
+
return c.json({ error: "lineNumber must be a positive integer" }, 400);
|
|
3277
|
+
}
|
|
3278
|
+
const sideCheck = checkEnum(body.side, "side", VALID_SIDES);
|
|
3279
|
+
if ("error" in sideCheck) return c.json({ error: sideCheck.error }, 400);
|
|
3280
|
+
const categoryCheck = checkEnum(body.category, "category", VALID_CATEGORIES);
|
|
3281
|
+
if ("error" in categoryCheck) return c.json({ error: categoryCheck.error }, 400);
|
|
3282
|
+
if (!isNonEmptyString(body.content)) {
|
|
3283
|
+
return c.json({ error: "content must be a non-empty string" }, 400);
|
|
3284
|
+
}
|
|
3285
|
+
const annotation = await addAnnotation(
|
|
3286
|
+
body.reviewFileId,
|
|
3287
|
+
body.lineNumber,
|
|
3288
|
+
sideCheck.ok,
|
|
3289
|
+
categoryCheck.ok,
|
|
3290
|
+
body.content
|
|
3291
|
+
);
|
|
3292
|
+
autoExport(c);
|
|
3293
|
+
return c.json(annotation, 201);
|
|
3294
|
+
});
|
|
3295
|
+
annotationsRoutes.patch("/annotations/:id", async (c) => {
|
|
3296
|
+
const { content, category } = await c.req.json();
|
|
3297
|
+
if (!isNonEmptyString(content)) {
|
|
3298
|
+
return c.json({ error: "content must be a non-empty string" }, 400);
|
|
3299
|
+
}
|
|
3300
|
+
const categoryCheck = checkEnum(category, "category", VALID_CATEGORIES);
|
|
3301
|
+
if ("error" in categoryCheck) return c.json({ error: categoryCheck.error }, 400);
|
|
3302
|
+
await updateAnnotation(c.req.param("id"), content, categoryCheck.ok);
|
|
3303
|
+
autoExport(c);
|
|
3304
|
+
return c.json({ ok: true });
|
|
3305
|
+
});
|
|
3306
|
+
annotationsRoutes.delete("/annotations/:id", async (c) => {
|
|
3307
|
+
await deleteAnnotation(c.req.param("id"));
|
|
3308
|
+
autoExport(c);
|
|
3309
|
+
return c.json({ ok: true });
|
|
3310
|
+
});
|
|
3311
|
+
annotationsRoutes.patch("/annotations/:id/move", async (c) => {
|
|
3312
|
+
const { lineNumber, side } = await c.req.json();
|
|
3313
|
+
if (typeof lineNumber !== "number" || !Number.isInteger(lineNumber) || lineNumber < 1) {
|
|
3314
|
+
return c.json({ error: "lineNumber must be a positive integer" }, 400);
|
|
3315
|
+
}
|
|
3316
|
+
const sideCheck = checkEnum(side, "side", VALID_SIDES);
|
|
3317
|
+
if ("error" in sideCheck) return c.json({ error: sideCheck.error }, 400);
|
|
3318
|
+
await moveAnnotation(c.req.param("id"), lineNumber, sideCheck.ok);
|
|
3319
|
+
autoExport(c);
|
|
3320
|
+
return c.json({ ok: true });
|
|
3321
|
+
});
|
|
3322
|
+
annotationsRoutes.post("/annotations/:id/keep", async (c) => {
|
|
3323
|
+
await markAnnotationCurrent(c.req.param("id"));
|
|
3324
|
+
autoExport(c);
|
|
3325
|
+
return c.json({ ok: true });
|
|
3326
|
+
});
|
|
3327
|
+
annotationsRoutes.post("/annotations/stale/delete-all", async (c) => {
|
|
3328
|
+
const reviewId = resolveReviewId(c);
|
|
3329
|
+
await deleteStaleAnnotations(reviewId);
|
|
3330
|
+
autoExport(c);
|
|
3331
|
+
return c.json({ ok: true });
|
|
3332
|
+
});
|
|
3333
|
+
annotationsRoutes.post("/annotations/stale/keep-all", async (c) => {
|
|
3334
|
+
const reviewId = resolveReviewId(c);
|
|
3335
|
+
await keepAllStaleAnnotations(reviewId);
|
|
3336
|
+
autoExport(c);
|
|
3337
|
+
return c.json({ ok: true });
|
|
3338
|
+
});
|
|
3339
|
+
annotationsRoutes.get("/annotations/all", async (c) => {
|
|
3340
|
+
const reviewId = resolveReviewId(c);
|
|
3341
|
+
const annotations = await getAnnotationsForReview(reviewId);
|
|
3342
|
+
return c.json(annotations);
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
// src/routes/api/context.ts
|
|
3346
|
+
init_queries();
|
|
3347
|
+
import { Hono as Hono5 } from "hono";
|
|
3348
|
+
var contextRoutes = new Hono5();
|
|
3349
|
+
contextRoutes.get("/context/:fileId", async (c) => {
|
|
3350
|
+
const repoRoot = c.get("repoRoot");
|
|
3351
|
+
const file = await getReviewFile(c.req.param("fileId"));
|
|
3352
|
+
if (!file) return c.json({ error: "Not found" }, 404);
|
|
3353
|
+
const startLine = parseInt(c.req.query("start") ?? "1", 10);
|
|
3354
|
+
const endLine = parseInt(c.req.query("end") ?? "20", 10);
|
|
3355
|
+
const content = getFileContent(file.file_path, "working", repoRoot);
|
|
3356
|
+
const allLines = content.split("\n");
|
|
3357
|
+
const clampedStart = Math.max(1, startLine);
|
|
3358
|
+
const clampedEnd = Math.min(allLines.length, endLine);
|
|
3359
|
+
const lines = [];
|
|
3360
|
+
for (let i = clampedStart; i <= clampedEnd; i++) {
|
|
3361
|
+
lines.push({ num: i, content: allLines[i - 1] || "" });
|
|
3362
|
+
}
|
|
3363
|
+
return c.json({ lines });
|
|
3364
|
+
});
|
|
3365
|
+
|
|
3366
|
+
// src/routes/api/files.ts
|
|
3367
|
+
init_queries();
|
|
3368
|
+
import { execFileSync } from "child_process";
|
|
3369
|
+
import { Hono as Hono6 } from "hono";
|
|
3370
|
+
import { resolve as resolve3 } from "path";
|
|
3371
|
+
var filesRoutes = new Hono6();
|
|
3372
|
+
var VALID_FILE_STATUSES = ["pending", "reviewed"];
|
|
3373
|
+
filesRoutes.get("/files", async (c) => {
|
|
3374
|
+
const reviewId = resolveReviewId(c);
|
|
3375
|
+
const files = await getReviewFiles(reviewId);
|
|
3376
|
+
const annotationCounts = {};
|
|
3377
|
+
for (const file of files) {
|
|
3378
|
+
const annotations = await getAnnotationsForFile(file.id);
|
|
3379
|
+
annotationCounts[file.id] = annotations.length;
|
|
3380
|
+
}
|
|
3381
|
+
const staleCounts = await getStaleCountsForReview(reviewId);
|
|
3382
|
+
return c.json({ files, annotationCounts, staleCounts });
|
|
3383
|
+
});
|
|
3384
|
+
filesRoutes.get("/files/:fileId", async (c) => {
|
|
3385
|
+
const file = await getReviewFile(c.req.param("fileId"));
|
|
3386
|
+
if (!file) return c.json({ error: "Not found" }, 404);
|
|
3387
|
+
const annotations = await getAnnotationsForFile(file.id);
|
|
3388
|
+
return c.json({ file, annotations });
|
|
3389
|
+
});
|
|
3390
|
+
filesRoutes.patch("/files/:fileId/status", async (c) => {
|
|
3391
|
+
const { status } = await c.req.json();
|
|
3392
|
+
const v = checkEnum(status, "status", VALID_FILE_STATUSES);
|
|
3393
|
+
if ("error" in v) return c.json({ error: v.error }, 400);
|
|
3394
|
+
await updateFileStatus(c.req.param("fileId"), v.ok);
|
|
3395
|
+
return c.json({ ok: true });
|
|
3396
|
+
});
|
|
3397
|
+
filesRoutes.post("/files/:fileId/reveal", async (c) => {
|
|
3398
|
+
const file = await getReviewFile(c.req.param("fileId"));
|
|
3399
|
+
if (!file) return c.json({ error: "Not found" }, 404);
|
|
3400
|
+
const repoRoot = c.get("repoRoot");
|
|
3401
|
+
const fullPath = resolve3(repoRoot, file.file_path);
|
|
3402
|
+
try {
|
|
3403
|
+
if (process.platform === "darwin") {
|
|
3404
|
+
execFileSync("open", ["-R", fullPath]);
|
|
3405
|
+
} else if (process.platform === "win32") {
|
|
3406
|
+
execFileSync("explorer", ["/select," + fullPath]);
|
|
3407
|
+
} else {
|
|
3408
|
+
execFileSync("xdg-open", [resolve3(fullPath, "..")]);
|
|
3409
|
+
}
|
|
3410
|
+
} catch {
|
|
3411
|
+
}
|
|
3412
|
+
return c.json({ ok: true });
|
|
3413
|
+
});
|
|
3414
|
+
|
|
3415
|
+
// src/routes/api/image.ts
|
|
3416
|
+
init_queries();
|
|
3417
|
+
import { Hono as Hono7 } from "hono";
|
|
3418
|
+
|
|
3268
3419
|
// src/git/image.ts
|
|
3269
3420
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
3270
3421
|
import { readFileSync as readFileSync6 } from "fs";
|
|
3271
|
-
import { resolve as
|
|
3422
|
+
import { resolve as resolve4 } from "path";
|
|
3272
3423
|
|
|
3273
3424
|
// src/git/image-metadata.ts
|
|
3274
3425
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
|
|
@@ -3538,7 +3689,7 @@ function gitShowFile(ref, filePath, repoRoot) {
|
|
|
3538
3689
|
}
|
|
3539
3690
|
function readWorkingFile(filePath, repoRoot) {
|
|
3540
3691
|
try {
|
|
3541
|
-
return readFileSync6(
|
|
3692
|
+
return readFileSync6(resolve4(repoRoot, filePath));
|
|
3542
3693
|
} catch {
|
|
3543
3694
|
return null;
|
|
3544
3695
|
}
|
|
@@ -3705,6 +3856,65 @@ async function rasterizeSvg(svgData) {
|
|
|
3705
3856
|
return png;
|
|
3706
3857
|
}
|
|
3707
3858
|
|
|
3859
|
+
// src/routes/api/image.ts
|
|
3860
|
+
var imageRoutes = new Hono7();
|
|
3861
|
+
imageRoutes.get("/image/:fileId/metadata", async (c) => {
|
|
3862
|
+
const fileId = c.req.param("fileId");
|
|
3863
|
+
const file = await getReviewFile(fileId);
|
|
3864
|
+
if (!file) return c.json({ error: "Not found" }, 404);
|
|
3865
|
+
const repoRoot = c.get("repoRoot");
|
|
3866
|
+
const review = await getReview(file.review_id);
|
|
3867
|
+
if (!review) return c.json({ error: "Review not found" }, 404);
|
|
3868
|
+
const mode = parseModeString(review.mode);
|
|
3869
|
+
const diff = parseDiffData(file.diff_data);
|
|
3870
|
+
const oldPath = diff?.oldPath ?? null;
|
|
3871
|
+
const status = diff?.status ?? "modified";
|
|
3872
|
+
const oldImage = status !== "added" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : null;
|
|
3873
|
+
const newImage = status !== "deleted" ? getNewImage(mode, file.file_path, repoRoot) : null;
|
|
3874
|
+
const oldMeta = oldImage !== null ? extractMetadata(oldImage.data, oldPath ?? file.file_path) : null;
|
|
3875
|
+
const newMeta = newImage !== null ? extractMetadata(newImage.data, file.file_path) : null;
|
|
3876
|
+
return c.json({
|
|
3877
|
+
old: oldMeta ? formatMetadataLines(oldMeta) : null,
|
|
3878
|
+
new: newMeta ? formatMetadataLines(newMeta) : null
|
|
3879
|
+
});
|
|
3880
|
+
});
|
|
3881
|
+
imageRoutes.get("/image/:fileId/:side", async (c) => {
|
|
3882
|
+
const fileId = c.req.param("fileId");
|
|
3883
|
+
const side = c.req.param("side");
|
|
3884
|
+
if (side !== "old" && side !== "new") return c.text("Invalid side", 400);
|
|
3885
|
+
const file = await getReviewFile(fileId);
|
|
3886
|
+
if (!file) return c.text("Not found", 404);
|
|
3887
|
+
const repoRoot = c.get("repoRoot");
|
|
3888
|
+
const review = await getReview(file.review_id);
|
|
3889
|
+
if (!review) return c.text("Review not found", 404);
|
|
3890
|
+
const mode = parseModeString(review.mode);
|
|
3891
|
+
const diff = parseDiffData(file.diff_data);
|
|
3892
|
+
const oldPath = diff?.oldPath ?? null;
|
|
3893
|
+
const image = side === "old" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : getNewImage(mode, file.file_path, repoRoot);
|
|
3894
|
+
if (!image) return c.text("Image not available", 404);
|
|
3895
|
+
if (isSvgFile(file.file_path)) {
|
|
3896
|
+
try {
|
|
3897
|
+
const png = await rasterizeSvg(image.data);
|
|
3898
|
+
return new Response(new Uint8Array(png), {
|
|
3899
|
+
headers: { "Content-Type": "image/png", "Cache-Control": "no-cache" }
|
|
3900
|
+
});
|
|
3901
|
+
} catch {
|
|
3902
|
+
return c.text("SVG rasterization failed", 500);
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
const contentType = getContentType(file.file_path);
|
|
3906
|
+
return new Response(new Uint8Array(image.data), {
|
|
3907
|
+
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
3908
|
+
});
|
|
3909
|
+
});
|
|
3910
|
+
|
|
3911
|
+
// src/routes/api/outline.ts
|
|
3912
|
+
init_queries();
|
|
3913
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
3914
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3915
|
+
import { Hono as Hono8 } from "hono";
|
|
3916
|
+
import { resolve as resolve5 } from "path";
|
|
3917
|
+
|
|
3708
3918
|
// src/outline/parser.ts
|
|
3709
3919
|
var BRACE_LANGS = /* @__PURE__ */ new Set([
|
|
3710
3920
|
"javascript",
|
|
@@ -4023,262 +4233,45 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
|
|
|
4023
4233
|
stack.push({ symbol: sym, indent });
|
|
4024
4234
|
}
|
|
4025
4235
|
|
|
4026
|
-
// src/routes/api.ts
|
|
4027
|
-
var
|
|
4028
|
-
|
|
4029
|
-
var VALID_SIDES = ["old", "new"];
|
|
4030
|
-
var VALID_FILE_STATUSES = ["pending", "reviewed"];
|
|
4031
|
-
function resolveReviewId(c) {
|
|
4032
|
-
return c.req.query("reviewId") ?? c.get("reviewId");
|
|
4033
|
-
}
|
|
4034
|
-
apiRoutes.get("/reviews", async (c) => {
|
|
4035
|
-
const repoRoot = c.get("repoRoot");
|
|
4036
|
-
const reviews = await listReviews(repoRoot);
|
|
4037
|
-
return c.json(reviews);
|
|
4038
|
-
});
|
|
4039
|
-
apiRoutes.get("/review", async (c) => {
|
|
4040
|
-
const reviewId = resolveReviewId(c);
|
|
4041
|
-
const review = await getReview(reviewId);
|
|
4042
|
-
return c.json(review);
|
|
4043
|
-
});
|
|
4044
|
-
apiRoutes.post("/review/complete", async (c) => {
|
|
4045
|
-
const reviewId = resolveReviewId(c);
|
|
4046
|
-
const currentReviewId = c.get("currentReviewId");
|
|
4047
|
-
const repoRoot = c.get("repoRoot");
|
|
4048
|
-
await updateReviewStatus(reviewId, "completed");
|
|
4049
|
-
const isCurrent = reviewId === currentReviewId;
|
|
4050
|
-
const exportPath = await generateReviewExport(reviewId, repoRoot, isCurrent);
|
|
4051
|
-
const gitignorePrompt = shouldPromptGitignore(repoRoot);
|
|
4052
|
-
return c.json({ status: "completed", exportPath, isCurrent, reviewId, gitignorePrompt });
|
|
4053
|
-
});
|
|
4054
|
-
apiRoutes.post("/gitignore/add", (c) => {
|
|
4236
|
+
// src/routes/api/outline.ts
|
|
4237
|
+
var outlineRoutes = new Hono8();
|
|
4238
|
+
outlineRoutes.get("/outline/:fileId", async (c) => {
|
|
4055
4239
|
const repoRoot = c.get("repoRoot");
|
|
4056
|
-
|
|
4057
|
-
return c.json({
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
}
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
return c.json({
|
|
4240
|
+
const file = await getReviewFile(c.req.param("fileId"));
|
|
4241
|
+
if (!file) return c.json({ error: "Not found" }, 404);
|
|
4242
|
+
const diff = parseDiffData(file.diff_data);
|
|
4243
|
+
const isDeleted = diff?.status === "deleted";
|
|
4244
|
+
let content = "";
|
|
4245
|
+
try {
|
|
4246
|
+
content = isDeleted ? getFileContent(file.file_path, "HEAD", repoRoot) : getFileContent(file.file_path, "working", repoRoot);
|
|
4247
|
+
} catch {
|
|
4248
|
+
}
|
|
4249
|
+
if (!content) return c.json({ symbols: [] });
|
|
4250
|
+
const symbols = parseOutline(content, file.file_path);
|
|
4251
|
+
return c.json({ symbols });
|
|
4068
4252
|
});
|
|
4069
|
-
|
|
4253
|
+
outlineRoutes.get("/symbol-definition", async (c) => {
|
|
4254
|
+
const name = c.req.query("name");
|
|
4255
|
+
const currentFileId = c.req.query("currentFileId");
|
|
4256
|
+
if (name === void 0 || name === "") return c.json({ definitions: [] });
|
|
4070
4257
|
const reviewId = resolveReviewId(c);
|
|
4071
4258
|
const repoRoot = c.get("repoRoot");
|
|
4072
|
-
const
|
|
4073
|
-
|
|
4074
|
-
const
|
|
4075
|
-
const
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
if (reviewId === currentReviewId) {
|
|
4089
|
-
return c.json({ error: "Cannot delete the current review" }, 400);
|
|
4090
|
-
}
|
|
4091
|
-
const repoRoot = c.get("repoRoot");
|
|
4092
|
-
deleteReviewExport(reviewId, repoRoot);
|
|
4093
|
-
await deleteReview(reviewId);
|
|
4094
|
-
return c.json({ ok: true });
|
|
4095
|
-
});
|
|
4096
|
-
apiRoutes.post("/reviews/delete-completed", async (c) => {
|
|
4097
|
-
const currentReviewId = c.get("currentReviewId");
|
|
4098
|
-
const repoRoot = c.get("repoRoot");
|
|
4099
|
-
const reviews = await listReviews(repoRoot);
|
|
4100
|
-
const toDelete = reviews.filter((r) => r.status === "completed" && r.id !== currentReviewId);
|
|
4101
|
-
for (const r of toDelete) {
|
|
4102
|
-
deleteReviewExport(r.id, repoRoot);
|
|
4103
|
-
await deleteReview(r.id);
|
|
4104
|
-
}
|
|
4105
|
-
return c.json({ deleted: toDelete.length });
|
|
4106
|
-
});
|
|
4107
|
-
apiRoutes.post("/reviews/delete-all", async (c) => {
|
|
4108
|
-
const currentReviewId = c.get("currentReviewId");
|
|
4109
|
-
const repoRoot = c.get("repoRoot");
|
|
4110
|
-
const reviews = await listReviews(repoRoot);
|
|
4111
|
-
const toDelete = reviews.filter((r) => r.id !== currentReviewId);
|
|
4112
|
-
for (const r of toDelete) {
|
|
4113
|
-
deleteReviewExport(r.id, repoRoot);
|
|
4114
|
-
await deleteReview(r.id);
|
|
4115
|
-
}
|
|
4116
|
-
return c.json({ deleted: toDelete.length });
|
|
4117
|
-
});
|
|
4118
|
-
apiRoutes.get("/files", async (c) => {
|
|
4119
|
-
const reviewId = resolveReviewId(c);
|
|
4120
|
-
const files = await getReviewFiles(reviewId);
|
|
4121
|
-
const annotationCounts = {};
|
|
4122
|
-
for (const file of files) {
|
|
4123
|
-
const annotations = await getAnnotationsForFile(file.id);
|
|
4124
|
-
annotationCounts[file.id] = annotations.length;
|
|
4125
|
-
}
|
|
4126
|
-
const staleCounts = await getStaleCountsForReview(reviewId);
|
|
4127
|
-
return c.json({ files, annotationCounts, staleCounts });
|
|
4128
|
-
});
|
|
4129
|
-
apiRoutes.get("/files/:fileId", async (c) => {
|
|
4130
|
-
const file = await getReviewFile(c.req.param("fileId"));
|
|
4131
|
-
if (!file) return c.json({ error: "Not found" }, 404);
|
|
4132
|
-
const annotations = await getAnnotationsForFile(file.id);
|
|
4133
|
-
return c.json({ file, annotations });
|
|
4134
|
-
});
|
|
4135
|
-
apiRoutes.patch("/files/:fileId/status", async (c) => {
|
|
4136
|
-
const { status } = await c.req.json();
|
|
4137
|
-
if (!VALID_FILE_STATUSES.includes(status)) {
|
|
4138
|
-
return c.json({ error: `status must be one of: ${VALID_FILE_STATUSES.join(", ")}` }, 400);
|
|
4139
|
-
}
|
|
4140
|
-
await updateFileStatus(c.req.param("fileId"), status);
|
|
4141
|
-
return c.json({ ok: true });
|
|
4142
|
-
});
|
|
4143
|
-
apiRoutes.post("/files/:fileId/reveal", async (c) => {
|
|
4144
|
-
const file = await getReviewFile(c.req.param("fileId"));
|
|
4145
|
-
if (!file) return c.json({ error: "Not found" }, 404);
|
|
4146
|
-
const repoRoot = c.get("repoRoot");
|
|
4147
|
-
const fullPath = resolve4(repoRoot, file.file_path);
|
|
4148
|
-
try {
|
|
4149
|
-
if (process.platform === "darwin") {
|
|
4150
|
-
execFileSync("open", ["-R", fullPath]);
|
|
4151
|
-
} else if (process.platform === "win32") {
|
|
4152
|
-
execFileSync("explorer", ["/select," + fullPath]);
|
|
4153
|
-
} else {
|
|
4154
|
-
execFileSync("xdg-open", [resolve4(fullPath, "..")]);
|
|
4155
|
-
}
|
|
4156
|
-
} catch {
|
|
4157
|
-
}
|
|
4158
|
-
return c.json({ ok: true });
|
|
4159
|
-
});
|
|
4160
|
-
function autoExport(c) {
|
|
4161
|
-
scheduleAutoExport(c.get("reviewId"), c.get("repoRoot"));
|
|
4162
|
-
}
|
|
4163
|
-
apiRoutes.post("/annotations", async (c) => {
|
|
4164
|
-
const body = await c.req.json();
|
|
4165
|
-
if (typeof body.reviewFileId !== "string" || body.reviewFileId === "") {
|
|
4166
|
-
return c.json({ error: "reviewFileId must be a non-empty string" }, 400);
|
|
4167
|
-
}
|
|
4168
|
-
if (typeof body.lineNumber !== "number" || !Number.isInteger(body.lineNumber) || body.lineNumber < 1) {
|
|
4169
|
-
return c.json({ error: "lineNumber must be a positive integer" }, 400);
|
|
4170
|
-
}
|
|
4171
|
-
if (!VALID_SIDES.includes(body.side)) {
|
|
4172
|
-
return c.json({ error: `side must be one of: ${VALID_SIDES.join(", ")}` }, 400);
|
|
4173
|
-
}
|
|
4174
|
-
if (!VALID_CATEGORIES.includes(body.category)) {
|
|
4175
|
-
return c.json({ error: `category must be one of: ${VALID_CATEGORIES.join(", ")}` }, 400);
|
|
4176
|
-
}
|
|
4177
|
-
if (typeof body.content !== "string" || body.content.trim() === "") {
|
|
4178
|
-
return c.json({ error: "content must be a non-empty string" }, 400);
|
|
4179
|
-
}
|
|
4180
|
-
const annotation = await addAnnotation(
|
|
4181
|
-
body.reviewFileId,
|
|
4182
|
-
body.lineNumber,
|
|
4183
|
-
body.side,
|
|
4184
|
-
body.category,
|
|
4185
|
-
body.content
|
|
4186
|
-
);
|
|
4187
|
-
autoExport(c);
|
|
4188
|
-
return c.json(annotation, 201);
|
|
4189
|
-
});
|
|
4190
|
-
apiRoutes.patch("/annotations/:id", async (c) => {
|
|
4191
|
-
const { content, category } = await c.req.json();
|
|
4192
|
-
if (typeof content !== "string" || content.trim() === "") {
|
|
4193
|
-
return c.json({ error: "content must be a non-empty string" }, 400);
|
|
4194
|
-
}
|
|
4195
|
-
if (!VALID_CATEGORIES.includes(category)) {
|
|
4196
|
-
return c.json({ error: `category must be one of: ${VALID_CATEGORIES.join(", ")}` }, 400);
|
|
4197
|
-
}
|
|
4198
|
-
await updateAnnotation(c.req.param("id"), content, category);
|
|
4199
|
-
autoExport(c);
|
|
4200
|
-
return c.json({ ok: true });
|
|
4201
|
-
});
|
|
4202
|
-
apiRoutes.delete("/annotations/:id", async (c) => {
|
|
4203
|
-
await deleteAnnotation(c.req.param("id"));
|
|
4204
|
-
autoExport(c);
|
|
4205
|
-
return c.json({ ok: true });
|
|
4206
|
-
});
|
|
4207
|
-
apiRoutes.patch("/annotations/:id/move", async (c) => {
|
|
4208
|
-
const { lineNumber, side } = await c.req.json();
|
|
4209
|
-
if (typeof lineNumber !== "number" || !Number.isInteger(lineNumber) || lineNumber < 1) {
|
|
4210
|
-
return c.json({ error: "lineNumber must be a positive integer" }, 400);
|
|
4211
|
-
}
|
|
4212
|
-
if (!VALID_SIDES.includes(side)) {
|
|
4213
|
-
return c.json({ error: `side must be one of: ${VALID_SIDES.join(", ")}` }, 400);
|
|
4214
|
-
}
|
|
4215
|
-
await moveAnnotation(c.req.param("id"), lineNumber, side);
|
|
4216
|
-
autoExport(c);
|
|
4217
|
-
return c.json({ ok: true });
|
|
4218
|
-
});
|
|
4219
|
-
apiRoutes.post("/annotations/:id/keep", async (c) => {
|
|
4220
|
-
await markAnnotationCurrent(c.req.param("id"));
|
|
4221
|
-
autoExport(c);
|
|
4222
|
-
return c.json({ ok: true });
|
|
4223
|
-
});
|
|
4224
|
-
apiRoutes.post("/annotations/stale/delete-all", async (c) => {
|
|
4225
|
-
const reviewId = resolveReviewId(c);
|
|
4226
|
-
await deleteStaleAnnotations(reviewId);
|
|
4227
|
-
autoExport(c);
|
|
4228
|
-
return c.json({ ok: true });
|
|
4229
|
-
});
|
|
4230
|
-
apiRoutes.post("/annotations/stale/keep-all", async (c) => {
|
|
4231
|
-
const reviewId = resolveReviewId(c);
|
|
4232
|
-
await keepAllStaleAnnotations(reviewId);
|
|
4233
|
-
autoExport(c);
|
|
4234
|
-
return c.json({ ok: true });
|
|
4235
|
-
});
|
|
4236
|
-
apiRoutes.get("/annotations/all", async (c) => {
|
|
4237
|
-
const reviewId = resolveReviewId(c);
|
|
4238
|
-
const annotations = await getAnnotationsForReview(reviewId);
|
|
4239
|
-
return c.json(annotations);
|
|
4240
|
-
});
|
|
4241
|
-
apiRoutes.get("/outline/:fileId", async (c) => {
|
|
4242
|
-
const repoRoot = c.get("repoRoot");
|
|
4243
|
-
const file = await getReviewFile(c.req.param("fileId"));
|
|
4244
|
-
if (!file) return c.json({ error: "Not found" }, 404);
|
|
4245
|
-
const diff = JSON.parse(file.diff_data ?? "{}");
|
|
4246
|
-
const isDeleted = diff.status === "deleted";
|
|
4247
|
-
let content = "";
|
|
4248
|
-
try {
|
|
4249
|
-
if (isDeleted) {
|
|
4250
|
-
content = getFileContent(file.file_path, "HEAD", repoRoot);
|
|
4251
|
-
} else {
|
|
4252
|
-
content = getFileContent(file.file_path, "working", repoRoot);
|
|
4253
|
-
}
|
|
4254
|
-
} catch {
|
|
4255
|
-
}
|
|
4256
|
-
if (!content) return c.json({ symbols: [] });
|
|
4257
|
-
const symbols = parseOutline(content, file.file_path);
|
|
4258
|
-
return c.json({ symbols });
|
|
4259
|
-
});
|
|
4260
|
-
apiRoutes.get("/symbol-definition", async (c) => {
|
|
4261
|
-
const name = c.req.query("name");
|
|
4262
|
-
const currentFileId = c.req.query("currentFileId");
|
|
4263
|
-
if (name === void 0 || name === "") return c.json({ definitions: [] });
|
|
4264
|
-
const reviewId = resolveReviewId(c);
|
|
4265
|
-
const repoRoot = c.get("repoRoot");
|
|
4266
|
-
const definitions = [];
|
|
4267
|
-
const searchedPaths = /* @__PURE__ */ new Set();
|
|
4268
|
-
const reviewFiles = await getReviewFiles(reviewId);
|
|
4269
|
-
for (const file of reviewFiles) {
|
|
4270
|
-
searchedPaths.add(file.file_path);
|
|
4271
|
-
const diff = JSON.parse(file.diff_data ?? "{}");
|
|
4272
|
-
const isDeleted = diff.status === "deleted";
|
|
4273
|
-
let content = "";
|
|
4274
|
-
try {
|
|
4275
|
-
content = isDeleted ? getFileContent(file.file_path, "HEAD", repoRoot) : getFileContent(file.file_path, "working", repoRoot);
|
|
4276
|
-
} catch {
|
|
4277
|
-
continue;
|
|
4278
|
-
}
|
|
4279
|
-
if (!content) continue;
|
|
4280
|
-
const symbols = parseOutline(content, file.file_path);
|
|
4281
|
-
collectDefinitions(symbols, name, file.id, file.file_path, definitions);
|
|
4259
|
+
const definitions = [];
|
|
4260
|
+
const searchedPaths = /* @__PURE__ */ new Set();
|
|
4261
|
+
const reviewFiles = await getReviewFiles(reviewId);
|
|
4262
|
+
for (const file of reviewFiles) {
|
|
4263
|
+
searchedPaths.add(file.file_path);
|
|
4264
|
+
const diff = parseDiffData(file.diff_data);
|
|
4265
|
+
const isDeleted = diff?.status === "deleted";
|
|
4266
|
+
let content = "";
|
|
4267
|
+
try {
|
|
4268
|
+
content = isDeleted ? getFileContent(file.file_path, "HEAD", repoRoot) : getFileContent(file.file_path, "working", repoRoot);
|
|
4269
|
+
} catch {
|
|
4270
|
+
continue;
|
|
4271
|
+
}
|
|
4272
|
+
if (!content) continue;
|
|
4273
|
+
const symbols = parseOutline(content, file.file_path);
|
|
4274
|
+
collectDefinitions(symbols, name, file.id, file.file_path, definitions);
|
|
4282
4275
|
}
|
|
4283
4276
|
if (definitions.length === 0) {
|
|
4284
4277
|
try {
|
|
@@ -4289,7 +4282,7 @@ apiRoutes.get("/symbol-definition", async (c) => {
|
|
|
4289
4282
|
if (!/\.(js|mjs|cjs|jsx|ts|tsx|mts|cts|java|go|rs|c|h|cpp|cc|cxx|hpp|cs|swift|php|kt|kts|scala|dart|groovy|py|rb)$/i.test(ext)) continue;
|
|
4290
4283
|
let content = "";
|
|
4291
4284
|
try {
|
|
4292
|
-
content = readFileSync8(
|
|
4285
|
+
content = readFileSync8(resolve5(repoRoot, filePath), "utf-8");
|
|
4293
4286
|
} catch {
|
|
4294
4287
|
continue;
|
|
4295
4288
|
}
|
|
@@ -4322,27 +4315,17 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
|
|
|
4322
4315
|
}
|
|
4323
4316
|
}
|
|
4324
4317
|
}
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
const content = getFileContent(file.file_path, "working", repoRoot);
|
|
4332
|
-
const allLines = content.split("\n");
|
|
4333
|
-
const clampedStart = Math.max(1, startLine);
|
|
4334
|
-
const clampedEnd = Math.min(allLines.length, endLine);
|
|
4335
|
-
const lines = [];
|
|
4336
|
-
for (let i = clampedStart; i <= clampedEnd; i++) {
|
|
4337
|
-
lines.push({ num: i, content: allLines[i - 1] || "" });
|
|
4338
|
-
}
|
|
4339
|
-
return c.json({ lines });
|
|
4340
|
-
});
|
|
4318
|
+
|
|
4319
|
+
// src/routes/api/project-settings.ts
|
|
4320
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
4321
|
+
import { Hono as Hono9 } from "hono";
|
|
4322
|
+
import { join as join7 } from "path";
|
|
4323
|
+
var projectSettingsRoutes = new Hono9();
|
|
4341
4324
|
function readProjectSettings(repoRoot) {
|
|
4342
4325
|
const settingsPath = join7(repoRoot, ".glassbox", "settings.json");
|
|
4343
4326
|
try {
|
|
4344
4327
|
if (existsSync6(settingsPath)) {
|
|
4345
|
-
return JSON.parse(
|
|
4328
|
+
return JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
4346
4329
|
}
|
|
4347
4330
|
} catch {
|
|
4348
4331
|
}
|
|
@@ -4353,11 +4336,11 @@ function writeProjectSettings(repoRoot, settings) {
|
|
|
4353
4336
|
mkdirSync4(dir, { recursive: true });
|
|
4354
4337
|
writeFileSync5(join7(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
|
|
4355
4338
|
}
|
|
4356
|
-
|
|
4339
|
+
projectSettingsRoutes.get("/project-settings", (c) => {
|
|
4357
4340
|
const repoRoot = c.get("repoRoot");
|
|
4358
4341
|
return c.json(readProjectSettings(repoRoot));
|
|
4359
4342
|
});
|
|
4360
|
-
|
|
4343
|
+
projectSettingsRoutes.patch("/project-settings", async (c) => {
|
|
4361
4344
|
const repoRoot = c.get("repoRoot");
|
|
4362
4345
|
const body = await c.req.json();
|
|
4363
4346
|
if (body.appName !== void 0 && typeof body.appName !== "string") {
|
|
@@ -4368,120 +4351,146 @@ apiRoutes.patch("/project-settings", async (c) => {
|
|
|
4368
4351
|
writeProjectSettings(repoRoot, current);
|
|
4369
4352
|
return c.json(current);
|
|
4370
4353
|
});
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4354
|
+
|
|
4355
|
+
// src/routes/api/reviews.ts
|
|
4356
|
+
init_queries();
|
|
4357
|
+
import { Hono as Hono10 } from "hono";
|
|
4358
|
+
var reviewsRoutes = new Hono10();
|
|
4359
|
+
reviewsRoutes.get("/reviews", async (c) => {
|
|
4375
4360
|
const repoRoot = c.get("repoRoot");
|
|
4376
|
-
const
|
|
4361
|
+
const reviews = await listReviews(repoRoot);
|
|
4362
|
+
return c.json(reviews);
|
|
4363
|
+
});
|
|
4364
|
+
reviewsRoutes.get("/review", async (c) => {
|
|
4365
|
+
const reviewId = resolveReviewId(c);
|
|
4366
|
+
const review = await getReview(reviewId);
|
|
4367
|
+
return c.json(review);
|
|
4368
|
+
});
|
|
4369
|
+
reviewsRoutes.post("/review/complete", async (c) => {
|
|
4370
|
+
const reviewId = resolveReviewId(c);
|
|
4371
|
+
const currentReviewId = c.get("currentReviewId");
|
|
4372
|
+
const repoRoot = c.get("repoRoot");
|
|
4373
|
+
await updateReviewStatus(reviewId, "completed");
|
|
4374
|
+
const isCurrent = reviewId === currentReviewId;
|
|
4375
|
+
const exportPath = await generateReviewExport(reviewId, repoRoot, isCurrent);
|
|
4376
|
+
const gitignorePrompt = shouldPromptGitignore(repoRoot);
|
|
4377
|
+
return c.json({ status: "completed", exportPath, isCurrent, reviewId, gitignorePrompt });
|
|
4378
|
+
});
|
|
4379
|
+
reviewsRoutes.post("/gitignore/add", (c) => {
|
|
4380
|
+
const repoRoot = c.get("repoRoot");
|
|
4381
|
+
addGlassboxToGitignore(repoRoot);
|
|
4382
|
+
return c.json({ ok: true });
|
|
4383
|
+
});
|
|
4384
|
+
reviewsRoutes.post("/gitignore/dismiss", (c) => {
|
|
4385
|
+
const repoRoot = c.get("repoRoot");
|
|
4386
|
+
dismissGitignorePrompt(repoRoot);
|
|
4387
|
+
return c.json({ ok: true });
|
|
4388
|
+
});
|
|
4389
|
+
reviewsRoutes.post("/review/reopen", async (c) => {
|
|
4390
|
+
const reviewId = resolveReviewId(c);
|
|
4391
|
+
await updateReviewStatus(reviewId, "in_progress");
|
|
4392
|
+
return c.json({ status: "in_progress" });
|
|
4393
|
+
});
|
|
4394
|
+
reviewsRoutes.post("/review/refresh", async (c) => {
|
|
4395
|
+
const reviewId = resolveReviewId(c);
|
|
4396
|
+
const repoRoot = c.get("repoRoot");
|
|
4397
|
+
const review = await getReview(reviewId);
|
|
4377
4398
|
if (!review) return c.json({ error: "Review not found" }, 404);
|
|
4378
4399
|
const mode = parseModeString(review.mode);
|
|
4379
|
-
const
|
|
4380
|
-
const
|
|
4381
|
-
const
|
|
4382
|
-
const oldImage = status !== "added" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : null;
|
|
4383
|
-
const newImage = status !== "deleted" ? getNewImage(mode, file.file_path, repoRoot) : null;
|
|
4384
|
-
const oldMeta = oldImage !== null ? extractMetadata(oldImage.data, oldPath ?? file.file_path) : null;
|
|
4385
|
-
const newMeta = newImage !== null ? extractMetadata(newImage.data, file.file_path) : null;
|
|
4400
|
+
const headCommit = getHeadCommit(repoRoot);
|
|
4401
|
+
const diffs = getFileDiffs(mode, repoRoot);
|
|
4402
|
+
const result = await updateReviewDiffs(reviewId, diffs, headCommit);
|
|
4386
4403
|
return c.json({
|
|
4387
|
-
|
|
4388
|
-
|
|
4404
|
+
updated: result.updated,
|
|
4405
|
+
added: result.added,
|
|
4406
|
+
stale: result.stale,
|
|
4407
|
+
fileCount: diffs.length
|
|
4389
4408
|
});
|
|
4390
4409
|
});
|
|
4391
|
-
|
|
4392
|
-
const
|
|
4393
|
-
const
|
|
4394
|
-
if (
|
|
4395
|
-
|
|
4396
|
-
|
|
4410
|
+
reviewsRoutes.delete("/review/:id", async (c) => {
|
|
4411
|
+
const reviewId = c.req.param("id");
|
|
4412
|
+
const currentReviewId = c.get("currentReviewId");
|
|
4413
|
+
if (reviewId === currentReviewId) {
|
|
4414
|
+
return c.json({ error: "Cannot delete the current review" }, 400);
|
|
4415
|
+
}
|
|
4397
4416
|
const repoRoot = c.get("repoRoot");
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
const
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
});
|
|
4411
|
-
} catch {
|
|
4412
|
-
return c.text("SVG rasterization failed", 500);
|
|
4413
|
-
}
|
|
4417
|
+
deleteReviewExport(reviewId, repoRoot);
|
|
4418
|
+
await deleteReview(reviewId);
|
|
4419
|
+
return c.json({ ok: true });
|
|
4420
|
+
});
|
|
4421
|
+
reviewsRoutes.post("/reviews/delete-completed", async (c) => {
|
|
4422
|
+
const currentReviewId = c.get("currentReviewId");
|
|
4423
|
+
const repoRoot = c.get("repoRoot");
|
|
4424
|
+
const reviews = await listReviews(repoRoot);
|
|
4425
|
+
const toDelete = reviews.filter((r) => r.status === "completed" && r.id !== currentReviewId);
|
|
4426
|
+
for (const r of toDelete) {
|
|
4427
|
+
deleteReviewExport(r.id, repoRoot);
|
|
4428
|
+
await deleteReview(r.id);
|
|
4414
4429
|
}
|
|
4415
|
-
|
|
4416
|
-
return new Response(new Uint8Array(image.data), {
|
|
4417
|
-
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
4418
|
-
});
|
|
4430
|
+
return c.json({ deleted: toDelete.length });
|
|
4419
4431
|
});
|
|
4420
|
-
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4432
|
+
reviewsRoutes.post("/reviews/delete-all", async (c) => {
|
|
4433
|
+
const currentReviewId = c.get("currentReviewId");
|
|
4434
|
+
const repoRoot = c.get("repoRoot");
|
|
4435
|
+
const reviews = await listReviews(repoRoot);
|
|
4436
|
+
const toDelete = reviews.filter((r) => r.id !== currentReviewId);
|
|
4437
|
+
for (const r of toDelete) {
|
|
4438
|
+
deleteReviewExport(r.id, repoRoot);
|
|
4439
|
+
await deleteReview(r.id);
|
|
4427
4440
|
}
|
|
4428
|
-
return {};
|
|
4429
|
-
}
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
apiRoutes.get("/share-prompt/state", (c) => {
|
|
4441
|
+
return c.json({ deleted: toDelete.length });
|
|
4442
|
+
});
|
|
4443
|
+
|
|
4444
|
+
// src/routes/api/share-prompt.ts
|
|
4445
|
+
import { Hono as Hono11 } from "hono";
|
|
4446
|
+
var sharePromptRoutes = new Hono11();
|
|
4447
|
+
sharePromptRoutes.get("/share-prompt/state", (c) => {
|
|
4436
4448
|
const config = readGlobalConfig();
|
|
4437
4449
|
const sp = config.sharePrompt;
|
|
4438
4450
|
const dismissedAt = sp !== void 0 && typeof sp.dismissedAt === "number" ? sp.dismissedAt : null;
|
|
4439
4451
|
const totalOpenMs = sp !== void 0 && typeof sp.totalOpenMs === "number" ? sp.totalOpenMs : 0;
|
|
4440
4452
|
return c.json({ dismissedAt, totalOpenMs });
|
|
4441
4453
|
});
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
writeGlobalConfig(config);
|
|
4454
|
+
sharePromptRoutes.post("/share-prompt/dismiss", (c) => {
|
|
4455
|
+
updateGlobalConfig((config) => {
|
|
4456
|
+
if (config.sharePrompt === void 0) config.sharePrompt = {};
|
|
4457
|
+
config.sharePrompt.dismissedAt = Date.now();
|
|
4458
|
+
});
|
|
4448
4459
|
return c.json({ ok: true });
|
|
4449
4460
|
});
|
|
4450
|
-
|
|
4461
|
+
sharePromptRoutes.post("/share-prompt/tick", async (c) => {
|
|
4451
4462
|
const body = await c.req.json();
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4463
|
+
let totalOpenMs = 0;
|
|
4464
|
+
updateGlobalConfig((config) => {
|
|
4465
|
+
if (config.sharePrompt === void 0) config.sharePrompt = {};
|
|
4466
|
+
const sp = config.sharePrompt;
|
|
4467
|
+
const current = typeof sp.totalOpenMs === "number" ? sp.totalOpenMs : 0;
|
|
4468
|
+
const next = current + (body.sessionMs > 0 ? body.sessionMs : 0);
|
|
4469
|
+
sp.totalOpenMs = next;
|
|
4470
|
+
totalOpenMs = next;
|
|
4471
|
+
});
|
|
4472
|
+
return c.json({ totalOpenMs });
|
|
4459
4473
|
});
|
|
4460
4474
|
|
|
4475
|
+
// src/routes/api.ts
|
|
4476
|
+
var apiRoutes = new Hono12();
|
|
4477
|
+
apiRoutes.route("/", reviewsRoutes);
|
|
4478
|
+
apiRoutes.route("/", filesRoutes);
|
|
4479
|
+
apiRoutes.route("/", annotationsRoutes);
|
|
4480
|
+
apiRoutes.route("/", outlineRoutes);
|
|
4481
|
+
apiRoutes.route("/", contextRoutes);
|
|
4482
|
+
apiRoutes.route("/", projectSettingsRoutes);
|
|
4483
|
+
apiRoutes.route("/", imageRoutes);
|
|
4484
|
+
apiRoutes.route("/", sharePromptRoutes);
|
|
4485
|
+
|
|
4461
4486
|
// src/routes/channel-api.ts
|
|
4462
4487
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
4463
|
-
import {
|
|
4464
|
-
import { Hono as
|
|
4465
|
-
import { homedir as homedir4 } from "os";
|
|
4488
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
4489
|
+
import { Hono as Hono13 } from "hono";
|
|
4466
4490
|
import { join as join8 } from "path";
|
|
4467
|
-
var channelApiRoutes = new
|
|
4468
|
-
var CONFIG_DIR2 = join8(homedir4(), ".glassbox");
|
|
4469
|
-
var CONFIG_PATH2 = join8(CONFIG_DIR2, "config.json");
|
|
4470
|
-
function readGlobalConfig2() {
|
|
4471
|
-
try {
|
|
4472
|
-
if (existsSync7(CONFIG_PATH2)) {
|
|
4473
|
-
return JSON.parse(readFileSync9(CONFIG_PATH2, "utf-8"));
|
|
4474
|
-
}
|
|
4475
|
-
} catch {
|
|
4476
|
-
}
|
|
4477
|
-
return {};
|
|
4478
|
-
}
|
|
4479
|
-
function writeGlobalConfig2(config) {
|
|
4480
|
-
mkdirSync5(CONFIG_DIR2, { recursive: true });
|
|
4481
|
-
writeFileSync6(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
4482
|
-
}
|
|
4491
|
+
var channelApiRoutes = new Hono13();
|
|
4483
4492
|
channelApiRoutes.get("/status", async (c) => {
|
|
4484
|
-
const config =
|
|
4493
|
+
const config = readGlobalConfig();
|
|
4485
4494
|
const enabled = config.channelEnabled === true;
|
|
4486
4495
|
const repoRoot = c.get("repoRoot");
|
|
4487
4496
|
const dataDir = join8(repoRoot, ".glassbox");
|
|
@@ -4489,9 +4498,9 @@ channelApiRoutes.get("/status", async (c) => {
|
|
|
4489
4498
|
return c.json({ enabled, connected });
|
|
4490
4499
|
});
|
|
4491
4500
|
channelApiRoutes.post("/enable", (c) => {
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4501
|
+
updateGlobalConfig((config) => {
|
|
4502
|
+
config.channelEnabled = true;
|
|
4503
|
+
});
|
|
4495
4504
|
const repoRoot = c.get("repoRoot");
|
|
4496
4505
|
const dataDir = join8(repoRoot, ".glassbox");
|
|
4497
4506
|
mkdirSync5(dataDir, { recursive: true });
|
|
@@ -4499,9 +4508,9 @@ channelApiRoutes.post("/enable", (c) => {
|
|
|
4499
4508
|
return c.json({ ok: true });
|
|
4500
4509
|
});
|
|
4501
4510
|
channelApiRoutes.post("/disable", (c) => {
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4511
|
+
updateGlobalConfig((config) => {
|
|
4512
|
+
config.channelEnabled = false;
|
|
4513
|
+
});
|
|
4505
4514
|
const repoRoot = c.get("repoRoot");
|
|
4506
4515
|
const dataDir = join8(repoRoot, ".glassbox");
|
|
4507
4516
|
unregisterChannel(dataDir);
|
|
@@ -4509,6 +4518,9 @@ channelApiRoutes.post("/disable", (c) => {
|
|
|
4509
4518
|
});
|
|
4510
4519
|
channelApiRoutes.post("/trigger", async (c) => {
|
|
4511
4520
|
const body = await c.req.json();
|
|
4521
|
+
if (!isNonEmptyString(body.message)) {
|
|
4522
|
+
return c.json({ error: "message must be a non-empty string" }, 400);
|
|
4523
|
+
}
|
|
4512
4524
|
const repoRoot = c.get("repoRoot");
|
|
4513
4525
|
const dataDir = join8(repoRoot, ".glassbox");
|
|
4514
4526
|
const sent = await triggerChannel(dataDir, body.message);
|
|
@@ -4539,8 +4551,8 @@ channelApiRoutes.get("/claude-check", (c) => {
|
|
|
4539
4551
|
|
|
4540
4552
|
// src/routes/pages.tsx
|
|
4541
4553
|
import { readFileSync as readFileSync11 } from "fs";
|
|
4542
|
-
import { Hono as
|
|
4543
|
-
import { resolve as
|
|
4554
|
+
import { Hono as Hono14 } from "hono";
|
|
4555
|
+
import { resolve as resolve6 } from "path";
|
|
4544
4556
|
|
|
4545
4557
|
// src/utils/escapeHtml.ts
|
|
4546
4558
|
function escapeHtml(str) {
|
|
@@ -5173,147 +5185,64 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
|
5173
5185
|
"data-new-start": hunk.newStart,
|
|
5174
5186
|
"data-new-count": hunk.newCount,
|
|
5175
5187
|
children: [
|
|
5176
|
-
"@@ -",
|
|
5177
|
-
hunk.oldStart,
|
|
5178
|
-
",",
|
|
5179
|
-
hunk.oldCount,
|
|
5180
|
-
" +",
|
|
5181
|
-
hunk.newStart,
|
|
5182
|
-
",",
|
|
5183
|
-
hunk.newCount,
|
|
5184
|
-
" @@"
|
|
5185
|
-
]
|
|
5186
|
-
}
|
|
5187
|
-
),
|
|
5188
|
-
hunk.lines.map((line) => {
|
|
5189
|
-
const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
|
|
5190
|
-
const side = line.type === "remove" ? "old" : "new";
|
|
5191
|
-
const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
|
|
5192
|
-
const segments = charDiffs.get(line);
|
|
5193
|
-
return /* @__PURE__ */ jsx("div", { children: [
|
|
5194
|
-
/* @__PURE__ */ jsx(
|
|
5195
|
-
"div",
|
|
5196
|
-
{
|
|
5197
|
-
className: `diff-line ${line.type}${anns.length ? " has-annotation" : ""}`,
|
|
5198
|
-
"data-line": lineNum,
|
|
5199
|
-
"data-side": side,
|
|
5200
|
-
children: [
|
|
5201
|
-
/* @__PURE__ */ jsx("span", { className: "gutter-old", "data-line-number": line.oldNum ?? "" }),
|
|
5202
|
-
/* @__PURE__ */ jsx("span", { className: "gutter-new", "data-line-number": line.newNum ?? "" }),
|
|
5203
|
-
/* @__PURE__ */ jsx("span", { className: "code", children: segments ? renderSegments(segments) : line.content })
|
|
5204
|
-
]
|
|
5205
|
-
}
|
|
5206
|
-
),
|
|
5207
|
-
anns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: anns }) : null
|
|
5208
|
-
] });
|
|
5209
|
-
})
|
|
5210
|
-
] });
|
|
5211
|
-
}),
|
|
5212
|
-
/* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
|
|
5213
|
-
] });
|
|
5214
|
-
}
|
|
5215
|
-
function AnnotationRows({ annotations }) {
|
|
5216
|
-
return /* @__PURE__ */ jsx("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx(
|
|
5217
|
-
"div",
|
|
5218
|
-
{
|
|
5219
|
-
className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
|
|
5220
|
-
"data-annotation-id": a.id,
|
|
5221
|
-
"data-is-stale": a.is_stale ? "true" : void 0,
|
|
5222
|
-
children: [
|
|
5223
|
-
/* @__PURE__ */ jsx("span", { className: "annotation-drag-handle", draggable: "true", title: "Drag to move", children: "\u283F" }),
|
|
5224
|
-
/* @__PURE__ */ jsx("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
|
|
5225
|
-
/* @__PURE__ */ jsx("span", { className: "annotation-text", children: a.content }),
|
|
5226
|
-
/* @__PURE__ */ jsx("div", { className: "annotation-actions", children: [
|
|
5227
|
-
a.is_stale ? /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
|
|
5228
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon", "data-action": "edit", title: "Edit", children: /* @__PURE__ */ jsx(IconEdit, {}) }),
|
|
5229
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon btn-danger", "data-action": "delete", title: "Delete", children: /* @__PURE__ */ jsx(IconTrash, {}) })
|
|
5230
|
-
] })
|
|
5231
|
-
]
|
|
5232
|
-
}
|
|
5233
|
-
)) });
|
|
5234
|
-
}
|
|
5235
|
-
|
|
5236
|
-
// src/components/fileList.tsx
|
|
5237
|
-
function buildFileTree(files) {
|
|
5238
|
-
const root = { name: "", children: [], files: [] };
|
|
5239
|
-
for (const f of files) {
|
|
5240
|
-
const parts = f.file_path.split("/");
|
|
5241
|
-
let node = root;
|
|
5242
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
5243
|
-
let child = node.children.find((c) => c.name === parts[i]);
|
|
5244
|
-
if (!child) {
|
|
5245
|
-
child = { name: parts[i], children: [], files: [] };
|
|
5246
|
-
node.children.push(child);
|
|
5247
|
-
}
|
|
5248
|
-
node = child;
|
|
5249
|
-
}
|
|
5250
|
-
node.files.push(f);
|
|
5251
|
-
}
|
|
5252
|
-
compressTree(root);
|
|
5253
|
-
return root;
|
|
5254
|
-
}
|
|
5255
|
-
function compressTree(node) {
|
|
5256
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
5257
|
-
let child = node.children[i];
|
|
5258
|
-
while (child.children.length === 1 && child.files.length === 0) {
|
|
5259
|
-
const grandchild = child.children[0];
|
|
5260
|
-
child = { name: child.name + "/" + grandchild.name, children: grandchild.children, files: grandchild.files };
|
|
5261
|
-
node.children[i] = child;
|
|
5262
|
-
}
|
|
5263
|
-
compressTree(child);
|
|
5264
|
-
}
|
|
5265
|
-
}
|
|
5266
|
-
function countFiles(node) {
|
|
5267
|
-
let count = node.files.length;
|
|
5268
|
-
for (const child of node.children) count += countFiles(child);
|
|
5269
|
-
return count;
|
|
5270
|
-
}
|
|
5271
|
-
function hasStale(node, staleCounts) {
|
|
5272
|
-
for (const f of node.files) {
|
|
5273
|
-
if (staleCounts[f.id]) return true;
|
|
5274
|
-
}
|
|
5275
|
-
for (const child of node.children) {
|
|
5276
|
-
if (hasStale(child, staleCounts)) return true;
|
|
5277
|
-
}
|
|
5278
|
-
return false;
|
|
5279
|
-
}
|
|
5280
|
-
function TreeView({ node, depth, annotationCounts, staleCounts }) {
|
|
5281
|
-
const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
|
|
5282
|
-
return /* @__PURE__ */ jsx("div", { children: [
|
|
5283
|
-
sortedChildren.map((child) => {
|
|
5284
|
-
const total = countFiles(child);
|
|
5285
|
-
const isCollapsible = total > 1;
|
|
5286
|
-
const stale = hasStale(child, staleCounts);
|
|
5287
|
-
return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
|
|
5288
|
-
/* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
5289
|
-
isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
|
|
5290
|
-
/* @__PURE__ */ jsx("span", { className: "folder-name", children: [
|
|
5291
|
-
child.name,
|
|
5292
|
-
"/"
|
|
5293
|
-
] }),
|
|
5294
|
-
stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
|
|
5295
|
-
] }),
|
|
5296
|
-
/* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
|
|
5188
|
+
"@@ -",
|
|
5189
|
+
hunk.oldStart,
|
|
5190
|
+
",",
|
|
5191
|
+
hunk.oldCount,
|
|
5192
|
+
" +",
|
|
5193
|
+
hunk.newStart,
|
|
5194
|
+
",",
|
|
5195
|
+
hunk.newCount,
|
|
5196
|
+
" @@"
|
|
5197
|
+
]
|
|
5198
|
+
}
|
|
5199
|
+
),
|
|
5200
|
+
hunk.lines.map((line) => {
|
|
5201
|
+
const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
|
|
5202
|
+
const side = line.type === "remove" ? "old" : "new";
|
|
5203
|
+
const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
|
|
5204
|
+
const segments = charDiffs.get(line);
|
|
5205
|
+
return /* @__PURE__ */ jsx("div", { children: [
|
|
5206
|
+
/* @__PURE__ */ jsx(
|
|
5207
|
+
"div",
|
|
5208
|
+
{
|
|
5209
|
+
className: `diff-line ${line.type}${anns.length ? " has-annotation" : ""}`,
|
|
5210
|
+
"data-line": lineNum,
|
|
5211
|
+
"data-side": side,
|
|
5212
|
+
children: [
|
|
5213
|
+
/* @__PURE__ */ jsx("span", { className: "gutter-old", "data-line-number": line.oldNum ?? "" }),
|
|
5214
|
+
/* @__PURE__ */ jsx("span", { className: "gutter-new", "data-line-number": line.newNum ?? "" }),
|
|
5215
|
+
/* @__PURE__ */ jsx("span", { className: "code", children: segments ? renderSegments(segments) : line.content })
|
|
5216
|
+
]
|
|
5217
|
+
}
|
|
5218
|
+
),
|
|
5219
|
+
anns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: anns }) : null
|
|
5220
|
+
] });
|
|
5221
|
+
})
|
|
5297
5222
|
] });
|
|
5298
5223
|
}),
|
|
5299
|
-
|
|
5300
|
-
const diff = JSON.parse(f.diff_data ?? "{}");
|
|
5301
|
-
const count = annotationCounts[f.id] || 0;
|
|
5302
|
-
const stale = staleCounts[f.id] || 0;
|
|
5303
|
-
const fileName = f.file_path.split("/").pop() ?? "";
|
|
5304
|
-
return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
5305
|
-
/* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
|
|
5306
|
-
/* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
|
|
5307
|
-
/* @__PURE__ */ jsx("span", { className: `file-status ${diff.status ?? ""}`, children: diff.status ?? "" }),
|
|
5308
|
-
stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
|
|
5309
|
-
count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
|
|
5310
|
-
] });
|
|
5311
|
-
})
|
|
5224
|
+
/* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
|
|
5312
5225
|
] });
|
|
5313
5226
|
}
|
|
5314
|
-
function
|
|
5315
|
-
|
|
5316
|
-
|
|
5227
|
+
function AnnotationRows({ annotations }) {
|
|
5228
|
+
return /* @__PURE__ */ jsx("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx(
|
|
5229
|
+
"div",
|
|
5230
|
+
{
|
|
5231
|
+
className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
|
|
5232
|
+
"data-annotation-id": a.id,
|
|
5233
|
+
"data-is-stale": a.is_stale ? "true" : void 0,
|
|
5234
|
+
children: [
|
|
5235
|
+
/* @__PURE__ */ jsx("span", { className: "annotation-drag-handle", draggable: "true", title: "Drag to move", children: "\u283F" }),
|
|
5236
|
+
/* @__PURE__ */ jsx("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
|
|
5237
|
+
/* @__PURE__ */ jsx("span", { className: "annotation-text", children: a.content }),
|
|
5238
|
+
/* @__PURE__ */ jsx("div", { className: "annotation-actions", children: [
|
|
5239
|
+
a.is_stale ? /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
|
|
5240
|
+
/* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon", "data-action": "edit", title: "Edit", children: /* @__PURE__ */ jsx(IconEdit, {}) }),
|
|
5241
|
+
/* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon btn-danger", "data-action": "delete", title: "Delete", children: /* @__PURE__ */ jsx(IconTrash, {}) })
|
|
5242
|
+
] })
|
|
5243
|
+
]
|
|
5244
|
+
}
|
|
5245
|
+
)) });
|
|
5317
5246
|
}
|
|
5318
5247
|
|
|
5319
5248
|
// src/themes/built-in.ts
|
|
@@ -5786,39 +5715,23 @@ function themeToInlineStyle(colors) {
|
|
|
5786
5715
|
}
|
|
5787
5716
|
|
|
5788
5717
|
// src/themes/config.ts
|
|
5789
|
-
import { existsSync as
|
|
5790
|
-
import { homedir as homedir5 } from "os";
|
|
5718
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
5791
5719
|
import { join as join9 } from "path";
|
|
5792
|
-
var
|
|
5793
|
-
var CONFIG_PATH3 = join9(CONFIG_DIR3, "config.json");
|
|
5794
|
-
var THEMES_DIR = join9(CONFIG_DIR3, "themes");
|
|
5795
|
-
function readConfigFile2() {
|
|
5796
|
-
try {
|
|
5797
|
-
if (existsSync8(CONFIG_PATH3)) {
|
|
5798
|
-
return JSON.parse(readFileSync10(CONFIG_PATH3, "utf-8"));
|
|
5799
|
-
}
|
|
5800
|
-
} catch {
|
|
5801
|
-
}
|
|
5802
|
-
return {};
|
|
5803
|
-
}
|
|
5804
|
-
function writeConfigFile2(config) {
|
|
5805
|
-
mkdirSync6(CONFIG_DIR3, { recursive: true });
|
|
5806
|
-
writeFileSync7(CONFIG_PATH3, JSON.stringify(config, null, 2), "utf-8");
|
|
5807
|
-
}
|
|
5720
|
+
var THEMES_DIR = join9(GLOBAL_CONFIG_DIR, "themes");
|
|
5808
5721
|
function getActiveThemeId() {
|
|
5809
|
-
const config =
|
|
5722
|
+
const config = readGlobalConfig();
|
|
5810
5723
|
const theme = config.theme;
|
|
5811
5724
|
const active = theme?.active;
|
|
5812
5725
|
return active ?? DEFAULT_THEME_ID;
|
|
5813
5726
|
}
|
|
5814
5727
|
function setActiveThemeId(id) {
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5728
|
+
updateGlobalConfig((config) => {
|
|
5729
|
+
if (config.theme === void 0) config.theme = {};
|
|
5730
|
+
config.theme.active = id;
|
|
5731
|
+
});
|
|
5819
5732
|
}
|
|
5820
5733
|
function loadCustomThemes() {
|
|
5821
|
-
if (!
|
|
5734
|
+
if (!existsSync7(THEMES_DIR)) return [];
|
|
5822
5735
|
const themes = [];
|
|
5823
5736
|
try {
|
|
5824
5737
|
const files = readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json"));
|
|
@@ -5838,17 +5751,17 @@ function loadCustomThemes() {
|
|
|
5838
5751
|
function saveCustomTheme(theme) {
|
|
5839
5752
|
mkdirSync6(THEMES_DIR, { recursive: true });
|
|
5840
5753
|
const filePath = join9(THEMES_DIR, `${theme.id}.json`);
|
|
5841
|
-
|
|
5754
|
+
writeFileSync6(filePath, JSON.stringify(theme, null, 2), "utf-8");
|
|
5842
5755
|
}
|
|
5843
5756
|
function deleteCustomTheme(id) {
|
|
5844
5757
|
const filePath = join9(THEMES_DIR, `${id}.json`);
|
|
5845
|
-
if (
|
|
5758
|
+
if (existsSync7(filePath)) {
|
|
5846
5759
|
unlinkSync2(filePath);
|
|
5847
5760
|
}
|
|
5848
5761
|
}
|
|
5849
5762
|
function getCustomTheme(id) {
|
|
5850
5763
|
const filePath = join9(THEMES_DIR, `${id}.json`);
|
|
5851
|
-
if (!
|
|
5764
|
+
if (!existsSync7(filePath)) return void 0;
|
|
5852
5765
|
try {
|
|
5853
5766
|
const data = JSON.parse(readFileSync10(filePath, "utf-8"));
|
|
5854
5767
|
return { ...data, builtIn: false };
|
|
@@ -5958,20 +5871,92 @@ function ReviewHistory({ reviews, currentReviewId }) {
|
|
|
5958
5871
|
] });
|
|
5959
5872
|
}
|
|
5960
5873
|
|
|
5961
|
-
// src/
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
pageRoutes.get("/", async (c) => {
|
|
5965
|
-
const reviewId = c.get("reviewId");
|
|
5966
|
-
const review = await getReview(reviewId);
|
|
5967
|
-
if (!review) return c.text("Review not found", 404);
|
|
5968
|
-
const files = await getReviewFiles(reviewId);
|
|
5969
|
-
const annotationCounts = {};
|
|
5874
|
+
// src/components/fileList.tsx
|
|
5875
|
+
function buildFileTree(files) {
|
|
5876
|
+
const root = { name: "", children: [], files: [] };
|
|
5970
5877
|
for (const f of files) {
|
|
5971
|
-
const
|
|
5972
|
-
|
|
5878
|
+
const parts = f.file_path.split("/");
|
|
5879
|
+
let node = root;
|
|
5880
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
5881
|
+
let child = node.children.find((c) => c.name === parts[i]);
|
|
5882
|
+
if (!child) {
|
|
5883
|
+
child = { name: parts[i], children: [], files: [] };
|
|
5884
|
+
node.children.push(child);
|
|
5885
|
+
}
|
|
5886
|
+
node = child;
|
|
5887
|
+
}
|
|
5888
|
+
node.files.push(f);
|
|
5889
|
+
}
|
|
5890
|
+
compressTree(root);
|
|
5891
|
+
return root;
|
|
5892
|
+
}
|
|
5893
|
+
function compressTree(node) {
|
|
5894
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
5895
|
+
let child = node.children[i];
|
|
5896
|
+
while (child.children.length === 1 && child.files.length === 0) {
|
|
5897
|
+
const grandchild = child.children[0];
|
|
5898
|
+
child = { name: child.name + "/" + grandchild.name, children: grandchild.children, files: grandchild.files };
|
|
5899
|
+
node.children[i] = child;
|
|
5900
|
+
}
|
|
5901
|
+
compressTree(child);
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
function countFiles(node) {
|
|
5905
|
+
let count = node.files.length;
|
|
5906
|
+
for (const child of node.children) count += countFiles(child);
|
|
5907
|
+
return count;
|
|
5908
|
+
}
|
|
5909
|
+
function hasStale(node, staleCounts) {
|
|
5910
|
+
for (const f of node.files) {
|
|
5911
|
+
if (staleCounts[f.id]) return true;
|
|
5973
5912
|
}
|
|
5974
|
-
|
|
5913
|
+
for (const child of node.children) {
|
|
5914
|
+
if (hasStale(child, staleCounts)) return true;
|
|
5915
|
+
}
|
|
5916
|
+
return false;
|
|
5917
|
+
}
|
|
5918
|
+
function TreeView({ node, depth, annotationCounts, staleCounts }) {
|
|
5919
|
+
const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
|
|
5920
|
+
return /* @__PURE__ */ jsx("div", { children: [
|
|
5921
|
+
sortedChildren.map((child) => {
|
|
5922
|
+
const total = countFiles(child);
|
|
5923
|
+
const isCollapsible = total > 1;
|
|
5924
|
+
const stale = hasStale(child, staleCounts);
|
|
5925
|
+
return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
|
|
5926
|
+
/* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
5927
|
+
isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
|
|
5928
|
+
/* @__PURE__ */ jsx("span", { className: "folder-name", children: [
|
|
5929
|
+
child.name,
|
|
5930
|
+
"/"
|
|
5931
|
+
] }),
|
|
5932
|
+
stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
|
|
5933
|
+
] }),
|
|
5934
|
+
/* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
|
|
5935
|
+
] });
|
|
5936
|
+
}),
|
|
5937
|
+
node.files.map((f) => {
|
|
5938
|
+
const diff = parseDiffData(f.diff_data);
|
|
5939
|
+
const count = annotationCounts[f.id] || 0;
|
|
5940
|
+
const stale = staleCounts[f.id] || 0;
|
|
5941
|
+
const fileName = f.file_path.split("/").pop() ?? "";
|
|
5942
|
+
return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
5943
|
+
/* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
|
|
5944
|
+
/* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
|
|
5945
|
+
/* @__PURE__ */ jsx("span", { className: `file-status ${diff?.status ?? ""}`, children: diff?.status ?? "" }),
|
|
5946
|
+
stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
|
|
5947
|
+
count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
|
|
5948
|
+
] });
|
|
5949
|
+
})
|
|
5950
|
+
] });
|
|
5951
|
+
}
|
|
5952
|
+
function FileList({ files, annotationCounts, staleCounts }) {
|
|
5953
|
+
const tree = buildFileTree(files);
|
|
5954
|
+
return /* @__PURE__ */ jsx("div", { className: "file-list", children: /* @__PURE__ */ jsx("div", { className: "file-list-items", children: /* @__PURE__ */ jsx(TreeView, { node: tree, depth: 0, annotationCounts, staleCounts }) }) });
|
|
5955
|
+
}
|
|
5956
|
+
|
|
5957
|
+
// src/components/reviewShell.tsx
|
|
5958
|
+
function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, footer }) {
|
|
5959
|
+
return /* @__PURE__ */ jsx("div", { className: "review-app", "data-review-id": reviewId, children: [
|
|
5975
5960
|
/* @__PURE__ */ jsx("div", { id: "update-banner", className: "update-banner", style: "display:none", children: [
|
|
5976
5961
|
/* @__PURE__ */ jsx("span", { id: "update-banner-label", children: "Update available" }),
|
|
5977
5962
|
/* @__PURE__ */ jsx("div", { className: "update-banner-actions", children: [
|
|
@@ -5989,12 +5974,9 @@ pageRoutes.get("/", async (c) => {
|
|
|
5989
5974
|
] })
|
|
5990
5975
|
] }),
|
|
5991
5976
|
/* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
|
|
5992
|
-
/* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts
|
|
5977
|
+
/* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts }),
|
|
5993
5978
|
/* @__PURE__ */ jsx("div", { className: "sidebar-share", id: "sidebar-share" }),
|
|
5994
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-footer", children:
|
|
5995
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
|
|
5996
|
-
/* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" })
|
|
5997
|
-
] })
|
|
5979
|
+
/* @__PURE__ */ jsx("div", { className: "sidebar-footer", children: footer })
|
|
5998
5980
|
] }),
|
|
5999
5981
|
/* @__PURE__ */ jsx("div", { className: "sidebar-resize", id: "sidebar-resize" }),
|
|
6000
5982
|
/* @__PURE__ */ jsx("main", { className: "main-content", children: [
|
|
@@ -6045,7 +6027,27 @@ pageRoutes.get("/", async (c) => {
|
|
|
6045
6027
|
] })
|
|
6046
6028
|
] })
|
|
6047
6029
|
] })
|
|
6048
|
-
] })
|
|
6030
|
+
] });
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
// src/routes/pages.tsx
|
|
6034
|
+
init_queries();
|
|
6035
|
+
var pageRoutes = new Hono14();
|
|
6036
|
+
pageRoutes.get("/", async (c) => {
|
|
6037
|
+
const reviewId = c.get("reviewId");
|
|
6038
|
+
const review = await getReview(reviewId);
|
|
6039
|
+
if (!review) return c.text("Review not found", 404);
|
|
6040
|
+
const files = await getReviewFiles(reviewId);
|
|
6041
|
+
const annotationCounts = {};
|
|
6042
|
+
for (const f of files) {
|
|
6043
|
+
const anns = await getAnnotationsForFile(f.id);
|
|
6044
|
+
annotationCounts[f.id] = anns.length;
|
|
6045
|
+
}
|
|
6046
|
+
const footer = /* @__PURE__ */ jsx(Fragment, { children: [
|
|
6047
|
+
/* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
|
|
6048
|
+
/* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" })
|
|
6049
|
+
] });
|
|
6050
|
+
const html = /* @__PURE__ */ jsx(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx(ReviewShell, { reviewId, review, files, annotationCounts, staleCounts: {}, footer }) });
|
|
6049
6051
|
return c.html(html.toString());
|
|
6050
6052
|
});
|
|
6051
6053
|
pageRoutes.get("/file/:fileId", async (c) => {
|
|
@@ -6055,7 +6057,7 @@ pageRoutes.get("/file/:fileId", async (c) => {
|
|
|
6055
6057
|
const view = c.req.query("view");
|
|
6056
6058
|
const file = await getReviewFile(fileId);
|
|
6057
6059
|
if (!file) return c.text("File not found", 404);
|
|
6058
|
-
const diff =
|
|
6060
|
+
const diff = parseDiffData(file.diff_data) ?? {};
|
|
6059
6061
|
if (view === "rendered" && isSvgFile(file.file_path)) {
|
|
6060
6062
|
const repoRoot = c.get("repoRoot");
|
|
6061
6063
|
const review = await getReview(file.review_id);
|
|
@@ -6119,7 +6121,7 @@ pageRoutes.get("/file-raw", (c) => {
|
|
|
6119
6121
|
const repoRoot = c.get("repoRoot");
|
|
6120
6122
|
let content;
|
|
6121
6123
|
try {
|
|
6122
|
-
content = readFileSync11(
|
|
6124
|
+
content = readFileSync11(resolve6(repoRoot, filePath), "utf-8");
|
|
6123
6125
|
} catch {
|
|
6124
6126
|
return c.text("File not found", 404);
|
|
6125
6127
|
}
|
|
@@ -6160,82 +6162,12 @@ pageRoutes.get("/review/:reviewId", async (c) => {
|
|
|
6160
6162
|
const anns = await getAnnotationsForFile(f.id);
|
|
6161
6163
|
annotationCounts[f.id] = anns.length;
|
|
6162
6164
|
}
|
|
6163
|
-
const
|
|
6164
|
-
/* @__PURE__ */ jsx("
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
] })
|
|
6170
|
-
] }),
|
|
6171
|
-
/* @__PURE__ */ jsx("div", { className: "review-body", children: [
|
|
6172
|
-
/* @__PURE__ */ jsx("aside", { className: "sidebar", children: [
|
|
6173
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-header", children: [
|
|
6174
|
-
/* @__PURE__ */ jsx("h2", { children: review.repo_name }),
|
|
6175
|
-
/* @__PURE__ */ jsx("span", { className: "review-mode", children: [
|
|
6176
|
-
review.mode,
|
|
6177
|
-
review.mode_args !== null && review.mode_args !== "" ? `: ${review.mode_args}` : ""
|
|
6178
|
-
] })
|
|
6179
|
-
] }),
|
|
6180
|
-
/* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
|
|
6181
|
-
/* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts: {} }),
|
|
6182
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-share", id: "sidebar-share" }),
|
|
6183
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-footer", children: [
|
|
6184
|
-
review.status === "completed" ? /* @__PURE__ */ jsx("button", { className: "btn btn-primary", id: "reopen-review", children: "Reopen Review" }) : /* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
|
|
6185
|
-
/* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" }),
|
|
6186
|
-
/* @__PURE__ */ jsx("a", { href: "/", className: "btn btn-sm btn-link", children: "Back to current review" })
|
|
6187
|
-
] })
|
|
6188
|
-
] }),
|
|
6189
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-resize", id: "sidebar-resize" }),
|
|
6190
|
-
/* @__PURE__ */ jsx("main", { className: "main-content", children: [
|
|
6191
|
-
/* @__PURE__ */ jsx("div", { className: "welcome-message", children: [
|
|
6192
|
-
/* @__PURE__ */ jsx("h3", { children: "Select a file to begin reviewing" }),
|
|
6193
|
-
/* @__PURE__ */ jsx("p", { children: [
|
|
6194
|
-
files.length,
|
|
6195
|
-
" file(s) to review"
|
|
6196
|
-
] }),
|
|
6197
|
-
/* @__PURE__ */ jsx("p", { className: "progress-summary", id: "progress-summary" })
|
|
6198
|
-
] }),
|
|
6199
|
-
/* @__PURE__ */ jsx("div", { className: "diff-nav-bar", id: "diff-nav-bar", style: "display:none", children: [
|
|
6200
|
-
/* @__PURE__ */ jsx("button", { className: "nav-btn disabled", id: "nav-back-btn", disabled: true, title: "Back", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) }) }),
|
|
6201
|
-
/* @__PURE__ */ jsx("button", { className: "nav-btn disabled", id: "nav-forward-btn", disabled: true, title: "Forward", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }) }) }),
|
|
6202
|
-
/* @__PURE__ */ jsx("span", { className: "nav-file-path", id: "nav-file-path" })
|
|
6203
|
-
] }),
|
|
6204
|
-
/* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
|
|
6205
|
-
/* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
|
|
6206
|
-
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-svg-toggle", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
|
|
6207
|
-
/* @__PURE__ */ jsx("button", { className: "segment active", "data-svg-mode": "code", children: "Code" }),
|
|
6208
|
-
/* @__PURE__ */ jsx("button", { className: "segment", "data-svg-mode": "rendered", children: "Rendered" })
|
|
6209
|
-
] }) }),
|
|
6210
|
-
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-text", children: [
|
|
6211
|
-
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
|
|
6212
|
-
/* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
|
|
6213
|
-
/* @__PURE__ */ jsx("button", { className: "segment active", "data-diff-mode": "split", children: "Split" }),
|
|
6214
|
-
/* @__PURE__ */ jsx("button", { className: "segment", "data-diff-mode": "unified", children: "Unified" })
|
|
6215
|
-
] }),
|
|
6216
|
-
/* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "wrap-toggle", children: "Wrap" }),
|
|
6217
|
-
/* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "whitespace-toggle", children: "Ignore Whitespace" })
|
|
6218
|
-
] }),
|
|
6219
|
-
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-right", children: /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "language-btn", children: "Plain Text" }) })
|
|
6220
|
-
] }),
|
|
6221
|
-
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-image", style: "display:none", children: [
|
|
6222
|
-
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
|
|
6223
|
-
/* @__PURE__ */ jsx("button", { className: "segment active", "data-image-mode": "metadata", children: "Metadata" }),
|
|
6224
|
-
/* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "difference", children: "Difference" }),
|
|
6225
|
-
/* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "slice", children: "Slice" }),
|
|
6226
|
-
/* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "image", style: "display:none", children: "Image" })
|
|
6227
|
-
] }) }),
|
|
6228
|
-
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-right", children: [
|
|
6229
|
-
/* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "out", title: "Zoom out", children: /* @__PURE__ */ jsx(IconZoomOut, {}) }),
|
|
6230
|
-
/* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "fit", title: "Fit to view", children: /* @__PURE__ */ jsx(IconFit, {}) }),
|
|
6231
|
-
/* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "actual", title: "Actual size (1:1)", children: /* @__PURE__ */ jsx(IconActualSize, {}) }),
|
|
6232
|
-
/* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "in", title: "Zoom in", children: /* @__PURE__ */ jsx(IconZoomIn, {}) })
|
|
6233
|
-
] })
|
|
6234
|
-
] })
|
|
6235
|
-
] })
|
|
6236
|
-
] })
|
|
6237
|
-
] })
|
|
6238
|
-
] }) });
|
|
6165
|
+
const footer = /* @__PURE__ */ jsx(Fragment, { children: [
|
|
6166
|
+
review.status === "completed" ? /* @__PURE__ */ jsx("button", { className: "btn btn-primary", id: "reopen-review", children: "Reopen Review" }) : /* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
|
|
6167
|
+
/* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" }),
|
|
6168
|
+
/* @__PURE__ */ jsx("a", { href: "/", className: "btn btn-sm btn-link", children: "Back to current review" })
|
|
6169
|
+
] });
|
|
6170
|
+
const html = /* @__PURE__ */ jsx(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx(ReviewShell, { reviewId, review, files, annotationCounts, staleCounts: {}, footer }) });
|
|
6239
6171
|
return c.html(html.toString());
|
|
6240
6172
|
});
|
|
6241
6173
|
pageRoutes.get("/history", async (c) => {
|
|
@@ -6247,8 +6179,8 @@ pageRoutes.get("/history", async (c) => {
|
|
|
6247
6179
|
});
|
|
6248
6180
|
|
|
6249
6181
|
// src/routes/theme-api.ts
|
|
6250
|
-
import { Hono as
|
|
6251
|
-
var themeApiRoutes = new
|
|
6182
|
+
import { Hono as Hono15 } from "hono";
|
|
6183
|
+
var themeApiRoutes = new Hono15();
|
|
6252
6184
|
function validateColors(colors) {
|
|
6253
6185
|
if (typeof colors !== "object" || colors === null || Array.isArray(colors)) {
|
|
6254
6186
|
return "colors must be an object";
|
|
@@ -6284,7 +6216,7 @@ themeApiRoutes.get("/active", (c) => {
|
|
|
6284
6216
|
});
|
|
6285
6217
|
themeApiRoutes.post("/active", async (c) => {
|
|
6286
6218
|
const body = await c.req.json();
|
|
6287
|
-
if (
|
|
6219
|
+
if (!isNonEmptyString(body.id)) return c.json({ error: "id must be a non-empty string" }, 400);
|
|
6288
6220
|
const theme = resolveTheme(body.id);
|
|
6289
6221
|
if (!theme) return c.json({ error: "Theme not found" }, 404);
|
|
6290
6222
|
setActiveThemeId(body.id);
|
|
@@ -6292,8 +6224,8 @@ themeApiRoutes.post("/active", async (c) => {
|
|
|
6292
6224
|
});
|
|
6293
6225
|
themeApiRoutes.post("/", async (c) => {
|
|
6294
6226
|
const body = await c.req.json();
|
|
6295
|
-
if (
|
|
6296
|
-
if (body.name !== void 0 && (
|
|
6227
|
+
if (!isNonEmptyString(body.sourceId)) return c.json({ error: "sourceId must be a non-empty string" }, 400);
|
|
6228
|
+
if (body.name !== void 0 && !isNonEmptyString(body.name)) {
|
|
6297
6229
|
return c.json({ error: "name must be a non-empty string when provided" }, 400);
|
|
6298
6230
|
}
|
|
6299
6231
|
const source = resolveTheme(body.sourceId);
|
|
@@ -6313,7 +6245,7 @@ themeApiRoutes.post("/", async (c) => {
|
|
|
6313
6245
|
themeApiRoutes.post("/:id/edit", async (c) => {
|
|
6314
6246
|
const id = c.req.param("id");
|
|
6315
6247
|
const body = await c.req.json();
|
|
6316
|
-
if (body.name !== void 0 && (
|
|
6248
|
+
if (body.name !== void 0 && !isNonEmptyString(body.name)) {
|
|
6317
6249
|
return c.json({ error: "name must be a non-empty string when provided" }, 400);
|
|
6318
6250
|
}
|
|
6319
6251
|
if (body.colors !== void 0) {
|
|
@@ -6353,7 +6285,7 @@ themeApiRoutes.patch("/:id", async (c) => {
|
|
|
6353
6285
|
return c.json({ error: "Theme not found" }, 404);
|
|
6354
6286
|
}
|
|
6355
6287
|
const body = await c.req.json();
|
|
6356
|
-
if (body.name !== void 0 && (
|
|
6288
|
+
if (body.name !== void 0 && !isNonEmptyString(body.name)) {
|
|
6357
6289
|
return c.json({ error: "name must be a non-empty string when provided" }, 400);
|
|
6358
6290
|
}
|
|
6359
6291
|
if (body.colors !== void 0) {
|
|
@@ -6382,10 +6314,10 @@ themeApiRoutes.delete("/:id", (c) => {
|
|
|
6382
6314
|
|
|
6383
6315
|
// src/server.ts
|
|
6384
6316
|
function tryServe(appFetch, port) {
|
|
6385
|
-
return new Promise((
|
|
6317
|
+
return new Promise((resolve8, reject) => {
|
|
6386
6318
|
const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
|
|
6387
6319
|
server.on("listening", () => {
|
|
6388
|
-
|
|
6320
|
+
resolve8(port);
|
|
6389
6321
|
});
|
|
6390
6322
|
server.on("error", (err) => {
|
|
6391
6323
|
if (err.code === "EADDRINUSE") {
|
|
@@ -6397,7 +6329,7 @@ function tryServe(appFetch, port) {
|
|
|
6397
6329
|
});
|
|
6398
6330
|
}
|
|
6399
6331
|
async function startServer(port, reviewId, repoRoot, options) {
|
|
6400
|
-
const app = new
|
|
6332
|
+
const app = new Hono16();
|
|
6401
6333
|
app.use("*", async (c, next) => {
|
|
6402
6334
|
c.set("reviewId", reviewId);
|
|
6403
6335
|
c.set("currentReviewId", reviewId);
|
|
@@ -6405,7 +6337,7 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
6405
6337
|
await next();
|
|
6406
6338
|
});
|
|
6407
6339
|
const selfDir = dirname2(fileURLToPath2(import.meta.url));
|
|
6408
|
-
const distDir =
|
|
6340
|
+
const distDir = existsSync8(join10(selfDir, "client", "styles.css")) ? join10(selfDir, "client") : join10(selfDir, "..", "dist", "client");
|
|
6409
6341
|
app.get("/static/styles.css", (c) => {
|
|
6410
6342
|
const css = readFileSync12(join10(distDir, "styles.css"), "utf-8");
|
|
6411
6343
|
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
@@ -6447,13 +6379,10 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
6447
6379
|
Glassbox running at ${url}
|
|
6448
6380
|
`);
|
|
6449
6381
|
try {
|
|
6450
|
-
const
|
|
6451
|
-
if (
|
|
6452
|
-
const
|
|
6453
|
-
|
|
6454
|
-
const dataDir = join10(repoRoot, ".glassbox");
|
|
6455
|
-
registerChannel(dataDir);
|
|
6456
|
-
}
|
|
6382
|
+
const globalConfig = readGlobalConfig();
|
|
6383
|
+
if (globalConfig.channelEnabled === true) {
|
|
6384
|
+
const dataDir = join10(repoRoot, ".glassbox");
|
|
6385
|
+
registerChannel(dataDir);
|
|
6457
6386
|
}
|
|
6458
6387
|
} catch {
|
|
6459
6388
|
}
|
|
@@ -6464,7 +6393,7 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
6464
6393
|
}
|
|
6465
6394
|
|
|
6466
6395
|
// src/skills.ts
|
|
6467
|
-
import { existsSync as
|
|
6396
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
|
|
6468
6397
|
import { join as join11 } from "path";
|
|
6469
6398
|
var SKILL_VERSION = 1;
|
|
6470
6399
|
function versionHeader() {
|
|
@@ -6476,14 +6405,14 @@ function parseVersionHeader(content) {
|
|
|
6476
6405
|
return parseInt(match[1], 10);
|
|
6477
6406
|
}
|
|
6478
6407
|
function updateFile(path, content) {
|
|
6479
|
-
if (
|
|
6408
|
+
if (existsSync9(path)) {
|
|
6480
6409
|
const existing = readFileSync13(path, "utf-8");
|
|
6481
6410
|
const version = parseVersionHeader(existing);
|
|
6482
6411
|
if (version !== null && version >= SKILL_VERSION) {
|
|
6483
6412
|
return false;
|
|
6484
6413
|
}
|
|
6485
6414
|
}
|
|
6486
|
-
|
|
6415
|
+
writeFileSync7(path, content, "utf-8");
|
|
6487
6416
|
return true;
|
|
6488
6417
|
}
|
|
6489
6418
|
function skillBody() {
|
|
@@ -6565,28 +6494,28 @@ function ensureWindsurfRules(cwd) {
|
|
|
6565
6494
|
function ensureSkills() {
|
|
6566
6495
|
const cwd = process.cwd();
|
|
6567
6496
|
const platforms = [];
|
|
6568
|
-
if (
|
|
6497
|
+
if (existsSync9(join11(cwd, ".claude"))) {
|
|
6569
6498
|
if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
|
|
6570
6499
|
}
|
|
6571
|
-
if (
|
|
6500
|
+
if (existsSync9(join11(cwd, ".cursor"))) {
|
|
6572
6501
|
if (ensureCursorRules(cwd)) platforms.push("Cursor");
|
|
6573
6502
|
}
|
|
6574
|
-
if (
|
|
6503
|
+
if (existsSync9(join11(cwd, ".github", "prompts")) || existsSync9(join11(cwd, ".github", "copilot-instructions.md"))) {
|
|
6575
6504
|
if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
|
|
6576
6505
|
}
|
|
6577
|
-
if (
|
|
6506
|
+
if (existsSync9(join11(cwd, ".windsurf"))) {
|
|
6578
6507
|
if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
|
|
6579
6508
|
}
|
|
6580
6509
|
return platforms;
|
|
6581
6510
|
}
|
|
6582
6511
|
|
|
6583
6512
|
// src/update-check.ts
|
|
6584
|
-
import { existsSync as
|
|
6513
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
|
|
6585
6514
|
import { get } from "https";
|
|
6586
|
-
import { homedir as
|
|
6515
|
+
import { homedir as homedir3 } from "os";
|
|
6587
6516
|
import { dirname as dirname3, join as join12 } from "path";
|
|
6588
6517
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6589
|
-
var DATA_DIR = join12(
|
|
6518
|
+
var DATA_DIR = join12(homedir3(), ".glassbox");
|
|
6590
6519
|
var CHECK_FILE = join12(DATA_DIR, "last-update-check");
|
|
6591
6520
|
var PACKAGE_NAME = "glassbox";
|
|
6592
6521
|
function getCurrentVersion() {
|
|
@@ -6600,7 +6529,7 @@ function getCurrentVersion() {
|
|
|
6600
6529
|
}
|
|
6601
6530
|
function getLastCheckDate() {
|
|
6602
6531
|
try {
|
|
6603
|
-
if (
|
|
6532
|
+
if (existsSync10(CHECK_FILE)) {
|
|
6604
6533
|
return readFileSync14(CHECK_FILE, "utf-8").trim();
|
|
6605
6534
|
}
|
|
6606
6535
|
} catch {
|
|
@@ -6609,7 +6538,7 @@ function getLastCheckDate() {
|
|
|
6609
6538
|
}
|
|
6610
6539
|
function saveCheckDate() {
|
|
6611
6540
|
mkdirSync8(DATA_DIR, { recursive: true });
|
|
6612
|
-
|
|
6541
|
+
writeFileSync8(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
|
|
6613
6542
|
}
|
|
6614
6543
|
function isFirstUseToday() {
|
|
6615
6544
|
const last = getLastCheckDate();
|
|
@@ -6618,10 +6547,10 @@ function isFirstUseToday() {
|
|
|
6618
6547
|
return last !== today;
|
|
6619
6548
|
}
|
|
6620
6549
|
function fetchLatestVersion() {
|
|
6621
|
-
return new Promise((
|
|
6550
|
+
return new Promise((resolve8) => {
|
|
6622
6551
|
const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
|
|
6623
6552
|
if (res.statusCode !== 200) {
|
|
6624
|
-
|
|
6553
|
+
resolve8(null);
|
|
6625
6554
|
return;
|
|
6626
6555
|
}
|
|
6627
6556
|
let data = "";
|
|
@@ -6630,18 +6559,18 @@ function fetchLatestVersion() {
|
|
|
6630
6559
|
});
|
|
6631
6560
|
res.on("end", () => {
|
|
6632
6561
|
try {
|
|
6633
|
-
|
|
6562
|
+
resolve8(JSON.parse(data).version);
|
|
6634
6563
|
} catch {
|
|
6635
|
-
|
|
6564
|
+
resolve8(null);
|
|
6636
6565
|
}
|
|
6637
6566
|
});
|
|
6638
6567
|
});
|
|
6639
6568
|
req.on("error", () => {
|
|
6640
|
-
|
|
6569
|
+
resolve8(null);
|
|
6641
6570
|
});
|
|
6642
6571
|
req.on("timeout", () => {
|
|
6643
6572
|
req.destroy();
|
|
6644
|
-
|
|
6573
|
+
resolve8(null);
|
|
6645
6574
|
});
|
|
6646
6575
|
});
|
|
6647
6576
|
}
|
|
@@ -6779,7 +6708,7 @@ function parseArgs(argv) {
|
|
|
6779
6708
|
port = parseInt(args[++i], 10);
|
|
6780
6709
|
break;
|
|
6781
6710
|
case "--data-dir":
|
|
6782
|
-
dataDir =
|
|
6711
|
+
dataDir = resolve7(args[++i]);
|
|
6783
6712
|
break;
|
|
6784
6713
|
case "--resume":
|
|
6785
6714
|
resume = true;
|
|
@@ -6835,7 +6764,7 @@ async function main() {
|
|
|
6835
6764
|
console.log("AI service test mode enabled \u2014 using mock AI responses");
|
|
6836
6765
|
}
|
|
6837
6766
|
if (debug) {
|
|
6838
|
-
console.log(`[debug] Build timestamp: ${"2026-
|
|
6767
|
+
console.log(`[debug] Build timestamp: ${"2026-05-02T23:58:45.229Z"}`);
|
|
6839
6768
|
}
|
|
6840
6769
|
if (projectDir !== null) {
|
|
6841
6770
|
process.chdir(projectDir);
|