@happy-nut/monacori 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -53
- package/assets/monacori-demo.gif +0 -0
- package/dist/app-main.js +47 -7
- package/dist/build.js +23 -1
- package/dist/commands.js +12 -326
- package/dist/i18n.js +10 -0
- package/dist/preload.cjs +9 -0
- package/dist/render.d.ts +11 -0
- package/dist/render.js +13 -5
- package/dist/server.js +6 -0
- package/dist/types.d.ts +12 -0
- package/dist/viewer.client.js +267 -29
- package/dist/viewer.css +56 -7
- package/package.json +1 -1
- package/assets/screenshots/diff-review.png +0 -0
- package/assets/screenshots/terminal.png +0 -0
package/README.md
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
# monacori
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**A local desktop review workspace for AI-generated code changes.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Run `mo` after an AI edits your repository. monacori opens a side-by-side diff, lets you attach line-level questions or change requests, and bundles that feedback back into the AI CLI (command-line interface) session running in the built-in terminal.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+

|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Why monacori
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
AI coding tools are fast, but their "done" message is not a review. monacori gives the human reviewer a dedicated control surface for the gap between generated code and trusted code:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
- See every changed, added, and untracked file in an IntelliJ-style review sidebar.
|
|
14
|
+
- Review side-by-side diffs with syntax highlighting, changed-line emphasis, and keyboard navigation.
|
|
15
|
+
- Leave questions or change requests directly on the relevant line.
|
|
16
|
+
- Send all reviewer comments, with file paths and code context, into `claude`, `codex`, or another terminal session without copy-paste.
|
|
17
|
+
- Keep all generated review state local, plain, and inspectable under `.monacori/`.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
1. Let an AI coding tool make changes in your repository.
|
|
22
|
+
2. Run `mo` from that repository.
|
|
23
|
+
3. Inspect the diff, mark files as viewed, and attach line comments where needed.
|
|
24
|
+
4. Open the built-in terminal and keep your AI CLI session beside the review.
|
|
25
|
+
5. Send the merged questions or change requests back to the session as a focused follow-up prompt.
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
*Local desktop review app: side-by-side diff with changed lines highlighted and IntelliJ-style git status colors in the sidebar.*
|
|
27
|
+
The result is a tighter review loop: the AI produces changes, the human reviews the actual diff, and the next prompt is grounded in exact file and line context.
|
|
22
28
|
|
|
23
29
|
## Install
|
|
24
30
|
|
|
@@ -26,63 +32,61 @@ You inspect the evidence before accepting the change.
|
|
|
26
32
|
npm install -g @happy-nut/monacori
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
The short command is `mo`.
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
Homebrew users can install from the tap as well:
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- **Verification logs & reports** — repeatable verification under `.monacori/logs/` and compact validation reports under `.monacori/reports/`.
|
|
37
|
-
|
|
38
|
-

|
|
39
|
-
*Run your AI CLI inside the review app, next to the diff it just produced.*
|
|
39
|
+
```bash
|
|
40
|
+
brew install happy-nut/monacori/monacori
|
|
41
|
+
```
|
|
40
42
|
|
|
41
|
-
## Quick
|
|
43
|
+
## Quick Start
|
|
42
44
|
|
|
43
|
-
Inside
|
|
45
|
+
Inside any Git repository:
|
|
44
46
|
|
|
45
47
|
```bash
|
|
46
|
-
mo
|
|
47
|
-
monacori check --include-untracked # run verification, write a log, diff, and report
|
|
48
|
+
mo
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
On first run, `mo` creates `.monacori/`, adds it to `.gitignore`, and includes untracked files so new AI-created files
|
|
51
|
+
On first run, `mo` creates `.monacori/`, adds it to `.gitignore`, and includes untracked files so new AI-created files appear immediately.
|
|
52
|
+
|
|
53
|
+
## Highlights
|
|
54
|
+
|
|
55
|
+
- **Desktop diff review**: reads the repository directly, refreshes from local Git state, and does not require a web server.
|
|
56
|
+
- **AI handoff comments**: questions and change requests are stored with their file, line, and code context.
|
|
57
|
+
- **Integrated terminal**: keep `claude`, `codex`, or a shell open inside the same window, with split panes when needed.
|
|
58
|
+
- **Source navigation**: jump between changed files, search indexed files, preview source, and move through hunks from the keyboard.
|
|
59
|
+
- **Plain local artifacts**: generated review files and state are Markdown, JSON, and static HTML under `.monacori/`.
|
|
51
60
|
|
|
52
61
|
## Commands
|
|
53
62
|
|
|
54
63
|
| Command | What it does |
|
|
55
64
|
| --- | --- |
|
|
56
|
-
| `mo` | Open the desktop review app
|
|
57
|
-
| `monacori
|
|
58
|
-
| `monacori
|
|
59
|
-
| `monacori
|
|
60
|
-
| `monacori
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.monacori/
|
|
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
|
|
65
|
+
| `mo` | Open the desktop diff-review app for the current repository. Alias for `monacori open`. |
|
|
66
|
+
| `monacori open` | Launch the review app, auto-initialize `.monacori/`, and include untracked files by default. |
|
|
67
|
+
| `monacori app` | Launch the same desktop app explicitly. |
|
|
68
|
+
| `monacori init` | Initialize `.monacori/` in the current directory. |
|
|
69
|
+
| `monacori install` | Initialize and write agent instruction snippets. Use `--apply-agent-docs` to patch `AGENTS.md` or `CLAUDE.md`. |
|
|
70
|
+
|
|
71
|
+
Useful review options:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
mo --staged # review only staged changes
|
|
75
|
+
mo --tracked-only # exclude untracked files
|
|
76
|
+
mo --base main # compare against a specific base
|
|
77
|
+
mo --context 20 # show more context around each hunk
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
## Local State
|
|
81
|
+
|
|
82
|
+
`monacori init` creates a git-ignored `.monacori/` directory for generated diff reviews, local config, comments, logs, and validation notes. Keep it ignored unless your team intentionally wants to version review artifacts.
|
|
78
83
|
|
|
79
|
-
## Design
|
|
84
|
+
## Design Principles
|
|
80
85
|
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
- A change is not accepted until the evidence is clear or the gap is documented.
|
|
86
|
+
- Real diffs beat chat summaries.
|
|
87
|
+
- Human review should stay close to the code and the running AI session.
|
|
88
|
+
- The core should be local, inspectable, and agent-agnostic.
|
|
89
|
+
- No required terminal multiplexer, editor plugin, hosted service, or worktree strategy.
|
|
86
90
|
|
|
87
91
|
## License
|
|
88
92
|
|
|
Binary file
|
package/dist/app-main.js
CHANGED
|
@@ -71,8 +71,37 @@ ipcMain.handle("monacori:pty-spawn", (_event, size) => {
|
|
|
71
71
|
if (mainWindow && !mainWindow.isDestroyed())
|
|
72
72
|
mainWindow.webContents.send(channel, payload);
|
|
73
73
|
};
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
});
|
|
76
105
|
return { ok: true, id };
|
|
77
106
|
});
|
|
78
107
|
ipcMain.on("monacori:pty-write", (_event, msg) => { terms.get(msg?.id)?.write(msg.data); });
|
|
@@ -151,6 +180,8 @@ app.whenReady().then(async () => {
|
|
|
151
180
|
submenu: [
|
|
152
181
|
{ label: "All questions", accelerator: "Control+Command+Shift+/", click: () => sendMerged("q") },
|
|
153
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") },
|
|
154
185
|
{ type: "separator" },
|
|
155
186
|
// Whitespace-ignore re-runs git diff with --ignore-all-space and reloads (main-process action,
|
|
156
187
|
// so a menu checkbox is simpler than a renderer IPC round-trip).
|
|
@@ -190,7 +221,7 @@ app.whenReady().then(async () => {
|
|
|
190
221
|
{ type: "separator" },
|
|
191
222
|
{ label: "Focus Previous Pane", accelerator: "CommandOrControl+Alt+[", click: () => mainWindow?.webContents.send("monacori:terminal-pane-focus", -1) },
|
|
192
223
|
{ label: "Focus Next Pane", accelerator: "CommandOrControl+Alt+]", click: () => mainWindow?.webContents.send("monacori:terminal-pane-focus", 1) },
|
|
193
|
-
{ label: "Rename Pane", accelerator: "
|
|
224
|
+
{ label: "Rename Pane", accelerator: "CommandOrControl+Alt+R", click: () => mainWindow?.webContents.send("monacori:terminal-pane-rename") },
|
|
194
225
|
],
|
|
195
226
|
});
|
|
196
227
|
Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
|
|
@@ -222,7 +253,10 @@ app.whenReady().then(async () => {
|
|
|
222
253
|
// paint and swap it in. The first build used to run synchronously *before* the window existed, so the
|
|
223
254
|
// screen stayed blank for the first few seconds of startup; now the user sees a loading screen instead.
|
|
224
255
|
await mainWindow.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(LOADING_HTML));
|
|
225
|
-
|
|
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(() => {
|
|
226
260
|
try {
|
|
227
261
|
const firstBuild = writeReviewFile(options);
|
|
228
262
|
currentSignature = firstBuild.signature;
|
|
@@ -235,7 +269,7 @@ app.whenReady().then(async () => {
|
|
|
235
269
|
console.error(error instanceof Error ? error.message : String(error));
|
|
236
270
|
app.quit();
|
|
237
271
|
}
|
|
238
|
-
});
|
|
272
|
+
}, 60);
|
|
239
273
|
}).catch((error) => {
|
|
240
274
|
console.error(error instanceof Error ? error.message : String(error));
|
|
241
275
|
app.quit();
|
|
@@ -260,7 +294,13 @@ async function refreshIfChanged() {
|
|
|
260
294
|
const next = writeReviewFile(options);
|
|
261
295
|
if (next.signature !== currentSignature) {
|
|
262
296
|
currentSignature = next.signature;
|
|
263
|
-
|
|
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);
|
|
264
304
|
}
|
|
265
305
|
}
|
|
266
306
|
catch (error) {
|
|
@@ -284,7 +324,7 @@ function writeReviewFile(input) {
|
|
|
284
324
|
writeFileSync(reviewPath(), build.html);
|
|
285
325
|
currentBodies = build.lazyBodies ?? [];
|
|
286
326
|
currentSourceData = build.lazySourceData ?? "[]";
|
|
287
|
-
return { signature: build.signature };
|
|
327
|
+
return { signature: build.signature, html: build.html, update: build.update };
|
|
288
328
|
}
|
|
289
329
|
function reviewPath() {
|
|
290
330
|
return join(options.root, FLOW_DIR, REVIEW_FILE);
|
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 {
|
|
@@ -65,6 +65,27 @@ export function buildDiffReview(input) {
|
|
|
65
65
|
signature,
|
|
66
66
|
generatedAt,
|
|
67
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
|
+
};
|
|
68
89
|
return {
|
|
69
90
|
html,
|
|
70
91
|
files: files.length,
|
|
@@ -73,5 +94,6 @@ export function buildDiffReview(input) {
|
|
|
73
94
|
generatedAt,
|
|
74
95
|
lazyBodies: diffSplit.bodies,
|
|
75
96
|
lazySourceData: lazyLoad ? JSON.stringify(sourceFiles) : undefined,
|
|
97
|
+
update,
|
|
76
98
|
};
|
|
77
99
|
}
|