@happy-nut/monacori 0.1.2 → 0.1.3
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 +44 -132
- package/assets/screenshots/diff-review.png +0 -0
- package/assets/screenshots/terminal.png +0 -0
- package/dist/app-main.js +163 -12
- package/dist/assets.d.ts +2 -0
- package/dist/assets.js +21 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +7 -4
- package/dist/diff.js +41 -0
- package/dist/i18n.d.ts +1 -0
- package/dist/i18n.js +256 -0
- package/dist/preload.cjs +61 -0
- package/dist/render.d.ts +1 -0
- package/dist/render.js +91 -19
- package/dist/types.d.ts +2 -0
- package/dist/viewer.client.js +693 -101
- package/dist/viewer.css +194 -39
- package/package.json +6 -2
- package/scripts/patch-electron-name.mjs +8 -0
package/README.md
CHANGED
|
@@ -1,177 +1,89 @@
|
|
|
1
1
|
# monacori
|
|
2
2
|
|
|
3
|
-
Validation control plane for AI-generated code changes.
|
|
3
|
+
**Validation control plane for AI-generated code changes.** After an AI edits your repository, `monacori` produces verification evidence a human can actually trust.
|
|
4
4
|
|
|
5
|
-
|
|
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/`
|
|
5
|
+
It is *not* an agent orchestrator. It does not manage panes, sessions, worktrees, or model adapters. Its job is narrow on purpose: validate what the AI just did.
|
|
15
6
|
|
|
16
7
|
## Why
|
|
17
8
|
|
|
18
|
-
AI coding output is hard to trust when review depends on chat memory or vague "done"
|
|
9
|
+
AI coding output is hard to trust when review depends on chat memory or a vague "done" claim. `monacori` keeps the review artifacts inside the repository, so the state of a change can be inspected, rerun, and discussed instead of taken on faith.
|
|
19
10
|
|
|
20
|
-
The
|
|
11
|
+
The loop is simple:
|
|
21
12
|
|
|
22
13
|
```text
|
|
23
14
|
AI edits code.
|
|
24
15
|
monacori runs verification.
|
|
25
16
|
monacori creates a reviewable diff artifact.
|
|
26
|
-
You inspect evidence before accepting the change.
|
|
17
|
+
You inspect the evidence before accepting the change.
|
|
27
18
|
```
|
|
28
19
|
|
|
20
|
+

|
|
21
|
+
*Local desktop review app: side-by-side diff with changed lines highlighted and IntelliJ-style git status colors in the sidebar.*
|
|
22
|
+
|
|
29
23
|
## Install
|
|
30
24
|
|
|
31
25
|
```bash
|
|
32
26
|
npm install -g @happy-nut/monacori
|
|
33
27
|
```
|
|
34
28
|
|
|
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:
|
|
29
|
+
After install, the short command is `mo`. A Homebrew tap (`happy-nut/monacori/monacori`) is also available.
|
|
42
30
|
|
|
43
|
-
|
|
44
|
-
brew install happy-nut/monacori/monacori
|
|
45
|
-
```
|
|
31
|
+
## What you get
|
|
46
32
|
|
|
47
|
-
|
|
33
|
+
- **Local desktop review app** — diff review 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.
|
|
34
|
+
- **Integrated terminal** — run AI CLIs like `claude` or `codex` inside the app, and send a "merged prompt" (your question or fix request bundled with the relevant code context) straight to the session.
|
|
35
|
+
- **Inline comments** — annotate the diff while you review.
|
|
36
|
+
- **Verification logs & reports** — repeatable verification under `.monacori/logs/` and compact validation reports under `.monacori/reports/`.
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
cd monacori
|
|
52
|
-
npm install
|
|
53
|
-
npm run build
|
|
54
|
-
npm link
|
|
55
|
-
```
|
|
38
|
+

|
|
39
|
+
*Run your AI CLI inside the review app, next to the diff it just produced.*
|
|
56
40
|
|
|
57
|
-
## Quick
|
|
41
|
+
## Quick start
|
|
58
42
|
|
|
59
43
|
Inside the repository you want to validate:
|
|
60
44
|
|
|
61
45
|
```bash
|
|
62
|
-
mo
|
|
63
|
-
monacori check --include-untracked
|
|
64
|
-
```
|
|
65
|
-
|
|
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.
|
|
67
|
-
|
|
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:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
mo
|
|
46
|
+
mo # open the desktop review app for the current dir
|
|
47
|
+
monacori check --include-untracked # run verification, write a log, diff, and report
|
|
80
48
|
```
|
|
81
49
|
|
|
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.
|
|
50
|
+
On first run, `mo` creates `.monacori/`, adds it to `.gitignore`, and includes untracked files so new AI-created files show up immediately.
|
|
96
51
|
|
|
97
52
|
## Commands
|
|
98
53
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
54
|
+
| Command | What it does |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `mo` | Open the desktop review app (alias for `monacori open`). |
|
|
57
|
+
| `monacori check` | Run verification, then create a diff and a validation report. `-- <cmd>` overrides commands for one run. |
|
|
58
|
+
| `monacori verify` | Run configured verification commands and store the log. Exits non-zero on failure. |
|
|
59
|
+
| `monacori diff` | Generate a browser-based side-by-side diff page. `--watch` serves a live-reloading review. |
|
|
60
|
+
| `monacori app` | Launch the desktop review app (same as `mo`). |
|
|
61
|
+
| `monacori report` | Store a manual report under `.monacori/reports/`. |
|
|
62
|
+
| `monacori status` | Print branch, git status, diff stat, verification commands, and recent reports/logs. |
|
|
104
63
|
|
|
105
|
-
|
|
106
|
-
monacori check [--include-untracked] [--staged] [--base HEAD] [--context 12] [--open] [--no-verify] [--no-diff] [-- <command>]
|
|
107
|
-
```
|
|
64
|
+
## Repository state
|
|
108
65
|
|
|
109
|
-
|
|
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
|
-
```
|
|
144
|
-
|
|
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:
|
|
66
|
+
`monacori init` (run automatically by `mo`) creates:
|
|
156
67
|
|
|
157
68
|
```text
|
|
158
69
|
.monacori/
|
|
159
|
-
config.json
|
|
160
|
-
state.md
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
logs/ verification logs
|
|
70
|
+
config.json verification commands and diff defaults
|
|
71
|
+
state.md compact validation history
|
|
72
|
+
diffs/ generated browser diff reviews
|
|
73
|
+
reports/ validation reports
|
|
74
|
+
logs/ verification logs
|
|
165
75
|
```
|
|
166
76
|
|
|
167
|
-
`monacori install` also writes `.monacori/agent-snippet.md` for projects that want to paste or apply agent-facing validation instructions.
|
|
168
|
-
|
|
169
77
|
Keep `.monacori/` ignored unless your team explicitly wants to commit validation state.
|
|
170
78
|
|
|
171
|
-
## Design
|
|
79
|
+
## Design principles
|
|
172
80
|
|
|
173
81
|
- Verification evidence beats chat memory.
|
|
174
|
-
- Generated artifacts
|
|
175
|
-
-
|
|
82
|
+
- Generated artifacts are plain Markdown, JSON, logs, or static HTML.
|
|
83
|
+
- No required AI agent, terminal multiplexer, editor, or worktree strategy.
|
|
176
84
|
- The default command should be useful after any AI-generated edit.
|
|
177
|
-
- A change is not accepted until
|
|
85
|
+
- A change is not accepted until the evidence is clear or the gap is documented.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
Binary file
|
|
Binary file
|
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,100 @@ 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
|
+
t.onData((data) => deliver("monacori:pty-data", { id, data }));
|
|
75
|
+
t.onExit(() => { terms.delete(id); deliver("monacori:pty-exit", { id }); });
|
|
76
|
+
return { ok: true, id };
|
|
77
|
+
});
|
|
78
|
+
ipcMain.on("monacori:pty-write", (_event, msg) => { terms.get(msg?.id)?.write(msg.data); });
|
|
79
|
+
ipcMain.on("monacori:pty-resize", (_event, msg) => {
|
|
80
|
+
try {
|
|
81
|
+
terms.get(msg?.id)?.resize(msg.cols, msg.rows);
|
|
82
|
+
}
|
|
83
|
+
catch { /* resize can race the pty teardown — ignore */ }
|
|
84
|
+
});
|
|
85
|
+
ipcMain.on("monacori:pty-kill", (_event, msg) => {
|
|
86
|
+
const t = terms.get(msg?.id);
|
|
87
|
+
if (t) {
|
|
88
|
+
try {
|
|
89
|
+
t.kill();
|
|
90
|
+
}
|
|
91
|
+
catch { /* already exited */ }
|
|
92
|
+
terms.delete(msg.id);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// Persisted global settings (locale, …) live in a JSON file under userData and reach the renderer
|
|
96
|
+
// via preload + the two handlers below. The renderer's file:// localStorage is NOT reliably persisted
|
|
97
|
+
// across app restarts, so settings that must survive a reopen round-trip through the main process.
|
|
98
|
+
function settingsFile() {
|
|
99
|
+
return join(app.getPath("userData"), "monacori-settings.json");
|
|
100
|
+
}
|
|
101
|
+
function readSettings() {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(readFileSync(settingsFile(), "utf8"));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function writeSettings(settings) {
|
|
110
|
+
try {
|
|
111
|
+
writeFileSync(settingsFile(), JSON.stringify(settings, null, 2));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* best-effort: a failed write just means the setting isn't persisted */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
ipcMain.on("monacori:get-settings", (event) => {
|
|
118
|
+
event.returnValue = readSettings();
|
|
119
|
+
});
|
|
120
|
+
ipcMain.on("monacori:set-setting", (_event, msg) => {
|
|
121
|
+
if (!msg || typeof msg.key !== "string")
|
|
122
|
+
return;
|
|
123
|
+
const settings = readSettings();
|
|
124
|
+
settings[msg.key] = msg.value;
|
|
125
|
+
writeSettings(settings);
|
|
126
|
+
});
|
|
21
127
|
const iconPath = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "icon.png");
|
|
22
128
|
const preloadPath = join(dirname(fileURLToPath(import.meta.url)), "preload.cjs");
|
|
23
129
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -38,13 +144,13 @@ app.whenReady().then(async () => {
|
|
|
38
144
|
if (process.platform === "darwin")
|
|
39
145
|
menuTemplate.push({ role: "appMenu" });
|
|
40
146
|
menuTemplate.push({ role: "editMenu" });
|
|
41
|
-
//
|
|
42
|
-
//
|
|
147
|
+
// Ctrl+Cmd+Shift+/ ("?") and Ctrl+Cmd+Shift+. (">") open the merged question / change-request views.
|
|
148
|
+
// ? and > are Shift+/ and Shift+. so Shift is part of the combo; Ctrl+Cmd avoids macOS's Cmd+? Help grab.
|
|
43
149
|
menuTemplate.push({
|
|
44
150
|
label: "Review",
|
|
45
151
|
submenu: [
|
|
46
|
-
{ label: "All questions", accelerator: "
|
|
47
|
-
{ label: "All change requests", accelerator: "
|
|
152
|
+
{ label: "All questions", accelerator: "Control+Command+Shift+/", click: () => sendMerged("q") },
|
|
153
|
+
{ label: "All change requests", accelerator: "Control+Command+Shift+.", click: () => sendMerged("c") },
|
|
48
154
|
{ type: "separator" },
|
|
49
155
|
// Whitespace-ignore re-runs git diff with --ignore-all-space and reloads (main-process action,
|
|
50
156
|
// so a menu checkbox is simpler than a renderer IPC round-trip).
|
|
@@ -61,14 +167,37 @@ app.whenReady().then(async () => {
|
|
|
61
167
|
},
|
|
62
168
|
],
|
|
63
169
|
});
|
|
64
|
-
|
|
170
|
+
// Cmd/Ctrl+W closes the active Files-mode tab (routed to the renderer) instead of the window, matching
|
|
171
|
+
// editor/browser tab behavior. Closing the window stays available via the menu item and Cmd/Ctrl+Q.
|
|
172
|
+
menuTemplate.push({
|
|
173
|
+
label: "Window",
|
|
174
|
+
submenu: [
|
|
175
|
+
{ role: "minimize" },
|
|
176
|
+
{ role: "zoom" },
|
|
177
|
+
{ type: "separator" },
|
|
178
|
+
{ label: "Close Tab", accelerator: "CommandOrControl+W", click: () => mainWindow?.webContents.send("monacori:close-tab") },
|
|
179
|
+
{ label: "Close Window", click: () => mainWindow?.close() },
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
// Terminal toggle/split as menu accelerators: Chromium swallows Cmd+D before it reaches the renderer
|
|
183
|
+
// (Cmd+A and friends arrive fine), so route the split — and the toggles — through the menu instead.
|
|
184
|
+
menuTemplate.push({
|
|
185
|
+
label: "Terminal",
|
|
186
|
+
submenu: [
|
|
187
|
+
{ label: "Toggle Terminal", accelerator: "Control+`", click: () => mainWindow?.webContents.send("monacori:terminal-toggle") },
|
|
188
|
+
{ label: "Toggle Terminal (F12)", accelerator: "Alt+F12", click: () => mainWindow?.webContents.send("monacori:terminal-toggle") },
|
|
189
|
+
{ label: "Split Terminal", accelerator: "CommandOrControl+D", click: () => mainWindow?.webContents.send("monacori:terminal-split") },
|
|
190
|
+
{ type: "separator" },
|
|
191
|
+
{ label: "Focus Previous Pane", accelerator: "CommandOrControl+Alt+[", click: () => mainWindow?.webContents.send("monacori:terminal-pane-focus", -1) },
|
|
192
|
+
{ label: "Focus Next Pane", accelerator: "CommandOrControl+Alt+]", click: () => mainWindow?.webContents.send("monacori:terminal-pane-focus", 1) },
|
|
193
|
+
{ label: "Rename Pane", accelerator: "F2", click: () => mainWindow?.webContents.send("monacori:terminal-pane-rename") },
|
|
194
|
+
],
|
|
195
|
+
});
|
|
65
196
|
Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
|
|
66
197
|
const appIcon = nativeImage.createFromPath(iconPath);
|
|
67
198
|
if (process.platform === "darwin" && app.dock && !appIcon.isEmpty()) {
|
|
68
199
|
app.dock.setIcon(appIcon);
|
|
69
200
|
}
|
|
70
|
-
const firstBuild = writeReviewFile(options);
|
|
71
|
-
currentSignature = firstBuild.signature;
|
|
72
201
|
mainWindow = new BrowserWindow({
|
|
73
202
|
width: 1440,
|
|
74
203
|
height: 960,
|
|
@@ -89,10 +218,24 @@ app.whenReady().then(async () => {
|
|
|
89
218
|
});
|
|
90
219
|
mainWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
91
220
|
mainWindow.once("ready-to-show", () => mainWindow?.show());
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
221
|
+
// Paint the window with a spinner immediately, then build the (potentially heavy) review off the first
|
|
222
|
+
// paint and swap it in. The first build used to run synchronously *before* the window existed, so the
|
|
223
|
+
// screen stayed blank for the first few seconds of startup; now the user sees a loading screen instead.
|
|
224
|
+
await mainWindow.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(LOADING_HTML));
|
|
225
|
+
setImmediate(() => {
|
|
226
|
+
try {
|
|
227
|
+
const firstBuild = writeReviewFile(options);
|
|
228
|
+
currentSignature = firstBuild.signature;
|
|
229
|
+
if (mainWindow && !mainWindow.isDestroyed())
|
|
230
|
+
void mainWindow.loadFile(reviewPath());
|
|
231
|
+
if (options.watch)
|
|
232
|
+
refreshTimer = setInterval(refreshIfChanged, WATCH_INTERVAL_MS);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
236
|
+
app.quit();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
96
239
|
}).catch((error) => {
|
|
97
240
|
console.error(error instanceof Error ? error.message : String(error));
|
|
98
241
|
app.quit();
|
|
@@ -100,6 +243,13 @@ app.whenReady().then(async () => {
|
|
|
100
243
|
app.on("window-all-closed", () => {
|
|
101
244
|
if (refreshTimer)
|
|
102
245
|
clearInterval(refreshTimer);
|
|
246
|
+
for (const t of terms.values()) {
|
|
247
|
+
try {
|
|
248
|
+
t.kill();
|
|
249
|
+
}
|
|
250
|
+
catch { /* already exited */ }
|
|
251
|
+
}
|
|
252
|
+
terms.clear();
|
|
103
253
|
app.quit();
|
|
104
254
|
});
|
|
105
255
|
async function refreshIfChanged() {
|
|
@@ -129,6 +279,7 @@ function writeReviewFile(input) {
|
|
|
129
279
|
title: "monacori",
|
|
130
280
|
ignoreWhitespace: input.ignoreWhitespace,
|
|
131
281
|
lazyLoad: true, // Electron streams per-file bodies/source over IPC (monacori:get-file / get-source)
|
|
282
|
+
app: true, // gate the integrated terminal (xterm) into the HTML — Electron only
|
|
132
283
|
});
|
|
133
284
|
writeFileSync(reviewPath(), build.html);
|
|
134
285
|
currentBodies = build.lazyBodies ?? [];
|
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
|
@@ -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,6 +61,7 @@ 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
|
});
|
package/dist/diff.js
CHANGED
|
@@ -174,6 +174,40 @@ function imageMimeForPath(path) {
|
|
|
174
174
|
default: return null;
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
+
// Working-tree git status per path (git status --porcelain) for IntelliJ-style sidebar coloring:
|
|
178
|
+
// untracked => "new" (red), index/staged change => "staged" (green, git add'd), unstaged worktree
|
|
179
|
+
// change => "edited" (blue). "git add까지 되었으면" the index column wins, so staged > new/edited.
|
|
180
|
+
function gitStatusMap(cwd) {
|
|
181
|
+
const map = new Map();
|
|
182
|
+
let out = "";
|
|
183
|
+
try {
|
|
184
|
+
out = git(cwd, ["status", "--porcelain"]);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return map;
|
|
188
|
+
}
|
|
189
|
+
for (const line of out.split(/\r?\n/)) {
|
|
190
|
+
if (line.length < 3)
|
|
191
|
+
continue;
|
|
192
|
+
const x = line[0];
|
|
193
|
+
const y = line[1];
|
|
194
|
+
let path = line.slice(3);
|
|
195
|
+
const arrow = path.indexOf(" -> ");
|
|
196
|
+
if (arrow >= 0)
|
|
197
|
+
path = path.slice(arrow + 4); // rename: color the new path
|
|
198
|
+
if (path.startsWith('"') && path.endsWith('"'))
|
|
199
|
+
path = path.slice(1, -1);
|
|
200
|
+
let kind;
|
|
201
|
+
if (x === "?" && y === "?")
|
|
202
|
+
kind = "new";
|
|
203
|
+
else if (x !== " " && x !== "?")
|
|
204
|
+
kind = "staged";
|
|
205
|
+
else
|
|
206
|
+
kind = "edited";
|
|
207
|
+
map.set(path, kind);
|
|
208
|
+
}
|
|
209
|
+
return map;
|
|
210
|
+
}
|
|
177
211
|
export function collectSourceFiles(diffFiles) {
|
|
178
212
|
const changed = new Set(diffFiles
|
|
179
213
|
.map((file) => file.displayPath)
|
|
@@ -191,6 +225,12 @@ export function collectSourceFiles(diffFiles) {
|
|
|
191
225
|
}
|
|
192
226
|
changedLinesByPath.set(file.displayPath, nums);
|
|
193
227
|
}
|
|
228
|
+
const vcsByPath = gitStatusMap(process.cwd());
|
|
229
|
+
for (const file of diffFiles) {
|
|
230
|
+
const kind = vcsByPath.get(file.displayPath);
|
|
231
|
+
if (kind)
|
|
232
|
+
file.vcs = kind; // color the Changes list from the same status map
|
|
233
|
+
}
|
|
194
234
|
const paths = new Set();
|
|
195
235
|
const gitFiles = git(process.cwd(), ["ls-files", "--cached", "--others", "--exclude-standard"]);
|
|
196
236
|
for (const file of gitFiles.split(/\r?\n/)) {
|
|
@@ -219,6 +259,7 @@ export function collectSourceFiles(diffFiles) {
|
|
|
219
259
|
embedded: false,
|
|
220
260
|
changedLines: changedLinesByPath.get(path) || [],
|
|
221
261
|
signature: "",
|
|
262
|
+
vcs: vcsByPath.get(path),
|
|
222
263
|
};
|
|
223
264
|
if (!existsSync(absolute)) {
|
|
224
265
|
const skippedReason = "file is not present in the working tree";
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MESSAGES: Record<string, Record<string, string>>;
|