@happy-nut/monacori 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/assets/icon.png +0 -0
- package/dist/app-main.js +52 -3
- package/dist/assets.d.ts +4 -0
- package/dist/assets.js +30 -0
- package/dist/build.d.ts +12 -0
- package/dist/build.js +74 -0
- package/dist/cli.d.ts +5 -33
- package/dist/cli.js +7 -3529
- package/dist/commands.d.ts +1 -0
- package/dist/commands.js +678 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/diff.d.ts +12 -0
- package/dist/diff.js +355 -0
- package/dist/git.d.ts +4 -0
- package/dist/git.js +23 -0
- package/dist/highlight.d.ts +1 -0
- package/dist/highlight.js +85 -0
- package/dist/preload.cjs +22 -0
- package/dist/preload.d.cts +1 -0
- package/dist/render.d.ts +32 -0
- package/dist/render.js +334 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +175 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +18 -0
- package/dist/util.js +144 -0
- package/dist/viewer.client.js +3343 -0
- package/dist/viewer.css +939 -0
- package/package.json +4 -2
- package/scripts/patch-electron-name.mjs +57 -0
package/dist/cli.js
CHANGED
|
@@ -1,3533 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { html as renderDiff2HtmlMarkup } from "diff2html";
|
|
10
|
-
import hljs from "highlight.js";
|
|
11
|
-
const FLOW_DIR = ".monacori";
|
|
12
|
-
const GITIGNORE_FILE = ".gitignore";
|
|
13
|
-
const CONFIG_FILE = "config.json";
|
|
14
|
-
const STATE_FILE = "state.md";
|
|
15
|
-
const DECISIONS_FILE = "decisions.md";
|
|
16
|
-
const AGENT_SNIPPET_FILE = "agent-snippet.md";
|
|
17
|
-
const SOURCE_MAX_FILE_BYTES = 220_000;
|
|
18
|
-
const SOURCE_MAX_TOTAL_BYTES = 50_000_000;
|
|
19
|
-
const SOURCE_MAX_FILES = 20000;
|
|
20
|
-
const nodeRequire = createRequire(import.meta.url);
|
|
21
|
-
const packageVersion = (() => {
|
|
22
|
-
try {
|
|
23
|
-
const pkg = nodeRequire("../package.json");
|
|
24
|
-
return typeof pkg.version === "string" ? pkg.version : "";
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return "";
|
|
28
|
-
}
|
|
29
|
-
})();
|
|
30
|
-
export function main() {
|
|
31
|
-
const rawArgs = process.argv.slice(2);
|
|
32
|
-
const [command, ...args] = rawArgs;
|
|
33
|
-
try {
|
|
34
|
-
if (!command) {
|
|
35
|
-
openCurrentRepository([]);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
if (command !== "--help" && command !== "-h" && command.startsWith("-")) {
|
|
39
|
-
openCurrentRepository(rawArgs);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
switch (command) {
|
|
43
|
-
case "init":
|
|
44
|
-
initFlow(args);
|
|
45
|
-
break;
|
|
46
|
-
case "install":
|
|
47
|
-
installFlow(args);
|
|
48
|
-
break;
|
|
49
|
-
case "check":
|
|
50
|
-
case "go":
|
|
51
|
-
runCheck(args);
|
|
52
|
-
break;
|
|
53
|
-
case "verify":
|
|
54
|
-
runVerification(args);
|
|
55
|
-
break;
|
|
56
|
-
case "diff":
|
|
57
|
-
renderDiffReview(args);
|
|
58
|
-
break;
|
|
59
|
-
case "app":
|
|
60
|
-
case "review":
|
|
61
|
-
launchReviewApp(args);
|
|
62
|
-
break;
|
|
63
|
-
case "open":
|
|
64
|
-
openCurrentRepository(args);
|
|
65
|
-
break;
|
|
66
|
-
case "status":
|
|
67
|
-
printStatus();
|
|
68
|
-
break;
|
|
69
|
-
case "report":
|
|
70
|
-
recordReport(args);
|
|
71
|
-
break;
|
|
72
|
-
case "--help":
|
|
73
|
-
case "-h":
|
|
74
|
-
case "help":
|
|
75
|
-
printHelp();
|
|
76
|
-
break;
|
|
77
|
-
default:
|
|
78
|
-
throw new Error(`Unknown command: ${command}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
-
console.error(`monacori: ${message}`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function initFlow(args) {
|
|
88
|
-
const force = args.includes("--force");
|
|
89
|
-
const quiet = args.includes("--quiet");
|
|
90
|
-
const root = process.cwd();
|
|
91
|
-
const flowPath = join(root, FLOW_DIR);
|
|
92
|
-
mkdirSync(flowPath, { recursive: true });
|
|
93
|
-
mkdirSync(join(flowPath, "reports"), { recursive: true });
|
|
94
|
-
mkdirSync(join(flowPath, "logs"), { recursive: true });
|
|
95
|
-
mkdirSync(join(flowPath, "diffs"), { recursive: true });
|
|
96
|
-
const config = {
|
|
97
|
-
version: 1,
|
|
98
|
-
projectName: basename(root),
|
|
99
|
-
verification: {
|
|
100
|
-
commands: detectVerificationCommands(root),
|
|
101
|
-
},
|
|
102
|
-
diff: {
|
|
103
|
-
context: 12,
|
|
104
|
-
includeUntracked: false,
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
writeIfMissing(join(flowPath, CONFIG_FILE), `${JSON.stringify(config, null, 2)}\n`, force);
|
|
108
|
-
writeIfMissing(join(flowPath, STATE_FILE), initialState(config), force);
|
|
109
|
-
writeIfMissing(join(flowPath, DECISIONS_FILE), initialDecisions(), force);
|
|
110
|
-
const ignored = ensureMonacoriGitignore(root);
|
|
111
|
-
if (!quiet) {
|
|
112
|
-
console.log(`Initialized ${FLOW_DIR}/ in ${root}`);
|
|
113
|
-
if (ignored) {
|
|
114
|
-
console.log(`Updated ${GITIGNORE_FILE} to ignore ${FLOW_DIR}/ validation artifacts.`);
|
|
115
|
-
}
|
|
116
|
-
console.log("Next: run `monacori app --include-untracked` to inspect changes, then `monacori check --include-untracked` to record verification.");
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
function installFlow(args) {
|
|
120
|
-
const force = args.includes("--force");
|
|
121
|
-
const applyAgentDocs = args.includes("--apply-agent-docs");
|
|
122
|
-
initFlow(["--quiet"]);
|
|
123
|
-
writeIfMissing(join(process.cwd(), FLOW_DIR, AGENT_SNIPPET_FILE), agentSnippet(), force);
|
|
124
|
-
if (applyAgentDocs) {
|
|
125
|
-
applyAgentDocSnippet("AGENTS.md");
|
|
126
|
-
applyAgentDocSnippet("CLAUDE.md");
|
|
127
|
-
}
|
|
128
|
-
console.log("Installed monacori validation instructions.");
|
|
129
|
-
console.log(`- ${FLOW_DIR}/${AGENT_SNIPPET_FILE}`);
|
|
130
|
-
if (applyAgentDocs) {
|
|
131
|
-
console.log("- Updated AGENTS.md / CLAUDE.md validation snippets where available.");
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
console.log(`Next: add ${FLOW_DIR}/${AGENT_SNIPPET_FILE} to your agent instructions if desired.`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function runCheck(args) {
|
|
138
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
139
|
-
printCheckHelp();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
ensureWritableFlowState();
|
|
143
|
-
const config = loadConfig();
|
|
144
|
-
const separator = args.indexOf("--");
|
|
145
|
-
const commandArgs = separator >= 0 ? args.slice(separator + 1) : [];
|
|
146
|
-
const optionArgs = separator >= 0 ? args.slice(0, separator) : args;
|
|
147
|
-
const noVerify = optionArgs.includes("--no-verify");
|
|
148
|
-
const noDiff = optionArgs.includes("--no-diff");
|
|
149
|
-
const openInBrowser = optionArgs.includes("--open");
|
|
150
|
-
const includeUntracked = optionArgs.includes("--include-untracked") || config.diff.includeUntracked;
|
|
151
|
-
const staged = optionArgs.includes("--staged");
|
|
152
|
-
const base = readOption(optionArgs, "--base");
|
|
153
|
-
const contextValue = readOption(optionArgs, "--context");
|
|
154
|
-
const context = contextValue ? parsePositiveInteger(contextValue, "--context") : config.diff.context;
|
|
155
|
-
const verification = noVerify
|
|
156
|
-
? { commands: [], failed: false, skipped: true }
|
|
157
|
-
: executeVerification(commandArgs.join(" "));
|
|
158
|
-
let review;
|
|
159
|
-
if (!noDiff) {
|
|
160
|
-
review = createDiffReview({
|
|
161
|
-
base,
|
|
162
|
-
staged,
|
|
163
|
-
includeUntracked,
|
|
164
|
-
context,
|
|
165
|
-
output: join(process.cwd(), FLOW_DIR, "diffs", `${timestampForFile()}-check.html`),
|
|
166
|
-
title: "monacori validation diff",
|
|
167
|
-
});
|
|
168
|
-
if (openInBrowser) {
|
|
169
|
-
spawnSync("open", [review.path], { stdio: "ignore" });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const reportPath = writeCheckReport({ verification, review });
|
|
173
|
-
console.log("# monacori check");
|
|
174
|
-
console.log(`Verification: ${verification.skipped ? "skipped" : verification.failed ? "failed" : "passed"}`);
|
|
175
|
-
if (verification.logPath) {
|
|
176
|
-
console.log(`Log: ${relative(process.cwd(), verification.logPath)}`);
|
|
177
|
-
}
|
|
178
|
-
if (review) {
|
|
179
|
-
console.log(`Diff review: ${relative(process.cwd(), review.path)}`);
|
|
180
|
-
console.log(`Files: ${review.files}`);
|
|
181
|
-
console.log(`Hunks: ${review.hunks}`);
|
|
182
|
-
}
|
|
183
|
-
console.log(`Report: ${relative(process.cwd(), reportPath)}`);
|
|
184
|
-
if (verification.failed) {
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
function runVerification(args) {
|
|
189
|
-
const separator = args.indexOf("--");
|
|
190
|
-
const explicitCommand = separator >= 0 ? args.slice(separator + 1).join(" ") : "";
|
|
191
|
-
const result = executeVerification(explicitCommand, { requireCommands: true });
|
|
192
|
-
if (result.logPath) {
|
|
193
|
-
console.log(`Verification log: ${relative(process.cwd(), result.logPath)}`);
|
|
194
|
-
}
|
|
195
|
-
if (result.failed) {
|
|
196
|
-
console.error("Verification failed.");
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
console.log("Verification passed.");
|
|
200
|
-
}
|
|
201
|
-
function renderDiffReview(args) {
|
|
202
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
203
|
-
printDiffHelp();
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
ensureWritableFlowState();
|
|
207
|
-
const config = loadConfig();
|
|
208
|
-
const contextValue = readOption(args, "--context");
|
|
209
|
-
const context = contextValue ? parsePositiveInteger(contextValue, "--context") : config.diff.context;
|
|
210
|
-
const base = readOption(args, "--base");
|
|
211
|
-
const staged = args.includes("--staged");
|
|
212
|
-
const includeUntracked = args.includes("--include-untracked") || config.diff.includeUntracked;
|
|
213
|
-
const openInBrowser = args.includes("--open");
|
|
214
|
-
const watch = args.includes("--watch");
|
|
215
|
-
if (watch) {
|
|
216
|
-
serveDiffWatch({
|
|
217
|
-
base,
|
|
218
|
-
staged,
|
|
219
|
-
includeUntracked,
|
|
220
|
-
context,
|
|
221
|
-
openInBrowser,
|
|
222
|
-
port: readOption(args, "--port"),
|
|
223
|
-
});
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
const output = readOption(args, "--output") ??
|
|
227
|
-
join(process.cwd(), FLOW_DIR, "diffs", `${timestampForFile()}-review.html`);
|
|
228
|
-
const result = createDiffReview({
|
|
229
|
-
base,
|
|
230
|
-
staged,
|
|
231
|
-
includeUntracked,
|
|
232
|
-
context,
|
|
233
|
-
output,
|
|
234
|
-
title: "monacori diff review",
|
|
235
|
-
});
|
|
236
|
-
if (openInBrowser) {
|
|
237
|
-
spawnSync("open", [result.path], { stdio: "ignore" });
|
|
238
|
-
}
|
|
239
|
-
console.log(`Diff review: ${relative(process.cwd(), result.path)}`);
|
|
240
|
-
console.log(`URL: ${result.url}`);
|
|
241
|
-
console.log(`Files: ${result.files}`);
|
|
242
|
-
console.log(`Hunks: ${result.hunks}`);
|
|
243
|
-
console.log("Keys: F7 next hunk, Shift+F7 previous hunk, Shift Shift search files, Cmd/Ctrl+E recent files, Cmd/Ctrl+Down jump to symbol.");
|
|
244
|
-
}
|
|
245
|
-
function launchReviewApp(args) {
|
|
246
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
247
|
-
printAppHelp();
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
ensureWritableFlowState();
|
|
251
|
-
const config = loadConfig();
|
|
252
|
-
const contextValue = readOption(args, "--context");
|
|
253
|
-
const context = contextValue ? parsePositiveInteger(contextValue, "--context") : config.diff.context;
|
|
254
|
-
const appArgs = [
|
|
255
|
-
appMainPath(),
|
|
256
|
-
"--cwd",
|
|
257
|
-
process.cwd(),
|
|
258
|
-
"--context",
|
|
259
|
-
String(context),
|
|
260
|
-
];
|
|
261
|
-
const base = readOption(args, "--base");
|
|
262
|
-
if (base)
|
|
263
|
-
appArgs.push("--base", base);
|
|
264
|
-
if (args.includes("--staged"))
|
|
265
|
-
appArgs.push("--staged");
|
|
266
|
-
if (args.includes("--include-untracked") || config.diff.includeUntracked)
|
|
267
|
-
appArgs.push("--include-untracked");
|
|
268
|
-
if (args.includes("--no-watch"))
|
|
269
|
-
appArgs.push("--no-watch");
|
|
270
|
-
const electronBinary = resolveElectronBinary();
|
|
271
|
-
if (args.includes("--foreground")) {
|
|
272
|
-
const result = spawnSync(electronBinary, appArgs, { stdio: "inherit" });
|
|
273
|
-
process.exit(result.status ?? 0);
|
|
274
|
-
}
|
|
275
|
-
const child = spawn(electronBinary, appArgs, {
|
|
276
|
-
detached: true,
|
|
277
|
-
stdio: "ignore",
|
|
278
|
-
});
|
|
279
|
-
child.unref();
|
|
280
|
-
console.log("Opened monacori review app.");
|
|
281
|
-
}
|
|
282
|
-
function openCurrentRepository(args) {
|
|
283
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
284
|
-
printOpenHelp();
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
const appArgs = args.filter((arg) => arg !== "--tracked-only");
|
|
288
|
-
if (!args.includes("--tracked-only") && !args.includes("--staged") && !args.includes("--include-untracked")) {
|
|
289
|
-
appArgs.push("--include-untracked");
|
|
290
|
-
}
|
|
291
|
-
launchReviewApp(appArgs);
|
|
292
|
-
}
|
|
293
|
-
function resolveElectronBinary() {
|
|
294
|
-
const electronModule = nodeRequire("electron");
|
|
295
|
-
if (typeof electronModule === "string") {
|
|
296
|
-
return electronModule;
|
|
297
|
-
}
|
|
298
|
-
if (electronModule && typeof electronModule === "object" && "default" in electronModule) {
|
|
299
|
-
const value = electronModule.default;
|
|
300
|
-
if (typeof value === "string") {
|
|
301
|
-
return value;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
throw new Error("Electron runtime is not available. Run `npm install` and try again.");
|
|
305
|
-
}
|
|
306
|
-
function appMainPath() {
|
|
307
|
-
return join(dirname(fileURLToPath(import.meta.url)), "app-main.js");
|
|
308
|
-
}
|
|
309
|
-
function printStatus() {
|
|
310
|
-
ensureInitialized();
|
|
311
|
-
const config = loadConfig();
|
|
312
|
-
const git = readGitSnapshot(process.cwd());
|
|
313
|
-
const reports = listRecentFiles(join(process.cwd(), FLOW_DIR, "reports"), 5);
|
|
314
|
-
const logs = listRecentFiles(join(process.cwd(), FLOW_DIR, "logs"), 5);
|
|
315
|
-
console.log(`# ${config.projectName} validation status`);
|
|
316
|
-
console.log("");
|
|
317
|
-
console.log(`Branch: ${git.branch || "(unknown)"}`);
|
|
318
|
-
console.log("");
|
|
319
|
-
console.log("## Git status");
|
|
320
|
-
console.log(git.status || "clean");
|
|
321
|
-
console.log("");
|
|
322
|
-
console.log("## Diff stat");
|
|
323
|
-
console.log(git.diffStat || "no diff");
|
|
324
|
-
console.log("");
|
|
325
|
-
console.log("## Verification commands");
|
|
326
|
-
const commands = getVerificationCommands(config);
|
|
327
|
-
if (commands.length === 0) {
|
|
328
|
-
console.log("none configured");
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
for (const command of commands) {
|
|
332
|
-
console.log(`- ${command}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
console.log("");
|
|
336
|
-
console.log("## Recent reports");
|
|
337
|
-
console.log(reports.length === 0 ? "none" : reports.map((path) => `- ${relative(process.cwd(), path)}`).join("\n"));
|
|
338
|
-
console.log("");
|
|
339
|
-
console.log("## Recent logs");
|
|
340
|
-
console.log(logs.length === 0 ? "none" : logs.map((path) => `- ${relative(process.cwd(), path)}`).join("\n"));
|
|
341
|
-
}
|
|
342
|
-
function recordReport(args) {
|
|
343
|
-
ensureWritableFlowState();
|
|
344
|
-
const file = readOption(args, "--file");
|
|
345
|
-
const label = readOption(args, "--label") ?? "manual";
|
|
346
|
-
const body = file ? readFileSync(file, "utf8") : readStdin();
|
|
347
|
-
if (body.trim().length === 0) {
|
|
348
|
-
throw new Error("No report content provided. Pass --file or pipe report text on stdin.");
|
|
349
|
-
}
|
|
350
|
-
const timestamp = timestampForFile();
|
|
351
|
-
const reportDir = join(process.cwd(), FLOW_DIR, "reports");
|
|
352
|
-
mkdirSync(reportDir, { recursive: true });
|
|
353
|
-
const reportPath = join(reportDir, `${timestamp}-${sanitizeFilePart(label)}.md`);
|
|
354
|
-
writeFileSync(reportPath, [
|
|
355
|
-
`# Monacori Report: ${label}`,
|
|
356
|
-
"",
|
|
357
|
-
`Recorded: ${new Date().toISOString()}`,
|
|
358
|
-
"",
|
|
359
|
-
body.trim(),
|
|
360
|
-
"",
|
|
361
|
-
].join("\n"));
|
|
362
|
-
appendToState(`\n## Report ${timestamp} (${label})\n\n${summarizeForState(body)}\n`);
|
|
363
|
-
console.log(`Recorded ${relative(process.cwd(), reportPath)}`);
|
|
364
|
-
}
|
|
365
|
-
function executeVerification(explicitCommand = "", options = {}) {
|
|
366
|
-
ensureWritableFlowState();
|
|
367
|
-
const config = loadConfig();
|
|
368
|
-
const commands = explicitCommand.trim() ? [explicitCommand.trim()] : getVerificationCommands(config);
|
|
369
|
-
if (commands.length === 0) {
|
|
370
|
-
if (options.requireCommands) {
|
|
371
|
-
throw new Error(`No verification commands found. Add them to ${FLOW_DIR}/${CONFIG_FILE} or pass \`-- <command>\`.`);
|
|
372
|
-
}
|
|
373
|
-
return { commands: [], failed: false, skipped: true };
|
|
374
|
-
}
|
|
375
|
-
const logPath = join(process.cwd(), FLOW_DIR, "logs", `verify-${timestampForFile()}.log`);
|
|
376
|
-
const chunks = [];
|
|
377
|
-
let failed = false;
|
|
378
|
-
for (const command of commands) {
|
|
379
|
-
chunks.push(`$ ${command}\n`);
|
|
380
|
-
const result = spawnSync(command, {
|
|
381
|
-
cwd: process.cwd(),
|
|
382
|
-
shell: true,
|
|
383
|
-
encoding: "utf8",
|
|
384
|
-
env: process.env,
|
|
385
|
-
maxBuffer: 1024 * 1024 * 100,
|
|
386
|
-
});
|
|
387
|
-
chunks.push(result.stdout ?? "");
|
|
388
|
-
chunks.push(result.stderr ?? "");
|
|
389
|
-
chunks.push(`\nexit: ${result.status ?? 1}\n\n`);
|
|
390
|
-
if ((result.status ?? 1) !== 0) {
|
|
391
|
-
failed = true;
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
writeFileSync(logPath, chunks.join(""));
|
|
396
|
-
return { commands, failed, skipped: false, logPath };
|
|
397
|
-
}
|
|
398
|
-
function writeCheckReport(input) {
|
|
399
|
-
const timestamp = timestampForFile();
|
|
400
|
-
const git = readGitSnapshot(process.cwd());
|
|
401
|
-
const reportDir = join(process.cwd(), FLOW_DIR, "reports");
|
|
402
|
-
mkdirSync(reportDir, { recursive: true });
|
|
403
|
-
const reportPath = join(reportDir, `${timestamp}-check.md`);
|
|
404
|
-
const verificationStatus = input.verification.skipped
|
|
405
|
-
? "skipped"
|
|
406
|
-
: input.verification.failed
|
|
407
|
-
? "failed"
|
|
408
|
-
: "passed";
|
|
409
|
-
const report = [
|
|
410
|
-
"# Monacori Validation Check",
|
|
411
|
-
"",
|
|
412
|
-
`Recorded: ${new Date().toISOString()}`,
|
|
413
|
-
`Branch: ${git.branch || "(unknown)"}`,
|
|
414
|
-
`Verification: ${verificationStatus}`,
|
|
415
|
-
input.verification.logPath ? `Log: ${relative(process.cwd(), input.verification.logPath)}` : "",
|
|
416
|
-
input.review ? `Diff review: ${relative(process.cwd(), input.review.path)}` : "",
|
|
417
|
-
input.review ? `Changed files: ${input.review.files}` : "",
|
|
418
|
-
input.review ? `Changed hunks: ${input.review.hunks}` : "",
|
|
419
|
-
"",
|
|
420
|
-
"## Commands",
|
|
421
|
-
input.verification.commands.length === 0
|
|
422
|
-
? "- none"
|
|
423
|
-
: input.verification.commands.map((command) => `- \`${command}\``).join("\n"),
|
|
424
|
-
"",
|
|
425
|
-
"## Git Status",
|
|
426
|
-
codeBlock(git.status || "clean"),
|
|
427
|
-
"",
|
|
428
|
-
"## Diff Stat",
|
|
429
|
-
codeBlock(git.diffStat || "no diff"),
|
|
430
|
-
"",
|
|
431
|
-
].filter((line) => line !== "").join("\n");
|
|
432
|
-
writeFileSync(reportPath, report);
|
|
433
|
-
appendToState(`\n## Check ${timestamp}\n\n- Verification: ${verificationStatus}\n${input.review ? `- Diff review: ${relative(process.cwd(), input.review.path)}\n` : ""}`);
|
|
434
|
-
return reportPath;
|
|
435
|
-
}
|
|
436
|
-
export function buildDiffReview(input) {
|
|
437
|
-
if (!isGitRepository(process.cwd())) {
|
|
438
|
-
return {
|
|
439
|
-
html: renderNotGitRepoHtml(process.cwd()),
|
|
440
|
-
files: 0,
|
|
441
|
-
hunks: 0,
|
|
442
|
-
signature: "not-a-git-repo",
|
|
443
|
-
generatedAt: new Date().toISOString(),
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
const diffText = readUnifiedDiff({
|
|
447
|
-
base: input.base,
|
|
448
|
-
staged: input.staged,
|
|
449
|
-
context: input.context,
|
|
450
|
-
includeUntracked: input.includeUntracked,
|
|
451
|
-
});
|
|
452
|
-
const files = parseUnifiedDiff(diffText);
|
|
453
|
-
const sourceFiles = collectSourceFiles(files);
|
|
454
|
-
const fileStates = collectReviewFileStates(files, sourceFiles);
|
|
455
|
-
const httpEnvironments = collectHttpEnvironments(process.cwd());
|
|
456
|
-
const hunks = files.reduce((sum, file) => sum + file.hunks.length, 0);
|
|
457
|
-
const generatedAt = new Date().toISOString();
|
|
458
|
-
const diffHtml = renderDiff2Html(diffText);
|
|
459
|
-
const signature = createHash("sha1")
|
|
460
|
-
.update(diffText)
|
|
461
|
-
.update("\n")
|
|
462
|
-
.update(sourceFiles.map((file) => `${file.path}\0${file.size}\0${file.embedded ? file.content : file.skippedReason ?? ""}`).join("\n"))
|
|
463
|
-
.update("\n")
|
|
464
|
-
.update(JSON.stringify(httpEnvironments))
|
|
465
|
-
.digest("hex");
|
|
466
|
-
const html = renderDiffHtml({
|
|
467
|
-
files,
|
|
468
|
-
diffHtml,
|
|
469
|
-
sourceFiles,
|
|
470
|
-
fileStates,
|
|
471
|
-
httpEnvironments,
|
|
472
|
-
title: input.title,
|
|
473
|
-
subtitle: diffSubtitle(input),
|
|
474
|
-
watch: Boolean(input.watch),
|
|
475
|
-
signature,
|
|
476
|
-
generatedAt,
|
|
477
|
-
});
|
|
478
|
-
return {
|
|
479
|
-
html,
|
|
480
|
-
files: files.length,
|
|
481
|
-
hunks,
|
|
482
|
-
signature,
|
|
483
|
-
generatedAt,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
function renderDiff2Html(diffText) {
|
|
487
|
-
if (diffText.trim().length === 0) {
|
|
488
|
-
return "";
|
|
489
|
-
}
|
|
490
|
-
const markup = renderDiff2HtmlMarkup(diffText, {
|
|
491
|
-
outputFormat: "side-by-side",
|
|
492
|
-
drawFileList: false,
|
|
493
|
-
matching: "lines",
|
|
494
|
-
});
|
|
495
|
-
return highlightDiffHtml(markup);
|
|
496
|
-
}
|
|
497
|
-
function highlightDiffHtml(markup) {
|
|
498
|
-
const parts = markup.split(/(?=<div [^>]*class="d2h-file-wrapper")/);
|
|
499
|
-
if (parts.length <= 1) {
|
|
500
|
-
return markup;
|
|
501
|
-
}
|
|
502
|
-
return parts
|
|
503
|
-
.map((part) => (part.includes('class="d2h-file-wrapper"') ? highlightDiffWrapper(part) : part))
|
|
504
|
-
.join("");
|
|
505
|
-
}
|
|
506
|
-
function highlightDiffWrapper(wrapper) {
|
|
507
|
-
const nameMatch = wrapper.match(/<span class="d2h-file-name">([\s\S]*?)<\/span>/);
|
|
508
|
-
const path = nameMatch ? decodeEntities(stripHtmlTags(nameMatch[1])).trim() : "";
|
|
509
|
-
const language = hljsLanguageForPath(path);
|
|
510
|
-
if (!language) {
|
|
511
|
-
return wrapper;
|
|
512
|
-
}
|
|
513
|
-
return wrapper.replace(/(<span class="d2h-code-line-ctn">)([\s\S]*?)(<\/span>\s*<\/div>)/g, (whole, open, content, close) => {
|
|
514
|
-
const highlighted = highlightCtnSegments(content, language);
|
|
515
|
-
return highlighted === null ? whole : `${open}${highlighted}${close}`;
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
// Apply hljs to a code-line container while preserving diff2html word-level
|
|
519
|
-
// change markup (e.g. <span class="d2h-change">...): tags are kept verbatim and
|
|
520
|
-
// only the text segments between them are syntax-highlighted.
|
|
521
|
-
function highlightCtnSegments(content, language) {
|
|
522
|
-
if (content.trim().length === 0) {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
|
-
if (content.indexOf("<") < 0) {
|
|
526
|
-
const text = decodeEntities(content);
|
|
527
|
-
if (text.trim().length === 0) {
|
|
528
|
-
return null;
|
|
529
|
-
}
|
|
530
|
-
try {
|
|
531
|
-
return hljs.highlight(text, { language, ignoreIllegals: true }).value;
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
let changed = false;
|
|
538
|
-
const out = content.replace(/(<[^>]+>)|([^<]+)/g, (_match, tag, text) => {
|
|
539
|
-
if (tag) {
|
|
540
|
-
return tag;
|
|
541
|
-
}
|
|
542
|
-
const decoded = decodeEntities(text);
|
|
543
|
-
if (decoded.trim().length === 0) {
|
|
544
|
-
return text;
|
|
545
|
-
}
|
|
546
|
-
try {
|
|
547
|
-
changed = true;
|
|
548
|
-
return hljs.highlight(decoded, { language, ignoreIllegals: true }).value;
|
|
549
|
-
}
|
|
550
|
-
catch {
|
|
551
|
-
return text;
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
return changed ? out : null;
|
|
555
|
-
}
|
|
556
|
-
function hljsLanguageForPath(path) {
|
|
557
|
-
if (!path) {
|
|
558
|
-
return "";
|
|
559
|
-
}
|
|
560
|
-
const lower = path.toLowerCase();
|
|
561
|
-
if (lower.endsWith(".kt") || lower.endsWith(".kts")) {
|
|
562
|
-
return "kotlin";
|
|
563
|
-
}
|
|
564
|
-
const base = languageForPath(path);
|
|
565
|
-
const mapped = base === "markup" ? "xml" : base === "text" ? "" : base;
|
|
566
|
-
return mapped && hljs.getLanguage(mapped) ? mapped : "";
|
|
567
|
-
}
|
|
568
|
-
function stripHtmlTags(value) {
|
|
569
|
-
return value.replace(/<[^>]*>/g, "");
|
|
570
|
-
}
|
|
571
|
-
function decodeEntities(value) {
|
|
572
|
-
return value
|
|
573
|
-
.replace(/&#x([0-9a-fA-F]+);/g, (_match, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
574
|
-
.replace(/&#(\d+);/g, (_match, dec) => String.fromCodePoint(parseInt(dec, 10)))
|
|
575
|
-
.replace(/</g, "<")
|
|
576
|
-
.replace(/>/g, ">")
|
|
577
|
-
.replace(/"/g, "\"")
|
|
578
|
-
.replace(/&/g, "&");
|
|
579
|
-
}
|
|
580
|
-
function isGitRepository(root) {
|
|
581
|
-
const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
582
|
-
cwd: root,
|
|
583
|
-
encoding: "utf8",
|
|
584
|
-
});
|
|
585
|
-
return result.status === 0 && (result.stdout ?? "").trim() === "true";
|
|
586
|
-
}
|
|
587
|
-
function renderNotGitRepoHtml(root) {
|
|
588
|
-
return [
|
|
589
|
-
"<!doctype html>",
|
|
590
|
-
'<html lang="en">',
|
|
591
|
-
"<head>",
|
|
592
|
-
'<meta charset="utf-8">',
|
|
593
|
-
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
594
|
-
"<title>monacori</title>",
|
|
595
|
-
"<style>",
|
|
596
|
-
"* { box-sizing: border-box; }",
|
|
597
|
-
"body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #2b2b2b; color: #a9b7c6; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }",
|
|
598
|
-
".card { max-width: 560px; padding: 40px; text-align: center; }",
|
|
599
|
-
".card .badge { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: #808080; }",
|
|
600
|
-
".card h1 { font-size: 22px; margin: 10px 0 16px; color: #ffc66d; }",
|
|
601
|
-
".card p { font-size: 14px; line-height: 1.7; margin: 10px 0; }",
|
|
602
|
-
".card code { background: #3c3f41; padding: 3px 9px; border-radius: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #6a8759; }",
|
|
603
|
-
".card .path { color: #808080; font-size: 12px; word-break: break-all; margin-top: 22px; }",
|
|
604
|
-
"</style>",
|
|
605
|
-
"</head>",
|
|
606
|
-
"<body>",
|
|
607
|
-
'<div class="card">',
|
|
608
|
-
'<div class="badge">monacori</div>',
|
|
609
|
-
"<h1>Not a Git repository</h1>",
|
|
610
|
-
"<p>monacori reviews changes tracked by Git, but this folder isn't a Git repository yet.</p>",
|
|
611
|
-
"<p>Open a terminal here, run <code>git init</code>, then reopen monacori.</p>",
|
|
612
|
-
`<p class="path">${escapeHtml(root)}</p>`,
|
|
613
|
-
"</div>",
|
|
614
|
-
"</body>",
|
|
615
|
-
"</html>",
|
|
616
|
-
].join("\n");
|
|
617
|
-
}
|
|
618
|
-
function createDiffReview(input) {
|
|
619
|
-
const outputPath = resolve(input.output);
|
|
620
|
-
const build = buildDiffReview({
|
|
621
|
-
base: input.base,
|
|
622
|
-
staged: input.staged,
|
|
623
|
-
includeUntracked: input.includeUntracked,
|
|
624
|
-
context: input.context,
|
|
625
|
-
title: input.title,
|
|
626
|
-
});
|
|
627
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
628
|
-
writeFileSync(outputPath, build.html);
|
|
629
|
-
return {
|
|
630
|
-
path: outputPath,
|
|
631
|
-
url: pathToFileURL(outputPath).href,
|
|
632
|
-
files: build.files,
|
|
633
|
-
hunks: build.hunks,
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
function serveDiffWatch(input) {
|
|
637
|
-
const host = "127.0.0.1";
|
|
638
|
-
const port = input.port ? parsePositiveInteger(input.port, "--port") : 0;
|
|
639
|
-
const build = () => buildDiffReview({
|
|
640
|
-
base: input.base,
|
|
641
|
-
staged: input.staged,
|
|
642
|
-
includeUntracked: input.includeUntracked,
|
|
643
|
-
context: input.context,
|
|
644
|
-
title: "monacori live diff",
|
|
645
|
-
watch: true,
|
|
646
|
-
});
|
|
647
|
-
const server = createServer((request, response) => {
|
|
648
|
-
const requestUrl = new URL(request.url ?? "/", `http://${host}`);
|
|
649
|
-
try {
|
|
650
|
-
if (requestUrl.pathname === "/__ai_flow_state") {
|
|
651
|
-
const latest = build();
|
|
652
|
-
writeHttpJson(response, {
|
|
653
|
-
signature: latest.signature,
|
|
654
|
-
generatedAt: latest.generatedAt,
|
|
655
|
-
files: latest.files,
|
|
656
|
-
hunks: latest.hunks,
|
|
657
|
-
});
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (requestUrl.pathname === "/__http_send" && request.method === "POST") {
|
|
661
|
-
void handleHttpProxy(request, response);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
if (requestUrl.pathname === "/" || requestUrl.pathname === "/review") {
|
|
665
|
-
const latest = build();
|
|
666
|
-
writeHttp(response, 200, "text/html; charset=utf-8", latest.html);
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
writeHttp(response, 404, "text/plain; charset=utf-8", "Not found\n");
|
|
670
|
-
}
|
|
671
|
-
catch (error) {
|
|
672
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
673
|
-
writeHttp(response, 500, "text/plain; charset=utf-8", `${message}\n`);
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
server.on("error", (error) => {
|
|
677
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
678
|
-
console.error(`monacori: diff watch server failed: ${message}`);
|
|
679
|
-
process.exit(1);
|
|
680
|
-
});
|
|
681
|
-
server.listen(port, host, () => {
|
|
682
|
-
const address = server.address();
|
|
683
|
-
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
684
|
-
const url = `http://${host}:${actualPort}/review`;
|
|
685
|
-
console.log(`Live diff review: ${url}`);
|
|
686
|
-
console.log("Watching working tree. Press Ctrl+C to stop.");
|
|
687
|
-
if (input.openInBrowser) {
|
|
688
|
-
spawnSync("open", [url], { stdio: "ignore" });
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
// Performs an HTTP request on behalf of the sandboxed renderer. Used by both the
|
|
693
|
-
// Electron IPC handler (app-main.ts) and the browser-mode proxy below.
|
|
694
|
-
export async function performHttpRequest(request) {
|
|
695
|
-
const startedAt = Date.now();
|
|
696
|
-
const method = (request.method || "GET").toUpperCase();
|
|
697
|
-
try {
|
|
698
|
-
const hasBody = typeof request.body === "string" && request.body.length > 0
|
|
699
|
-
&& method !== "GET" && method !== "HEAD";
|
|
700
|
-
const response = await fetch(request.url, {
|
|
701
|
-
method,
|
|
702
|
-
headers: request.headers ?? {},
|
|
703
|
-
body: hasBody ? request.body : undefined,
|
|
704
|
-
redirect: "follow",
|
|
705
|
-
});
|
|
706
|
-
const body = await response.text();
|
|
707
|
-
const headers = {};
|
|
708
|
-
response.headers.forEach((value, key) => {
|
|
709
|
-
headers[key] = value;
|
|
710
|
-
});
|
|
711
|
-
return {
|
|
712
|
-
ok: true,
|
|
713
|
-
status: response.status,
|
|
714
|
-
statusText: response.statusText,
|
|
715
|
-
headers,
|
|
716
|
-
body,
|
|
717
|
-
durationMs: Date.now() - startedAt,
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
catch (error) {
|
|
721
|
-
return {
|
|
722
|
-
ok: false,
|
|
723
|
-
error: error instanceof Error ? error.message : String(error),
|
|
724
|
-
durationMs: Date.now() - startedAt,
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
async function handleHttpProxy(request, response) {
|
|
729
|
-
try {
|
|
730
|
-
const chunks = [];
|
|
731
|
-
for await (const chunk of request) {
|
|
732
|
-
chunks.push(chunk);
|
|
733
|
-
}
|
|
734
|
-
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
735
|
-
const result = await performHttpRequest(payload);
|
|
736
|
-
writeHttpJson(response, result);
|
|
737
|
-
}
|
|
738
|
-
catch (error) {
|
|
739
|
-
writeHttpJson(response, {
|
|
740
|
-
ok: false,
|
|
741
|
-
error: error instanceof Error ? error.message : String(error),
|
|
742
|
-
durationMs: 0,
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
function renderDiffHtml(input) {
|
|
747
|
-
const totalHunks = input.files.reduce((sum, file) => sum + file.hunks.length, 0);
|
|
748
|
-
const fileNav = renderDiffTree(input.files);
|
|
749
|
-
const sourceNav = renderSourceTree(input.sourceFiles);
|
|
750
|
-
const embeddedFiles = input.sourceFiles.filter((file) => file.embedded).length;
|
|
751
|
-
return [
|
|
752
|
-
"<!doctype html>",
|
|
753
|
-
'<html lang="en">',
|
|
754
|
-
"<head>",
|
|
755
|
-
'<meta charset="utf-8">',
|
|
756
|
-
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
757
|
-
'<link rel="icon" href="data:,">',
|
|
758
|
-
`<title>${escapeHtml(input.title)}</title>`,
|
|
759
|
-
"<style>",
|
|
760
|
-
diff2HtmlCss(),
|
|
761
|
-
diffCss(),
|
|
762
|
-
"</style>",
|
|
763
|
-
"</head>",
|
|
764
|
-
"<body>",
|
|
765
|
-
'<aside class="sidebar" aria-label="Review navigation">',
|
|
766
|
-
'<label class="search"><span class="visually-hidden">Search</span><input id="review-search" type="search" placeholder="Search files or code"></label>',
|
|
767
|
-
'<div class="tabs"><button type="button" class="tab" data-tab="changes">Changes</button><button type="button" class="tab active" data-tab="files">Files</button></div>',
|
|
768
|
-
`<div class="tab-panel hidden" id="changes-panel">${fileNav}</div>`,
|
|
769
|
-
`<div class="tab-panel" id="files-panel">${sourceNav}</div>`,
|
|
770
|
-
"</aside>",
|
|
771
|
-
'<div class="sidebar-resizer" aria-hidden="true"></div>',
|
|
772
|
-
'<main class="content">',
|
|
773
|
-
'<section id="diff-view" class="hidden">',
|
|
774
|
-
'<div class="toolbar">',
|
|
775
|
-
'<div class="breadcrumb" id="diff-breadcrumb"></div>',
|
|
776
|
-
`<div class="review-status"><span>${input.files.length} files</span><span>${totalHunks} hunks</span><span>${embeddedFiles}/${input.sourceFiles.length} indexed</span><span class="live-status ${input.watch ? "watching" : ""}" id="live-status">${input.watch ? "watching" : escapeHtml(input.generatedAt ?? new Date().toISOString())}</span></div>`,
|
|
777
|
-
`<div class="counter"><span id="file-counter" class="file-counter"></span><span id="hunk-counter">0</span> / ${totalHunks}</div>`,
|
|
778
|
-
"</div>",
|
|
779
|
-
`<div id="diff2html-container" class="diff2html-container">${input.diffHtml || '<div class="empty">No diff to review.</div>'}</div>`,
|
|
780
|
-
"</section>",
|
|
781
|
-
'<section id="source-viewer" class="source-viewer">',
|
|
782
|
-
'<div class="toolbar source-toolbar">',
|
|
783
|
-
'<div class="source-file-meta"><span id="source-title">Source</span><span id="source-meta">Select a file from the Files tab.</span></div>',
|
|
784
|
-
'<button type="button" id="back-to-diff" class="plain-button">Diff</button>',
|
|
785
|
-
"</div>",
|
|
786
|
-
'<div id="source-body" class="source-body empty">Select a file from the Files tab.</div>',
|
|
787
|
-
"</section>",
|
|
788
|
-
"</main>",
|
|
789
|
-
'<div id="update-badge" class="update-badge hidden" title="npm install -g @happy-nut/monacori"></div>',
|
|
790
|
-
'<div id="quick-open" class="quick-open hidden" role="dialog" aria-modal="true" aria-label="Quick open">',
|
|
791
|
-
'<div class="quick-open-panel">',
|
|
792
|
-
'<div class="quick-open-title"><span id="quick-open-mode">Search files</span></div>',
|
|
793
|
-
'<input id="quick-open-input" type="search" autocomplete="off" spellcheck="false" placeholder="Search files">',
|
|
794
|
-
'<div id="quick-open-results" class="quick-open-results"></div>',
|
|
795
|
-
'<div id="quick-open-preview" class="quick-open-preview"></div>',
|
|
796
|
-
"</div>",
|
|
797
|
-
"</div>",
|
|
798
|
-
`<script type="application/json" id="review-meta" data-watch="${input.watch ? "true" : "false"}" data-signature="${escapeAttr(input.signature ?? "")}" data-generated-at="${escapeAttr(input.generatedAt ?? "")}">{}</script>`,
|
|
799
|
-
`<script type="application/json" id="source-files-data">${jsonForScript(input.sourceFiles)}</script>`,
|
|
800
|
-
`<script type="application/json" id="file-state-data">${jsonForScript(input.fileStates)}</script>`,
|
|
801
|
-
`<script type="application/json" id="http-env-data">${jsonForScript(input.httpEnvironments)}</script>`,
|
|
802
|
-
`<script>window.__MONACORI_VERSION__=${JSON.stringify(packageVersion)};</script>`,
|
|
803
|
-
"<script>",
|
|
804
|
-
diffScript(),
|
|
805
|
-
"</script>",
|
|
806
|
-
"</body>",
|
|
807
|
-
"</html>",
|
|
808
|
-
].join("\n");
|
|
809
|
-
}
|
|
810
|
-
function renderDiffTree(files) {
|
|
811
|
-
if (files.length === 0) {
|
|
812
|
-
return '<div class="empty-nav">No changed files</div>';
|
|
813
|
-
}
|
|
814
|
-
let hunkIndex = 0;
|
|
815
|
-
const rows = files.map((file, fileIndex) => {
|
|
816
|
-
const firstHunk = hunkIndex;
|
|
817
|
-
hunkIndex += file.hunks.length;
|
|
818
|
-
let adds = 0;
|
|
819
|
-
let dels = 0;
|
|
820
|
-
for (const hunk of file.hunks) {
|
|
821
|
-
for (const line of hunk.lines) {
|
|
822
|
-
if (line.kind === "add")
|
|
823
|
-
adds += 1;
|
|
824
|
-
else if (line.kind === "delete")
|
|
825
|
-
dels += 1;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
const slash = file.displayPath.lastIndexOf("/");
|
|
829
|
-
const name = slash >= 0 ? file.displayPath.slice(slash + 1) : file.displayPath;
|
|
830
|
-
const dir = slash > 0 ? file.displayPath.slice(0, slash) : "";
|
|
831
|
-
return [
|
|
832
|
-
`<a class="file-link change-row" href="#file-${fileIndex}" data-hunk="${firstHunk}" data-file="${escapeAttr(file.displayPath)}">`,
|
|
833
|
-
`<span class="status status-${escapeAttr(file.status)}">${escapeHtml(file.status)}</span>`,
|
|
834
|
-
`<span class="change-name"><span class="path" title="${escapeAttr(file.displayPath)}">${escapeHtml(name)}</span>${dir ? `<span class="change-dir">${escapeHtml(dir)}</span>` : ""}</span>`,
|
|
835
|
-
`<span class="diffstat">${adds ? `<span class="adds">+${adds}</span>` : ""}${dels ? `<span class="dels">−${dels}</span>` : ""}</span>`,
|
|
836
|
-
"</a>",
|
|
837
|
-
].join("");
|
|
838
|
-
});
|
|
839
|
-
return `<nav class="tree changes-flat">${rows.join("")}</nav>`;
|
|
840
|
-
}
|
|
841
|
-
function renderTreeChildren(node, depth) {
|
|
842
|
-
return Array.from(node.children.values())
|
|
843
|
-
.sort((a, b) => {
|
|
844
|
-
if (Boolean(a.file) !== Boolean(b.file)) {
|
|
845
|
-
return a.file ? 1 : -1;
|
|
846
|
-
}
|
|
847
|
-
return a.name.localeCompare(b.name);
|
|
848
|
-
})
|
|
849
|
-
.map((child) => renderTreeNode(child, depth))
|
|
850
|
-
.join("\n");
|
|
851
|
-
}
|
|
852
|
-
function renderTreeNode(node, depth) {
|
|
853
|
-
if (node.file) {
|
|
854
|
-
const file = node.file;
|
|
855
|
-
return [
|
|
856
|
-
`<a class="file-link tree-file" href="#file-${file.index}" data-hunk="${file.firstHunk}" data-file="${escapeAttr(file.displayPath)}" style="--depth:${depth}">`,
|
|
857
|
-
`<span class="status status-${escapeAttr(file.status)}">${escapeHtml(file.status)}</span>`,
|
|
858
|
-
`<span class="path" title="${escapeAttr(file.displayPath)}">${escapeHtml(node.name)}</span>`,
|
|
859
|
-
`<span class="count">${file.hunkCount}</span>`,
|
|
860
|
-
"</a>",
|
|
861
|
-
].join("");
|
|
862
|
-
}
|
|
863
|
-
let labelNode = node;
|
|
864
|
-
const names = [node.name];
|
|
865
|
-
for (;;) {
|
|
866
|
-
const entries = Array.from(labelNode.children.values());
|
|
867
|
-
if (entries.length !== 1 || entries[0].file)
|
|
868
|
-
break;
|
|
869
|
-
names.push(entries[0].name);
|
|
870
|
-
labelNode = entries[0];
|
|
871
|
-
}
|
|
872
|
-
return [
|
|
873
|
-
`<details class="tree-dir" open style="--depth:${depth}">`,
|
|
874
|
-
`<summary><span class="folder-icon">v</span><span class="path">${escapeHtml(names.join("/"))}</span></summary>`,
|
|
875
|
-
renderTreeChildren(labelNode, depth + 1),
|
|
876
|
-
"</details>",
|
|
877
|
-
].join("\n");
|
|
878
|
-
}
|
|
879
|
-
function renderSourceTree(files) {
|
|
880
|
-
if (files.length === 0) {
|
|
881
|
-
return '<div class="empty-nav">No source files indexed</div>';
|
|
882
|
-
}
|
|
883
|
-
const root = { name: "", path: "", children: new Map() };
|
|
884
|
-
files.forEach((file) => {
|
|
885
|
-
const parts = file.path.split("/").filter(Boolean);
|
|
886
|
-
let node = root;
|
|
887
|
-
let currentPath = "";
|
|
888
|
-
for (const part of parts.slice(0, -1)) {
|
|
889
|
-
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
890
|
-
let child = node.children.get(part);
|
|
891
|
-
if (!child) {
|
|
892
|
-
child = { name: part, path: currentPath, children: new Map() };
|
|
893
|
-
node.children.set(part, child);
|
|
894
|
-
}
|
|
895
|
-
node = child;
|
|
896
|
-
}
|
|
897
|
-
const leafName = parts[parts.length - 1] ?? file.path;
|
|
898
|
-
node.children.set(`${leafName}\0${file.path}`, {
|
|
899
|
-
name: leafName,
|
|
900
|
-
path: file.path,
|
|
901
|
-
children: new Map(),
|
|
902
|
-
file,
|
|
903
|
-
});
|
|
904
|
-
});
|
|
905
|
-
return `<nav class="tree source-tree">${renderSourceChildren(root, 0)}</nav>`;
|
|
906
|
-
}
|
|
907
|
-
function renderSourceChildren(node, depth) {
|
|
908
|
-
return Array.from(node.children.values())
|
|
909
|
-
.sort((a, b) => {
|
|
910
|
-
if (Boolean(a.file) !== Boolean(b.file)) {
|
|
911
|
-
return a.file ? 1 : -1;
|
|
912
|
-
}
|
|
913
|
-
return a.name.localeCompare(b.name);
|
|
914
|
-
})
|
|
915
|
-
.map((child) => renderSourceNode(child, depth))
|
|
916
|
-
.join("\n");
|
|
917
|
-
}
|
|
918
|
-
function renderSourceNode(node, depth) {
|
|
919
|
-
if (node.file) {
|
|
920
|
-
const file = node.file;
|
|
921
|
-
const flags = [
|
|
922
|
-
file.changed ? "changed" : "",
|
|
923
|
-
file.embedded ? "" : "not embedded",
|
|
924
|
-
].filter(Boolean).join(" | ");
|
|
925
|
-
return [
|
|
926
|
-
`<button type="button" class="file-link source-link tree-file" data-source-file="${escapeAttr(file.path)}" style="--depth:${depth}">`,
|
|
927
|
-
`<span class="status status-${file.changed ? "modified" : "source"}">${file.changed ? "diff" : "file"}</span>`,
|
|
928
|
-
`<span class="path" title="${escapeAttr(file.path)}">${escapeHtml(node.name)}</span>`,
|
|
929
|
-
`<span class="count">${escapeHtml(flags || file.language)}</span>`,
|
|
930
|
-
"</button>",
|
|
931
|
-
].join("");
|
|
932
|
-
}
|
|
933
|
-
let labelNode = node;
|
|
934
|
-
const names = [node.name];
|
|
935
|
-
for (;;) {
|
|
936
|
-
const entries = Array.from(labelNode.children.values());
|
|
937
|
-
if (entries.length !== 1 || entries[0].file)
|
|
938
|
-
break;
|
|
939
|
-
names.push(entries[0].name);
|
|
940
|
-
labelNode = entries[0];
|
|
941
|
-
}
|
|
942
|
-
return [
|
|
943
|
-
`<details class="tree-dir source-dir" open style="--depth:${depth}">`,
|
|
944
|
-
`<summary><span class="folder-icon">v</span><span class="path">${escapeHtml(names.join("/"))}</span></summary>`,
|
|
945
|
-
renderSourceChildren(labelNode, depth + 1),
|
|
946
|
-
"</details>",
|
|
947
|
-
].join("\n");
|
|
948
|
-
}
|
|
949
|
-
function readUnifiedDiff(options) {
|
|
950
|
-
const args = ["diff", "--no-ext-diff", "--find-renames", `--unified=${options.context}`];
|
|
951
|
-
if (options.staged) {
|
|
952
|
-
args.push("--cached");
|
|
953
|
-
}
|
|
954
|
-
else {
|
|
955
|
-
args.push(options.base ?? "HEAD");
|
|
956
|
-
}
|
|
957
|
-
args.push("--");
|
|
958
|
-
const result = spawnSync("git", args, {
|
|
959
|
-
cwd: process.cwd(),
|
|
960
|
-
encoding: "utf8",
|
|
961
|
-
maxBuffer: 1024 * 1024 * 100,
|
|
962
|
-
});
|
|
963
|
-
if (result.status !== 0) {
|
|
964
|
-
throw new Error(result.stderr || "git diff failed");
|
|
965
|
-
}
|
|
966
|
-
const chunks = [result.stdout ?? ""];
|
|
967
|
-
if (options.includeUntracked && !options.staged) {
|
|
968
|
-
chunks.push(readUntrackedDiff(options.context));
|
|
969
|
-
}
|
|
970
|
-
return chunks.filter(Boolean).join("\n");
|
|
971
|
-
}
|
|
972
|
-
function readUntrackedDiff(context) {
|
|
973
|
-
const files = git(process.cwd(), ["ls-files", "--others", "--exclude-standard"])
|
|
974
|
-
.split(/\r?\n/)
|
|
975
|
-
.map((line) => line.trim())
|
|
976
|
-
.filter((line) => line && !line.startsWith(`${FLOW_DIR}/`));
|
|
977
|
-
const chunks = [];
|
|
978
|
-
for (const file of files) {
|
|
979
|
-
const absolute = join(process.cwd(), file);
|
|
980
|
-
if (!existsSync(absolute) || !statSync(absolute).isFile()) {
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
|
-
const size = statSync(absolute).size;
|
|
984
|
-
if (size > 500_000 || isLikelyBinary(absolute)) {
|
|
985
|
-
chunks.push([
|
|
986
|
-
`diff --git a/${file} b/${file}`,
|
|
987
|
-
"new file mode 100644",
|
|
988
|
-
`Binary files /dev/null and b/${file} differ`,
|
|
989
|
-
].join("\n"));
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
const content = readFileSync(absolute, "utf8");
|
|
993
|
-
const lines = content.split(/\r?\n/);
|
|
994
|
-
if (lines[lines.length - 1] === "") {
|
|
995
|
-
lines.pop();
|
|
996
|
-
}
|
|
997
|
-
const limited = context > 0 ? lines : lines;
|
|
998
|
-
chunks.push([
|
|
999
|
-
`diff --git a/${file} b/${file}`,
|
|
1000
|
-
"new file mode 100644",
|
|
1001
|
-
"--- /dev/null",
|
|
1002
|
-
`+++ b/${file}`,
|
|
1003
|
-
`@@ -0,0 +1,${limited.length} @@`,
|
|
1004
|
-
...limited.map((line) => `+${line}`),
|
|
1005
|
-
].join("\n"));
|
|
1006
|
-
}
|
|
1007
|
-
return chunks.join("\n");
|
|
1008
|
-
}
|
|
1009
|
-
function parseUnifiedDiff(content) {
|
|
1010
|
-
const files = [];
|
|
1011
|
-
let current;
|
|
1012
|
-
let hunk;
|
|
1013
|
-
let oldLine = 0;
|
|
1014
|
-
let newLine = 0;
|
|
1015
|
-
for (const line of content.split(/\r?\n/)) {
|
|
1016
|
-
if (line.startsWith("diff --git ")) {
|
|
1017
|
-
const match = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
1018
|
-
const oldPath = match?.[1] ?? "unknown";
|
|
1019
|
-
const newPath = match?.[2] ?? oldPath;
|
|
1020
|
-
current = {
|
|
1021
|
-
oldPath,
|
|
1022
|
-
newPath,
|
|
1023
|
-
displayPath: newPath === "/dev/null" ? oldPath : newPath,
|
|
1024
|
-
status: "modified",
|
|
1025
|
-
binary: false,
|
|
1026
|
-
hunks: [],
|
|
1027
|
-
};
|
|
1028
|
-
files.push(current);
|
|
1029
|
-
hunk = undefined;
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
if (!current) {
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
if (line.startsWith("new file mode ")) {
|
|
1036
|
-
current.status = "added";
|
|
1037
|
-
continue;
|
|
1038
|
-
}
|
|
1039
|
-
if (line.startsWith("deleted file mode ")) {
|
|
1040
|
-
current.status = "deleted";
|
|
1041
|
-
continue;
|
|
1042
|
-
}
|
|
1043
|
-
if (line.startsWith("rename from ")) {
|
|
1044
|
-
current.status = "renamed";
|
|
1045
|
-
current.oldPath = line.slice("rename from ".length);
|
|
1046
|
-
continue;
|
|
1047
|
-
}
|
|
1048
|
-
if (line.startsWith("rename to ")) {
|
|
1049
|
-
current.newPath = line.slice("rename to ".length);
|
|
1050
|
-
current.displayPath = current.newPath;
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
if (line.startsWith("--- ")) {
|
|
1054
|
-
current.oldPath = stripDiffPath(line.slice(4));
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
if (line.startsWith("+++ ")) {
|
|
1058
|
-
current.newPath = stripDiffPath(line.slice(4));
|
|
1059
|
-
current.displayPath = current.newPath === "/dev/null" ? current.oldPath : current.newPath;
|
|
1060
|
-
continue;
|
|
1061
|
-
}
|
|
1062
|
-
if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) {
|
|
1063
|
-
current.binary = true;
|
|
1064
|
-
continue;
|
|
1065
|
-
}
|
|
1066
|
-
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
|
|
1067
|
-
if (hunkMatch) {
|
|
1068
|
-
oldLine = Number(hunkMatch[1]);
|
|
1069
|
-
newLine = Number(hunkMatch[3]);
|
|
1070
|
-
hunk = {
|
|
1071
|
-
header: line,
|
|
1072
|
-
title: hunkMatch[5]?.trim() ?? "",
|
|
1073
|
-
oldStart: oldLine,
|
|
1074
|
-
newStart: newLine,
|
|
1075
|
-
lines: [],
|
|
1076
|
-
};
|
|
1077
|
-
current.hunks.push(hunk);
|
|
1078
|
-
continue;
|
|
1079
|
-
}
|
|
1080
|
-
if (!hunk) {
|
|
1081
|
-
continue;
|
|
1082
|
-
}
|
|
1083
|
-
if (line.startsWith("+")) {
|
|
1084
|
-
hunk.lines.push({ kind: "add", newLine, text: line.slice(1) });
|
|
1085
|
-
newLine += 1;
|
|
1086
|
-
}
|
|
1087
|
-
else if (line.startsWith("-")) {
|
|
1088
|
-
hunk.lines.push({ kind: "delete", oldLine, text: line.slice(1) });
|
|
1089
|
-
oldLine += 1;
|
|
1090
|
-
}
|
|
1091
|
-
else if (line.startsWith(" ")) {
|
|
1092
|
-
hunk.lines.push({ kind: "context", oldLine, newLine, text: line.slice(1) });
|
|
1093
|
-
oldLine += 1;
|
|
1094
|
-
newLine += 1;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
return files.filter((file) => file.binary || file.hunks.length > 0);
|
|
1098
|
-
}
|
|
1099
|
-
function collectSourceFiles(diffFiles) {
|
|
1100
|
-
const changed = new Set(diffFiles
|
|
1101
|
-
.map((file) => file.displayPath)
|
|
1102
|
-
.filter((path) => path && path !== "/dev/null"));
|
|
1103
|
-
const changedLinesByPath = new Map();
|
|
1104
|
-
for (const file of diffFiles) {
|
|
1105
|
-
if (!file.displayPath || file.displayPath === "/dev/null")
|
|
1106
|
-
continue;
|
|
1107
|
-
const nums = [];
|
|
1108
|
-
for (const hunk of file.hunks) {
|
|
1109
|
-
for (const line of hunk.lines) {
|
|
1110
|
-
if (line.kind === "add" && typeof line.newLine === "number")
|
|
1111
|
-
nums.push(line.newLine);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
changedLinesByPath.set(file.displayPath, nums);
|
|
1115
|
-
}
|
|
1116
|
-
const paths = new Set();
|
|
1117
|
-
const gitFiles = git(process.cwd(), ["ls-files", "--cached", "--others", "--exclude-standard"]);
|
|
1118
|
-
for (const file of gitFiles.split(/\r?\n/)) {
|
|
1119
|
-
const path = file.trim();
|
|
1120
|
-
if (path && isSourceCandidate(path)) {
|
|
1121
|
-
paths.add(path);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
for (const path of changed) {
|
|
1125
|
-
if (isSourceCandidate(path)) {
|
|
1126
|
-
paths.add(path);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
const sourceFiles = [];
|
|
1130
|
-
let embeddedFiles = 0;
|
|
1131
|
-
let embeddedBytes = 0;
|
|
1132
|
-
for (const path of Array.from(paths).sort((a, b) => a.localeCompare(b))) {
|
|
1133
|
-
const absolute = join(process.cwd(), path);
|
|
1134
|
-
const base = {
|
|
1135
|
-
path,
|
|
1136
|
-
name: basename(path),
|
|
1137
|
-
language: languageForPath(path),
|
|
1138
|
-
content: "",
|
|
1139
|
-
size: 0,
|
|
1140
|
-
changed: changed.has(path),
|
|
1141
|
-
embedded: false,
|
|
1142
|
-
changedLines: changedLinesByPath.get(path) || [],
|
|
1143
|
-
signature: "",
|
|
1144
|
-
};
|
|
1145
|
-
if (!existsSync(absolute)) {
|
|
1146
|
-
const skippedReason = "file is not present in the working tree";
|
|
1147
|
-
sourceFiles.push({ ...base, signature: hashText(`${path}\0missing\0${skippedReason}`), skippedReason });
|
|
1148
|
-
continue;
|
|
1149
|
-
}
|
|
1150
|
-
const stats = statSync(absolute);
|
|
1151
|
-
if (!stats.isFile()) {
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
if (isLikelyBinary(absolute)) {
|
|
1155
|
-
const skippedReason = "binary file";
|
|
1156
|
-
sourceFiles.push({ ...base, size: stats.size, signature: hashText(`${path}\0binary\0${stats.size}`), skippedReason });
|
|
1157
|
-
continue;
|
|
1158
|
-
}
|
|
1159
|
-
if (stats.size > SOURCE_MAX_FILE_BYTES) {
|
|
1160
|
-
const skippedReason = `larger than ${formatBytes(SOURCE_MAX_FILE_BYTES)}`;
|
|
1161
|
-
sourceFiles.push({ ...base, size: stats.size, signature: hashText(`${path}\0large\0${stats.size}`), skippedReason });
|
|
1162
|
-
continue;
|
|
1163
|
-
}
|
|
1164
|
-
if (embeddedFiles >= SOURCE_MAX_FILES || embeddedBytes + stats.size > SOURCE_MAX_TOTAL_BYTES) {
|
|
1165
|
-
const skippedReason = "source index budget reached";
|
|
1166
|
-
sourceFiles.push({ ...base, size: stats.size, signature: hashText(`${path}\0budget\0${stats.size}`), skippedReason });
|
|
1167
|
-
continue;
|
|
1168
|
-
}
|
|
1169
|
-
const content = readFileSync(absolute, "utf8");
|
|
1170
|
-
sourceFiles.push({
|
|
1171
|
-
...base,
|
|
1172
|
-
content,
|
|
1173
|
-
size: stats.size,
|
|
1174
|
-
embedded: true,
|
|
1175
|
-
signature: hashText(`${path}\0${content}`),
|
|
1176
|
-
});
|
|
1177
|
-
embeddedFiles += 1;
|
|
1178
|
-
embeddedBytes += stats.size;
|
|
1179
|
-
}
|
|
1180
|
-
return sourceFiles;
|
|
1181
|
-
}
|
|
1182
|
-
function collectReviewFileStates(diffFiles, sourceFiles) {
|
|
1183
|
-
const states = new Map();
|
|
1184
|
-
for (const file of sourceFiles) {
|
|
1185
|
-
states.set(file.path, file.signature);
|
|
1186
|
-
}
|
|
1187
|
-
for (const file of diffFiles) {
|
|
1188
|
-
const hunkText = file.hunks
|
|
1189
|
-
.map((hunk) => [
|
|
1190
|
-
hunk.header,
|
|
1191
|
-
...hunk.lines.map((line) => `${line.kind}:${line.oldLine ?? ""}:${line.newLine ?? ""}:${line.text}`),
|
|
1192
|
-
].join("\n"))
|
|
1193
|
-
.join("\n---\n");
|
|
1194
|
-
states.set(file.displayPath, hashText(`${file.displayPath}\0${file.status}\0${file.binary}\0${hunkText}`));
|
|
1195
|
-
}
|
|
1196
|
-
return Array.from(states.entries())
|
|
1197
|
-
.map(([path, signature]) => ({ path, signature }))
|
|
1198
|
-
.sort((a, b) => a.path.localeCompare(b.path));
|
|
1199
|
-
}
|
|
1200
|
-
// Reads IntelliJ-style HTTP Client environment files from the project root and
|
|
1201
|
-
// merges them into { envName: { varName: value } }. The private file overrides
|
|
1202
|
-
// the public one so secrets stay out of source control.
|
|
1203
|
-
function collectHttpEnvironments(root) {
|
|
1204
|
-
const result = {};
|
|
1205
|
-
for (const fileName of ["http-client.env.json", "http-client.private.env.json"]) {
|
|
1206
|
-
const filePath = join(root, fileName);
|
|
1207
|
-
if (!existsSync(filePath))
|
|
1208
|
-
continue;
|
|
1209
|
-
let parsed;
|
|
1210
|
-
try {
|
|
1211
|
-
parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
1212
|
-
}
|
|
1213
|
-
catch {
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
if (!parsed || typeof parsed !== "object")
|
|
1217
|
-
continue;
|
|
1218
|
-
for (const [envName, rawVars] of Object.entries(parsed)) {
|
|
1219
|
-
if (!rawVars || typeof rawVars !== "object")
|
|
1220
|
-
continue;
|
|
1221
|
-
const target = result[envName] ?? (result[envName] = {});
|
|
1222
|
-
for (const [key, value] of Object.entries(rawVars)) {
|
|
1223
|
-
if (typeof value === "string")
|
|
1224
|
-
target[key] = value;
|
|
1225
|
-
else if (typeof value === "number" || typeof value === "boolean")
|
|
1226
|
-
target[key] = String(value);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return result;
|
|
1231
|
-
}
|
|
1232
|
-
function isSourceCandidate(path) {
|
|
1233
|
-
const normalized = path.replace(/\\/g, "/");
|
|
1234
|
-
if (!normalized || normalized.startsWith(`${FLOW_DIR}/`)) {
|
|
1235
|
-
return false;
|
|
1236
|
-
}
|
|
1237
|
-
const blocked = [
|
|
1238
|
-
".git/",
|
|
1239
|
-
".omc/",
|
|
1240
|
-
".claude/",
|
|
1241
|
-
".playwright-mcp/",
|
|
1242
|
-
"node_modules/",
|
|
1243
|
-
"dist/",
|
|
1244
|
-
"build/",
|
|
1245
|
-
"coverage/",
|
|
1246
|
-
"test-results/",
|
|
1247
|
-
"release/",
|
|
1248
|
-
".next/",
|
|
1249
|
-
".turbo/",
|
|
1250
|
-
".cache/",
|
|
1251
|
-
".granite/",
|
|
1252
|
-
".pytest_cache/",
|
|
1253
|
-
"__pycache__/",
|
|
1254
|
-
"tmp/",
|
|
1255
|
-
"vendor/",
|
|
1256
|
-
];
|
|
1257
|
-
if (blocked.some((part) => normalized === part.slice(0, -1) || normalized.includes(`/${part}`) || normalized.startsWith(part))) {
|
|
1258
|
-
return false;
|
|
1259
|
-
}
|
|
1260
|
-
const fileName = basename(normalized);
|
|
1261
|
-
if (fileName === ".DS_Store" || fileName.endsWith(".lockb")) {
|
|
1262
|
-
return false;
|
|
1263
|
-
}
|
|
1264
|
-
return true;
|
|
1265
|
-
}
|
|
1266
|
-
function diff2HtmlCss() {
|
|
1267
|
-
try {
|
|
1268
|
-
return readFileSync(nodeRequire.resolve("diff2html/bundles/css/diff2html.min.css"), "utf8");
|
|
1269
|
-
}
|
|
1270
|
-
catch {
|
|
1271
|
-
return "";
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
function diffCss() {
|
|
1275
|
-
return `
|
|
1276
|
-
:root {
|
|
1277
|
-
color-scheme: dark;
|
|
1278
|
-
--bg: #2b2b2b;
|
|
1279
|
-
--panel: #2b2b2b;
|
|
1280
|
-
--text: #a9b7c6;
|
|
1281
|
-
--muted: #808080;
|
|
1282
|
-
--border: #393b3d;
|
|
1283
|
-
--line: #313335;
|
|
1284
|
-
--add: #2f3d2c;
|
|
1285
|
-
--del: #4b3434;
|
|
1286
|
-
--add-strong: #3d5238;
|
|
1287
|
-
--del-strong: #6b4242;
|
|
1288
|
-
--active: #4a88c7;
|
|
1289
|
-
--sidebar: #3c3f41;
|
|
1290
|
-
--token-comment: #808080;
|
|
1291
|
-
--token-keyword: #cc7832;
|
|
1292
|
-
--token-string: #6a8759;
|
|
1293
|
-
--token-number: #6897bb;
|
|
1294
|
-
--token-literal: #cc7832;
|
|
1295
|
-
--token-tag: #e8bf6a;
|
|
1296
|
-
--d2h-bg-color: var(--panel);
|
|
1297
|
-
--d2h-border-color: var(--border);
|
|
1298
|
-
--d2h-dim-color: var(--muted);
|
|
1299
|
-
--d2h-line-border-color: var(--border);
|
|
1300
|
-
--d2h-file-header-bg-color: var(--line);
|
|
1301
|
-
--d2h-file-header-border-color: var(--border);
|
|
1302
|
-
--d2h-empty-placeholder-bg-color: var(--line);
|
|
1303
|
-
--d2h-code-line-bg-color: var(--panel);
|
|
1304
|
-
--d2h-code-line-color: var(--text);
|
|
1305
|
-
--d2h-code-side-line-border-color: var(--border);
|
|
1306
|
-
--d2h-del-bg-color: var(--del);
|
|
1307
|
-
--d2h-ins-bg-color: var(--add);
|
|
1308
|
-
--d2h-info-bg-color: var(--line);
|
|
1309
|
-
--d2h-info-color: var(--muted);
|
|
1310
|
-
}
|
|
1311
|
-
* { box-sizing: border-box; }
|
|
1312
|
-
html, body { margin: 0; min-height: 100%; }
|
|
1313
|
-
body {
|
|
1314
|
-
display: grid;
|
|
1315
|
-
grid-template-columns: var(--sidebar-width, 280px) minmax(0, 1fr);
|
|
1316
|
-
background: var(--bg);
|
|
1317
|
-
color: var(--text);
|
|
1318
|
-
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1319
|
-
}
|
|
1320
|
-
.sidebar {
|
|
1321
|
-
position: sticky;
|
|
1322
|
-
top: 0;
|
|
1323
|
-
height: 100vh;
|
|
1324
|
-
overflow: auto;
|
|
1325
|
-
border-right: 1px solid var(--border);
|
|
1326
|
-
background: var(--sidebar);
|
|
1327
|
-
padding: 12px;
|
|
1328
|
-
}
|
|
1329
|
-
.sidebar-resizer {
|
|
1330
|
-
position: fixed;
|
|
1331
|
-
top: 0;
|
|
1332
|
-
left: var(--sidebar-width, 280px);
|
|
1333
|
-
width: 9px;
|
|
1334
|
-
height: 100vh;
|
|
1335
|
-
margin-left: -5px;
|
|
1336
|
-
cursor: col-resize;
|
|
1337
|
-
z-index: 30;
|
|
1338
|
-
}
|
|
1339
|
-
.sidebar-resizer::after {
|
|
1340
|
-
content: "";
|
|
1341
|
-
position: absolute;
|
|
1342
|
-
inset: 0 4px;
|
|
1343
|
-
background: transparent;
|
|
1344
|
-
transition: background 120ms ease;
|
|
1345
|
-
}
|
|
1346
|
-
.sidebar-resizer:hover::after, .sidebar-resizer.resizing::after { background: var(--active); }
|
|
1347
|
-
.visually-hidden {
|
|
1348
|
-
position: absolute;
|
|
1349
|
-
width: 1px;
|
|
1350
|
-
height: 1px;
|
|
1351
|
-
padding: 0;
|
|
1352
|
-
margin: -1px;
|
|
1353
|
-
overflow: hidden;
|
|
1354
|
-
clip: rect(0, 0, 0, 0);
|
|
1355
|
-
white-space: nowrap;
|
|
1356
|
-
border: 0;
|
|
1357
|
-
}
|
|
1358
|
-
.live-status { color: var(--muted); }
|
|
1359
|
-
.live-status.watching { color: var(--active); }
|
|
1360
|
-
.search { display: grid; gap: 6px; margin-bottom: 8px; color: var(--muted); font-size: 12px; }
|
|
1361
|
-
.search input {
|
|
1362
|
-
width: 100%;
|
|
1363
|
-
border: 1px solid var(--border);
|
|
1364
|
-
border-radius: 6px;
|
|
1365
|
-
padding: 8px 9px;
|
|
1366
|
-
color: var(--text);
|
|
1367
|
-
background: var(--bg);
|
|
1368
|
-
font: 13px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1369
|
-
}
|
|
1370
|
-
.tabs { display: none; }
|
|
1371
|
-
.tab, .plain-button {
|
|
1372
|
-
border: 1px solid var(--border);
|
|
1373
|
-
border-radius: 6px;
|
|
1374
|
-
padding: 6px 9px;
|
|
1375
|
-
color: var(--text);
|
|
1376
|
-
background: var(--panel);
|
|
1377
|
-
font: 12px ui-sans-serif, system-ui, sans-serif;
|
|
1378
|
-
cursor: pointer;
|
|
1379
|
-
}
|
|
1380
|
-
.tab.active, .plain-button:hover { border-color: var(--active); color: var(--active); }
|
|
1381
|
-
.hidden { display: none !important; }
|
|
1382
|
-
.update-badge {
|
|
1383
|
-
position: fixed;
|
|
1384
|
-
left: 12px;
|
|
1385
|
-
bottom: 10px;
|
|
1386
|
-
z-index: 60;
|
|
1387
|
-
font-size: 11px;
|
|
1388
|
-
line-height: 1;
|
|
1389
|
-
padding: 5px 11px;
|
|
1390
|
-
border-radius: 11px;
|
|
1391
|
-
background: var(--active);
|
|
1392
|
-
color: #fff;
|
|
1393
|
-
font-weight: 500;
|
|
1394
|
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
|
|
1395
|
-
pointer-events: none;
|
|
1396
|
-
}
|
|
1397
|
-
.diff2html-container { min-width: 0; caret-color: var(--active); }
|
|
1398
|
-
#diff2html-container[contenteditable] { outline: none; }
|
|
1399
|
-
#diff2html-container [contenteditable="false"] { caret-color: transparent; }
|
|
1400
|
-
.d2h-wrapper { background: transparent; color: var(--text); }
|
|
1401
|
-
.d2h-file-wrapper {
|
|
1402
|
-
margin: 0 0 28px;
|
|
1403
|
-
overflow: hidden;
|
|
1404
|
-
border: 1px solid var(--border);
|
|
1405
|
-
border-radius: 8px;
|
|
1406
|
-
background: var(--panel);
|
|
1407
|
-
}
|
|
1408
|
-
.d2h-file-header {
|
|
1409
|
-
border-bottom: 1px solid var(--border);
|
|
1410
|
-
background: var(--line);
|
|
1411
|
-
color: var(--text);
|
|
1412
|
-
font: 12px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1413
|
-
}
|
|
1414
|
-
.d2h-file-wrapper.file-viewed {
|
|
1415
|
-
opacity: 0.68;
|
|
1416
|
-
}
|
|
1417
|
-
.d2h-file-wrapper.file-viewed:hover {
|
|
1418
|
-
opacity: 1;
|
|
1419
|
-
}
|
|
1420
|
-
.d2h-file-name { color: var(--text); }
|
|
1421
|
-
.d2h-icon { fill: var(--muted); }
|
|
1422
|
-
.d2h-tag { border-color: var(--border); }
|
|
1423
|
-
.d2h-files-diff { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
|
1424
|
-
.d2h-file-side-diff { min-width: 0; width: 100%; overflow-x: auto; }
|
|
1425
|
-
.d2h-file-side-diff:first-child { border-right: 1px solid var(--border); }
|
|
1426
|
-
.d2h-code-wrapper { width: 100%; }
|
|
1427
|
-
.d2h-diff-table {
|
|
1428
|
-
width: 100%;
|
|
1429
|
-
font: 12px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1430
|
-
}
|
|
1431
|
-
.d2h-diff-table td { line-height: 1.45; }
|
|
1432
|
-
.d2h-code-side-linenumber, .d2h-code-linenumber {
|
|
1433
|
-
width: 58px;
|
|
1434
|
-
color: var(--muted);
|
|
1435
|
-
background: var(--line);
|
|
1436
|
-
border-color: var(--border);
|
|
1437
|
-
}
|
|
1438
|
-
.d2h-code-side-line, .d2h-code-line {
|
|
1439
|
-
padding: 0 0.6em 0 4.5em;
|
|
1440
|
-
width: 100%;
|
|
1441
|
-
color: var(--text);
|
|
1442
|
-
cursor: text;
|
|
1443
|
-
-webkit-user-select: text;
|
|
1444
|
-
user-select: text;
|
|
1445
|
-
}
|
|
1446
|
-
.d2h-code-line-prefix { -webkit-user-select: none; user-select: none; }
|
|
1447
|
-
.d2h-code-side-linenumber, .d2h-code-linenumber { -webkit-user-select: none; user-select: none; }
|
|
1448
|
-
.d2h-code-line-ctn .hljs-keyword,
|
|
1449
|
-
.d2h-code-line-ctn .hljs-built_in,
|
|
1450
|
-
.d2h-code-line-ctn .hljs-literal,
|
|
1451
|
-
.d2h-code-line-ctn .hljs-selector-tag,
|
|
1452
|
-
.d2h-code-line-ctn .hljs-section { color: #cc7832; }
|
|
1453
|
-
.d2h-code-line-ctn .hljs-string,
|
|
1454
|
-
.d2h-code-line-ctn .hljs-regexp,
|
|
1455
|
-
.d2h-code-line-ctn .hljs-char.escape_ { color: #6a8759; }
|
|
1456
|
-
.d2h-code-line-ctn .hljs-number { color: #6897bb; }
|
|
1457
|
-
.d2h-code-line-ctn .hljs-comment,
|
|
1458
|
-
.d2h-code-line-ctn .hljs-quote { color: #808080; font-style: italic; }
|
|
1459
|
-
.d2h-code-line-ctn .hljs-meta,
|
|
1460
|
-
.d2h-code-line-ctn .hljs-doctag { color: #bbb529; }
|
|
1461
|
-
.d2h-code-line-ctn .hljs-title,
|
|
1462
|
-
.d2h-code-line-ctn .hljs-title.function_,
|
|
1463
|
-
.d2h-code-line-ctn .hljs-function .hljs-title { color: #ffc66d; }
|
|
1464
|
-
.d2h-code-line-ctn .hljs-title.class_,
|
|
1465
|
-
.d2h-code-line-ctn .hljs-class .hljs-title,
|
|
1466
|
-
.d2h-code-line-ctn .hljs-type { color: #a9b7c6; }
|
|
1467
|
-
.d2h-code-line-ctn .hljs-attr,
|
|
1468
|
-
.d2h-code-line-ctn .hljs-variable,
|
|
1469
|
-
.d2h-code-line-ctn .hljs-template-variable,
|
|
1470
|
-
.d2h-code-line-ctn .hljs-property { color: #9876aa; }
|
|
1471
|
-
.d2h-code-line-ctn .hljs-attribute { color: #a9b7c6; }
|
|
1472
|
-
.d2h-code-line-ctn .hljs-tag,
|
|
1473
|
-
.d2h-code-line-ctn .hljs-name { color: #e8bf6a; }
|
|
1474
|
-
.d2h-code-line-ctn .hljs-symbol,
|
|
1475
|
-
.d2h-code-line-ctn .hljs-bullet,
|
|
1476
|
-
.d2h-code-line-ctn .hljs-link { color: #6897bb; }
|
|
1477
|
-
.d2h-code-line-ctn .hljs-emphasis { font-style: italic; }
|
|
1478
|
-
.d2h-code-line-ctn .hljs-strong { font-weight: 700; }
|
|
1479
|
-
.d2h-info { background: var(--line); color: var(--muted); border-color: var(--border); }
|
|
1480
|
-
.d2h-info .d2h-code-side-line, .d2h-info .d2h-code-line { color: transparent; user-select: none; }
|
|
1481
|
-
.d2h-info td, td.d2h-info { border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
|
1482
|
-
.d2h-file-wrapper.df-inactive { display: none; }
|
|
1483
|
-
.d2h-del { background: var(--del); }
|
|
1484
|
-
.d2h-ins { background: var(--add); }
|
|
1485
|
-
.d2h-del .d2h-change { background: var(--del-strong); }
|
|
1486
|
-
.d2h-ins .d2h-change { background: var(--add-strong); }
|
|
1487
|
-
.d2h-code-line-ctn ins, .d2h-code-line-ctn del {
|
|
1488
|
-
text-decoration: none;
|
|
1489
|
-
border-radius: 2px;
|
|
1490
|
-
padding: 0 1px;
|
|
1491
|
-
box-decoration-break: clone;
|
|
1492
|
-
-webkit-box-decoration-break: clone;
|
|
1493
|
-
}
|
|
1494
|
-
.d2h-code-line-ctn ins { background: var(--add-strong); }
|
|
1495
|
-
.d2h-code-line-ctn del { background: var(--del-strong); }
|
|
1496
|
-
.d2h-code-side-linenumber.d2h-del, .d2h-code-linenumber.d2h-del { background: var(--del); }
|
|
1497
|
-
.d2h-code-side-linenumber.d2h-ins, .d2h-code-linenumber.d2h-ins { background: var(--add); }
|
|
1498
|
-
.d2h-diff-table tr.hunk, .d2h-diff-table tr.hunk-peer { scroll-margin-top: 76px; }
|
|
1499
|
-
.d2h-diff-table tr.hunk.active td, .d2h-diff-table tr.hunk-peer.active td {
|
|
1500
|
-
box-shadow: none;
|
|
1501
|
-
}
|
|
1502
|
-
.d2h-diff-table tr.diff-active-row td { background: rgba(74, 136, 199, 0.16) !important; }
|
|
1503
|
-
.d2h-diff-table tr.diff-active-row td.d2h-code-side-linenumber { box-shadow: inset 2px 0 0 var(--active); }
|
|
1504
|
-
.file-counter:not(:empty) { margin-right: 14px; color: var(--muted); }
|
|
1505
|
-
.d2h-file-collapse {
|
|
1506
|
-
display: flex;
|
|
1507
|
-
align-items: center;
|
|
1508
|
-
justify-content: center;
|
|
1509
|
-
width: 22px;
|
|
1510
|
-
height: 22px;
|
|
1511
|
-
margin-left: 8px;
|
|
1512
|
-
border: 1px solid var(--border);
|
|
1513
|
-
border-radius: 999px;
|
|
1514
|
-
color: transparent;
|
|
1515
|
-
background: var(--panel);
|
|
1516
|
-
overflow: hidden;
|
|
1517
|
-
padding: 0;
|
|
1518
|
-
}
|
|
1519
|
-
.d2h-file-collapse::after {
|
|
1520
|
-
content: "";
|
|
1521
|
-
width: 8px;
|
|
1522
|
-
height: 8px;
|
|
1523
|
-
border-radius: 999px;
|
|
1524
|
-
background: transparent;
|
|
1525
|
-
}
|
|
1526
|
-
.d2h-file-wrapper.file-viewed .d2h-file-collapse::after {
|
|
1527
|
-
background: var(--active);
|
|
1528
|
-
}
|
|
1529
|
-
.d2h-file-collapse-input {
|
|
1530
|
-
display: none;
|
|
1531
|
-
}
|
|
1532
|
-
.tree { display: grid; gap: 1px; font-size: 11.5px; font-family: Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
1533
|
-
.tree-dir { display: grid; gap: 1px; }
|
|
1534
|
-
.tree-dir summary {
|
|
1535
|
-
display: grid;
|
|
1536
|
-
grid-template-columns: 14px minmax(0, 1fr);
|
|
1537
|
-
align-items: center;
|
|
1538
|
-
gap: 4px;
|
|
1539
|
-
min-height: 18px;
|
|
1540
|
-
padding: 1px 5px 1px calc(7px + (var(--depth) * 14px));
|
|
1541
|
-
color: var(--muted);
|
|
1542
|
-
border-radius: 6px;
|
|
1543
|
-
cursor: default;
|
|
1544
|
-
list-style: none;
|
|
1545
|
-
}
|
|
1546
|
-
.tree-dir summary::-webkit-details-marker { display: none; }
|
|
1547
|
-
.tree-dir summary:hover { background: var(--bg); }
|
|
1548
|
-
.tree-dir:not([open]) .folder-icon { transform: rotate(-90deg); }
|
|
1549
|
-
.folder-icon {
|
|
1550
|
-
display: inline-grid;
|
|
1551
|
-
place-items: center;
|
|
1552
|
-
font-size: 9px;
|
|
1553
|
-
color: var(--muted);
|
|
1554
|
-
transition: transform 120ms ease;
|
|
1555
|
-
}
|
|
1556
|
-
.file-link.tree-file { padding-left: calc(8px + (var(--depth) * 14px)); }
|
|
1557
|
-
.tree-focus { box-shadow: inset 0 0 0 1px var(--active); border-radius: 6px; }
|
|
1558
|
-
summary.tree-focus { background: var(--bg); }
|
|
1559
|
-
.file-link {
|
|
1560
|
-
display: grid;
|
|
1561
|
-
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
1562
|
-
align-items: center;
|
|
1563
|
-
gap: 6px;
|
|
1564
|
-
min-height: 18px;
|
|
1565
|
-
padding: 1px 6px;
|
|
1566
|
-
color: var(--text);
|
|
1567
|
-
text-decoration: none;
|
|
1568
|
-
border-radius: 6px;
|
|
1569
|
-
border: 1px solid transparent;
|
|
1570
|
-
background: transparent;
|
|
1571
|
-
width: 100%;
|
|
1572
|
-
text-align: left;
|
|
1573
|
-
font: inherit;
|
|
1574
|
-
cursor: pointer;
|
|
1575
|
-
}
|
|
1576
|
-
.file-link:hover, .file-link.active { background: var(--bg); border-color: var(--border); }
|
|
1577
|
-
.file-link.viewed { opacity: 0.58; }
|
|
1578
|
-
.file-link.viewed:hover, .file-link.viewed.active { opacity: 1; }
|
|
1579
|
-
.path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
|
1580
|
-
.count { color: var(--muted); font-size: 11px; }
|
|
1581
|
-
.change-name { display: flex; align-items: baseline; gap: 7px; min-width: 0; overflow: hidden; }
|
|
1582
|
-
.change-dir { color: var(--muted); opacity: 0.5; font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
1583
|
-
.diffstat { display: flex; gap: 6px; font-size: 11px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
1584
|
-
.diffstat .adds { color: #6ab04c; }
|
|
1585
|
-
.diffstat .dels { color: #cf6679; }
|
|
1586
|
-
.file-link.viewed .status::after { content: '✓'; margin-left: 4px; color: #6ab04c; font-weight: 700; }
|
|
1587
|
-
.status {
|
|
1588
|
-
display: inline-grid;
|
|
1589
|
-
place-items: center;
|
|
1590
|
-
min-width: 16px;
|
|
1591
|
-
height: 16px;
|
|
1592
|
-
border-radius: 4px;
|
|
1593
|
-
padding: 0 3px;
|
|
1594
|
-
font-size: 9px;
|
|
1595
|
-
font-weight: 700;
|
|
1596
|
-
text-transform: uppercase;
|
|
1597
|
-
background: var(--line);
|
|
1598
|
-
color: var(--muted);
|
|
1599
|
-
}
|
|
1600
|
-
.status-added { background: var(--add); color: #1a7f37; }
|
|
1601
|
-
.status-deleted { background: var(--del); color: #cf222e; }
|
|
1602
|
-
.status-renamed { background: #fff8c5; color: #9a6700; }
|
|
1603
|
-
.status-source { background: var(--line); color: var(--muted); }
|
|
1604
|
-
.content { min-width: 0; padding: 20px 24px 80px; }
|
|
1605
|
-
.toolbar {
|
|
1606
|
-
position: sticky;
|
|
1607
|
-
top: 0;
|
|
1608
|
-
z-index: 5;
|
|
1609
|
-
display: flex;
|
|
1610
|
-
justify-content: space-between;
|
|
1611
|
-
align-items: center;
|
|
1612
|
-
gap: 16px;
|
|
1613
|
-
margin: -20px -24px 20px;
|
|
1614
|
-
padding: 10px 24px;
|
|
1615
|
-
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
1616
|
-
backdrop-filter: blur(12px);
|
|
1617
|
-
border-bottom: 1px solid var(--border);
|
|
1618
|
-
}
|
|
1619
|
-
h1 { margin: 0; font-size: 18px; }
|
|
1620
|
-
.breadcrumb {
|
|
1621
|
-
display: flex;
|
|
1622
|
-
align-items: center;
|
|
1623
|
-
gap: 5px;
|
|
1624
|
-
min-width: 0;
|
|
1625
|
-
flex: 1 1 auto;
|
|
1626
|
-
overflow: hidden;
|
|
1627
|
-
white-space: nowrap;
|
|
1628
|
-
font: 13px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1629
|
-
}
|
|
1630
|
-
.crumb { color: var(--muted); }
|
|
1631
|
-
.crumb-leaf { color: var(--text); font-weight: 500; }
|
|
1632
|
-
.crumb-sep { color: var(--muted); opacity: 0.55; }
|
|
1633
|
-
.review-status {
|
|
1634
|
-
display: flex;
|
|
1635
|
-
align-items: center;
|
|
1636
|
-
gap: 12px;
|
|
1637
|
-
min-width: 0;
|
|
1638
|
-
color: var(--muted);
|
|
1639
|
-
font-size: 12px;
|
|
1640
|
-
}
|
|
1641
|
-
.toolbar p { margin: 4px 0 0; color: var(--muted); font-size: 12px; }
|
|
1642
|
-
.counter {
|
|
1643
|
-
min-width: 96px;
|
|
1644
|
-
text-align: right;
|
|
1645
|
-
color: var(--muted);
|
|
1646
|
-
font: 13px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1647
|
-
}
|
|
1648
|
-
.empty { padding: 24px; color: var(--muted); }
|
|
1649
|
-
.source-viewer { min-height: 100vh; }
|
|
1650
|
-
.source-toolbar { margin-bottom: 0; }
|
|
1651
|
-
.source-file-meta {
|
|
1652
|
-
display: flex;
|
|
1653
|
-
align-items: center;
|
|
1654
|
-
gap: 12px;
|
|
1655
|
-
min-width: 0;
|
|
1656
|
-
color: var(--muted);
|
|
1657
|
-
font-size: 12px;
|
|
1658
|
-
}
|
|
1659
|
-
.source-file-meta #source-title {
|
|
1660
|
-
min-width: 0;
|
|
1661
|
-
max-width: min(56vw, 720px);
|
|
1662
|
-
overflow: hidden;
|
|
1663
|
-
text-overflow: ellipsis;
|
|
1664
|
-
white-space: nowrap;
|
|
1665
|
-
color: var(--text);
|
|
1666
|
-
font: 13px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1667
|
-
}
|
|
1668
|
-
.source-file-meta #source-meta {
|
|
1669
|
-
overflow: hidden;
|
|
1670
|
-
text-overflow: ellipsis;
|
|
1671
|
-
white-space: nowrap;
|
|
1672
|
-
}
|
|
1673
|
-
.source-body {
|
|
1674
|
-
border: 1px solid var(--border);
|
|
1675
|
-
border-radius: 8px;
|
|
1676
|
-
overflow: auto;
|
|
1677
|
-
background: var(--panel);
|
|
1678
|
-
user-select: text;
|
|
1679
|
-
}
|
|
1680
|
-
.source-table {
|
|
1681
|
-
width: 100%;
|
|
1682
|
-
border-collapse: collapse;
|
|
1683
|
-
font: 12px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1684
|
-
}
|
|
1685
|
-
.source-table td {
|
|
1686
|
-
vertical-align: top;
|
|
1687
|
-
white-space: pre-wrap;
|
|
1688
|
-
overflow-wrap: anywhere;
|
|
1689
|
-
line-height: 1.45;
|
|
1690
|
-
}
|
|
1691
|
-
.source-row.search-hit .source-code { background: color-mix(in srgb, var(--active) 14%, transparent); }
|
|
1692
|
-
.source-row.changed-line .source-code { background: color-mix(in srgb, var(--active) 9%, transparent); box-shadow: inset 2px 0 0 color-mix(in srgb, var(--active) 55%, transparent); }
|
|
1693
|
-
.source-row.symbol-target .source-code {
|
|
1694
|
-
background: color-mix(in srgb, var(--active) 18%, transparent);
|
|
1695
|
-
}
|
|
1696
|
-
.source-code {
|
|
1697
|
-
padding: 2px 10px;
|
|
1698
|
-
cursor: text;
|
|
1699
|
-
user-select: text;
|
|
1700
|
-
}
|
|
1701
|
-
.code-cursor {
|
|
1702
|
-
display: inline-block;
|
|
1703
|
-
width: 2px;
|
|
1704
|
-
height: 1.25em;
|
|
1705
|
-
margin: -1px -1px;
|
|
1706
|
-
background: var(--active);
|
|
1707
|
-
vertical-align: text-bottom;
|
|
1708
|
-
pointer-events: none;
|
|
1709
|
-
animation: cursor-blink 1s steps(2, start) infinite;
|
|
1710
|
-
}
|
|
1711
|
-
@keyframes cursor-blink {
|
|
1712
|
-
50% { opacity: 0; }
|
|
1713
|
-
}
|
|
1714
|
-
.num {
|
|
1715
|
-
width: 58px;
|
|
1716
|
-
user-select: none;
|
|
1717
|
-
text-align: right;
|
|
1718
|
-
color: var(--muted);
|
|
1719
|
-
background: var(--line);
|
|
1720
|
-
border-right: 1px solid var(--border);
|
|
1721
|
-
padding: 2px 8px;
|
|
1722
|
-
}
|
|
1723
|
-
.tok-comment { color: var(--token-comment); font-style: italic; }
|
|
1724
|
-
.tok-keyword { color: var(--token-keyword); font-weight: 650; }
|
|
1725
|
-
.tok-string { color: var(--token-string); }
|
|
1726
|
-
.tok-number { color: var(--token-number); }
|
|
1727
|
-
.tok-literal { color: var(--token-literal); }
|
|
1728
|
-
.tok-tag { color: var(--token-tag); font-weight: 650; }
|
|
1729
|
-
.quick-open {
|
|
1730
|
-
position: fixed;
|
|
1731
|
-
inset: 0;
|
|
1732
|
-
z-index: 50;
|
|
1733
|
-
display: grid;
|
|
1734
|
-
place-items: start center;
|
|
1735
|
-
padding-top: min(12vh, 96px);
|
|
1736
|
-
background: color-mix(in srgb, #000 24%, transparent);
|
|
1737
|
-
}
|
|
1738
|
-
.quick-open-panel {
|
|
1739
|
-
width: min(720px, calc(100vw - 32px));
|
|
1740
|
-
max-height: min(680px, calc(100vh - 64px));
|
|
1741
|
-
display: grid;
|
|
1742
|
-
grid-template-rows: auto auto minmax(0, 1fr);
|
|
1743
|
-
border: 1px solid var(--border);
|
|
1744
|
-
border-radius: 8px;
|
|
1745
|
-
background: var(--panel);
|
|
1746
|
-
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.28);
|
|
1747
|
-
overflow: hidden;
|
|
1748
|
-
}
|
|
1749
|
-
.quick-open-title {
|
|
1750
|
-
display: flex;
|
|
1751
|
-
justify-content: space-between;
|
|
1752
|
-
gap: 12px;
|
|
1753
|
-
padding: 10px 12px;
|
|
1754
|
-
border-bottom: 1px solid var(--border);
|
|
1755
|
-
color: var(--muted);
|
|
1756
|
-
font-size: 12px;
|
|
1757
|
-
}
|
|
1758
|
-
.quick-open-hint { font-family: Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
1759
|
-
#quick-open-input {
|
|
1760
|
-
width: 100%;
|
|
1761
|
-
border: 0;
|
|
1762
|
-
border-bottom: 1px solid var(--border);
|
|
1763
|
-
outline: 0;
|
|
1764
|
-
padding: 13px 14px;
|
|
1765
|
-
background: var(--bg);
|
|
1766
|
-
color: var(--text);
|
|
1767
|
-
font: 15px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1768
|
-
}
|
|
1769
|
-
.quick-open-results { overflow: auto; padding: 6px; max-height: 232px; }
|
|
1770
|
-
.quick-open-main { min-width: 0; display: flex; align-items: baseline; gap: 8px; }
|
|
1771
|
-
.quick-open-path { flex: 1 1 auto; }
|
|
1772
|
-
.quick-open-preview {
|
|
1773
|
-
border-top: 1px solid var(--border);
|
|
1774
|
-
max-height: 320px;
|
|
1775
|
-
overflow: auto;
|
|
1776
|
-
background: var(--bg);
|
|
1777
|
-
}
|
|
1778
|
-
.qp-head {
|
|
1779
|
-
position: sticky;
|
|
1780
|
-
top: 0;
|
|
1781
|
-
padding: 5px 10px;
|
|
1782
|
-
background: var(--panel);
|
|
1783
|
-
border-bottom: 1px solid var(--border);
|
|
1784
|
-
color: var(--muted);
|
|
1785
|
-
font: 11px Monaco, ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1786
|
-
}
|
|
1787
|
-
.qp-body { padding: 4px 0; font: 12px Monaco, ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
1788
|
-
.qp-line { display: grid; grid-template-columns: 46px minmax(0, 1fr); gap: 6px; padding: 0 8px; white-space: pre; line-height: 1.5; }
|
|
1789
|
-
.qp-num { color: var(--muted); text-align: right; user-select: none; }
|
|
1790
|
-
.qp-hit { background: color-mix(in srgb, var(--active) 20%, transparent); }
|
|
1791
|
-
.qp-empty { padding: 20px; color: var(--muted); }
|
|
1792
|
-
.quick-open-item {
|
|
1793
|
-
display: grid;
|
|
1794
|
-
grid-template-columns: minmax(0, 1fr) auto;
|
|
1795
|
-
gap: 8px;
|
|
1796
|
-
width: 100%;
|
|
1797
|
-
min-height: 24px;
|
|
1798
|
-
border: 1px solid transparent;
|
|
1799
|
-
border-radius: 5px;
|
|
1800
|
-
padding: 2px 8px;
|
|
1801
|
-
background: transparent;
|
|
1802
|
-
color: var(--text);
|
|
1803
|
-
text-align: left;
|
|
1804
|
-
cursor: pointer;
|
|
1805
|
-
}
|
|
1806
|
-
.quick-open-item.active, .quick-open-item:hover { background: var(--bg); border-color: var(--active); }
|
|
1807
|
-
.quick-open-main { min-width: 0; }
|
|
1808
|
-
.quick-open-name {
|
|
1809
|
-
display: block;
|
|
1810
|
-
overflow: hidden;
|
|
1811
|
-
text-overflow: ellipsis;
|
|
1812
|
-
white-space: nowrap;
|
|
1813
|
-
font: 13px Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1814
|
-
}
|
|
1815
|
-
.quick-open-path {
|
|
1816
|
-
display: block;
|
|
1817
|
-
overflow: hidden;
|
|
1818
|
-
text-overflow: ellipsis;
|
|
1819
|
-
white-space: nowrap;
|
|
1820
|
-
color: var(--muted);
|
|
1821
|
-
font-size: 12px;
|
|
1822
|
-
margin-top: 2px;
|
|
1823
|
-
}
|
|
1824
|
-
.quick-open-badge { align-self: center; color: var(--muted); font-size: 12px; }
|
|
1825
|
-
.quick-open-empty { padding: 28px 14px; color: var(--muted); font-size: 13px; }
|
|
1826
|
-
@media (max-width: 900px) {
|
|
1827
|
-
body { grid-template-columns: 1fr; }
|
|
1828
|
-
.sidebar { position: relative; height: auto; border-right: 0; border-bottom: 1px solid var(--border); }
|
|
1829
|
-
.content { padding: 16px; }
|
|
1830
|
-
.toolbar { margin: -16px -16px 16px; padding: 12px 16px; }
|
|
1831
|
-
.d2h-files-diff { grid-template-columns: 1fr; }
|
|
1832
|
-
.d2h-file-side-diff:first-child { border-right: 0; border-bottom: 1px solid var(--border); }
|
|
1833
|
-
}
|
|
1834
|
-
`;
|
|
1835
|
-
}
|
|
1836
|
-
function diffScript() {
|
|
1837
|
-
return String.raw `
|
|
1838
|
-
prepareDiff2HtmlHunks();
|
|
1839
|
-
const hunks = Array.from(document.querySelectorAll('.hunk'));
|
|
1840
|
-
const hunkPeers = Array.from(document.querySelectorAll('.hunk-peer'));
|
|
1841
|
-
const links = Array.from(document.querySelectorAll('#changes-panel .file-link'));
|
|
1842
|
-
const sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
1843
|
-
const sourceFiles = JSON.parse(document.getElementById('source-files-data')?.textContent || '[]');
|
|
1844
|
-
const fileStates = JSON.parse(document.getElementById('file-state-data')?.textContent || '[]');
|
|
1845
|
-
const sourceByPath = new Map(sourceFiles.map((file) => [file.path, file]));
|
|
1846
|
-
const fileSignatureByPath = new Map(fileStates.map((file) => [file.path, file.signature]));
|
|
1847
|
-
const searchInput = document.getElementById('review-search');
|
|
1848
|
-
const reviewMeta = document.getElementById('review-meta');
|
|
1849
|
-
const watchEnabled = reviewMeta?.dataset.watch === 'true';
|
|
1850
|
-
const currentSignature = reviewMeta?.dataset.signature || '';
|
|
1851
|
-
const uiStateKey = 'monacori-diff-ui:' + location.pathname;
|
|
1852
|
-
const recentKey = 'monacori-diff-recent:' + location.pathname;
|
|
1853
|
-
const viewedKey = 'monacori-diff-viewed:' + location.pathname;
|
|
1854
|
-
const quickOpen = document.getElementById('quick-open');
|
|
1855
|
-
const quickInput = document.getElementById('quick-open-input');
|
|
1856
|
-
const quickResults = document.getElementById('quick-open-results');
|
|
1857
|
-
const quickModeLabel = document.getElementById('quick-open-mode');
|
|
1858
|
-
let current = -1;
|
|
1859
|
-
let checkingForUpdates = false;
|
|
1860
|
-
let lastShiftAt = 0;
|
|
1861
|
-
let quickMode = 'all';
|
|
1862
|
-
let quickItems = [];
|
|
1863
|
-
let quickActive = 0;
|
|
1864
|
-
let viewerCursor = null;
|
|
1865
|
-
let treeFocusIndex = -1;
|
|
1866
|
-
let selectionAnchor = null;
|
|
1867
|
-
let measuredCharWidth = 0;
|
|
1868
|
-
|
|
1869
|
-
function prepareDiff2HtmlHunks() {
|
|
1870
|
-
const wrappers = Array.from(document.querySelectorAll('.d2h-file-wrapper'));
|
|
1871
|
-
let globalHunkIndex = 0;
|
|
1872
|
-
wrappers.forEach((wrapper, fileIndex) => {
|
|
1873
|
-
wrapper.id = 'file-' + fileIndex;
|
|
1874
|
-
const fileName = wrapper.querySelector('.d2h-file-name')?.textContent?.trim() || '';
|
|
1875
|
-
const headerToIndex = new Map();
|
|
1876
|
-
const rows = Array.from(wrapper.querySelectorAll('tr'));
|
|
1877
|
-
rows.forEach((row) => {
|
|
1878
|
-
const header = row.textContent.trim();
|
|
1879
|
-
if (!header.startsWith('@@')) return;
|
|
1880
|
-
let index = headerToIndex.get(header);
|
|
1881
|
-
if (index === undefined) {
|
|
1882
|
-
index = globalHunkIndex;
|
|
1883
|
-
headerToIndex.set(header, index);
|
|
1884
|
-
row.classList.add('hunk');
|
|
1885
|
-
row.id = 'hunk-' + index;
|
|
1886
|
-
globalHunkIndex += 1;
|
|
1887
|
-
} else {
|
|
1888
|
-
row.classList.add('hunk-peer');
|
|
1889
|
-
}
|
|
1890
|
-
row.dataset.hunkIndex = String(index);
|
|
1891
|
-
row.dataset.file = fileName;
|
|
1892
|
-
});
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
prepareViewedControls();
|
|
1897
|
-
|
|
1898
|
-
function prepareViewedControls() {
|
|
1899
|
-
pruneViewedState();
|
|
1900
|
-
document.querySelectorAll('.d2h-file-wrapper').forEach((wrapper) => {
|
|
1901
|
-
const fileName = wrapper.querySelector('.d2h-file-name')?.textContent?.trim() || '';
|
|
1902
|
-
const toggle = wrapper.querySelector('.d2h-file-collapse');
|
|
1903
|
-
const input = toggle?.querySelector('input');
|
|
1904
|
-
if (!fileName || !toggle || !input) return;
|
|
1905
|
-
toggle.title = 'Mark viewed';
|
|
1906
|
-
input.tabIndex = -1;
|
|
1907
|
-
toggle.addEventListener('click', (event) => {
|
|
1908
|
-
event.preventDefault();
|
|
1909
|
-
setFileViewed(fileName, !isFileViewed(fileName));
|
|
1910
|
-
});
|
|
1911
|
-
});
|
|
1912
|
-
applyViewedState();
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
function loadViewedState() {
|
|
1916
|
-
try {
|
|
1917
|
-
const value = JSON.parse(localStorage.getItem(viewedKey) || '{}');
|
|
1918
|
-
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
1919
|
-
} catch {
|
|
1920
|
-
return {};
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
function saveViewedState(value) {
|
|
1925
|
-
try {
|
|
1926
|
-
localStorage.setItem(viewedKey, JSON.stringify(value));
|
|
1927
|
-
} catch {}
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
function currentFileSignature(path) {
|
|
1931
|
-
return fileSignatureByPath.get(path) || '';
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
function isFileViewed(path) {
|
|
1935
|
-
const viewed = loadViewedState();
|
|
1936
|
-
const signature = currentFileSignature(path);
|
|
1937
|
-
return Boolean(signature && viewed[path] === signature);
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
function setFileViewed(path, viewed) {
|
|
1941
|
-
const state = loadViewedState();
|
|
1942
|
-
if (viewed) {
|
|
1943
|
-
const signature = currentFileSignature(path);
|
|
1944
|
-
if (signature) state[path] = signature;
|
|
1945
|
-
} else {
|
|
1946
|
-
delete state[path];
|
|
1947
|
-
}
|
|
1948
|
-
saveViewedState(state);
|
|
1949
|
-
applyViewedState();
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
function pruneViewedState() {
|
|
1953
|
-
const state = loadViewedState();
|
|
1954
|
-
let changed = false;
|
|
1955
|
-
Object.keys(state).forEach((path) => {
|
|
1956
|
-
if (state[path] !== currentFileSignature(path)) {
|
|
1957
|
-
delete state[path];
|
|
1958
|
-
changed = true;
|
|
1959
|
-
}
|
|
1960
|
-
});
|
|
1961
|
-
if (changed) saveViewedState(state);
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
function applyViewedState() {
|
|
1965
|
-
document.querySelectorAll('.d2h-file-wrapper').forEach((wrapper) => {
|
|
1966
|
-
const fileName = wrapper.querySelector('.d2h-file-name')?.textContent?.trim() || '';
|
|
1967
|
-
const viewed = isFileViewed(fileName);
|
|
1968
|
-
wrapper.classList.toggle('file-viewed', viewed);
|
|
1969
|
-
const checkbox = wrapper.querySelector('.d2h-file-collapse-input');
|
|
1970
|
-
if (checkbox) checkbox.checked = viewed;
|
|
1971
|
-
});
|
|
1972
|
-
links.forEach((link) => {
|
|
1973
|
-
link.classList.toggle('viewed', isFileViewed(link.dataset.file || ''));
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
let activeDiffRow = null;
|
|
1978
|
-
function firstCodeRowOfHunk(hunkRow) {
|
|
1979
|
-
let row = hunkRow.nextElementSibling;
|
|
1980
|
-
let firstRow = null;
|
|
1981
|
-
while (row && !row.classList.contains('hunk') && !row.classList.contains('hunk-peer')) {
|
|
1982
|
-
if (row.querySelector && row.querySelector('.d2h-code-side-line')) {
|
|
1983
|
-
if (!firstRow) firstRow = row;
|
|
1984
|
-
if (row.querySelector('.d2h-ins, .d2h-del, ins, del')) return row;
|
|
1985
|
-
}
|
|
1986
|
-
row = row.nextElementSibling;
|
|
1987
|
-
}
|
|
1988
|
-
return firstRow || hunkRow;
|
|
1989
|
-
}
|
|
1990
|
-
function focusDiffRow(row) {
|
|
1991
|
-
if (activeDiffRow) activeDiffRow.classList.remove('diff-active-row');
|
|
1992
|
-
activeDiffRow = row || null;
|
|
1993
|
-
if (!row) return;
|
|
1994
|
-
row.classList.add('diff-active-row');
|
|
1995
|
-
const cell = row.querySelector('.d2h-code-line-ctn');
|
|
1996
|
-
const container = document.getElementById('diff2html-container');
|
|
1997
|
-
if (!cell || !container) return;
|
|
1998
|
-
try {
|
|
1999
|
-
const range = document.createRange();
|
|
2000
|
-
range.selectNodeContents(cell);
|
|
2001
|
-
range.collapse(true);
|
|
2002
|
-
const sel = window.getSelection();
|
|
2003
|
-
sel.removeAllRanges();
|
|
2004
|
-
sel.addRange(range);
|
|
2005
|
-
container.focus({ preventScroll: true });
|
|
2006
|
-
} catch (e) {}
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
function renderBreadcrumb(container, path) {
|
|
2010
|
-
if (!container) return;
|
|
2011
|
-
container.textContent = '';
|
|
2012
|
-
const parts = (path || '').split('/').filter(Boolean);
|
|
2013
|
-
parts.forEach((seg, i) => {
|
|
2014
|
-
if (i > 0) {
|
|
2015
|
-
const sep = document.createElement('span');
|
|
2016
|
-
sep.className = 'crumb-sep';
|
|
2017
|
-
sep.textContent = '›';
|
|
2018
|
-
container.appendChild(sep);
|
|
2019
|
-
}
|
|
2020
|
-
const span = document.createElement('span');
|
|
2021
|
-
span.className = i === parts.length - 1 ? 'crumb crumb-leaf' : 'crumb';
|
|
2022
|
-
span.textContent = seg;
|
|
2023
|
-
container.appendChild(span);
|
|
2024
|
-
});
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
function setActive(index, shouldScroll = true) {
|
|
2028
|
-
if (hunks.length === 0) return;
|
|
2029
|
-
current = ((index % hunks.length) + hunks.length) % hunks.length;
|
|
2030
|
-
document.getElementById('source-viewer')?.classList.add('hidden');
|
|
2031
|
-
document.getElementById('diff-view')?.classList.remove('hidden');
|
|
2032
|
-
setTab('changes');
|
|
2033
|
-
const active = hunks[current];
|
|
2034
|
-
const file = active.dataset.file;
|
|
2035
|
-
showOnlyFile(file);
|
|
2036
|
-
hunks.forEach((hunk, i) => hunk.classList.toggle('active', i === current));
|
|
2037
|
-
hunkPeers.forEach((hunk) => hunk.classList.toggle('active', Number(hunk.dataset.hunkIndex) === current));
|
|
2038
|
-
links.forEach((link) => link.classList.toggle('active', link.dataset.file === file));
|
|
2039
|
-
renderBreadcrumb(document.getElementById('diff-breadcrumb'), file);
|
|
2040
|
-
document.getElementById('hunk-counter').textContent = String(current + 1);
|
|
2041
|
-
const targetRow = firstCodeRowOfHunk(active);
|
|
2042
|
-
focusDiffRow(targetRow);
|
|
2043
|
-
if (shouldScroll) targetRow.scrollIntoView({ block: 'center' });
|
|
2044
|
-
if (file) rememberRecent(file, 'change');
|
|
2045
|
-
history.replaceState(null, '', '#hunk-' + current);
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
function showOnlyFile(fileName) {
|
|
2049
|
-
let activeNum = 0;
|
|
2050
|
-
const wrappers = Array.from(document.querySelectorAll('.d2h-file-wrapper'));
|
|
2051
|
-
wrappers.forEach((wrapper, i) => {
|
|
2052
|
-
const name = wrapper.querySelector('.d2h-file-name')?.textContent?.trim() || '';
|
|
2053
|
-
const isActive = name === fileName;
|
|
2054
|
-
wrapper.classList.toggle('df-inactive', !isActive);
|
|
2055
|
-
if (isActive) activeNum = i + 1;
|
|
2056
|
-
});
|
|
2057
|
-
const counter = document.getElementById('file-counter');
|
|
2058
|
-
if (counter) counter.textContent = activeNum + ' / ' + wrappers.length + ' files';
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
function next(delta) {
|
|
2062
|
-
if (hunks.length === 0) return;
|
|
2063
|
-
let idx = current < 0 ? initialHunkForNavigation(delta) : current + delta;
|
|
2064
|
-
for (let step = 0; step < hunks.length; step++) {
|
|
2065
|
-
const norm = ((idx % hunks.length) + hunks.length) % hunks.length;
|
|
2066
|
-
if (!isFileViewed(hunks[norm].dataset.file || '')) { setActive(norm); return; }
|
|
2067
|
-
idx += delta;
|
|
2068
|
-
}
|
|
2069
|
-
setActive((((current < 0 ? 0 : current + delta) % hunks.length) + hunks.length) % hunks.length);
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
function initialHunkForNavigation(delta) {
|
|
2073
|
-
const openPath = document.getElementById('source-viewer')?.dataset.openPath || '';
|
|
2074
|
-
const sourceHunk = firstHunkForPath(openPath);
|
|
2075
|
-
if (sourceHunk >= 0) return sourceHunk;
|
|
2076
|
-
return delta < 0 ? hunks.length - 1 : 0;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
function firstHunkForPath(path) {
|
|
2080
|
-
if (!path) return -1;
|
|
2081
|
-
const link = links.find((candidate) => candidate.dataset.file === path);
|
|
2082
|
-
if (!link) return -1;
|
|
2083
|
-
const index = Number(link.dataset.hunk);
|
|
2084
|
-
return Number.isNaN(index) ? -1 : index;
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
function openQuickOpen(mode) {
|
|
2088
|
-
if (!quickOpen || !quickInput || !quickModeLabel) return;
|
|
2089
|
-
quickMode = mode;
|
|
2090
|
-
quickModeLabel.textContent = mode === 'recent' ? 'Recent files' : mode === 'content' ? 'Find in Files' : 'Search files';
|
|
2091
|
-
quickOpen.classList.remove('hidden');
|
|
2092
|
-
quickInput.value = '';
|
|
2093
|
-
renderQuickOpenResults();
|
|
2094
|
-
setTimeout(() => quickInput.focus(), 0);
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
function closeQuickOpen() {
|
|
2098
|
-
quickOpen?.classList.add('hidden');
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
function handleQuickOpenKey(event) {
|
|
2102
|
-
if (event.key === 'Escape') {
|
|
2103
|
-
event.preventDefault();
|
|
2104
|
-
closeQuickOpen();
|
|
2105
|
-
return true;
|
|
2106
|
-
}
|
|
2107
|
-
if (event.key === 'ArrowDown') {
|
|
2108
|
-
event.preventDefault();
|
|
2109
|
-
quickActive = Math.min(quickActive + 1, Math.max(quickItems.length - 1, 0));
|
|
2110
|
-
updateQuickActive();
|
|
2111
|
-
return true;
|
|
2112
|
-
}
|
|
2113
|
-
if (event.key === 'ArrowUp') {
|
|
2114
|
-
event.preventDefault();
|
|
2115
|
-
quickActive = Math.max(quickActive - 1, 0);
|
|
2116
|
-
updateQuickActive();
|
|
2117
|
-
return true;
|
|
2118
|
-
}
|
|
2119
|
-
if (event.key === 'Enter') {
|
|
2120
|
-
event.preventDefault();
|
|
2121
|
-
openQuickItem(quickItems[quickActive]);
|
|
2122
|
-
return true;
|
|
2123
|
-
}
|
|
2124
|
-
return false;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
function renderQuickOpenResults() {
|
|
2128
|
-
if (!quickResults) return;
|
|
2129
|
-
const query = quickInput?.value.trim().toLowerCase() || '';
|
|
2130
|
-
const candidates = quickMode === 'recent' && query.length === 0 ? recentItems() : allQuickItems();
|
|
2131
|
-
quickItems = candidates
|
|
2132
|
-
.filter((item) => quickMode !== 'recent' || query.length > 0 || item.recent)
|
|
2133
|
-
.filter((item) => {
|
|
2134
|
-
if (query.length === 0) return true;
|
|
2135
|
-
if (quickMode === 'content') {
|
|
2136
|
-
const file = sourceByPath.get(item.path);
|
|
2137
|
-
return Boolean(file && file.embedded && file.content.toLowerCase().includes(query));
|
|
2138
|
-
}
|
|
2139
|
-
return (item.path + '\n' + item.name + '\n' + item.detail).toLowerCase().includes(query);
|
|
2140
|
-
})
|
|
2141
|
-
.sort((a, b) => scoreQuickItem(a, query) - scoreQuickItem(b, query) || a.path.localeCompare(b.path))
|
|
2142
|
-
.slice(0, 80);
|
|
2143
|
-
quickActive = Math.min(quickActive, Math.max(quickItems.length - 1, 0));
|
|
2144
|
-
if (quickItems.length === 0) {
|
|
2145
|
-
quickResults.innerHTML = '<div class="quick-open-empty">No files found.</div>';
|
|
2146
|
-
return;
|
|
2147
|
-
}
|
|
2148
|
-
quickResults.innerHTML = quickItems.map((item, index) => [
|
|
2149
|
-
'<button type="button" class="quick-open-item' + (index === quickActive ? ' active' : '') + '" data-index="' + index + '">',
|
|
2150
|
-
'<span class="quick-open-main">',
|
|
2151
|
-
'<span class="quick-open-name">' + escapeHtml(item.name) + '</span>',
|
|
2152
|
-
'<span class="quick-open-path">' + escapeHtml(item.path) + '</span>',
|
|
2153
|
-
'</span>',
|
|
2154
|
-
'<span class="quick-open-badge">' + escapeHtml(item.detail) + '</span>',
|
|
2155
|
-
'</button>',
|
|
2156
|
-
].join('')).join('');
|
|
2157
|
-
renderQuickPreview(quickItems[quickActive]);
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
function updateQuickActive() {
|
|
2161
|
-
quickResults?.querySelectorAll('.quick-open-item').forEach((element, index) => {
|
|
2162
|
-
const active = index === quickActive;
|
|
2163
|
-
element.classList.toggle('active', active);
|
|
2164
|
-
if (active) element.scrollIntoView({ block: 'nearest' });
|
|
2165
|
-
});
|
|
2166
|
-
renderQuickPreview(quickItems[quickActive]);
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
function renderQuickPreview(item) {
|
|
2170
|
-
const preview = document.getElementById('quick-open-preview');
|
|
2171
|
-
if (!preview) return;
|
|
2172
|
-
if (!item) { preview.innerHTML = ''; return; }
|
|
2173
|
-
const file = sourceByPath.get(item.path);
|
|
2174
|
-
if (!file || !file.embedded) {
|
|
2175
|
-
preview.innerHTML = '<div class="qp-empty">' + escapeHtml(item.path) + '</div>';
|
|
2176
|
-
return;
|
|
2177
|
-
}
|
|
2178
|
-
const query = ((quickInput && quickInput.value) || '').trim().toLowerCase();
|
|
2179
|
-
const lines = file.content.split(/\r?\n/);
|
|
2180
|
-
let firstHit = -1;
|
|
2181
|
-
const rows = lines.map((line, i) => {
|
|
2182
|
-
const hit = query.length > 0 && line.toLowerCase().includes(query);
|
|
2183
|
-
if (hit && firstHit < 0) firstHit = i;
|
|
2184
|
-
return '<div class="qp-line' + (hit ? ' qp-hit' : '') + '"><span class="qp-num">' + (i + 1) + '</span><span class="qp-code">' + highlightLine(line, file.language || 'text') + '</span></div>';
|
|
2185
|
-
}).join('');
|
|
2186
|
-
preview.innerHTML = '<div class="qp-head">' + escapeHtml(item.path) + '</div><div class="qp-body">' + rows + '</div>';
|
|
2187
|
-
if (firstHit >= 0) {
|
|
2188
|
-
const target = preview.querySelectorAll('.qp-line')[firstHit];
|
|
2189
|
-
if (target) target.scrollIntoView({ block: 'center' });
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
function openQuickItem(item) {
|
|
2194
|
-
if (!item) return;
|
|
2195
|
-
closeQuickOpen();
|
|
2196
|
-
rememberRecent(item.path, item.kind);
|
|
2197
|
-
if (sourceByPath.has(item.path)) {
|
|
2198
|
-
openSourceFile(item.path);
|
|
2199
|
-
return;
|
|
2200
|
-
}
|
|
2201
|
-
const link = links.find((candidate) => candidate.dataset.file === item.path);
|
|
2202
|
-
if (!link) return;
|
|
2203
|
-
const target = Number(link.dataset.hunk);
|
|
2204
|
-
if (!Number.isNaN(target) && target >= 0 && target < hunks.length) {
|
|
2205
|
-
setActive(target);
|
|
2206
|
-
} else {
|
|
2207
|
-
showDiffView(false);
|
|
2208
|
-
const targetId = link.getAttribute('href')?.slice(1);
|
|
2209
|
-
if (targetId) document.getElementById(targetId)?.scrollIntoView({ block: 'center' });
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
function allQuickItems() {
|
|
2214
|
-
const items = sourceFiles.map((file) => ({
|
|
2215
|
-
path: file.path,
|
|
2216
|
-
name: baseName(file.path),
|
|
2217
|
-
detail: [file.changed ? 'changed' : 'file', file.language || 'text'].join(' - '),
|
|
2218
|
-
kind: 'source',
|
|
2219
|
-
recent: false,
|
|
2220
|
-
}));
|
|
2221
|
-
links.forEach((link) => {
|
|
2222
|
-
const path = link.dataset.file || '';
|
|
2223
|
-
if (!path || sourceByPath.has(path)) return;
|
|
2224
|
-
items.push({ path, name: baseName(path), detail: 'diff', kind: 'change', recent: false });
|
|
2225
|
-
});
|
|
2226
|
-
const recent = loadRecent();
|
|
2227
|
-
const recentRank = new Map(recent.map((item, index) => [item.path, index]));
|
|
2228
|
-
return items.map((item) => ({
|
|
2229
|
-
...item,
|
|
2230
|
-
recent: recentRank.has(item.path),
|
|
2231
|
-
recentRank: recentRank.get(item.path) ?? 9999,
|
|
2232
|
-
}));
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
function recentItems() {
|
|
2236
|
-
const all = allQuickItems();
|
|
2237
|
-
const byPath = new Map(all.map((item) => [item.path, item]));
|
|
2238
|
-
return loadRecent()
|
|
2239
|
-
.map((item) => byPath.get(item.path) || {
|
|
2240
|
-
path: item.path,
|
|
2241
|
-
name: baseName(item.path),
|
|
2242
|
-
detail: item.kind === 'change' ? 'diff' : 'file',
|
|
2243
|
-
kind: item.kind,
|
|
2244
|
-
recent: true,
|
|
2245
|
-
recentRank: 0,
|
|
2246
|
-
})
|
|
2247
|
-
.map((item, index) => ({ ...item, recent: true, recentRank: index }));
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
function scoreQuickItem(item, query) {
|
|
2251
|
-
let score = item.recentRank ?? 9999;
|
|
2252
|
-
if (!query) return score;
|
|
2253
|
-
const path = item.path.toLowerCase();
|
|
2254
|
-
const name = item.name.toLowerCase();
|
|
2255
|
-
if (name === query) score -= 3000;
|
|
2256
|
-
else if (name.startsWith(query)) score -= 2000;
|
|
2257
|
-
else if (path.includes('/' + query)) score -= 1000;
|
|
2258
|
-
else if (path.includes(query)) score -= 500;
|
|
2259
|
-
if (item.recent) score -= 100;
|
|
2260
|
-
return score;
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
function loadRecent() {
|
|
2264
|
-
try {
|
|
2265
|
-
const value = JSON.parse(localStorage.getItem(recentKey) || '[]');
|
|
2266
|
-
return Array.isArray(value) ? value.filter((item) => item && typeof item.path === 'string') : [];
|
|
2267
|
-
} catch {
|
|
2268
|
-
return [];
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
function rememberRecent(path, kind) {
|
|
2273
|
-
if (!path) return;
|
|
2274
|
-
const next = [{ path, kind }, ...loadRecent().filter((item) => item.path !== path)].slice(0, 30);
|
|
2275
|
-
try {
|
|
2276
|
-
localStorage.setItem(recentKey, JSON.stringify(next));
|
|
2277
|
-
} catch {}
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
function baseName(path) {
|
|
2281
|
-
return String(path).split('/').filter(Boolean).pop() || String(path);
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
function treeRows() {
|
|
2285
|
-
const panel = document.querySelector('.tab-panel:not(.hidden)');
|
|
2286
|
-
if (!panel) return [];
|
|
2287
|
-
return Array.from(panel.querySelectorAll('summary, .file-link')).filter((el) => el.getClientRects().length > 0);
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
function focusTree(index) {
|
|
2291
|
-
const rows = treeRows();
|
|
2292
|
-
if (rows.length === 0) return;
|
|
2293
|
-
treeFocusIndex = Math.max(0, Math.min(rows.length - 1, index));
|
|
2294
|
-
rows.forEach((row, i) => row.classList.toggle('tree-focus', i === treeFocusIndex));
|
|
2295
|
-
const el = rows[treeFocusIndex];
|
|
2296
|
-
if (el) el.scrollIntoView({ block: 'nearest' });
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
function clearTreeFocus() {
|
|
2300
|
-
treeFocusIndex = -1;
|
|
2301
|
-
document.querySelectorAll('.tree-focus').forEach((el) => el.classList.remove('tree-focus'));
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
function handleTreeKey(event) {
|
|
2305
|
-
const rows = treeRows();
|
|
2306
|
-
if (rows.length === 0) return false;
|
|
2307
|
-
if (treeFocusIndex >= rows.length) treeFocusIndex = rows.length - 1;
|
|
2308
|
-
const row = rows[treeFocusIndex];
|
|
2309
|
-
const isFolder = row && row.tagName === 'SUMMARY';
|
|
2310
|
-
if (event.key === 'ArrowDown') { event.preventDefault(); focusTree(treeFocusIndex + 1); return true; }
|
|
2311
|
-
if (event.key === 'ArrowUp') { event.preventDefault(); focusTree(treeFocusIndex - 1); return true; }
|
|
2312
|
-
if (event.key === 'Enter') {
|
|
2313
|
-
event.preventDefault();
|
|
2314
|
-
if (row && row.classList.contains('file-link')) { row.click(); clearTreeFocus(); }
|
|
2315
|
-
else if (isFolder && row.parentElement) row.parentElement.open = !row.parentElement.open;
|
|
2316
|
-
return true;
|
|
2317
|
-
}
|
|
2318
|
-
if (event.key === 'ArrowRight') {
|
|
2319
|
-
event.preventDefault();
|
|
2320
|
-
if (isFolder && row.parentElement && !row.parentElement.open) row.parentElement.open = true;
|
|
2321
|
-
else focusTree(treeFocusIndex + 1);
|
|
2322
|
-
return true;
|
|
2323
|
-
}
|
|
2324
|
-
if (event.key === 'ArrowLeft') {
|
|
2325
|
-
event.preventDefault();
|
|
2326
|
-
if (isFolder && row.parentElement && row.parentElement.open) row.parentElement.open = false;
|
|
2327
|
-
else focusTree(treeFocusIndex - 1);
|
|
2328
|
-
return true;
|
|
2329
|
-
}
|
|
2330
|
-
if (event.key === 'Escape') { event.preventDefault(); clearTreeFocus(); return true; }
|
|
2331
|
-
return false;
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
document.addEventListener('keydown', (event) => {
|
|
2335
|
-
if (!quickOpen?.classList.contains('hidden')) {
|
|
2336
|
-
if (handleQuickOpenKey(event)) return;
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
if ((event.metaKey || event.ctrlKey) && event.key === '1') {
|
|
2340
|
-
event.preventDefault();
|
|
2341
|
-
setTab('files');
|
|
2342
|
-
focusTree(0);
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
if ((event.metaKey || event.ctrlKey) && event.key === '0') {
|
|
2346
|
-
event.preventDefault();
|
|
2347
|
-
setTab('changes');
|
|
2348
|
-
focusTree(0);
|
|
2349
|
-
return;
|
|
2350
|
-
}
|
|
2351
|
-
if (treeFocusIndex >= 0 && handleTreeKey(event)) return;
|
|
2352
|
-
if (treeFocusIndex < 0 && !event.metaKey && !event.ctrlKey && !event.altKey && isSourceViewerVisible() && handleSourceCaretKey(event)) return;
|
|
2353
|
-
|
|
2354
|
-
if (event.key === 'Shift' && !event.repeat) {
|
|
2355
|
-
const now = performance.now();
|
|
2356
|
-
if (now - lastShiftAt < 1000) {
|
|
2357
|
-
event.preventDefault();
|
|
2358
|
-
lastShiftAt = 0;
|
|
2359
|
-
openQuickOpen('all');
|
|
2360
|
-
return;
|
|
2361
|
-
}
|
|
2362
|
-
lastShiftAt = now;
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === 'f') {
|
|
2366
|
-
event.preventDefault();
|
|
2367
|
-
openQuickOpen('content');
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2370
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'e') {
|
|
2371
|
-
event.preventDefault();
|
|
2372
|
-
openQuickOpen('recent');
|
|
2373
|
-
return;
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
if ((event.metaKey || event.ctrlKey) && event.key === 'ArrowDown') {
|
|
2377
|
-
event.preventDefault();
|
|
2378
|
-
if (isSourceViewerVisible()) goToSymbolUnderCursor();
|
|
2379
|
-
else openDiffFileAtCaret();
|
|
2380
|
-
return;
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
if ((event.metaKey || event.ctrlKey) && !event.altKey && (event.key === 'ArrowLeft' || event.key === 'ArrowRight') && isSourceViewerVisible() && viewerCursor) {
|
|
2384
|
-
event.preventDefault();
|
|
2385
|
-
const lineEdgeFile = sourceByPath.get(viewerCursor.path);
|
|
2386
|
-
if (lineEdgeFile && lineEdgeFile.embedded) {
|
|
2387
|
-
const lineEdgeLines = lineEdgeFile.content.split(/\r?\n/);
|
|
2388
|
-
const lineEdgeCol = event.key === 'ArrowLeft' ? 0 : (lineEdgeLines[viewerCursor.lineIndex] || '').length;
|
|
2389
|
-
if (event.shiftKey) { if (!selectionAnchor) selectionAnchor = { lineIndex: viewerCursor.lineIndex, column: viewerCursor.column }; }
|
|
2390
|
-
else selectionAnchor = null;
|
|
2391
|
-
setSourceCursor(viewerCursor.path, viewerCursor.lineIndex, lineEdgeCol, true, -1);
|
|
2392
|
-
applySourceSelection();
|
|
2393
|
-
}
|
|
2394
|
-
return;
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
if (event.key === 'F7') {
|
|
2398
|
-
event.preventDefault();
|
|
2399
|
-
if (!document.getElementById('source-viewer')?.classList.contains('hidden')) {
|
|
2400
|
-
const sourceHunk = firstHunkForPath(document.getElementById('source-viewer')?.dataset.openPath || '');
|
|
2401
|
-
if (sourceHunk >= 0) {
|
|
2402
|
-
setActive(sourceHunk);
|
|
2403
|
-
return;
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
next(event.shiftKey ? -1 : 1);
|
|
2407
|
-
} else if (event.key === ']') {
|
|
2408
|
-
event.preventDefault();
|
|
2409
|
-
next(1);
|
|
2410
|
-
} else if (event.key === '[') {
|
|
2411
|
-
event.preventDefault();
|
|
2412
|
-
next(-1);
|
|
2413
|
-
}
|
|
2414
|
-
});
|
|
2415
|
-
|
|
2416
|
-
quickInput?.addEventListener('input', () => renderQuickOpenResults());
|
|
2417
|
-
quickResults?.addEventListener('mousemove', (event) => {
|
|
2418
|
-
const item = event.target.closest?.('.quick-open-item');
|
|
2419
|
-
if (!item) return;
|
|
2420
|
-
quickActive = Number(item.dataset.index || 0);
|
|
2421
|
-
updateQuickActive();
|
|
2422
|
-
});
|
|
2423
|
-
quickResults?.addEventListener('click', (event) => {
|
|
2424
|
-
const item = event.target.closest?.('.quick-open-item');
|
|
2425
|
-
if (!item) return;
|
|
2426
|
-
const index = Number(item.dataset.index || 0);
|
|
2427
|
-
openQuickItem(quickItems[index]);
|
|
2428
|
-
});
|
|
2429
|
-
quickOpen?.addEventListener('click', (event) => {
|
|
2430
|
-
if (event.target === quickOpen) closeQuickOpen();
|
|
2431
|
-
});
|
|
2432
|
-
|
|
2433
|
-
links.forEach((link) => {
|
|
2434
|
-
link.addEventListener('click', (event) => {
|
|
2435
|
-
showDiffView(false);
|
|
2436
|
-
const target = Number(link.dataset.hunk);
|
|
2437
|
-
if (!Number.isNaN(target) && target >= 0 && target < hunks.length) {
|
|
2438
|
-
event.preventDefault();
|
|
2439
|
-
setActive(target);
|
|
2440
|
-
}
|
|
2441
|
-
});
|
|
2442
|
-
});
|
|
2443
|
-
|
|
2444
|
-
sourceLinks.forEach((link) => {
|
|
2445
|
-
link.addEventListener('click', () => {
|
|
2446
|
-
const path = link.dataset.sourceFile;
|
|
2447
|
-
if (path) openSourceFile(path);
|
|
2448
|
-
});
|
|
2449
|
-
});
|
|
2450
|
-
|
|
2451
|
-
document.querySelectorAll('.tab').forEach((button) => {
|
|
2452
|
-
button.addEventListener('click', () => setTab(button.dataset.tab || 'changes'));
|
|
2453
|
-
});
|
|
2454
|
-
|
|
2455
|
-
document.getElementById('back-to-diff')?.addEventListener('click', () => showDiffView(true));
|
|
2456
|
-
document.getElementById('source-body')?.addEventListener('click', handleSourceClick);
|
|
2457
|
-
document.addEventListener('copy', handleSourceCopy);
|
|
2458
|
-
|
|
2459
|
-
searchInput?.addEventListener('input', () => {
|
|
2460
|
-
filterNavigation(searchInput.value);
|
|
2461
|
-
const openPath = document.getElementById('source-viewer')?.dataset.openPath;
|
|
2462
|
-
if (openPath) openSourceFile(openPath, false);
|
|
2463
|
-
});
|
|
2464
|
-
|
|
2465
|
-
const restored = restoreUiState();
|
|
2466
|
-
if (!restored) {
|
|
2467
|
-
const initial = location.hash.match(/^#hunk-(\\d+)$/);
|
|
2468
|
-
if (initial) setActive(Number(initial[1]), false);
|
|
2469
|
-
else openDefaultSourceFile();
|
|
2470
|
-
}
|
|
2471
|
-
if (watchEnabled) setInterval(checkForLiveUpdate, 1500);
|
|
2472
|
-
window.addEventListener('beforeunload', saveUiState);
|
|
2473
|
-
|
|
2474
|
-
(function setupSidebarResize() {
|
|
2475
|
-
const resizer = document.querySelector('.sidebar-resizer');
|
|
2476
|
-
if (!resizer) return;
|
|
2477
|
-
const sidebarKey = 'monacori-sidebar-width:' + location.pathname;
|
|
2478
|
-
const saved = localStorage.getItem(sidebarKey);
|
|
2479
|
-
if (saved) document.documentElement.style.setProperty('--sidebar-width', saved);
|
|
2480
|
-
let resizing = false;
|
|
2481
|
-
resizer.addEventListener('mousedown', (event) => {
|
|
2482
|
-
resizing = true;
|
|
2483
|
-
resizer.classList.add('resizing');
|
|
2484
|
-
document.body.style.userSelect = 'none';
|
|
2485
|
-
event.preventDefault();
|
|
2486
|
-
});
|
|
2487
|
-
document.addEventListener('mousemove', (event) => {
|
|
2488
|
-
if (!resizing) return;
|
|
2489
|
-
const width = Math.min(640, Math.max(180, event.clientX));
|
|
2490
|
-
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
|
|
2491
|
-
});
|
|
2492
|
-
document.addEventListener('mouseup', () => {
|
|
2493
|
-
if (!resizing) return;
|
|
2494
|
-
resizing = false;
|
|
2495
|
-
resizer.classList.remove('resizing');
|
|
2496
|
-
document.body.style.userSelect = '';
|
|
2497
|
-
try { localStorage.setItem(sidebarKey, getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width').trim()); } catch (e) {}
|
|
2498
|
-
});
|
|
2499
|
-
})();
|
|
2500
|
-
|
|
2501
|
-
(function setupDiffCaret() {
|
|
2502
|
-
const container = document.getElementById('diff2html-container');
|
|
2503
|
-
if (!container) return;
|
|
2504
|
-
container.setAttribute('contenteditable', 'true');
|
|
2505
|
-
container.setAttribute('spellcheck', 'false');
|
|
2506
|
-
container.setAttribute('aria-readonly', 'true');
|
|
2507
|
-
container.querySelectorAll('.d2h-code-side-linenumber, .d2h-code-linenumber, .d2h-code-line-prefix').forEach((el) => el.setAttribute('contenteditable', 'false'));
|
|
2508
|
-
const block = (event) => event.preventDefault();
|
|
2509
|
-
container.addEventListener('focusin', () => clearTreeFocus());
|
|
2510
|
-
container.addEventListener('mousedown', () => clearTreeFocus());
|
|
2511
|
-
container.addEventListener('beforeinput', block);
|
|
2512
|
-
container.addEventListener('paste', block);
|
|
2513
|
-
container.addEventListener('drop', block);
|
|
2514
|
-
container.addEventListener('dragstart', block);
|
|
2515
|
-
container.addEventListener('keydown', (event) => {
|
|
2516
|
-
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
2517
|
-
if (event.key.length === 1 || event.key === 'Enter' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Tab') {
|
|
2518
|
-
event.preventDefault();
|
|
2519
|
-
}
|
|
2520
|
-
});
|
|
2521
|
-
})();
|
|
2522
|
-
|
|
2523
|
-
(function checkForUpdate() {
|
|
2524
|
-
try { if (sessionStorage.getItem('monacori-update-checked')) return; } catch (e) {}
|
|
2525
|
-
var current = window.__MONACORI_VERSION__ || '';
|
|
2526
|
-
if (!current || typeof fetch !== 'function') return;
|
|
2527
|
-
try { sessionStorage.setItem('monacori-update-checked', '1'); } catch (e) {}
|
|
2528
|
-
var isNewer = function (a, b) {
|
|
2529
|
-
var pa = String(a).split('.'), pb = String(b).split('.');
|
|
2530
|
-
for (var i = 0; i < 3; i++) {
|
|
2531
|
-
var x = parseInt(pa[i], 10) || 0, y = parseInt(pb[i], 10) || 0;
|
|
2532
|
-
if (x > y) return true;
|
|
2533
|
-
if (x < y) return false;
|
|
2534
|
-
}
|
|
2535
|
-
return false;
|
|
2536
|
-
};
|
|
2537
|
-
fetch('https://registry.npmjs.org/@happy-nut/monacori/latest', { cache: 'no-store' })
|
|
2538
|
-
.then(function (res) { return res && res.ok ? res.json() : null; })
|
|
2539
|
-
.then(function (data) {
|
|
2540
|
-
if (!data || !data.version || !isNewer(data.version, current)) return;
|
|
2541
|
-
var badge = document.getElementById('update-badge');
|
|
2542
|
-
if (!badge) return;
|
|
2543
|
-
badge.textContent = 'Update available: v' + data.version;
|
|
2544
|
-
badge.classList.remove('hidden');
|
|
2545
|
-
})
|
|
2546
|
-
.catch(function () {});
|
|
2547
|
-
})();
|
|
2548
|
-
|
|
2549
|
-
function setTab(name) {
|
|
2550
|
-
document.querySelectorAll('.tab').forEach((button) => {
|
|
2551
|
-
button.classList.toggle('active', button.dataset.tab === name);
|
|
2552
|
-
});
|
|
2553
|
-
document.getElementById('changes-panel')?.classList.toggle('hidden', name !== 'changes');
|
|
2554
|
-
document.getElementById('files-panel')?.classList.toggle('hidden', name !== 'files');
|
|
2555
|
-
}
|
|
2556
|
-
|
|
2557
|
-
function showDiffView(shouldScroll) {
|
|
2558
|
-
document.getElementById('source-viewer')?.classList.add('hidden');
|
|
2559
|
-
document.getElementById('diff-view')?.classList.remove('hidden');
|
|
2560
|
-
setTab('changes');
|
|
2561
|
-
if (current < 0 && hunks.length) {
|
|
2562
|
-
setActive(0, shouldScroll);
|
|
2563
|
-
return;
|
|
2564
|
-
}
|
|
2565
|
-
if (current >= 0 && hunks[current]) {
|
|
2566
|
-
showOnlyFile(hunks[current].dataset.file);
|
|
2567
|
-
if (shouldScroll) hunks[current].scrollIntoView({ block: 'start' });
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
function showSourceView() {
|
|
2572
|
-
document.getElementById('diff-view')?.classList.add('hidden');
|
|
2573
|
-
document.getElementById('source-viewer')?.classList.remove('hidden');
|
|
2574
|
-
setTab('files');
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
function saveUiState() {
|
|
2578
|
-
const activeTab = document.querySelector('.tab.active')?.dataset.tab || 'changes';
|
|
2579
|
-
const sourcePath = document.getElementById('source-viewer')?.dataset.openPath || '';
|
|
2580
|
-
sessionStorage.setItem(uiStateKey, JSON.stringify({
|
|
2581
|
-
search: searchInput?.value || '',
|
|
2582
|
-
tab: activeTab,
|
|
2583
|
-
view: document.getElementById('source-viewer')?.classList.contains('hidden') ? 'diff' : 'source',
|
|
2584
|
-
sourcePath,
|
|
2585
|
-
hash: location.hash,
|
|
2586
|
-
}));
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
function restoreUiState() {
|
|
2590
|
-
const raw = sessionStorage.getItem(uiStateKey);
|
|
2591
|
-
if (!raw) return false;
|
|
2592
|
-
try {
|
|
2593
|
-
const state = JSON.parse(raw);
|
|
2594
|
-
if (searchInput && state.search) {
|
|
2595
|
-
searchInput.value = state.search;
|
|
2596
|
-
filterNavigation(state.search);
|
|
2597
|
-
}
|
|
2598
|
-
if (state.view === 'diff') {
|
|
2599
|
-
const match = String(state.hash || location.hash || '').match(/^#hunk-(\\d+)$/);
|
|
2600
|
-
setActive(match ? Number(match[1]) : current >= 0 ? current : 0, false);
|
|
2601
|
-
return true;
|
|
2602
|
-
}
|
|
2603
|
-
if (state.sourcePath && sourceByPath.has(state.sourcePath)) {
|
|
2604
|
-
openSourceFile(state.sourcePath);
|
|
2605
|
-
return true;
|
|
2606
|
-
}
|
|
2607
|
-
} catch {
|
|
2608
|
-
sessionStorage.removeItem(uiStateKey);
|
|
2609
|
-
}
|
|
2610
|
-
return false;
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
async function checkForLiveUpdate() {
|
|
2614
|
-
if (checkingForUpdates) return;
|
|
2615
|
-
checkingForUpdates = true;
|
|
2616
|
-
const liveStatus = document.getElementById('live-status');
|
|
2617
|
-
try {
|
|
2618
|
-
const response = await fetch('/__ai_flow_state', { cache: 'no-store' });
|
|
2619
|
-
if (!response.ok) return;
|
|
2620
|
-
const state = await response.json();
|
|
2621
|
-
if (liveStatus && state.generatedAt) {
|
|
2622
|
-
liveStatus.textContent = 'Live: updated ' + new Date(state.generatedAt).toLocaleTimeString();
|
|
2623
|
-
}
|
|
2624
|
-
if (state.signature && state.signature !== currentSignature) {
|
|
2625
|
-
saveUiState();
|
|
2626
|
-
location.reload();
|
|
2627
|
-
}
|
|
2628
|
-
} catch {
|
|
2629
|
-
if (liveStatus) liveStatus.textContent = 'Live: waiting for diff server';
|
|
2630
|
-
} finally {
|
|
2631
|
-
checkingForUpdates = false;
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
function filterNavigation(rawQuery) {
|
|
2636
|
-
const query = rawQuery.trim().toLowerCase();
|
|
2637
|
-
links.forEach((link) => {
|
|
2638
|
-
const path = link.dataset.file || '';
|
|
2639
|
-
const source = sourceByPath.get(path);
|
|
2640
|
-
const haystack = (path + '\n' + (source?.content || '')).toLowerCase();
|
|
2641
|
-
link.hidden = query.length > 0 && !haystack.includes(query);
|
|
2642
|
-
});
|
|
2643
|
-
sourceLinks.forEach((link) => {
|
|
2644
|
-
const path = link.dataset.sourceFile || '';
|
|
2645
|
-
const source = sourceByPath.get(path);
|
|
2646
|
-
const haystack = (path + '\n' + (source?.content || '')).toLowerCase();
|
|
2647
|
-
link.hidden = query.length > 0 && !haystack.includes(query);
|
|
2648
|
-
});
|
|
2649
|
-
updateTreeVisibility(document.getElementById('changes-panel'), query);
|
|
2650
|
-
updateTreeVisibility(document.getElementById('files-panel'), query);
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
function updateTreeVisibility(root, query) {
|
|
2654
|
-
if (!root) return;
|
|
2655
|
-
Array.from(root.querySelectorAll('details')).reverse().forEach((details) => {
|
|
2656
|
-
const hasVisibleLeaf = Array.from(details.children).some((child) => {
|
|
2657
|
-
if (child.tagName === 'SUMMARY') return false;
|
|
2658
|
-
return !child.hidden;
|
|
2659
|
-
});
|
|
2660
|
-
details.hidden = query.length > 0 && !hasVisibleLeaf;
|
|
2661
|
-
if (query.length > 0 && hasVisibleLeaf) details.open = true;
|
|
2662
|
-
});
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
function openDefaultSourceFile() {
|
|
2666
|
-
const file = sourceFiles.find((candidate) => candidate.changed && candidate.embedded)
|
|
2667
|
-
|| sourceFiles.find((candidate) => candidate.embedded)
|
|
2668
|
-
|| sourceFiles.find((candidate) => candidate.changed)
|
|
2669
|
-
|| sourceFiles[0];
|
|
2670
|
-
if (file) {
|
|
2671
|
-
openSourceFile(file.path);
|
|
2672
|
-
return;
|
|
2673
|
-
}
|
|
2674
|
-
if (hunks.length > 0) setActive(0, false);
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
function handleSourceCopy(event) {
|
|
2678
|
-
const selection = window.getSelection();
|
|
2679
|
-
const sourceBody = document.getElementById('source-body');
|
|
2680
|
-
const viewer = document.getElementById('source-viewer');
|
|
2681
|
-
if (!selection || selection.isCollapsed || !sourceBody || !viewer || viewer.classList.contains('hidden')) return;
|
|
2682
|
-
if (!selection.anchorNode || !selection.focusNode) return;
|
|
2683
|
-
if (!sourceBody.contains(selection.anchorNode) || !sourceBody.contains(selection.focusNode)) return;
|
|
2684
|
-
|
|
2685
|
-
const path = viewer.dataset.openPath || '';
|
|
2686
|
-
const file = sourceByPath.get(path);
|
|
2687
|
-
if (!file || !file.embedded) return;
|
|
2688
|
-
const rows = selectedSourceRows(selection);
|
|
2689
|
-
if (rows.length === 0) return;
|
|
2690
|
-
|
|
2691
|
-
const lineNumbers = rows
|
|
2692
|
-
.map((row) => Number(row.dataset.lineIndex || 0) + 1)
|
|
2693
|
-
.filter((line) => Number.isFinite(line))
|
|
2694
|
-
.sort((a, b) => a - b);
|
|
2695
|
-
const startLine = lineNumbers[0];
|
|
2696
|
-
const endLine = lineNumbers[lineNumbers.length - 1];
|
|
2697
|
-
if (!startLine || !endLine) return;
|
|
2698
|
-
|
|
2699
|
-
const selectedText = cleanSelectedSourceText(selection.toString(), rows);
|
|
2700
|
-
const code = selectedText || sourceLinesForRows(file, rows);
|
|
2701
|
-
if (!code.trim()) return;
|
|
2702
|
-
|
|
2703
|
-
const reference = path + ':' + (startLine === endLine ? String(startLine) : startLine + '-' + endLine);
|
|
2704
|
-
const language = file.language && file.language !== 'text' ? file.language : '';
|
|
2705
|
-
const fence = String.fromCharCode(96).repeat(3);
|
|
2706
|
-
const payload = reference + '\n\n' + fence + language + '\n' + code.replace(/\s+$/g, '') + '\n' + fence;
|
|
2707
|
-
event.clipboardData?.setData('text/plain', payload);
|
|
2708
|
-
event.preventDefault();
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
|
-
function selectedSourceRows(selection) {
|
|
2712
|
-
if (!selection.rangeCount) return [];
|
|
2713
|
-
const ranges = Array.from({ length: selection.rangeCount }, (_, index) => selection.getRangeAt(index));
|
|
2714
|
-
return Array.from(document.querySelectorAll('#source-body .source-row'))
|
|
2715
|
-
.filter((row) => ranges.some((range) => {
|
|
2716
|
-
try {
|
|
2717
|
-
return range.intersectsNode(row);
|
|
2718
|
-
} catch {
|
|
2719
|
-
return false;
|
|
2720
|
-
}
|
|
2721
|
-
}))
|
|
2722
|
-
.sort((a, b) => Number(a.dataset.lineIndex || 0) - Number(b.dataset.lineIndex || 0));
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
function cleanSelectedSourceText(text, rows) {
|
|
2726
|
-
const value = String(text || '').replace(/\r/g, '').replace(/\u200b/g, '');
|
|
2727
|
-
if (!value.trim()) return '';
|
|
2728
|
-
const lineNumbers = rows.map((row) => Number(row.dataset.lineIndex || 0) + 1);
|
|
2729
|
-
const lines = value.split('\n');
|
|
2730
|
-
if (lines.length >= lineNumbers.length) {
|
|
2731
|
-
return lines
|
|
2732
|
-
.map((line, index) => {
|
|
2733
|
-
const lineNumber = lineNumbers[index];
|
|
2734
|
-
return lineNumber ? line.replace(new RegExp('^\\s*' + lineNumber + '\\s+'), '') : line;
|
|
2735
|
-
})
|
|
2736
|
-
.join('\n')
|
|
2737
|
-
.trimEnd();
|
|
2738
|
-
}
|
|
2739
|
-
return value.trimEnd();
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
function sourceLinesForRows(file, rows) {
|
|
2743
|
-
const lines = file.content.split(/\r?\n/);
|
|
2744
|
-
return rows
|
|
2745
|
-
.map((row) => lines[Number(row.dataset.lineIndex || 0)] || '')
|
|
2746
|
-
.join('\n')
|
|
2747
|
-
.trimEnd();
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
|
-
function handleSourceClick(event) {
|
|
2751
|
-
const target = event.target;
|
|
2752
|
-
const row = target?.closest?.('.source-row');
|
|
2753
|
-
if (!row) return;
|
|
2754
|
-
clearTreeFocus();
|
|
2755
|
-
const viewer = document.getElementById('source-viewer');
|
|
2756
|
-
const path = viewer?.dataset.openPath || '';
|
|
2757
|
-
const file = sourceByPath.get(path);
|
|
2758
|
-
if (!file || !file.embedded) return;
|
|
2759
|
-
const lineIndex = Number(row.dataset.lineIndex || 0);
|
|
2760
|
-
const lines = file.content.split(/\r?\n/);
|
|
2761
|
-
const line = lines[lineIndex] || '';
|
|
2762
|
-
const codeCell = row.querySelector('.source-code');
|
|
2763
|
-
const column = estimateColumnFromClick(codeCell, event, line);
|
|
2764
|
-
setSourceCursor(path, lineIndex, column, false, -1);
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
function estimateColumnFromClick(codeCell, event, line) {
|
|
2768
|
-
if (!codeCell) return 0;
|
|
2769
|
-
const rect = codeCell.getBoundingClientRect();
|
|
2770
|
-
const style = getComputedStyle(codeCell);
|
|
2771
|
-
const paddingLeft = Number.parseFloat(style.paddingLeft || '0') || 0;
|
|
2772
|
-
const x = event.clientX - rect.left - paddingLeft;
|
|
2773
|
-
const width = measuredCharWidth || measureCharWidth(codeCell);
|
|
2774
|
-
const column = Math.round(x / Math.max(width, 1));
|
|
2775
|
-
return Math.max(0, Math.min(line.length, column));
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
function measureCharWidth(element) {
|
|
2779
|
-
const probe = document.createElement('span');
|
|
2780
|
-
probe.textContent = 'mmmmmmmmmm';
|
|
2781
|
-
probe.style.position = 'absolute';
|
|
2782
|
-
probe.style.visibility = 'hidden';
|
|
2783
|
-
probe.style.whiteSpace = 'pre';
|
|
2784
|
-
probe.style.font = getComputedStyle(element).font;
|
|
2785
|
-
document.body.appendChild(probe);
|
|
2786
|
-
const width = probe.getBoundingClientRect().width / 10;
|
|
2787
|
-
probe.remove();
|
|
2788
|
-
measuredCharWidth = width || 7;
|
|
2789
|
-
return measuredCharWidth;
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
function setSourceCursor(path, lineIndex, column, shouldReveal = false, targetLine = -1) {
|
|
2793
|
-
const file = sourceByPath.get(path);
|
|
2794
|
-
if (!file || !file.embedded) return;
|
|
2795
|
-
const lines = file.content.split(/\r?\n/);
|
|
2796
|
-
const boundedLine = Math.max(0, Math.min(lineIndex, Math.max(lines.length - 1, 0)));
|
|
2797
|
-
const boundedColumn = Math.max(0, Math.min(column, (lines[boundedLine] || '').length));
|
|
2798
|
-
viewerCursor = {
|
|
2799
|
-
path,
|
|
2800
|
-
lineIndex: boundedLine,
|
|
2801
|
-
column: boundedColumn,
|
|
2802
|
-
targetLine,
|
|
2803
|
-
};
|
|
2804
|
-
|
|
2805
|
-
const viewer = document.getElementById('source-viewer');
|
|
2806
|
-
const shouldSwitch = !viewer || viewer.dataset.openPath !== path || viewer.classList.contains('hidden');
|
|
2807
|
-
openSourceFile(path, shouldSwitch);
|
|
2808
|
-
if (shouldReveal) {
|
|
2809
|
-
requestAnimationFrame(() => {
|
|
2810
|
-
document.querySelector('.source-row.cursor-line')?.scrollIntoView({ block: 'center' });
|
|
2811
|
-
});
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
function openSourceAt(path, lineIndex, column) {
|
|
2816
|
-
setSourceCursor(path, lineIndex, column, true, lineIndex);
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
function isSourceViewerVisible() {
|
|
2820
|
-
const viewer = document.getElementById('source-viewer');
|
|
2821
|
-
return Boolean(viewer && !viewer.classList.contains('hidden'));
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
function openDiffFileAtCaret() {
|
|
2825
|
-
const sel = window.getSelection();
|
|
2826
|
-
const node = sel && sel.anchorNode;
|
|
2827
|
-
const el = node ? (node.nodeType === 1 ? node : node.parentElement) : null;
|
|
2828
|
-
const wrapper = (el && el.closest && el.closest('.d2h-file-wrapper')) || document.querySelector('.d2h-file-wrapper:not(.df-inactive)');
|
|
2829
|
-
if (!wrapper) return;
|
|
2830
|
-
const fileName = (wrapper.querySelector('.d2h-file-name')?.textContent || '').trim();
|
|
2831
|
-
if (!fileName) return;
|
|
2832
|
-
if (!sourceByPath.has(fileName)) { openSourceFile(fileName); return; }
|
|
2833
|
-
let lineIndex = 0;
|
|
2834
|
-
const lineEl = el && el.closest && el.closest('.d2h-code-side-line');
|
|
2835
|
-
if (lineEl) {
|
|
2836
|
-
const row = lineEl.closest('tr');
|
|
2837
|
-
const numEl = row && row.querySelector('.d2h-code-side-linenumber');
|
|
2838
|
-
const num = numEl ? parseInt((numEl.textContent || '').trim(), 10) : NaN;
|
|
2839
|
-
if (Number.isFinite(num)) lineIndex = Math.max(0, num - 1);
|
|
2840
|
-
}
|
|
2841
|
-
setSourceCursor(fileName, lineIndex, 0, true, -1);
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
function handleSourceCaretKey(event) {
|
|
2845
|
-
if (!viewerCursor) return false;
|
|
2846
|
-
const extend = event.shiftKey;
|
|
2847
|
-
if (event.key === 'ArrowDown') { event.preventDefault(); moveSourceCursor(1, 0, extend); return true; }
|
|
2848
|
-
if (event.key === 'ArrowUp') { event.preventDefault(); moveSourceCursor(-1, 0, extend); return true; }
|
|
2849
|
-
if (event.key === 'ArrowLeft') { event.preventDefault(); moveSourceCursor(0, -1, extend); return true; }
|
|
2850
|
-
if (event.key === 'ArrowRight') { event.preventDefault(); moveSourceCursor(0, 1, extend); return true; }
|
|
2851
|
-
return false;
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
function moveSourceCursor(dLine, dColumn, extend) {
|
|
2855
|
-
if (!viewerCursor) return;
|
|
2856
|
-
const file = sourceByPath.get(viewerCursor.path);
|
|
2857
|
-
if (!file || !file.embedded) return;
|
|
2858
|
-
const lines = file.content.split(/\r?\n/);
|
|
2859
|
-
let line = viewerCursor.lineIndex;
|
|
2860
|
-
let col = viewerCursor.column;
|
|
2861
|
-
if (dColumn < 0) {
|
|
2862
|
-
if (col > 0) col -= 1;
|
|
2863
|
-
else if (line > 0) { line -= 1; col = (lines[line] || '').length; }
|
|
2864
|
-
} else if (dColumn > 0) {
|
|
2865
|
-
if (col < (lines[line] || '').length) col += 1;
|
|
2866
|
-
else if (line < lines.length - 1) { line += 1; col = 0; }
|
|
2867
|
-
}
|
|
2868
|
-
if (dLine !== 0) {
|
|
2869
|
-
line = Math.max(0, Math.min(lines.length - 1, line + dLine));
|
|
2870
|
-
col = Math.min(col, (lines[line] || '').length);
|
|
2871
|
-
}
|
|
2872
|
-
if (extend) {
|
|
2873
|
-
if (!selectionAnchor) selectionAnchor = { lineIndex: viewerCursor.lineIndex, column: viewerCursor.column };
|
|
2874
|
-
} else {
|
|
2875
|
-
selectionAnchor = null;
|
|
2876
|
-
}
|
|
2877
|
-
setSourceCursor(viewerCursor.path, line, col, true, -1);
|
|
2878
|
-
applySourceSelection();
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
function applySourceSelection() {
|
|
2882
|
-
const sel = window.getSelection();
|
|
2883
|
-
if (!sel) return;
|
|
2884
|
-
if (!selectionAnchor || !viewerCursor) { sel.removeAllRanges(); return; }
|
|
2885
|
-
const a = caretDomPosition(selectionAnchor.lineIndex, selectionAnchor.column);
|
|
2886
|
-
const c = caretDomPosition(viewerCursor.lineIndex, viewerCursor.column);
|
|
2887
|
-
if (a && c) {
|
|
2888
|
-
try { sel.setBaseAndExtent(a.node, a.offset, c.node, c.offset); } catch (e) {}
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
|
-
function caretDomPosition(lineIndex, column) {
|
|
2893
|
-
const cell = document.querySelector('.source-row[data-line-index="' + lineIndex + '"] .source-code');
|
|
2894
|
-
if (!cell) return null;
|
|
2895
|
-
let remaining = column;
|
|
2896
|
-
const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
|
|
2897
|
-
let node;
|
|
2898
|
-
while ((node = walker.nextNode())) {
|
|
2899
|
-
const len = node.textContent.length;
|
|
2900
|
-
if (remaining <= len) return { node, offset: remaining };
|
|
2901
|
-
remaining -= len;
|
|
2902
|
-
}
|
|
2903
|
-
return { node: cell, offset: cell.childNodes.length };
|
|
2904
|
-
}
|
|
2905
|
-
|
|
2906
|
-
function wordAtCursor() {
|
|
2907
|
-
if (!viewerCursor) return null;
|
|
2908
|
-
const file = sourceByPath.get(viewerCursor.path);
|
|
2909
|
-
if (!file || !file.embedded) return null;
|
|
2910
|
-
const line = file.content.split(/\r?\n/)[viewerCursor.lineIndex] || '';
|
|
2911
|
-
const column = Math.max(0, Math.min(viewerCursor.column, line.length));
|
|
2912
|
-
const identifier = /[A-Za-z_$][A-Za-z0-9_$]*/g;
|
|
2913
|
-
let match = null;
|
|
2914
|
-
while ((match = identifier.exec(line))) {
|
|
2915
|
-
const start = match.index;
|
|
2916
|
-
const end = start + match[0].length;
|
|
2917
|
-
if (column >= start && column <= end) {
|
|
2918
|
-
return { name: match[0], path: viewerCursor.path, lineIndex: viewerCursor.lineIndex, column: start };
|
|
2919
|
-
}
|
|
2920
|
-
}
|
|
2921
|
-
return null;
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
function goToSymbolUnderCursor() {
|
|
2925
|
-
const symbol = wordAtCursor();
|
|
2926
|
-
if (!symbol) return;
|
|
2927
|
-
const target = findSymbolDefinition(symbol.name);
|
|
2928
|
-
if (!target) return;
|
|
2929
|
-
openSourceAt(target.path, target.lineIndex, target.column);
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
function findSymbolDefinition(name) {
|
|
2933
|
-
const matchers = definitionMatchers(name);
|
|
2934
|
-
const currentPath = viewerCursor?.path || '';
|
|
2935
|
-
const orderedFiles = [
|
|
2936
|
-
...sourceFiles.filter((file) => file.path === currentPath),
|
|
2937
|
-
...sourceFiles.filter((file) => file.path !== currentPath),
|
|
2938
|
-
].filter((file) => file.embedded);
|
|
2939
|
-
|
|
2940
|
-
for (const file of orderedFiles) {
|
|
2941
|
-
const lines = file.content.split(/\r?\n/);
|
|
2942
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
2943
|
-
const line = lines[lineIndex];
|
|
2944
|
-
if (matchers.some((matcher) => matcher.test(line))) {
|
|
2945
|
-
return { path: file.path, lineIndex, column: Math.max(0, line.indexOf(name)) };
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
|
-
return null;
|
|
2950
|
-
}
|
|
2951
|
-
|
|
2952
|
-
function definitionMatchers(name) {
|
|
2953
|
-
const escaped = escapeRegExp(name);
|
|
2954
|
-
const mod = '(?:(?:public|private|protected|internal|abstract|final|open|sealed|data|inner|enum|annotation|static|export|default|expect|actual|value)\\s+)*';
|
|
2955
|
-
const funMod = '(?:(?:public|private|protected|internal|abstract|final|open|override|suspend|inline|operator|static|async)\\s+)*';
|
|
2956
|
-
return [
|
|
2957
|
-
new RegExp('^\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?function\\s+' + escaped + '\\b'),
|
|
2958
|
-
new RegExp('^\\s*' + mod + '(?:class|interface|object|enum|trait|struct)\\s+' + escaped + '\\b'),
|
|
2959
|
-
new RegExp('^\\s*(?:export\\s+)?(?:interface|type|enum)\\s+' + escaped + '\\b'),
|
|
2960
|
-
new RegExp('^\\s*(?:export\\s+)?(?:const|let|var|val)\\s+' + escaped + '\\b'),
|
|
2961
|
-
new RegExp('^\\s*' + funMod + '(?:fun|def|fn|func)\\s+' + escaped + '\\b'),
|
|
2962
|
-
new RegExp('^\\s*' + funMod + escaped + '\\s*\\([^)]*\\)\\s*(?::\\s*[^=]+)?\\s*(?:\\{|=>)'),
|
|
2963
|
-
new RegExp('^\\s*' + escaped + '\\s*[:=]\\s*(?:async\\s*)?(?:function\\b|\\([^)]*\\)\\s*=>)'),
|
|
2964
|
-
];
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
function escapeRegExp(value) {
|
|
2968
|
-
return String(value).replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
|
|
2969
|
-
}
|
|
2970
|
-
|
|
2971
|
-
function openSourceFile(path, shouldSwitch = true) {
|
|
2972
|
-
const file = sourceByPath.get(path);
|
|
2973
|
-
if (!file) return;
|
|
2974
|
-
rememberRecent(path, 'source');
|
|
2975
|
-
document.getElementById('source-viewer').dataset.openPath = path;
|
|
2976
|
-
sourceLinks.forEach((link) => link.classList.toggle('active', link.dataset.sourceFile === path));
|
|
2977
|
-
renderBreadcrumb(document.getElementById('source-title'), path);
|
|
2978
|
-
const meta = [
|
|
2979
|
-
file.language || 'text',
|
|
2980
|
-
formatBytes(file.size || 0),
|
|
2981
|
-
file.changed ? 'changed' : 'unchanged',
|
|
2982
|
-
file.embedded ? 'searchable' : file.skippedReason || 'not embedded',
|
|
2983
|
-
].join(' | ');
|
|
2984
|
-
document.getElementById('source-meta').textContent = meta;
|
|
2985
|
-
const body = document.getElementById('source-body');
|
|
2986
|
-
if (!file.embedded) {
|
|
2987
|
-
body.className = 'source-body empty';
|
|
2988
|
-
body.textContent = file.skippedReason ? 'Source preview unavailable: ' + file.skippedReason + '.' : 'Source preview unavailable.';
|
|
2989
|
-
if (shouldSwitch) showSourceView();
|
|
2990
|
-
return;
|
|
2991
|
-
}
|
|
2992
|
-
if (!viewerCursor || viewerCursor.path !== path) {
|
|
2993
|
-
viewerCursor = { path, lineIndex: 0, column: 0, targetLine: -1 };
|
|
2994
|
-
}
|
|
2995
|
-
body.className = 'source-body';
|
|
2996
|
-
body.innerHTML = renderSourceTable(file, searchInput?.value || '');
|
|
2997
|
-
if (shouldSwitch) showSourceView();
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
function renderSourceTable(file, query) {
|
|
3001
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
3002
|
-
const lines = file.content.split(/\r?\n/);
|
|
3003
|
-
const cursor = viewerCursor && viewerCursor.path === file.path ? viewerCursor : null;
|
|
3004
|
-
const changedSet = new Set(file.changedLines || []);
|
|
3005
|
-
const rows = lines.map((line, index) => {
|
|
3006
|
-
const hit = normalizedQuery.length > 0 && line.toLowerCase().includes(normalizedQuery);
|
|
3007
|
-
const isCursorLine = Boolean(cursor && cursor.lineIndex === index);
|
|
3008
|
-
const isSymbolTarget = Boolean(cursor && cursor.targetLine === index);
|
|
3009
|
-
const isChanged = changedSet.has(index + 1);
|
|
3010
|
-
const classes = [
|
|
3011
|
-
'source-row',
|
|
3012
|
-
hit ? 'search-hit' : '',
|
|
3013
|
-
isChanged ? 'changed-line' : '',
|
|
3014
|
-
isCursorLine ? 'cursor-line' : '',
|
|
3015
|
-
isSymbolTarget ? 'symbol-target' : '',
|
|
3016
|
-
].filter(Boolean).join(' ');
|
|
3017
|
-
return [
|
|
3018
|
-
'<tr class="' + classes + '" data-line-index="' + index + '">',
|
|
3019
|
-
'<td class="num">' + String(index + 1) + '</td>',
|
|
3020
|
-
'<td class="source-code">' + (isCursorLine ? renderLineWithCursor(line, file.language || 'text', cursor.column) : highlightLine(line, file.language || 'text')) + '</td>',
|
|
3021
|
-
'</tr>',
|
|
3022
|
-
].join('');
|
|
3023
|
-
}).join('');
|
|
3024
|
-
return '<table class="source-table"><tbody>' + rows + '</tbody></table>';
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
function renderLineWithCursor(text, language, column) {
|
|
3028
|
-
const boundedColumn = Math.max(0, Math.min(column, text.length));
|
|
3029
|
-
const before = text.slice(0, boundedColumn);
|
|
3030
|
-
const after = text.slice(boundedColumn);
|
|
3031
|
-
return highlightLine(before, language) + '<span class="code-cursor" aria-hidden="true"></span>' + highlightLine(after, language);
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
function highlightLine(text, language) {
|
|
3035
|
-
if (language === 'text') return escapeHtml(text);
|
|
3036
|
-
if (language === 'markup') {
|
|
3037
|
-
return escapeHtml(text).replace(/(<\/?)([\w:-]+)([^&]*?)(\/?>)/g, '$1<span class="tok-tag">$2</span>$3$4');
|
|
3038
|
-
}
|
|
3039
|
-
if (language === 'markdown') {
|
|
3040
|
-
const escaped = escapeHtml(text);
|
|
3041
|
-
if (/^\s{0,3}#{1,6}\s/.test(text)) return '<span class="tok-keyword">' + escaped + '</span>';
|
|
3042
|
-
return escaped.replace(new RegExp(String.fromCharCode(96) + '[^' + String.fromCharCode(96) + ']+' + String.fromCharCode(96), 'g'), '<span class="tok-string">$&</span>');
|
|
3043
|
-
}
|
|
3044
|
-
const keywords = new Set(['as','async','await','break','case','catch','class','const','continue','def','default','defer','do','else','enum','export','extends','final','finally','fn','for','from','func','function','go','if','impl','import','in','interface','let','match','module','new','package','private','protected','public','return','select','static','struct','switch','throw','try','type','val','var','while','yield']);
|
|
3045
|
-
const literals = new Set(['False','None','True','false','nil','null','self','this','true','undefined']);
|
|
3046
|
-
const commentPrefixes = ['python','ruby','shell','yaml','toml'].includes(language) ? ['#'] : ['//'];
|
|
3047
|
-
let output = '';
|
|
3048
|
-
let index = 0;
|
|
3049
|
-
while (index < text.length) {
|
|
3050
|
-
const rest = text.slice(index);
|
|
3051
|
-
const commentPrefix = commentPrefixes.find((prefix) => rest.startsWith(prefix));
|
|
3052
|
-
if (commentPrefix) {
|
|
3053
|
-
output += '<span class="tok-comment">' + escapeHtml(rest) + '</span>';
|
|
3054
|
-
break;
|
|
3055
|
-
}
|
|
3056
|
-
const char = text[index];
|
|
3057
|
-
if (char === '"' || char === "'" || char === String.fromCharCode(96)) {
|
|
3058
|
-
const quote = char;
|
|
3059
|
-
let end = index + 1;
|
|
3060
|
-
let escaped = false;
|
|
3061
|
-
while (end < text.length) {
|
|
3062
|
-
const currentChar = text[end];
|
|
3063
|
-
if (currentChar === quote && !escaped) {
|
|
3064
|
-
end += 1;
|
|
3065
|
-
break;
|
|
3066
|
-
}
|
|
3067
|
-
escaped = currentChar === '\\' && !escaped;
|
|
3068
|
-
if (currentChar !== '\\') escaped = false;
|
|
3069
|
-
end += 1;
|
|
3070
|
-
}
|
|
3071
|
-
output += '<span class="tok-string">' + escapeHtml(text.slice(index, end)) + '</span>';
|
|
3072
|
-
index = end;
|
|
3073
|
-
continue;
|
|
3074
|
-
}
|
|
3075
|
-
const number = rest.match(/^\b\d+(?:\.\d+)?\b/);
|
|
3076
|
-
if (number) {
|
|
3077
|
-
output += '<span class="tok-number">' + escapeHtml(number[0]) + '</span>';
|
|
3078
|
-
index += number[0].length;
|
|
3079
|
-
continue;
|
|
3080
|
-
}
|
|
3081
|
-
const identifier = rest.match(/^[A-Za-z_$][\w$-]*/);
|
|
3082
|
-
if (identifier) {
|
|
3083
|
-
const value = identifier[0];
|
|
3084
|
-
if (keywords.has(value)) output += '<span class="tok-keyword">' + escapeHtml(value) + '</span>';
|
|
3085
|
-
else if (literals.has(value)) output += '<span class="tok-literal">' + escapeHtml(value) + '</span>';
|
|
3086
|
-
else output += escapeHtml(value);
|
|
3087
|
-
index += value.length;
|
|
3088
|
-
continue;
|
|
3089
|
-
}
|
|
3090
|
-
output += escapeHtml(char);
|
|
3091
|
-
index += 1;
|
|
3092
|
-
}
|
|
3093
|
-
return output;
|
|
3094
|
-
}
|
|
3095
|
-
|
|
3096
|
-
function escapeHtml(value) {
|
|
3097
|
-
return String(value)
|
|
3098
|
-
.replace(/&/g, '&')
|
|
3099
|
-
.replace(/</g, '<')
|
|
3100
|
-
.replace(/>/g, '>')
|
|
3101
|
-
.replace(/"/g, '"')
|
|
3102
|
-
.replace(/'/g, ''');
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
function formatBytes(bytes) {
|
|
3106
|
-
if (bytes < 1024) return bytes + ' B';
|
|
3107
|
-
const kib = bytes / 1024;
|
|
3108
|
-
if (kib < 1024) return kib.toFixed(1) + ' KiB';
|
|
3109
|
-
return (kib / 1024).toFixed(1) + ' MiB';
|
|
3110
|
-
}
|
|
3111
|
-
`;
|
|
3112
|
-
}
|
|
3113
|
-
function initialState(config) {
|
|
3114
|
-
return [
|
|
3115
|
-
"# Monacori Validation State",
|
|
3116
|
-
"",
|
|
3117
|
-
`Project: ${config.projectName}`,
|
|
3118
|
-
`Initialized: ${new Date().toISOString()}`,
|
|
3119
|
-
"",
|
|
3120
|
-
"## Goal",
|
|
3121
|
-
"- Keep AI-generated changes reviewable, test-backed, and easy to inspect.",
|
|
3122
|
-
"",
|
|
3123
|
-
"## Checks",
|
|
3124
|
-
"",
|
|
3125
|
-
"## Reports",
|
|
3126
|
-
"",
|
|
3127
|
-
].join("\n");
|
|
3128
|
-
}
|
|
3129
|
-
function initialDecisions() {
|
|
3130
|
-
return [
|
|
3131
|
-
"# Monacori Decisions",
|
|
3132
|
-
"",
|
|
3133
|
-
"Record durable validation decisions here so future checks do not depend on chat memory.",
|
|
3134
|
-
"",
|
|
3135
|
-
].join("\n");
|
|
3136
|
-
}
|
|
3137
|
-
function agentSnippet() {
|
|
3138
|
-
return [
|
|
3139
|
-
"<!-- MONACORI:START -->",
|
|
3140
|
-
"## monacori Validation",
|
|
3141
|
-
"",
|
|
3142
|
-
"This repository uses monacori to verify AI-generated code changes.",
|
|
3143
|
-
"",
|
|
3144
|
-
"Before claiming completion on a code change:",
|
|
3145
|
-
"",
|
|
3146
|
-
"- Run `monacori check --include-untracked` or a more specific `monacori verify -- <command>`.",
|
|
3147
|
-
"- Use `monacori app --include-untracked` while changes are still moving.",
|
|
3148
|
-
"- Inspect changed hunks with F7 / Shift+F7.",
|
|
3149
|
-
"- Use Shift Shift in the diff review to search indexed files, including unchanged files.",
|
|
3150
|
-
"- In source previews, use Cmd/Ctrl+Down to jump to the declaration-like match under the cursor.",
|
|
3151
|
-
"- Report the verification commands, results, and remaining risks.",
|
|
3152
|
-
"",
|
|
3153
|
-
"Do not claim a change is done without verification evidence or a precise explanation of why verification could not run.",
|
|
3154
|
-
"<!-- MONACORI:END -->",
|
|
3155
|
-
"",
|
|
3156
|
-
].join("\n");
|
|
3157
|
-
}
|
|
3158
|
-
function applyAgentDocSnippet(fileName) {
|
|
3159
|
-
const path = join(process.cwd(), fileName);
|
|
3160
|
-
const snippet = agentSnippet();
|
|
3161
|
-
if (!existsSync(path)) {
|
|
3162
|
-
writeFileSync(path, `# ${fileName}\n\n${snippet}`);
|
|
3163
|
-
return;
|
|
3164
|
-
}
|
|
3165
|
-
const current = readFileSync(path, "utf8");
|
|
3166
|
-
const markerPattern = /<!-- MONACORI:START -->[\s\S]*?<!-- MONACORI:END -->\n?/;
|
|
3167
|
-
const next = markerPattern.test(current)
|
|
3168
|
-
? current.replace(markerPattern, snippet)
|
|
3169
|
-
: `${current.trimEnd()}\n\n${snippet}`;
|
|
3170
|
-
writeFileSync(path, next);
|
|
3171
|
-
}
|
|
3172
|
-
function ensureInitialized() {
|
|
3173
|
-
if (!existsSync(join(process.cwd(), FLOW_DIR, CONFIG_FILE))) {
|
|
3174
|
-
throw new Error(`Missing ${FLOW_DIR}/. Run \`monacori init\` first.`);
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3177
|
-
function ensureWritableFlowState() {
|
|
3178
|
-
if (!existsSync(join(process.cwd(), FLOW_DIR, CONFIG_FILE))) {
|
|
3179
|
-
initFlow(["--quiet"]);
|
|
3180
|
-
return;
|
|
3181
|
-
}
|
|
3182
|
-
ensureMonacoriGitignore(process.cwd());
|
|
3183
|
-
}
|
|
3184
|
-
function loadConfig() {
|
|
3185
|
-
ensureInitialized();
|
|
3186
|
-
const raw = JSON.parse(readFileSync(join(process.cwd(), FLOW_DIR, CONFIG_FILE), "utf8"));
|
|
3187
|
-
return {
|
|
3188
|
-
version: 1,
|
|
3189
|
-
projectName: raw.projectName ?? basename(process.cwd()),
|
|
3190
|
-
verification: {
|
|
3191
|
-
commands: Array.isArray(raw.verification?.commands) ? raw.verification.commands : [],
|
|
3192
|
-
},
|
|
3193
|
-
diff: {
|
|
3194
|
-
context: typeof raw.diff?.context === "number" ? raw.diff.context : 12,
|
|
3195
|
-
includeUntracked: typeof raw.diff?.includeUntracked === "boolean" ? raw.diff.includeUntracked : false,
|
|
3196
|
-
},
|
|
3197
|
-
};
|
|
3198
|
-
}
|
|
3199
|
-
function getVerificationCommands(config) {
|
|
3200
|
-
return config.verification.commands.filter((command) => command.trim().length > 0);
|
|
3201
|
-
}
|
|
3202
|
-
function writeIfMissing(path, content, force) {
|
|
3203
|
-
if (!force && existsSync(path)) {
|
|
3204
|
-
return;
|
|
3205
|
-
}
|
|
3206
|
-
writeFileSync(path, content);
|
|
3207
|
-
}
|
|
3208
|
-
function ensureMonacoriGitignore(root) {
|
|
3209
|
-
if (git(root, ["rev-parse", "--is-inside-work-tree"]) !== "true") {
|
|
3210
|
-
return false;
|
|
3211
|
-
}
|
|
3212
|
-
const path = join(root, GITIGNORE_FILE);
|
|
3213
|
-
const content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
3214
|
-
const hasEntry = content
|
|
3215
|
-
.split(/\r?\n/)
|
|
3216
|
-
.map((line) => line.trim())
|
|
3217
|
-
.some((line) => line === FLOW_DIR || line === `${FLOW_DIR}/`);
|
|
3218
|
-
if (hasEntry) {
|
|
3219
|
-
return false;
|
|
3220
|
-
}
|
|
3221
|
-
const prefix = content.length === 0 ? "" : content.endsWith("\n") ? "\n" : "\n\n";
|
|
3222
|
-
writeFileSync(path, `${content}${prefix}# monacori local validation artifacts\n${FLOW_DIR}/\n`);
|
|
3223
|
-
return true;
|
|
3224
|
-
}
|
|
3225
|
-
function detectVerificationCommands(root) {
|
|
3226
|
-
const commands = new Set();
|
|
3227
|
-
const packagePath = join(root, "package.json");
|
|
3228
|
-
if (existsSync(packagePath)) {
|
|
3229
|
-
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
3230
|
-
const packageManager = detectPackageManager(root);
|
|
3231
|
-
const scripts = packageJson.scripts ?? {};
|
|
3232
|
-
for (const script of ["typecheck", "lint", "test", "build"]) {
|
|
3233
|
-
if (scripts[script]) {
|
|
3234
|
-
commands.add(packageScriptCommand(packageManager, script));
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
if (existsSync(join(root, "pyproject.toml"))) {
|
|
3239
|
-
commands.add(existsSync(join(root, "poetry.lock")) ? "poetry run pytest" : "pytest");
|
|
3240
|
-
}
|
|
3241
|
-
if (existsSync(join(root, "Cargo.toml"))) {
|
|
3242
|
-
commands.add("cargo test");
|
|
3243
|
-
}
|
|
3244
|
-
if (existsSync(join(root, "go.mod"))) {
|
|
3245
|
-
commands.add("go test ./...");
|
|
3246
|
-
}
|
|
3247
|
-
return Array.from(commands);
|
|
3248
|
-
}
|
|
3249
|
-
function detectPackageManager(root) {
|
|
3250
|
-
if (existsSync(join(root, "pnpm-lock.yaml")))
|
|
3251
|
-
return "pnpm";
|
|
3252
|
-
if (existsSync(join(root, "yarn.lock")))
|
|
3253
|
-
return "yarn";
|
|
3254
|
-
if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb")))
|
|
3255
|
-
return "bun";
|
|
3256
|
-
return "npm";
|
|
3257
|
-
}
|
|
3258
|
-
function packageScriptCommand(manager, script) {
|
|
3259
|
-
if (manager === "npm") {
|
|
3260
|
-
return script === "test" ? "npm test" : `npm run ${script}`;
|
|
3261
|
-
}
|
|
3262
|
-
if (manager === "yarn") {
|
|
3263
|
-
return `yarn ${script}`;
|
|
3264
|
-
}
|
|
3265
|
-
if (manager === "bun") {
|
|
3266
|
-
return `bun run ${script}`;
|
|
3267
|
-
}
|
|
3268
|
-
return `pnpm ${script}`;
|
|
3269
|
-
}
|
|
3270
|
-
function readGitSnapshot(root) {
|
|
3271
|
-
return {
|
|
3272
|
-
branch: git(root, ["branch", "--show-current"]),
|
|
3273
|
-
status: git(root, ["status", "--short"]),
|
|
3274
|
-
diffStat: git(root, ["diff", "--stat"]),
|
|
3275
|
-
recentCommits: git(root, ["log", "--oneline", "-5"]),
|
|
3276
|
-
};
|
|
3277
|
-
}
|
|
3278
|
-
function git(root, args) {
|
|
3279
|
-
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
3280
|
-
if (result.status !== 0) {
|
|
3281
|
-
return "";
|
|
3282
|
-
}
|
|
3283
|
-
return (result.stdout ?? "").trim();
|
|
3284
|
-
}
|
|
3285
|
-
function writeHttp(response, status, contentType, body) {
|
|
3286
|
-
response.writeHead(status, {
|
|
3287
|
-
"content-type": contentType,
|
|
3288
|
-
"cache-control": "no-store",
|
|
3289
|
-
});
|
|
3290
|
-
response.end(body);
|
|
3291
|
-
}
|
|
3292
|
-
function writeHttpJson(response, body) {
|
|
3293
|
-
writeHttp(response, 200, "application/json; charset=utf-8", JSON.stringify(body));
|
|
3294
|
-
}
|
|
3295
|
-
function diffSubtitle(options) {
|
|
3296
|
-
const source = options.staged ? "staged changes" : `working tree vs ${options.base ?? "HEAD"}`;
|
|
3297
|
-
const untracked = options.includeUntracked ? "including untracked files" : "tracked files only";
|
|
3298
|
-
return `${source}; ${untracked}; ${options.context} context lines`;
|
|
3299
|
-
}
|
|
3300
|
-
function stripDiffPath(value) {
|
|
3301
|
-
if (value === "/dev/null") {
|
|
3302
|
-
return value;
|
|
3303
|
-
}
|
|
3304
|
-
return value.replace(/^[ab]\//, "");
|
|
3305
|
-
}
|
|
3306
|
-
function languageForPath(path) {
|
|
3307
|
-
const lower = path.toLowerCase();
|
|
3308
|
-
if (lower.endsWith(".ts") || lower.endsWith(".tsx"))
|
|
3309
|
-
return "typescript";
|
|
3310
|
-
if (lower.endsWith(".js") || lower.endsWith(".jsx") || lower.endsWith(".mjs") || lower.endsWith(".cjs"))
|
|
3311
|
-
return "javascript";
|
|
3312
|
-
if (lower.endsWith(".json"))
|
|
3313
|
-
return "json";
|
|
3314
|
-
if (lower.endsWith(".css") || lower.endsWith(".scss") || lower.endsWith(".sass"))
|
|
3315
|
-
return "css";
|
|
3316
|
-
if (lower.endsWith(".html") || lower.endsWith(".htm") || lower.endsWith(".xml") || lower.endsWith(".svg"))
|
|
3317
|
-
return "markup";
|
|
3318
|
-
if (lower.endsWith(".md") || lower.endsWith(".mdx"))
|
|
3319
|
-
return "markdown";
|
|
3320
|
-
if (lower.endsWith(".py"))
|
|
3321
|
-
return "python";
|
|
3322
|
-
if (lower.endsWith(".rb"))
|
|
3323
|
-
return "ruby";
|
|
3324
|
-
if (lower.endsWith(".go"))
|
|
3325
|
-
return "go";
|
|
3326
|
-
if (lower.endsWith(".rs"))
|
|
3327
|
-
return "rust";
|
|
3328
|
-
if (lower.endsWith(".java") || lower.endsWith(".kt") || lower.endsWith(".kts"))
|
|
3329
|
-
return "java";
|
|
3330
|
-
if (lower.endsWith(".sh") || lower.endsWith(".bash") || lower.endsWith(".zsh"))
|
|
3331
|
-
return "shell";
|
|
3332
|
-
if (lower.endsWith(".yml") || lower.endsWith(".yaml"))
|
|
3333
|
-
return "yaml";
|
|
3334
|
-
if (lower.endsWith(".toml"))
|
|
3335
|
-
return "toml";
|
|
3336
|
-
if (lower.endsWith(".sql"))
|
|
3337
|
-
return "sql";
|
|
3338
|
-
if (lower.endsWith(".http") || lower.endsWith(".rest"))
|
|
3339
|
-
return "http";
|
|
3340
|
-
return "text";
|
|
3341
|
-
}
|
|
3342
|
-
function isLikelyBinary(path) {
|
|
3343
|
-
const sample = readFileSync(path).subarray(0, 8000);
|
|
3344
|
-
return sample.includes(0);
|
|
3345
|
-
}
|
|
3346
|
-
function readOption(args, name) {
|
|
3347
|
-
const index = args.indexOf(name);
|
|
3348
|
-
if (index < 0) {
|
|
3349
|
-
return undefined;
|
|
3350
|
-
}
|
|
3351
|
-
const value = args[index + 1];
|
|
3352
|
-
if (!value || value.startsWith("--")) {
|
|
3353
|
-
throw new Error(`Missing value for ${name}`);
|
|
3354
|
-
}
|
|
3355
|
-
return value;
|
|
3356
|
-
}
|
|
3357
|
-
function parsePositiveInteger(value, optionName) {
|
|
3358
|
-
const parsed = Number(value);
|
|
3359
|
-
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
3360
|
-
throw new Error(`${optionName} must be a non-negative integer`);
|
|
3361
|
-
}
|
|
3362
|
-
return parsed;
|
|
3363
|
-
}
|
|
3364
|
-
function readStdin() {
|
|
3365
|
-
if (process.stdin.isTTY) {
|
|
3366
|
-
return "";
|
|
3367
|
-
}
|
|
3368
|
-
return readFileSync(0, "utf8");
|
|
3369
|
-
}
|
|
3370
|
-
function appendToState(content) {
|
|
3371
|
-
const path = join(process.cwd(), FLOW_DIR, STATE_FILE);
|
|
3372
|
-
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
3373
|
-
writeFileSync(path, `${current.trimEnd()}\n${content}`);
|
|
3374
|
-
}
|
|
3375
|
-
function summarizeForState(content) {
|
|
3376
|
-
const lines = content
|
|
3377
|
-
.split(/\r?\n/)
|
|
3378
|
-
.map((line) => line.trim())
|
|
3379
|
-
.filter(Boolean)
|
|
3380
|
-
.slice(0, 12);
|
|
3381
|
-
return lines.map((line) => `- ${line.replace(/^-+\s*/, "")}`).join("\n");
|
|
3382
|
-
}
|
|
3383
|
-
function codeBlock(content) {
|
|
3384
|
-
return ["```", content, "```"].join("\n");
|
|
3385
|
-
}
|
|
3386
|
-
function timestampForFile() {
|
|
3387
|
-
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
3388
|
-
}
|
|
3389
|
-
function hashText(value) {
|
|
3390
|
-
return createHash("sha1").update(value).digest("hex");
|
|
3391
|
-
}
|
|
3392
|
-
function sanitizeFilePart(value) {
|
|
3393
|
-
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-|-$/g, "");
|
|
3394
|
-
}
|
|
3395
|
-
function escapeHtml(value) {
|
|
3396
|
-
return value
|
|
3397
|
-
.replace(/&/g, "&")
|
|
3398
|
-
.replace(/</g, "<")
|
|
3399
|
-
.replace(/>/g, ">")
|
|
3400
|
-
.replace(/"/g, """)
|
|
3401
|
-
.replace(/'/g, "'");
|
|
3402
|
-
}
|
|
3403
|
-
function jsonForScript(value) {
|
|
3404
|
-
return JSON.stringify(value)
|
|
3405
|
-
.replace(/</g, "\\u003c")
|
|
3406
|
-
.replace(/>/g, "\\u003e")
|
|
3407
|
-
.replace(/&/g, "\\u0026")
|
|
3408
|
-
.replace(/\u2028/g, "\\u2028")
|
|
3409
|
-
.replace(/\u2029/g, "\\u2029");
|
|
3410
|
-
}
|
|
3411
|
-
function escapeAttr(value) {
|
|
3412
|
-
return escapeHtml(value);
|
|
3413
|
-
}
|
|
3414
|
-
function formatBytes(bytes) {
|
|
3415
|
-
if (bytes < 1024) {
|
|
3416
|
-
return `${bytes} B`;
|
|
3417
|
-
}
|
|
3418
|
-
const kib = bytes / 1024;
|
|
3419
|
-
if (kib < 1024) {
|
|
3420
|
-
return `${kib.toFixed(1)} KiB`;
|
|
3421
|
-
}
|
|
3422
|
-
return `${(kib / 1024).toFixed(1)} MiB`;
|
|
3423
|
-
}
|
|
3424
|
-
function listRecentFiles(dir, limit) {
|
|
3425
|
-
if (!existsSync(dir)) {
|
|
3426
|
-
return [];
|
|
3427
|
-
}
|
|
3428
|
-
return readdirSync(dir)
|
|
3429
|
-
.map((name) => join(dir, name))
|
|
3430
|
-
.filter((path) => statSync(path).isFile())
|
|
3431
|
-
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
|
|
3432
|
-
.slice(0, limit);
|
|
3433
|
-
}
|
|
3434
|
-
function printHelp() {
|
|
3435
|
-
console.log(`monacori
|
|
3436
|
-
|
|
3437
|
-
Validation control plane for AI-generated code changes.
|
|
3438
|
-
|
|
3439
|
-
Usage:
|
|
3440
|
-
mo
|
|
3441
|
-
monacori open [--base HEAD] [--staged] [--tracked-only]
|
|
3442
|
-
monacori check [--include-untracked] [--open] [--no-verify] [--no-diff] [-- <command>]
|
|
3443
|
-
monacori init [--force]
|
|
3444
|
-
monacori install [--force] [--apply-agent-docs]
|
|
3445
|
-
monacori verify [-- <command>]
|
|
3446
|
-
monacori diff [--base HEAD] [--staged] [--include-untracked] [--open] [--watch]
|
|
3447
|
-
monacori app [--base HEAD] [--staged] [--include-untracked]
|
|
3448
|
-
monacori review [--base HEAD] [--staged] [--include-untracked]
|
|
3449
|
-
monacori status
|
|
3450
|
-
monacori report [--label manual] [--file report.md]
|
|
3451
|
-
|
|
3452
|
-
Default loop:
|
|
3453
|
-
1. Let an AI agent edit code.
|
|
3454
|
-
2. Run: mo
|
|
3455
|
-
3. Run: monacori check --include-untracked
|
|
3456
|
-
4. Only accept the change when verification evidence is clear.
|
|
3457
|
-
|
|
3458
|
-
Diff review keys:
|
|
3459
|
-
F7 next changed hunk
|
|
3460
|
-
Shift+F7 previous changed hunk
|
|
3461
|
-
Shift Shift file search across indexed files
|
|
3462
|
-
Cmd/Ctrl+E recent files
|
|
3463
|
-
Cmd/Ctrl+Down jump to symbol under cursor
|
|
3464
|
-
`);
|
|
3465
|
-
}
|
|
3466
|
-
function printOpenHelp() {
|
|
3467
|
-
console.log(`monacori open
|
|
3468
|
-
|
|
3469
|
-
Open the local desktop review app for the current directory. This is the default command behind \`mo\` and \`monacori\` with no arguments.
|
|
3470
|
-
|
|
3471
|
-
It auto-initializes .monacori/ when needed, makes sure .monacori/ is ignored in Git worktrees, and includes untracked files by default so new AI-created files are visible.
|
|
3472
|
-
|
|
3473
|
-
Usage:
|
|
3474
|
-
mo
|
|
3475
|
-
monacori open [--base HEAD] [--staged] [--tracked-only] [--context 12] [--no-watch] [--foreground]
|
|
3476
|
-
|
|
3477
|
-
Options:
|
|
3478
|
-
--tracked-only inspect tracked changes only
|
|
3479
|
-
`);
|
|
3480
|
-
}
|
|
3481
|
-
function printCheckHelp() {
|
|
3482
|
-
console.log(`monacori check
|
|
3483
|
-
|
|
3484
|
-
Run configured verification and create a reviewable diff artifact.
|
|
3485
|
-
|
|
3486
|
-
Usage:
|
|
3487
|
-
monacori check [--include-untracked] [--staged] [--base HEAD] [--context 12] [--open] [--no-verify] [--no-diff] [-- <command>]
|
|
3488
|
-
|
|
3489
|
-
Examples:
|
|
3490
|
-
monacori check --include-untracked --open
|
|
3491
|
-
monacori check -- npm test
|
|
3492
|
-
monacori check --no-verify --include-untracked
|
|
3493
|
-
`);
|
|
3494
|
-
}
|
|
3495
|
-
function printDiffHelp() {
|
|
3496
|
-
console.log(`monacori diff
|
|
3497
|
-
|
|
3498
|
-
Generate a browser-based side-by-side Git diff review.
|
|
3499
|
-
|
|
3500
|
-
Usage:
|
|
3501
|
-
monacori diff [--base HEAD] [--staged] [--include-untracked] [--context 12] [--output review.html] [--open] [--watch] [--port 0]
|
|
3502
|
-
|
|
3503
|
-
Keys in the review page:
|
|
3504
|
-
F7 next changed hunk
|
|
3505
|
-
Shift+F7 previous changed hunk
|
|
3506
|
-
] / [ fallback hunk navigation
|
|
3507
|
-
Shift Shift search indexed files, including unchanged files
|
|
3508
|
-
Cmd/Ctrl+E recent files
|
|
3509
|
-
Cmd/Ctrl+Down jump to symbol under cursor
|
|
3510
|
-
|
|
3511
|
-
The sidebar groups changed files as a folder tree. Use Search to filter paths and indexed file contents.
|
|
3512
|
-
The Files tab opens read-only source previews, including unchanged files when they fit the local review budget.
|
|
3513
|
-
Viewed marks are tied to file signatures, so a changed file becomes unviewed again after reload.
|
|
3514
|
-
Use --watch to serve a live review that reloads when the working tree changes.
|
|
3515
|
-
`);
|
|
3516
|
-
}
|
|
3517
|
-
function printAppHelp() {
|
|
3518
|
-
console.log(`monacori app
|
|
3519
|
-
|
|
3520
|
-
Launch the local desktop review app. The app reads Git diff and source files directly from this repository, writes a local review file under .monacori/, and refreshes when the working tree changes. It does not start an HTTP server.
|
|
3521
|
-
|
|
3522
|
-
Usage:
|
|
3523
|
-
monacori app [--base HEAD] [--staged] [--include-untracked] [--context 12] [--no-watch] [--foreground]
|
|
3524
|
-
|
|
3525
|
-
Aliases:
|
|
3526
|
-
mo
|
|
3527
|
-
monacori open
|
|
3528
|
-
monacori review
|
|
3529
|
-
`);
|
|
3530
|
-
}
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { main } from "./commands.js";
|
|
6
|
+
export { main };
|
|
7
|
+
export { buildDiffReview } from "./build.js";
|
|
8
|
+
export { performHttpRequest } from "./server.js";
|
|
3531
9
|
function isDirectRun() {
|
|
3532
10
|
const entry = process.argv[1];
|
|
3533
11
|
if (!entry) {
|