@happy-nut/monacori 0.1.2 → 0.1.5

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 CHANGED
@@ -1,30 +1,10 @@
1
1
  # monacori
2
2
 
3
- Validation control plane for AI-generated code changes.
4
-
5
- `monacori` is not an agent orchestrator. It does not manage panes, sessions, worktrees, or model adapters. Its job is narrower: after an AI edits a repository, produce verification evidence that a human can trust.
6
-
7
- It gives you:
8
-
9
- - detected verification commands in `.monacori/config.json`
10
- - repeatable verification logs under `.monacori/logs/`
11
- - a local desktop review app for live diff inspection
12
- - diff2html review pages under `.monacori/diffs/`
13
- - searchable source previews, including unchanged indexed files
14
- - compact validation reports under `.monacori/reports/`
3
+ **A local desktop diff-review app for AI-generated code changes.** After an AI edits your repo, run `mo` to open a side-by-side diff — read it, comment on it, and send your comments straight to an AI CLI running in the built-in terminal.
15
4
 
16
5
  ## Why
17
6
 
18
- AI coding output is hard to trust when review depends on chat memory or vague "done" claims. `monacori` keeps the review artifacts in the repository so the state can be inspected, rerun, and discussed.
19
-
20
- The intended loop is simple:
21
-
22
- ```text
23
- AI edits code.
24
- monacori runs verification.
25
- monacori creates a reviewable diff artifact.
26
- You inspect evidence before accepting the change.
27
- ```
7
+ A chat log or a "done" claim is a poor way to review what an AI changed. monacori puts the change in front of you as a real diff you can read and annotate — then turns your comments into a prompt you hand right back to `claude` or `codex`, without leaving the app or copy-pasting between windows.
28
8
 
29
9
  ## Install
30
10
 
@@ -32,146 +12,44 @@ You inspect evidence before accepting the change.
32
12
  npm install -g @happy-nut/monacori
33
13
  ```
34
14
 
35
- After installation, the short command is:
36
-
37
- ```bash
38
- mo
39
- ```
40
-
41
- Homebrew tap distribution is prepared through `Formula/monacori.rb`. Once the scoped npm package is published and the formula is copied to the `happy-nut/homebrew-monacori` tap, the intended install path is:
42
-
43
- ```bash
44
- brew install happy-nut/monacori/monacori
45
- ```
46
-
47
- For local development:
48
-
49
- ```bash
50
- git clone https://github.com/happy-nut/monacori.git
51
- cd monacori
52
- npm install
53
- npm run build
54
- npm link
55
- ```
56
-
57
- ## Quick Start
15
+ After install, the short command is `mo`. A Homebrew tap (`happy-nut/monacori/monacori`) is also available.
58
16
 
59
- Inside the repository you want to validate:
17
+ ## What you get
60
18
 
61
- ```bash
62
- mo
63
- monacori check --include-untracked
64
- ```
19
+ - **Desktop diff review** — side-by-side diff with changed-line highlighting and an IntelliJ-style sidebar that colors files by git status. Reads the repo directly, refreshes on change, no HTTP server.
20
+ - **Integrated terminal** — run AI CLIs like `claude` or `codex` right inside the app, split into panes.
21
+ - **Comments → session** — annotate any line, then send your comments (bundled with their code context) into a terminal pane as one merged prompt: pick the target pane visually and press Enter.
65
22
 
66
- `mo` opens the local desktop review app for the current directory. On first run it creates `.monacori/`, updates Git ignore rules for `.monacori/`, and includes untracked files so new AI-created files show up immediately.
23
+ ## Quick start
67
24
 
68
- `check` runs configured verification commands, writes a log, creates a browser diff review, and records a compact report.
69
-
70
- For a one-off command:
71
-
72
- ```bash
73
- monacori check -- npm test
74
- ```
75
-
76
- For live diff review while an AI is still editing:
25
+ Inside the repository you want to review:
77
26
 
78
27
  ```bash
79
28
  mo
80
29
  ```
81
30
 
82
- ## Diff Review
83
-
84
- The desktop review app is powered by Electron and diff2html. It reads Git diff and source files directly from the local repository, writes a local `.monacori/app-review.html` file, and refreshes when the working tree changes. It does not start an HTTP server.
85
-
86
- - `F7`: show the next changed hunk, starting from the current source file when possible
87
- - `Shift+F7`: previous changed hunk
88
- - `]` / `[`: fallback keys if the browser captures function keys
89
- - `Shift Shift`: search indexed files, including unchanged files
90
- - `Cmd/Ctrl+E`: open recent files
91
- - `Cmd/Ctrl+Down`: jump from the source cursor to a declaration-like match for the symbol under it
92
-
93
- The app opens as a read-only source viewer by default. The `Files` tab opens indexed files even when they are unchanged, and `F7` switches into the diff view when you want to inspect changes. Drag-copying source text adds `path:line-range` plus a fenced code block to the clipboard so the snippet can be pasted into an AI prompt with context. Viewed marks are stored with file signatures, so a file becomes unviewed again when it changes.
94
-
95
- The browser artifact path is still available through `monacori diff`. Add `--watch` there only when you specifically want a browser-served live review.
31
+ On first run, `mo` creates `.monacori/`, adds it to `.gitignore`, and includes untracked files so new AI-created files show up immediately.
96
32
 
97
33
  ## Commands
98
34
 
99
- ```bash
100
- monacori open [--base HEAD] [--staged] [--tracked-only] [--context 12] [--no-watch]
101
- ```
102
-
103
- Opens the local desktop review app for the current directory. `mo` and bare `monacori` are aliases for this default flow. It auto-initializes local state when needed and includes untracked files by default; pass `--tracked-only` to inspect tracked changes only.
35
+ | Command | What it does |
36
+ | --- | --- |
37
+ | `mo` | Open the desktop diff-review app (alias for `monacori open`). |
38
+ | `monacori app` | Launch the desktop review app (same as `mo`). |
39
+ | `monacori init` | Initialize `.monacori/` in the current directory. |
40
+ | `monacori install` | Initialize and write agent instruction snippets. `--apply-agent-docs` patches `AGENTS.md` / `CLAUDE.md`. |
104
41
 
105
- ```bash
106
- monacori check [--include-untracked] [--staged] [--base HEAD] [--context 12] [--open] [--no-verify] [--no-diff] [-- <command>]
107
- ```
108
-
109
- Runs verification and creates a validation report. By default it uses commands from `.monacori/config.json`; pass `-- <command>` to override for one run.
110
-
111
- ```bash
112
- monacori init [--force]
113
- ```
114
-
115
- Creates `.monacori/` with config, state, decisions, reports, logs, and diff directories. Because these are local validation artifacts, monacori also adds `.monacori/` to `.gitignore` inside Git worktrees.
116
-
117
- ```bash
118
- monacori install [--force] [--apply-agent-docs]
119
- ```
120
-
121
- Writes `.monacori/agent-snippet.md`, a short instruction block telling AI agents to run validation before claiming completion. With `--apply-agent-docs`, it updates `AGENTS.md` and `CLAUDE.md` marker blocks where available.
122
-
123
- ```bash
124
- monacori verify [-- <command>]
125
- ```
126
-
127
- Runs configured verification commands and stores the log in `.monacori/logs/`. Exits non-zero on failure.
128
-
129
- ```bash
130
- monacori app [--base HEAD] [--staged] [--include-untracked] [--context 12] [--no-watch]
131
- ```
132
-
133
- Launches the local desktop review app. `mo`, `monacori open`, and `monacori review` are aliases. Prefer `mo` for normal use.
134
-
135
- ```bash
136
- monacori diff [--base HEAD] [--staged] [--include-untracked] [--context 12] [--output review.html] [--open] [--watch] [--port 0]
137
- ```
138
-
139
- Generates a browser-based side-by-side diff review. Add `--watch` to serve a live review that reloads when the working tree changes.
140
-
141
- ```bash
142
- monacori status
143
- ```
42
+ ## Repository state
144
43
 
145
- Prints current branch, git status, diff stat, configured verification commands, recent reports, and recent logs.
146
-
147
- ```bash
148
- monacori report [--label manual] [--file report.md]
149
- ```
150
-
151
- Stores a manual report under `.monacori/reports/` and appends a compact summary to `.monacori/state.md`.
152
-
153
- ## Repository State
154
-
155
- `monacori init` creates:
156
-
157
- ```text
158
- .monacori/
159
- config.json verification commands and diff defaults
160
- state.md compact validation history
161
- decisions.md durable validation decisions
162
- diffs/ generated browser diff reviews
163
- reports/ validation reports
164
- logs/ verification logs
165
- ```
44
+ `monacori init` (run automatically by `mo`) creates a git-ignored `.monacori/` directory holding generated diff reviews and local config. Keep it ignored unless your team explicitly wants to commit review state.
166
45
 
167
- `monacori install` also writes `.monacori/agent-snippet.md` for projects that want to paste or apply agent-facing validation instructions.
46
+ ## Design principles
168
47
 
169
- Keep `.monacori/` ignored unless your team explicitly wants to commit validation state.
48
+ - A real diff beats a chat log or a "done" claim.
49
+ - Review, comment, and hand-off live in one window — no copy-paste loop.
50
+ - Generated artifacts are plain static HTML and JSON.
51
+ - No required AI agent, terminal multiplexer, editor, or worktree strategy.
170
52
 
171
- ## Design Principles
53
+ ## License
172
54
 
173
- - Verification evidence beats chat memory.
174
- - Generated artifacts should be plain Markdown, JSON, logs, or static HTML.
175
- - The tool should not require a specific AI agent, terminal multiplexer, editor, or worktree strategy.
176
- - The default command should be useful after any AI-generated edit.
177
- - A change is not accepted until verification evidence is clear or the gap is explicitly documented.
55
+ MIT
package/dist/app-main.js CHANGED
@@ -1,11 +1,23 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { dirname, join, resolve } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { app, BrowserWindow, ipcMain, Menu, nativeImage } from "electron";
5
6
  import { buildDiffReview, performHttpRequest } from "./cli.js";
7
+ import { spawn as spawnPty } from "node-pty";
6
8
  const FLOW_DIR = ".monacori";
7
9
  const REVIEW_FILE = "app-review.html";
8
10
  const WATCH_INTERVAL_MS = 1000;
11
+ // Painted immediately while the first review build + HTML render run, so startup shows a spinner instead
12
+ // of a blank window. Inlined as a data: URL so it needs no file on disk and appears before any review work.
13
+ const LOADING_HTML = `<!doctype html><html><head><meta charset="utf-8"><style>
14
+ html,body{margin:0;height:100vh;background:#2b2b2b;color:#9aa4af;display:flex;flex-direction:column;
15
+ align-items:center;justify-content:center;gap:18px;
16
+ font:13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
17
+ .s{width:34px;height:34px;border:3px solid #3a3a3a;border-top-color:#4a9eff;border-radius:50%;
18
+ animation:spin .8s linear infinite}
19
+ @keyframes spin{to{transform:rotate(360deg)}}
20
+ </style></head><body><div class="s"></div><div>monacori</div></body></html>`;
9
21
  app.setName("monacori");
10
22
  ipcMain.handle("monacori:http-send", (_event, request) => performHttpRequest(request));
11
23
  // Phase 2 lazy-LOAD: serve a single file's diff body to the renderer on demand. Retained from the
@@ -18,6 +30,129 @@ ipcMain.handle("monacori:get-file", (_event, request) => {
18
30
  });
19
31
  // Phase 2b lazy-LOAD: serve the full source files JSON (with content) on demand.
20
32
  ipcMain.handle("monacori:get-source-data", () => currentSourceData);
33
+ // Self-update: install the latest published package globally, then relaunch so the updated code loads.
34
+ // Runs in the main process because the sandboxed renderer can't spawn npm. Returns {ok:true} (and
35
+ // relaunches shortly after) or {ok:false,error} so the renderer can fall back to the manual command.
36
+ ipcMain.handle("monacori:self-update", () => {
37
+ const result = spawnSync("npm", ["install", "-g", "@happy-nut/monacori@latest"], {
38
+ encoding: "utf8",
39
+ shell: true,
40
+ env: process.env,
41
+ timeout: 5 * 60 * 1000,
42
+ });
43
+ if ((result.status ?? 1) === 0) {
44
+ // Let the renderer paint "Restarting…" before we relaunch with the new code.
45
+ setTimeout(() => { app.relaunch(); app.exit(0); }, 500);
46
+ return { ok: true };
47
+ }
48
+ const detail = (result.stderr || result.stdout || (result.error && result.error.message) || "npm install failed").trim();
49
+ return { ok: false, error: detail.slice(-600) };
50
+ });
51
+ // Integrated terminal: own node-pty sessions in the main process (the sandboxed renderer can't spawn
52
+ // them) and relay bytes to the renderer's xterm panes. Each split pane gets its own pty, keyed by id, so
53
+ // the renderer can route data/resize/kill per pane.
54
+ const terms = new Map();
55
+ let nextPtyId = 0;
56
+ ipcMain.handle("monacori:pty-spawn", (_event, size) => {
57
+ const id = ++nextPtyId;
58
+ const shell = process.env.SHELL || (process.platform === "win32" ? "powershell.exe" : "/bin/zsh");
59
+ const t = spawnPty(shell, [], {
60
+ name: "xterm-color",
61
+ cols: size?.cols ?? 80,
62
+ rows: size?.rows ?? 24,
63
+ cwd: options.root,
64
+ env: process.env,
65
+ });
66
+ terms.set(id, t);
67
+ // mainWindow?. only guards null, NOT a *destroyed* window — sending to a closed window's webContents
68
+ // throws "Object has been destroyed". The pty can outlive the window (close races pty teardown), so
69
+ // guard every relay with isDestroyed().
70
+ const deliver = (channel, payload) => {
71
+ if (mainWindow && !mainWindow.isDestroyed())
72
+ mainWindow.webContents.send(channel, payload);
73
+ };
74
+ // pty output can arrive as many tiny chunks (thousands/sec under fast output). One IPC per chunk floods
75
+ // the renderer, so coalesce: buffer chunks and flush on a short timer, or immediately once the buffer is
76
+ // large — bounding both IPC traffic and added latency.
77
+ let outBuf = "";
78
+ let flushTimer;
79
+ const flushOut = () => {
80
+ flushTimer = undefined;
81
+ if (!outBuf)
82
+ return;
83
+ const data = outBuf;
84
+ outBuf = "";
85
+ deliver("monacori:pty-data", { id, data });
86
+ };
87
+ t.onData((data) => {
88
+ outBuf += data;
89
+ if (outBuf.length >= 64 * 1024) {
90
+ if (flushTimer)
91
+ clearTimeout(flushTimer);
92
+ flushOut();
93
+ }
94
+ else if (!flushTimer) {
95
+ flushTimer = setTimeout(flushOut, 12);
96
+ }
97
+ });
98
+ t.onExit(() => {
99
+ if (flushTimer)
100
+ clearTimeout(flushTimer);
101
+ flushOut(); // deliver any buffered tail before signaling exit
102
+ terms.delete(id);
103
+ deliver("monacori:pty-exit", { id });
104
+ });
105
+ return { ok: true, id };
106
+ });
107
+ ipcMain.on("monacori:pty-write", (_event, msg) => { terms.get(msg?.id)?.write(msg.data); });
108
+ ipcMain.on("monacori:pty-resize", (_event, msg) => {
109
+ try {
110
+ terms.get(msg?.id)?.resize(msg.cols, msg.rows);
111
+ }
112
+ catch { /* resize can race the pty teardown — ignore */ }
113
+ });
114
+ ipcMain.on("monacori:pty-kill", (_event, msg) => {
115
+ const t = terms.get(msg?.id);
116
+ if (t) {
117
+ try {
118
+ t.kill();
119
+ }
120
+ catch { /* already exited */ }
121
+ terms.delete(msg.id);
122
+ }
123
+ });
124
+ // Persisted global settings (locale, …) live in a JSON file under userData and reach the renderer
125
+ // via preload + the two handlers below. The renderer's file:// localStorage is NOT reliably persisted
126
+ // across app restarts, so settings that must survive a reopen round-trip through the main process.
127
+ function settingsFile() {
128
+ return join(app.getPath("userData"), "monacori-settings.json");
129
+ }
130
+ function readSettings() {
131
+ try {
132
+ return JSON.parse(readFileSync(settingsFile(), "utf8"));
133
+ }
134
+ catch {
135
+ return {};
136
+ }
137
+ }
138
+ function writeSettings(settings) {
139
+ try {
140
+ writeFileSync(settingsFile(), JSON.stringify(settings, null, 2));
141
+ }
142
+ catch {
143
+ /* best-effort: a failed write just means the setting isn't persisted */
144
+ }
145
+ }
146
+ ipcMain.on("monacori:get-settings", (event) => {
147
+ event.returnValue = readSettings();
148
+ });
149
+ ipcMain.on("monacori:set-setting", (_event, msg) => {
150
+ if (!msg || typeof msg.key !== "string")
151
+ return;
152
+ const settings = readSettings();
153
+ settings[msg.key] = msg.value;
154
+ writeSettings(settings);
155
+ });
21
156
  const iconPath = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "icon.png");
22
157
  const preloadPath = join(dirname(fileURLToPath(import.meta.url)), "preload.cjs");
23
158
  const options = parseArgs(process.argv.slice(2));
@@ -38,13 +173,15 @@ app.whenReady().then(async () => {
38
173
  if (process.platform === "darwin")
39
174
  menuTemplate.push({ role: "appMenu" });
40
175
  menuTemplate.push({ role: "editMenu" });
41
- // Claim Cmd/Ctrl+Shift+/ ("?") and Cmd/Ctrl+Shift+. (">") as menu accelerators so macOS does not
42
- // swallow Cmd+? for its Help search; clicking routes to the renderer's merged comment views.
176
+ // Ctrl+Cmd+Shift+/ ("?") and Ctrl+Cmd+Shift+. (">") open the merged question / change-request views.
177
+ // ? and > are Shift+/ and Shift+. so Shift is part of the combo; Ctrl+Cmd avoids macOS's Cmd+? Help grab.
43
178
  menuTemplate.push({
44
179
  label: "Review",
45
180
  submenu: [
46
- { label: "All questions", accelerator: "CommandOrControl+Shift+/", click: () => sendMerged("q") },
47
- { label: "All change requests", accelerator: "CommandOrControl+Shift+.", click: () => sendMerged("c") },
181
+ { label: "All questions", accelerator: "Control+Command+Shift+/", click: () => sendMerged("q") },
182
+ { label: "All change requests", accelerator: "Control+Command+Shift+.", click: () => sendMerged("c") },
183
+ // Cmd/Ctrl+Shift+N opens (and toggles) the single freeform prompt memo — a Markdown scratchpad.
184
+ { label: "Prompt memo", accelerator: "CommandOrControl+Shift+N", click: () => mainWindow?.webContents.send("monacori:open-memo") },
48
185
  { type: "separator" },
49
186
  // Whitespace-ignore re-runs git diff with --ignore-all-space and reloads (main-process action,
50
187
  // so a menu checkbox is simpler than a renderer IPC round-trip).
@@ -61,14 +198,37 @@ app.whenReady().then(async () => {
61
198
  },
62
199
  ],
63
200
  });
64
- menuTemplate.push({ role: "windowMenu" });
201
+ // Cmd/Ctrl+W closes the active Files-mode tab (routed to the renderer) instead of the window, matching
202
+ // editor/browser tab behavior. Closing the window stays available via the menu item and Cmd/Ctrl+Q.
203
+ menuTemplate.push({
204
+ label: "Window",
205
+ submenu: [
206
+ { role: "minimize" },
207
+ { role: "zoom" },
208
+ { type: "separator" },
209
+ { label: "Close Tab", accelerator: "CommandOrControl+W", click: () => mainWindow?.webContents.send("monacori:close-tab") },
210
+ { label: "Close Window", click: () => mainWindow?.close() },
211
+ ],
212
+ });
213
+ // Terminal toggle/split as menu accelerators: Chromium swallows Cmd+D before it reaches the renderer
214
+ // (Cmd+A and friends arrive fine), so route the split — and the toggles — through the menu instead.
215
+ menuTemplate.push({
216
+ label: "Terminal",
217
+ submenu: [
218
+ { label: "Toggle Terminal", accelerator: "Control+`", click: () => mainWindow?.webContents.send("monacori:terminal-toggle") },
219
+ { label: "Toggle Terminal (F12)", accelerator: "Alt+F12", click: () => mainWindow?.webContents.send("monacori:terminal-toggle") },
220
+ { label: "Split Terminal", accelerator: "CommandOrControl+D", click: () => mainWindow?.webContents.send("monacori:terminal-split") },
221
+ { type: "separator" },
222
+ { label: "Focus Previous Pane", accelerator: "CommandOrControl+Alt+[", click: () => mainWindow?.webContents.send("monacori:terminal-pane-focus", -1) },
223
+ { label: "Focus Next Pane", accelerator: "CommandOrControl+Alt+]", click: () => mainWindow?.webContents.send("monacori:terminal-pane-focus", 1) },
224
+ { label: "Rename Pane", accelerator: "CommandOrControl+Alt+R", click: () => mainWindow?.webContents.send("monacori:terminal-pane-rename") },
225
+ ],
226
+ });
65
227
  Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
66
228
  const appIcon = nativeImage.createFromPath(iconPath);
67
229
  if (process.platform === "darwin" && app.dock && !appIcon.isEmpty()) {
68
230
  app.dock.setIcon(appIcon);
69
231
  }
70
- const firstBuild = writeReviewFile(options);
71
- currentSignature = firstBuild.signature;
72
232
  mainWindow = new BrowserWindow({
73
233
  width: 1440,
74
234
  height: 960,
@@ -89,10 +249,27 @@ app.whenReady().then(async () => {
89
249
  });
90
250
  mainWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
91
251
  mainWindow.once("ready-to-show", () => mainWindow?.show());
92
- await mainWindow.loadFile(reviewPath());
93
- if (options.watch) {
94
- refreshTimer = setInterval(refreshIfChanged, WATCH_INTERVAL_MS);
95
- }
252
+ // Paint the window with a spinner immediately, then build the (potentially heavy) review off the first
253
+ // paint and swap it in. The first build used to run synchronously *before* the window existed, so the
254
+ // screen stayed blank for the first few seconds of startup; now the user sees a loading screen instead.
255
+ await mainWindow.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(LOADING_HTML));
256
+ // Give the loading spinner a few frames to actually paint before the (synchronous) first build blocks
257
+ // the main process — otherwise the spinner looks frozen until the build finishes. The boot overlay in
258
+ // the review HTML then takes over, so there's no blank gap when loadFile swaps the page in.
259
+ setTimeout(() => {
260
+ try {
261
+ const firstBuild = writeReviewFile(options);
262
+ currentSignature = firstBuild.signature;
263
+ if (mainWindow && !mainWindow.isDestroyed())
264
+ void mainWindow.loadFile(reviewPath());
265
+ if (options.watch)
266
+ refreshTimer = setInterval(refreshIfChanged, WATCH_INTERVAL_MS);
267
+ }
268
+ catch (error) {
269
+ console.error(error instanceof Error ? error.message : String(error));
270
+ app.quit();
271
+ }
272
+ }, 60);
96
273
  }).catch((error) => {
97
274
  console.error(error instanceof Error ? error.message : String(error));
98
275
  app.quit();
@@ -100,6 +277,13 @@ app.whenReady().then(async () => {
100
277
  app.on("window-all-closed", () => {
101
278
  if (refreshTimer)
102
279
  clearInterval(refreshTimer);
280
+ for (const t of terms.values()) {
281
+ try {
282
+ t.kill();
283
+ }
284
+ catch { /* already exited */ }
285
+ }
286
+ terms.clear();
103
287
  app.quit();
104
288
  });
105
289
  async function refreshIfChanged() {
@@ -110,7 +294,13 @@ async function refreshIfChanged() {
110
294
  const next = writeReviewFile(options);
111
295
  if (next.signature !== currentSignature) {
112
296
  currentSignature = next.signature;
113
- mainWindow.webContents.reloadIgnoringCache();
297
+ // Refresh the diff in place instead of reloading the window. A full reload re-runs the renderer,
298
+ // whose beforeunload kills every pty — so an integrated terminal running claude/codex would die on
299
+ // each working-tree change. We send only the compact update payload (diff/trees/status/data — no
300
+ // xterm blob), and the renderer transplants it + re-fetches per-file bodies/source over the existing
301
+ // IPC (currentBodies/currentSourceData were just refreshed by writeReviewFile above).
302
+ if (next.update)
303
+ mainWindow.webContents.send("monacori:diff-update", next.update);
114
304
  }
115
305
  }
116
306
  catch (error) {
@@ -129,11 +319,12 @@ function writeReviewFile(input) {
129
319
  title: "monacori",
130
320
  ignoreWhitespace: input.ignoreWhitespace,
131
321
  lazyLoad: true, // Electron streams per-file bodies/source over IPC (monacori:get-file / get-source)
322
+ app: true, // gate the integrated terminal (xterm) into the HTML — Electron only
132
323
  });
133
324
  writeFileSync(reviewPath(), build.html);
134
325
  currentBodies = build.lazyBodies ?? [];
135
326
  currentSourceData = build.lazySourceData ?? "[]";
136
- return { signature: build.signature };
327
+ return { signature: build.signature, html: build.html, update: build.update };
137
328
  }
138
329
  function reviewPath() {
139
330
  return join(options.root, FLOW_DIR, REVIEW_FILE);
package/dist/assets.d.ts CHANGED
@@ -2,3 +2,5 @@ export declare function readViewerAsset(name: string): string;
2
2
  export declare function diff2HtmlCss(): string;
3
3
  export declare function diffCss(): string;
4
4
  export declare function diffScript(): string;
5
+ export declare function xtermCss(): string;
6
+ export declare function xtermScript(): string;
package/dist/assets.js CHANGED
@@ -28,3 +28,24 @@ export function diffCss() {
28
28
  export function diffScript() {
29
29
  return readViewerAsset("viewer.client.js");
30
30
  }
31
+ // xterm.js (terminal renderer) for the integrated terminal panel. UMD bundles that expose
32
+ // window.Terminal + window.FitAddon when inlined. Resolved from node_modules like diff2HtmlCss();
33
+ // pure JS, no native binding — the pty itself lives in the main process via node-pty.
34
+ export function xtermCss() {
35
+ try {
36
+ return readFileSync(nodeRequire.resolve("@xterm/xterm/css/xterm.css"), "utf8");
37
+ }
38
+ catch {
39
+ return "";
40
+ }
41
+ }
42
+ export function xtermScript() {
43
+ try {
44
+ const core = readFileSync(nodeRequire.resolve("@xterm/xterm/lib/xterm.js"), "utf8");
45
+ const fit = readFileSync(nodeRequire.resolve("@xterm/addon-fit/lib/addon-fit.js"), "utf8");
46
+ return core + "\n" + fit;
47
+ }
48
+ catch {
49
+ return "";
50
+ }
51
+ }
package/dist/build.d.ts CHANGED
@@ -9,4 +9,5 @@ export declare function buildDiffReview(input: {
9
9
  ignoreWhitespace?: boolean;
10
10
  lazy?: boolean;
11
11
  lazyLoad?: boolean;
12
+ app?: boolean;
12
13
  }): DiffReviewBuild;
package/dist/build.js CHANGED
@@ -3,7 +3,7 @@ import { basename } from "node:path";
3
3
  import { isGitRepository } from "./git.js";
4
4
  import { collectHttpEnvironments, collectReviewFileStates, collectSourceFiles, parseUnifiedDiff, readUnifiedDiff } from "./diff.js";
5
5
  import { renderDiff2Html } from "./highlight.js";
6
- import { diffSubtitle, renderDiffHtml, renderNotGitRepoHtml, shouldLazyRender, splitDiffForLazy } from "./render.js";
6
+ import { diffSubtitle, renderDiffHtml, renderDiffTree, renderNotGitRepoHtml, renderReviewStatus, renderSourceTree, shouldLazyRender, splitDiffForLazy } from "./render.js";
7
7
  export function buildDiffReview(input) {
8
8
  if (!isGitRepository(process.cwd())) {
9
9
  return {
@@ -30,11 +30,13 @@ export function buildDiffReview(input) {
30
30
  const diffHtml = renderDiff2Html(diffText);
31
31
  const totalLines = files.reduce((sum, file) => sum + file.hunks.reduce((t, h) => t + h.lines.length, 0), 0);
32
32
  // lazy-LOAD (Phase 2) serves each file body + source on demand instead of embedding them; it implies
33
- // lazy (shells). Gated by size: only big reviews lazy-LOAD small ones embed (no IPC round-trips, no
34
- // "Loading source…" flash). The transport opts in (serve/Electron pass lazyLoad:true); standalone has
35
- // no server. A big standalone review still lazy-materializes from embedded islands (Phase 1).
33
+ // lazy (shells). The transport opts in (serve/Electron pass lazyLoad:true) and we honor it regardless
34
+ // of size: it used to be gated by `&& big`, but a mid-size repo (dozens of files, just under the
35
+ // threshold) then embedded a 600KB+ source blob + every diff body inline, forcing the renderer to
36
+ // parse + lay out a huge document before the first click — the startup "freeze". Standalone (no
37
+ // transport) has no server, so it auto-decides by size and lazy-materializes from embedded islands.
36
38
  const big = shouldLazyRender(files.length, totalLines);
37
- const lazyLoad = (input.lazyLoad ?? false) && big;
39
+ const lazyLoad = input.lazyLoad ?? false;
38
40
  const lazy = lazyLoad || (input.lazy ?? big);
39
41
  const diffSplit = lazy ? splitDiffForLazy(diffHtml, files) : { container: diffHtml, islands: "", bodies: [] };
40
42
  const signature = createHash("sha1")
@@ -59,9 +61,31 @@ export function buildDiffReview(input) {
59
61
  projectPath: process.cwd(),
60
62
  watch: Boolean(input.watch),
61
63
  ignoreWhitespace: Boolean(input.ignoreWhitespace),
64
+ app: Boolean(input.app),
62
65
  signature,
63
66
  generatedAt,
64
67
  });
68
+ // Compact payload for in-place refresh: just the regions the renderer swaps on a watch change. Reuses
69
+ // the same fragment renderers as the full page, minus the heavy bits (xterm blob, embedded source).
70
+ const update = {
71
+ signature,
72
+ generatedAt,
73
+ diffContainer: diffSplit.container || '<div class="empty" data-i18n="diff.noDiff">No diff to review.</div>',
74
+ changesPanel: renderDiffTree(files),
75
+ filesTree: renderSourceTree(sourceFiles),
76
+ reviewStatus: renderReviewStatus({
77
+ files: files.length,
78
+ hunks,
79
+ embeddedFiles: sourceFiles.filter((file) => file.embedded).length,
80
+ sourceFileCount: sourceFiles.length,
81
+ ignoreWhitespace: input.ignoreWhitespace,
82
+ watch: input.watch,
83
+ generatedAt,
84
+ }),
85
+ fileStates,
86
+ sourceFilesMeta: lazyLoad ? sourceFiles.map((file) => ({ ...file, content: "", image: "" })) : sourceFiles,
87
+ httpEnvironments,
88
+ };
65
89
  return {
66
90
  html,
67
91
  files: files.length,
@@ -70,5 +94,6 @@ export function buildDiffReview(input) {
70
94
  generatedAt,
71
95
  lazyBodies: diffSplit.bodies,
72
96
  lazySourceData: lazyLoad ? JSON.stringify(sourceFiles) : undefined,
97
+ update,
73
98
  };
74
99
  }