@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 +25 -147
- package/dist/app-main.js +205 -14
- package/dist/assets.d.ts +2 -0
- package/dist/assets.js +21 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +30 -5
- package/dist/commands.js +12 -326
- package/dist/diff.js +41 -0
- package/dist/i18n.d.ts +1 -0
- package/dist/i18n.js +266 -0
- package/dist/preload.cjs +70 -0
- package/dist/render.d.ts +12 -0
- package/dist/render.js +102 -22
- package/dist/server.js +6 -0
- package/dist/types.d.ts +14 -0
- package/dist/viewer.client.js +950 -125
- package/dist/viewer.css +248 -44
- package/package.json +6 -2
- package/scripts/patch-electron-name.mjs +8 -0
package/README.md
CHANGED
|
@@ -1,30 +1,10 @@
|
|
|
1
1
|
# monacori
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
17
|
+
## What you get
|
|
60
18
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
23
|
+
## Quick start
|
|
67
24
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
## Design principles
|
|
168
47
|
|
|
169
|
-
|
|
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
|
-
##
|
|
53
|
+
## License
|
|
172
54
|
|
|
173
|
-
|
|
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
|
-
//
|
|
42
|
-
//
|
|
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: "
|
|
47
|
-
{ label: "All change requests", accelerator: "
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
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).
|
|
34
|
-
//
|
|
35
|
-
//
|
|
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 =
|
|
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
|
}
|