@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 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
- `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/`
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" claims. `monacori` keeps the review artifacts in the repository so the state can be inspected, rerun, and discussed.
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 intended loop is simple:
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
+ ![Diff review](assets/screenshots/diff-review.png)
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 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:
29
+ After install, the short command is `mo`. A Homebrew tap (`happy-nut/monacori/monacori`) is also available.
42
30
 
43
- ```bash
44
- brew install happy-nut/monacori/monacori
45
- ```
31
+ ## What you get
46
32
 
47
- For local development:
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
- ```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
- ```
38
+ ![Integrated terminal](assets/screenshots/terminal.png)
39
+ *Run your AI CLI inside the review app, next to the diff it just produced.*
56
40
 
57
- ## Quick Start
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
- ## 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.
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
- ```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.
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
- ```bash
106
- monacori check [--include-untracked] [--staged] [--base HEAD] [--context 12] [--open] [--no-verify] [--no-diff] [-- <command>]
107
- ```
64
+ ## Repository state
108
65
 
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
- ```
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 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
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 Principles
79
+ ## Design principles
172
80
 
173
81
  - 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.
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 verification evidence is clear or the gap is explicitly documented.
85
+ - A change is not accepted until the evidence is clear or the gap is documented.
86
+
87
+ ## License
88
+
89
+ MIT
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
- // 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.
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: "CommandOrControl+Shift+/", click: () => sendMerged("q") },
47
- { label: "All change requests", accelerator: "CommandOrControl+Shift+.", click: () => sendMerged("c") },
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
- menuTemplate.push({ role: "windowMenu" });
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
- await mainWindow.loadFile(reviewPath());
93
- if (options.watch) {
94
- refreshTimer = setInterval(refreshIfChanged, WATCH_INTERVAL_MS);
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
@@ -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
@@ -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,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>>;