@happy-nut/monacori 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commands.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join, relative } from "node:path";
2
+ import { basename, dirname, join } from "node:path";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { createRequire } from "node:module";
6
6
  import { AGENT_SNIPPET_FILE, CONFIG_FILE, DECISIONS_FILE, FLOW_DIR, GITIGNORE_FILE, STATE_FILE } from "./constants.js";
7
- import { codeBlock, listRecentFiles, parsePositiveInteger, readOption, readStdin, sanitizeFilePart, summarizeForState, timestampForFile } from "./util.js";
8
- import { git, readGitSnapshot } from "./git.js";
9
- import { createDiffReview, serveDiffWatch } from "./server.js";
7
+ import { parsePositiveInteger, readOption } from "./util.js";
8
+ import { git } from "./git.js";
10
9
  const nodeRequire = createRequire(import.meta.url);
11
10
  export function main() {
12
11
  const rawArgs = process.argv.slice(2);
@@ -27,16 +26,6 @@ export function main() {
27
26
  case "install":
28
27
  installFlow(args);
29
28
  break;
30
- case "check":
31
- case "go":
32
- runCheck(args);
33
- break;
34
- case "verify":
35
- runVerification(args);
36
- break;
37
- case "diff":
38
- renderDiffReview(args);
39
- break;
40
29
  case "app":
41
30
  case "review":
42
31
  launchReviewApp(args);
@@ -44,12 +33,6 @@ export function main() {
44
33
  case "open":
45
34
  openCurrentRepository(args);
46
35
  break;
47
- case "status":
48
- printStatus();
49
- break;
50
- case "report":
51
- recordReport(args);
52
- break;
53
36
  case "--help":
54
37
  case "-h":
55
38
  case "help":
@@ -94,7 +77,7 @@ function initFlow(args) {
94
77
  if (ignored) {
95
78
  console.log(`Updated ${GITIGNORE_FILE} to ignore ${FLOW_DIR}/ validation artifacts.`);
96
79
  }
97
- console.log("Next: run `monacori app --include-untracked` to inspect changes, then `monacori check --include-untracked` to record verification.");
80
+ console.log("Next: run `mo` to open the diff review app.");
98
81
  }
99
82
  }
100
83
  function installFlow(args) {
@@ -115,117 +98,6 @@ function installFlow(args) {
115
98
  console.log(`Next: add ${FLOW_DIR}/${AGENT_SNIPPET_FILE} to your agent instructions if desired.`);
116
99
  }
117
100
  }
118
- function runCheck(args) {
119
- if (args.includes("--help") || args.includes("-h")) {
120
- printCheckHelp();
121
- return;
122
- }
123
- ensureWritableFlowState();
124
- const config = loadConfig();
125
- const separator = args.indexOf("--");
126
- const commandArgs = separator >= 0 ? args.slice(separator + 1) : [];
127
- const optionArgs = separator >= 0 ? args.slice(0, separator) : args;
128
- const noVerify = optionArgs.includes("--no-verify");
129
- const noDiff = optionArgs.includes("--no-diff");
130
- const openInBrowser = optionArgs.includes("--open");
131
- const includeUntracked = optionArgs.includes("--include-untracked") || config.diff.includeUntracked;
132
- const staged = optionArgs.includes("--staged");
133
- const base = readOption(optionArgs, "--base");
134
- const contextValue = readOption(optionArgs, "--context");
135
- const context = contextValue ? parsePositiveInteger(contextValue, "--context") : config.diff.context;
136
- const verification = noVerify
137
- ? { commands: [], failed: false, skipped: true }
138
- : executeVerification(commandArgs.join(" "));
139
- let review;
140
- if (!noDiff) {
141
- review = createDiffReview({
142
- base,
143
- staged,
144
- includeUntracked,
145
- context,
146
- output: join(process.cwd(), FLOW_DIR, "diffs", `${timestampForFile()}-check.html`),
147
- title: "monacori validation diff",
148
- });
149
- if (openInBrowser) {
150
- spawnSync("open", [review.path], { stdio: "ignore" });
151
- }
152
- }
153
- const reportPath = writeCheckReport({ verification, review });
154
- console.log("# monacori check");
155
- console.log(`Verification: ${verification.skipped ? "skipped" : verification.failed ? "failed" : "passed"}`);
156
- if (verification.logPath) {
157
- console.log(`Log: ${relative(process.cwd(), verification.logPath)}`);
158
- }
159
- if (review) {
160
- console.log(`Diff review: ${relative(process.cwd(), review.path)}`);
161
- console.log(`Files: ${review.files}`);
162
- console.log(`Hunks: ${review.hunks}`);
163
- }
164
- console.log(`Report: ${relative(process.cwd(), reportPath)}`);
165
- if (verification.failed) {
166
- process.exit(1);
167
- }
168
- }
169
- function runVerification(args) {
170
- const separator = args.indexOf("--");
171
- const explicitCommand = separator >= 0 ? args.slice(separator + 1).join(" ") : "";
172
- const result = executeVerification(explicitCommand, { requireCommands: true });
173
- if (result.logPath) {
174
- console.log(`Verification log: ${relative(process.cwd(), result.logPath)}`);
175
- }
176
- if (result.failed) {
177
- console.error("Verification failed.");
178
- process.exit(1);
179
- }
180
- console.log("Verification passed.");
181
- }
182
- function renderDiffReview(args) {
183
- if (args.includes("--help") || args.includes("-h")) {
184
- printDiffHelp();
185
- return;
186
- }
187
- ensureWritableFlowState();
188
- const config = loadConfig();
189
- const contextValue = readOption(args, "--context");
190
- const context = contextValue ? parsePositiveInteger(contextValue, "--context") : config.diff.context;
191
- const base = readOption(args, "--base");
192
- const staged = args.includes("--staged");
193
- const includeUntracked = args.includes("--include-untracked") || config.diff.includeUntracked;
194
- const openInBrowser = args.includes("--open");
195
- const watch = args.includes("--watch");
196
- const ignoreWhitespace = args.includes("--ignore-whitespace");
197
- if (watch) {
198
- serveDiffWatch({
199
- base,
200
- staged,
201
- includeUntracked,
202
- context,
203
- openInBrowser,
204
- port: readOption(args, "--port"),
205
- ignoreWhitespace,
206
- });
207
- return;
208
- }
209
- const output = readOption(args, "--output") ??
210
- join(process.cwd(), FLOW_DIR, "diffs", `${timestampForFile()}-review.html`);
211
- const result = createDiffReview({
212
- base,
213
- staged,
214
- includeUntracked,
215
- context,
216
- output,
217
- title: "monacori diff review",
218
- ignoreWhitespace,
219
- });
220
- if (openInBrowser) {
221
- spawnSync("open", [result.path], { stdio: "ignore" });
222
- }
223
- console.log(`Diff review: ${relative(process.cwd(), result.path)}`);
224
- console.log(`URL: ${result.url}`);
225
- console.log(`Files: ${result.files}`);
226
- console.log(`Hunks: ${result.hunks}`);
227
- 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.");
228
- }
229
101
  function launchReviewApp(args) {
230
102
  if (args.includes("--help") || args.includes("-h")) {
231
103
  printAppHelp();
@@ -290,138 +162,6 @@ function resolveElectronBinary() {
290
162
  function appMainPath() {
291
163
  return join(dirname(fileURLToPath(import.meta.url)), "app-main.js");
292
164
  }
293
- function printStatus() {
294
- ensureInitialized();
295
- const config = loadConfig();
296
- const git = readGitSnapshot(process.cwd());
297
- const reports = listRecentFiles(join(process.cwd(), FLOW_DIR, "reports"), 5);
298
- const logs = listRecentFiles(join(process.cwd(), FLOW_DIR, "logs"), 5);
299
- console.log(`# ${config.projectName} validation status`);
300
- console.log("");
301
- console.log(`Branch: ${git.branch || "(unknown)"}`);
302
- console.log("");
303
- console.log("## Git status");
304
- console.log(git.status || "clean");
305
- console.log("");
306
- console.log("## Diff stat");
307
- console.log(git.diffStat || "no diff");
308
- console.log("");
309
- console.log("## Verification commands");
310
- const commands = getVerificationCommands(config);
311
- if (commands.length === 0) {
312
- console.log("none configured");
313
- }
314
- else {
315
- for (const command of commands) {
316
- console.log(`- ${command}`);
317
- }
318
- }
319
- console.log("");
320
- console.log("## Recent reports");
321
- console.log(reports.length === 0 ? "none" : reports.map((path) => `- ${relative(process.cwd(), path)}`).join("\n"));
322
- console.log("");
323
- console.log("## Recent logs");
324
- console.log(logs.length === 0 ? "none" : logs.map((path) => `- ${relative(process.cwd(), path)}`).join("\n"));
325
- }
326
- function recordReport(args) {
327
- ensureWritableFlowState();
328
- const file = readOption(args, "--file");
329
- const label = readOption(args, "--label") ?? "manual";
330
- const body = file ? readFileSync(file, "utf8") : readStdin();
331
- if (body.trim().length === 0) {
332
- throw new Error("No report content provided. Pass --file or pipe report text on stdin.");
333
- }
334
- const timestamp = timestampForFile();
335
- const reportDir = join(process.cwd(), FLOW_DIR, "reports");
336
- mkdirSync(reportDir, { recursive: true });
337
- const reportPath = join(reportDir, `${timestamp}-${sanitizeFilePart(label)}.md`);
338
- writeFileSync(reportPath, [
339
- `# Monacori Report: ${label}`,
340
- "",
341
- `Recorded: ${new Date().toISOString()}`,
342
- "",
343
- body.trim(),
344
- "",
345
- ].join("\n"));
346
- appendToState(`\n## Report ${timestamp} (${label})\n\n${summarizeForState(body)}\n`);
347
- console.log(`Recorded ${relative(process.cwd(), reportPath)}`);
348
- }
349
- function executeVerification(explicitCommand = "", options = {}) {
350
- ensureWritableFlowState();
351
- const config = loadConfig();
352
- const commands = explicitCommand.trim() ? [explicitCommand.trim()] : getVerificationCommands(config);
353
- if (commands.length === 0) {
354
- if (options.requireCommands) {
355
- throw new Error(`No verification commands found. Add them to ${FLOW_DIR}/${CONFIG_FILE} or pass \`-- <command>\`.`);
356
- }
357
- return { commands: [], failed: false, skipped: true };
358
- }
359
- const logPath = join(process.cwd(), FLOW_DIR, "logs", `verify-${timestampForFile()}.log`);
360
- const chunks = [];
361
- let failed = false;
362
- for (const command of commands) {
363
- chunks.push(`$ ${command}\n`);
364
- const result = spawnSync(command, {
365
- cwd: process.cwd(),
366
- shell: true,
367
- encoding: "utf8",
368
- env: process.env,
369
- maxBuffer: 1024 * 1024 * 100,
370
- });
371
- chunks.push(result.stdout ?? "");
372
- chunks.push(result.stderr ?? "");
373
- chunks.push(`\nexit: ${result.status ?? 1}\n\n`);
374
- if ((result.status ?? 1) !== 0) {
375
- failed = true;
376
- break;
377
- }
378
- }
379
- writeFileSync(logPath, chunks.join(""));
380
- return { commands, failed, skipped: false, logPath };
381
- }
382
- function writeCheckReport(input) {
383
- const timestamp = timestampForFile();
384
- const git = readGitSnapshot(process.cwd());
385
- const reportDir = join(process.cwd(), FLOW_DIR, "reports");
386
- mkdirSync(reportDir, { recursive: true });
387
- const reportPath = join(reportDir, `${timestamp}-check.md`);
388
- const verificationStatus = input.verification.skipped
389
- ? "skipped"
390
- : input.verification.failed
391
- ? "failed"
392
- : "passed";
393
- const report = [
394
- "# Monacori Validation Check",
395
- "",
396
- `Recorded: ${new Date().toISOString()}`,
397
- `Branch: ${git.branch || "(unknown)"}`,
398
- `Verification: ${verificationStatus}`,
399
- input.verification.logPath ? `Log: ${relative(process.cwd(), input.verification.logPath)}` : "",
400
- input.review ? `Diff review: ${relative(process.cwd(), input.review.path)}` : "",
401
- input.review ? `Changed files: ${input.review.files}` : "",
402
- input.review ? `Changed hunks: ${input.review.hunks}` : "",
403
- "",
404
- "## Commands",
405
- input.verification.commands.length === 0
406
- ? "- none"
407
- : input.verification.commands.map((command) => `- \`${command}\``).join("\n"),
408
- "",
409
- "## Git Status",
410
- codeBlock(git.status || "clean"),
411
- "",
412
- "## Diff Stat",
413
- codeBlock(git.diffStat || "no diff"),
414
- "",
415
- ].filter((line) => line !== "").join("\n");
416
- writeFileSync(reportPath, report);
417
- appendToState(`\n## Check ${timestamp}\n\n- Verification: ${verificationStatus}\n${input.review ? `- Diff review: ${relative(process.cwd(), input.review.path)}\n` : ""}`);
418
- return reportPath;
419
- }
420
- function appendToState(content) {
421
- const path = join(process.cwd(), FLOW_DIR, STATE_FILE);
422
- const current = existsSync(path) ? readFileSync(path, "utf8") : "";
423
- writeFileSync(path, `${current.trimEnd()}\n${content}`);
424
- }
425
165
  function initialState(config) {
426
166
  return [
427
167
  "# Monacori Validation State",
@@ -442,27 +182,24 @@ function initialDecisions() {
442
182
  return [
443
183
  "# Monacori Decisions",
444
184
  "",
445
- "Record durable validation decisions here so future checks do not depend on chat memory.",
185
+ "Record durable review decisions here so they do not depend on chat memory.",
446
186
  "",
447
187
  ].join("\n");
448
188
  }
449
189
  function agentSnippet() {
450
190
  return [
451
191
  "<!-- MONACORI:START -->",
452
- "## monacori Validation",
192
+ "## monacori Diff Review",
453
193
  "",
454
- "This repository uses monacori to verify AI-generated code changes.",
194
+ "This repository uses monacori to help humans review AI-generated code changes side-by-side.",
455
195
  "",
456
- "Before claiming completion on a code change:",
196
+ "After making code changes:",
457
197
  "",
458
- "- Run `monacori check --include-untracked` or a more specific `monacori verify -- <command>`.",
459
- "- Use `monacori app --include-untracked` while changes are still moving.",
198
+ "- The user can run `mo` to open the diff review app and inspect your changes.",
460
199
  "- Inspect changed hunks with F7 / Shift+F7.",
461
200
  "- Use Shift Shift in the diff review to search indexed files, including unchanged files.",
462
201
  "- In source previews, use Cmd/Ctrl+Down to jump to the declaration-like match under the cursor.",
463
- "- Report the verification commands, results, and remaining risks.",
464
- "",
465
- "Do not claim a change is done without verification evidence or a precise explanation of why verification could not run.",
202
+ "- Inline comments left in the review are bundled into a prompt and sent back to the session.",
466
203
  "<!-- MONACORI:END -->",
467
204
  "",
468
205
  ].join("\n");
@@ -508,9 +245,6 @@ function loadConfig() {
508
245
  },
509
246
  };
510
247
  }
511
- function getVerificationCommands(config) {
512
- return config.verification.commands.filter((command) => command.trim().length > 0);
513
- }
514
248
  function writeIfMissing(path, content, force) {
515
249
  if (!force && existsSync(path)) {
516
250
  return;
@@ -582,26 +316,14 @@ function packageScriptCommand(manager, script) {
582
316
  function printHelp() {
583
317
  console.log(`monacori
584
318
 
585
- Validation control plane for AI-generated code changes.
319
+ Desktop review app for AI-generated code changes.
586
320
 
587
321
  Usage:
588
322
  mo
589
323
  monacori open [--base HEAD] [--staged] [--tracked-only]
590
- monacori check [--include-untracked] [--open] [--no-verify] [--no-diff] [-- <command>]
324
+ monacori app [--base HEAD] [--staged] [--include-untracked]
591
325
  monacori init [--force]
592
326
  monacori install [--force] [--apply-agent-docs]
593
- monacori verify [-- <command>]
594
- monacori diff [--base HEAD] [--staged] [--include-untracked] [--open] [--watch]
595
- monacori app [--base HEAD] [--staged] [--include-untracked]
596
- monacori review [--base HEAD] [--staged] [--include-untracked]
597
- monacori status
598
- monacori report [--label manual] [--file report.md]
599
-
600
- Default loop:
601
- 1. Let an AI agent edit code.
602
- 2. Run: mo
603
- 3. Run: monacori check --include-untracked
604
- 4. Only accept the change when verification evidence is clear.
605
327
 
606
328
  Diff review keys:
607
329
  F7 next changed hunk
@@ -626,42 +348,6 @@ Options:
626
348
  --tracked-only inspect tracked changes only
627
349
  `);
628
350
  }
629
- function printCheckHelp() {
630
- console.log(`monacori check
631
-
632
- Run configured verification and create a reviewable diff artifact.
633
-
634
- Usage:
635
- monacori check [--include-untracked] [--staged] [--base HEAD] [--context 12] [--open] [--no-verify] [--no-diff] [-- <command>]
636
-
637
- Examples:
638
- monacori check --include-untracked --open
639
- monacori check -- npm test
640
- monacori check --no-verify --include-untracked
641
- `);
642
- }
643
- function printDiffHelp() {
644
- console.log(`monacori diff
645
-
646
- Generate a browser-based side-by-side Git diff review.
647
-
648
- Usage:
649
- monacori diff [--base HEAD] [--staged] [--include-untracked] [--context 12] [--output review.html] [--open] [--watch] [--port 0]
650
-
651
- Keys in the review page:
652
- F7 next changed hunk
653
- Shift+F7 previous changed hunk
654
- ] / [ fallback hunk navigation
655
- Shift Shift search indexed files, including unchanged files
656
- Cmd/Ctrl+E recent files
657
- Cmd/Ctrl+Down jump to symbol under cursor
658
-
659
- The sidebar groups changed files as a folder tree. Use Search to filter paths and indexed file contents.
660
- The Files tab opens read-only source previews, including unchanged files when they fit the local review budget.
661
- Viewed marks are tied to file signatures, so a changed file becomes unviewed again after reload.
662
- Use --watch to serve a live review that reloads when the working tree changes.
663
- `);
664
- }
665
351
  function printAppHelp() {
666
352
  console.log(`monacori app
667
353
 
package/dist/i18n.js CHANGED
@@ -94,6 +94,7 @@ export const MESSAGES = {
94
94
  "kbd.allQuestionsChanges": "All questions / changes",
95
95
  "kbd.ignoreWhitespace": "Ignore whitespace",
96
96
  "kbd.saveComment": "Save comment",
97
+ "kbd.promptMemo": "Prompt memo",
97
98
  "kbd.toggleTerminal": "Toggle terminal",
98
99
  "kbd.splitPane": "Split pane",
99
100
  "kbd.focusPane": "Focus prev / next pane",
@@ -127,6 +128,10 @@ export const MESSAGES = {
127
128
  "merged.close": "Close",
128
129
  "merged.qHeading": "# Questions",
129
130
  "merged.cHeading": "# Change requests",
131
+ // Prompt memo (Cmd/Ctrl+Shift+N) — a single freeform Markdown scratchpad with a live split preview.
132
+ "memo.title": "Prompt memo",
133
+ "memo.placeholder": "Jot down what you're planning, in Markdown…",
134
+ "memo.previewEmpty": "Markdown preview shows up here as you type.",
130
135
  // Merge-prompt default agent contracts (these follow the locale — a Korean user gets Korean defaults)
131
136
  "mergePrompt.default.q": "The following are questions about code you just wrote. Answer each one — explain the intent, rationale, or context. Do not change any code; this clarifies understanding before any revisions.",
132
137
  "mergePrompt.default.c": "The following are change requests for code you just wrote. For each, edit the code at the quoted location to satisfy the request. Keep changes minimal and focused; do not make unrelated edits.",
@@ -215,6 +220,7 @@ export const MESSAGES = {
215
220
  "kbd.allQuestionsChanges": "전체 질문 / 변경요청",
216
221
  "kbd.ignoreWhitespace": "공백 무시",
217
222
  "kbd.saveComment": "코멘트 저장",
223
+ "kbd.promptMemo": "프롬프트 메모",
218
224
  "kbd.toggleTerminal": "터미널 토글",
219
225
  "kbd.splitPane": "패널 분할",
220
226
  "kbd.focusPane": "이전 / 다음 패널로 이동",
@@ -249,6 +255,10 @@ export const MESSAGES = {
249
255
  // Structural markers stay English in both locales (the preamble prose below follows the locale).
250
256
  "merged.qHeading": "# Questions",
251
257
  "merged.cHeading": "# Change requests",
258
+ // 프롬프트 메모 (Cmd/Ctrl+Shift+N) — 라이브 분할 미리보기가 있는 자유 형식 마크다운 메모 한 장.
259
+ "memo.title": "프롬프트 메모",
260
+ "memo.placeholder": "구상 중인 것을 마크다운으로 적어 보세요…",
261
+ "memo.previewEmpty": "입력하면 여기에 마크다운 미리보기가 나타납니다.",
252
262
  // Merge-prompt default agent contracts (Korean default for Korean users)
253
263
  "mergePrompt.default.q": "다음은 방금 작성한 코드에 대한 질문입니다. 각 질문에 답하면서 의도, 근거, 맥락을 설명하세요. 코드는 변경하지 마세요. 이 단계는 수정에 앞서 이해를 명확히 하기 위한 것입니다.",
254
264
  "mergePrompt.default.c": "다음은 방금 작성한 코드에 대한 변경 요청입니다. 각 요청에 대해 인용된 위치의 코드를 수정하여 요구사항을 충족하세요. 변경은 최소한으로 집중해서 하고, 관련 없는 수정은 하지 마세요.",
package/dist/preload.cjs CHANGED
@@ -13,6 +13,15 @@ electron_1.contextBridge.exposeInMainWorld("monacoriMenu", {
13
13
  onMergedView: (cb) => {
14
14
  electron_1.ipcRenderer.on("monacori:merged-view", (_event, kind) => cb(kind));
15
15
  },
16
+ // Review menu's Cmd/Ctrl+Shift+N -> open/close the prompt memo in the renderer.
17
+ onOpenMemo: (cb) => {
18
+ electron_1.ipcRenderer.on("monacori:open-memo", () => cb());
19
+ },
20
+ // Electron watch: main pushes the rebuilt review HTML so the renderer refreshes the diff in place
21
+ // (no window reload), keeping the integrated terminal's pty sessions alive.
22
+ onDiffUpdate: (cb) => {
23
+ electron_1.ipcRenderer.on("monacori:diff-update", (_event, html) => cb(html));
24
+ },
16
25
  // Cmd/Ctrl+W from the Window menu -> close the active Files-mode tab in the renderer.
17
26
  onCloseTab: (cb) => {
18
27
  electron_1.ipcRenderer.on("monacori:close-tab", () => cb());
package/dist/render.d.ts CHANGED
@@ -6,6 +6,15 @@ export declare function splitDiffForLazy(diffHtml: string, files: DiffFile[]): {
6
6
  islands: string;
7
7
  bodies: string[];
8
8
  };
9
+ export declare function renderReviewStatus(input: {
10
+ files: number;
11
+ hunks: number;
12
+ embeddedFiles: number;
13
+ sourceFileCount: number;
14
+ ignoreWhitespace?: boolean;
15
+ watch?: boolean;
16
+ generatedAt?: string;
17
+ }): string;
9
18
  export declare function renderDiffHtml(input: {
10
19
  files: DiffFile[];
11
20
  diffHtml: string;
@@ -25,6 +34,8 @@ export declare function renderDiffHtml(input: {
25
34
  signature?: string;
26
35
  generatedAt?: string;
27
36
  }): string;
37
+ export declare function renderDiffTree(files: DiffFile[]): string;
38
+ export declare function renderSourceTree(files: SourceFile[]): string;
28
39
  export declare function diffSubtitle(options: {
29
40
  base?: string;
30
41
  staged: boolean;
package/dist/render.js CHANGED
@@ -82,6 +82,11 @@ export function splitDiffForLazy(diffHtml, files) {
82
82
  });
83
83
  return { container: shells.join("\n"), islands: islands.join("\n"), bodies };
84
84
  }
85
+ // The toolbar's review-status row (file/hunk counts, index + live status). Extracted so the in-place
86
+ // update path can re-render just this strip; renderDiffHtml wraps it in <div class="review-status">.
87
+ export function renderReviewStatus(input) {
88
+ return `<span>${input.files} <span data-i18n="status.files">files</span></span><span>${input.hunks} <span data-i18n="status.hunks">hunks</span></span>${input.ignoreWhitespace ? '<span class="ws-ignored" data-i18n="status.wsIgnored" data-i18n-title="status.wsIgnored.title" title="Whitespace ignored — Cmd/Ctrl+Shift+W">ws ignored</span>' : ""}<span class="index-status" id="index-status" data-i18n-title="status.index.title" title="Go-to-definition index">${input.embeddedFiles}/${input.sourceFileCount} indexed</span><span class="index-progress hidden" id="index-progress" aria-hidden="true"><span class="index-progress-bar"></span></span><span class="live-status ${input.watch ? "watching" : ""}" id="live-status"${input.watch ? ' data-i18n="status.watching"' : ""}>${input.watch ? "watching" : escapeHtml(input.generatedAt ?? new Date().toISOString())}</span>`;
89
+ }
85
90
  export function renderDiffHtml(input) {
86
91
  const totalHunks = input.files.reduce((sum, file) => sum + file.hunks.length, 0);
87
92
  const fileNav = renderDiffTree(input.files);
@@ -102,6 +107,8 @@ export function renderDiffHtml(input) {
102
107
  "</style>",
103
108
  "</head>",
104
109
  "<body>",
110
+ // Boot overlay (removed by the renderer once bootstrap has painted) covers the blank gap after loadFile.
111
+ '<div id="boot-overlay"><div class="boot-spinner"></div><div>monacori</div></div>',
105
112
  '<aside class="sidebar" aria-label="Review navigation">',
106
113
  '<div class="sidebar-scroll">',
107
114
  `<div class="sidebar-brand" title="${escapeAttr(input.projectPath)}"><span class="brand-mark">monacori</span><span class="brand-project">${escapeHtml(input.projectName)}</span></div>`,
@@ -122,7 +129,7 @@ export function renderDiffHtml(input) {
122
129
  '<section id="diff-view" class="hidden">',
123
130
  '<div class="toolbar">',
124
131
  '<div class="breadcrumb" id="diff-breadcrumb"></div>',
125
- `<div class="review-status"><span>${input.files.length} <span data-i18n="status.files">files</span></span><span>${totalHunks} <span data-i18n="status.hunks">hunks</span></span>${input.ignoreWhitespace ? '<span class="ws-ignored" data-i18n="status.wsIgnored" data-i18n-title="status.wsIgnored.title" title="Whitespace ignored — Cmd/Ctrl+Shift+W">ws ignored</span>' : ""}<span class="index-status" id="index-status" data-i18n-title="status.index.title" title="Go-to-definition index">${embeddedFiles}/${input.sourceFiles.length} indexed</span><span class="index-progress hidden" id="index-progress" aria-hidden="true"><span class="index-progress-bar"></span></span><span class="live-status ${input.watch ? "watching" : ""}" id="live-status"${input.watch ? ' data-i18n="status.watching"' : ""}>${input.watch ? "watching" : escapeHtml(input.generatedAt ?? new Date().toISOString())}</span></div>`,
132
+ `<div class="review-status">${renderReviewStatus({ files: input.files.length, hunks: totalHunks, embeddedFiles, sourceFileCount: input.sourceFiles.length, ignoreWhitespace: input.ignoreWhitespace, watch: input.watch, generatedAt: input.generatedAt })}</div>`,
126
133
  '<button type="button" id="diff-viewed-toggle" class="diff-viewed-toggle" aria-pressed="false" data-i18n="btn.viewed" data-i18n-title="btn.viewed.title" title="Toggle viewed (<)" hidden>Viewed</button>',
127
134
  "</div>",
128
135
  `<div id="diff2html-container" class="diff2html-container">${input.diffHtml || '<div class="empty" data-i18n="diff.noDiff">No diff to review.</div>'}</div>`,
@@ -194,13 +201,14 @@ export function renderDiffHtml(input) {
194
201
  '<kbd>Cmd/Ctrl+Shift+/ .</kbd><span data-i18n="kbd.allQuestionsChanges">All questions / changes</span>' +
195
202
  '<kbd>Cmd/Ctrl+Shift+W</kbd><span data-i18n="kbd.ignoreWhitespace">Ignore whitespace</span>' +
196
203
  '<kbd>Cmd/Ctrl+Enter</kbd><span data-i18n="kbd.saveComment">Save comment</span>' +
204
+ '<kbd>Cmd/Ctrl+Shift+N</kbd><span data-i18n="kbd.promptMemo">Prompt memo</span>' +
197
205
  '</div>' +
198
206
  '<div class="keys-cat" data-i18n="settings.kbd.cat.terminal">Terminal</div>' +
199
207
  '<div class="keys-grid">' +
200
208
  '<kbd>Ctrl+`</kbd><span data-i18n="kbd.toggleTerminal">Toggle terminal</span>' +
201
209
  '<kbd>Cmd/Ctrl+D</kbd><span data-i18n="kbd.splitPane">Split pane</span>' +
202
210
  '<kbd>Cmd/Ctrl+Alt+[ / ]</kbd><span data-i18n="kbd.focusPane">Focus prev / next pane</span>' +
203
- '<kbd>F2</kbd><span data-i18n="kbd.renamePane">Rename pane</span>' +
211
+ '<kbd>Cmd/Ctrl+Alt+R</kbd><span data-i18n="kbd.renamePane">Rename pane</span>' +
204
212
  '<kbd>Cmd/Ctrl+W</kbd><span data-i18n="kbd.closeTerminal">Close terminal (when focused)</span>' +
205
213
  '</div>' +
206
214
  '</div>',
@@ -234,7 +242,7 @@ export function renderDiffHtml(input) {
234
242
  "</html>",
235
243
  ].join("\n");
236
244
  }
237
- function renderDiffTree(files) {
245
+ export function renderDiffTree(files) {
238
246
  if (files.length === 0) {
239
247
  return '<div class="empty-nav">No changed files</div>';
240
248
  }
@@ -266,7 +274,7 @@ function renderDiffTree(files) {
266
274
  });
267
275
  return `<nav class="tree changes-flat">${rows.join("")}</nav>`;
268
276
  }
269
- function renderSourceTree(files) {
277
+ export function renderSourceTree(files) {
270
278
  if (files.length === 0) {
271
279
  return '<div class="empty-nav">No source files indexed</div>';
272
280
  }
@@ -394,7 +402,7 @@ function renderSourceNode(node, depth) {
394
402
  }
395
403
  return [
396
404
  `<details class="tree-dir source-dir" data-dir="${escapeAttr(labelNode.path)}" style="--depth:${depth}">`,
397
- `<summary><span class="folder-icon">v</span><span class="path">${escapeHtml(names.join("/"))}</span></summary>`,
405
+ `<summary><span class="folder-icon"><svg class="folder-ic fi-closed" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg><svg class="folder-ic fi-open" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 14 1.45-2.9A2 2 0 0 1 9.24 10H21a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg></span><span class="path">${escapeHtml(names.join("/"))}</span></summary>`,
398
406
  renderSourceChildren(labelNode, depth + 1),
399
407
  "</details>",
400
408
  ].join("\n");
package/dist/server.js CHANGED
@@ -108,6 +108,12 @@ export function serveDiffWatch(input) {
108
108
  });
109
109
  return;
110
110
  }
111
+ // Compact in-place refresh payload — the poller fetches this only when the signature changed.
112
+ if (requestUrl.pathname === "/__ai_flow_update") {
113
+ const latest = lastBuild ?? build();
114
+ writeHttpJson(response, latest.update ?? {});
115
+ return;
116
+ }
111
117
  if (requestUrl.pathname === "/__http_send" && request.method === "POST") {
112
118
  void handleHttpProxy(request, response);
113
119
  return;
package/dist/types.d.ts CHANGED
@@ -78,12 +78,24 @@ export type DiffReviewResult = {
78
78
  files: number;
79
79
  hunks: number;
80
80
  };
81
+ export type DiffReviewUpdate = {
82
+ signature: string;
83
+ generatedAt: string;
84
+ diffContainer: string;
85
+ changesPanel: string;
86
+ filesTree: string;
87
+ reviewStatus: string;
88
+ fileStates: ReviewFileState[];
89
+ sourceFilesMeta: SourceFile[];
90
+ httpEnvironments: Record<string, Record<string, string>>;
91
+ };
81
92
  export type DiffReviewBuild = {
82
93
  html: string;
83
94
  files: number;
84
95
  hunks: number;
85
96
  signature: string;
86
97
  generatedAt: string;
98
+ update?: DiffReviewUpdate;
87
99
  lazyBodies?: string[];
88
100
  lazySourceData?: string;
89
101
  };