@happy-nut/monacori 0.1.3 → 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 +14 -48
- 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 +261 -28
- 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/dist/i18n.js
CHANGED
|
@@ -94,6 +94,7 @@ export const MESSAGES = {
|
|
|
94
94
|
"kbd.allQuestionsChanges": "All questions / changes",
|
|
95
95
|
"kbd.ignoreWhitespace": "Ignore whitespace",
|
|
96
96
|
"kbd.saveComment": "Save comment",
|
|
97
|
+
"kbd.promptMemo": "Prompt memo",
|
|
97
98
|
"kbd.toggleTerminal": "Toggle terminal",
|
|
98
99
|
"kbd.splitPane": "Split pane",
|
|
99
100
|
"kbd.focusPane": "Focus prev / next pane",
|
|
@@ -127,6 +128,10 @@ export const MESSAGES = {
|
|
|
127
128
|
"merged.close": "Close",
|
|
128
129
|
"merged.qHeading": "# Questions",
|
|
129
130
|
"merged.cHeading": "# Change requests",
|
|
131
|
+
// Prompt memo (Cmd/Ctrl+Shift+N) — a single freeform Markdown scratchpad with a live split preview.
|
|
132
|
+
"memo.title": "Prompt memo",
|
|
133
|
+
"memo.placeholder": "Jot down what you're planning, in Markdown…",
|
|
134
|
+
"memo.previewEmpty": "Markdown preview shows up here as you type.",
|
|
130
135
|
// Merge-prompt default agent contracts (these follow the locale — a Korean user gets Korean defaults)
|
|
131
136
|
"mergePrompt.default.q": "The following are questions about code you just wrote. Answer each one — explain the intent, rationale, or context. Do not change any code; this clarifies understanding before any revisions.",
|
|
132
137
|
"mergePrompt.default.c": "The following are change requests for code you just wrote. For each, edit the code at the quoted location to satisfy the request. Keep changes minimal and focused; do not make unrelated edits.",
|
|
@@ -215,6 +220,7 @@ export const MESSAGES = {
|
|
|
215
220
|
"kbd.allQuestionsChanges": "전체 질문 / 변경요청",
|
|
216
221
|
"kbd.ignoreWhitespace": "공백 무시",
|
|
217
222
|
"kbd.saveComment": "코멘트 저장",
|
|
223
|
+
"kbd.promptMemo": "프롬프트 메모",
|
|
218
224
|
"kbd.toggleTerminal": "터미널 토글",
|
|
219
225
|
"kbd.splitPane": "패널 분할",
|
|
220
226
|
"kbd.focusPane": "이전 / 다음 패널로 이동",
|
|
@@ -249,6 +255,10 @@ export const MESSAGES = {
|
|
|
249
255
|
// Structural markers stay English in both locales (the preamble prose below follows the locale).
|
|
250
256
|
"merged.qHeading": "# Questions",
|
|
251
257
|
"merged.cHeading": "# Change requests",
|
|
258
|
+
// 프롬프트 메모 (Cmd/Ctrl+Shift+N) — 라이브 분할 미리보기가 있는 자유 형식 마크다운 메모 한 장.
|
|
259
|
+
"memo.title": "프롬프트 메모",
|
|
260
|
+
"memo.placeholder": "구상 중인 것을 마크다운으로 적어 보세요…",
|
|
261
|
+
"memo.previewEmpty": "입력하면 여기에 마크다운 미리보기가 나타납니다.",
|
|
252
262
|
// Merge-prompt default agent contracts (Korean default for Korean users)
|
|
253
263
|
"mergePrompt.default.q": "다음은 방금 작성한 코드에 대한 질문입니다. 각 질문에 답하면서 의도, 근거, 맥락을 설명하세요. 코드는 변경하지 마세요. 이 단계는 수정에 앞서 이해를 명확히 하기 위한 것입니다.",
|
|
254
264
|
"mergePrompt.default.c": "다음은 방금 작성한 코드에 대한 변경 요청입니다. 각 요청에 대해 인용된 위치의 코드를 수정하여 요구사항을 충족하세요. 변경은 최소한으로 집중해서 하고, 관련 없는 수정은 하지 마세요.",
|
package/dist/preload.cjs
CHANGED
|
@@ -13,6 +13,15 @@ electron_1.contextBridge.exposeInMainWorld("monacoriMenu", {
|
|
|
13
13
|
onMergedView: (cb) => {
|
|
14
14
|
electron_1.ipcRenderer.on("monacori:merged-view", (_event, kind) => cb(kind));
|
|
15
15
|
},
|
|
16
|
+
// Review menu's Cmd/Ctrl+Shift+N -> open/close the prompt memo in the renderer.
|
|
17
|
+
onOpenMemo: (cb) => {
|
|
18
|
+
electron_1.ipcRenderer.on("monacori:open-memo", () => cb());
|
|
19
|
+
},
|
|
20
|
+
// Electron watch: main pushes the rebuilt review HTML so the renderer refreshes the diff in place
|
|
21
|
+
// (no window reload), keeping the integrated terminal's pty sessions alive.
|
|
22
|
+
onDiffUpdate: (cb) => {
|
|
23
|
+
electron_1.ipcRenderer.on("monacori:diff-update", (_event, html) => cb(html));
|
|
24
|
+
},
|
|
16
25
|
// Cmd/Ctrl+W from the Window menu -> close the active Files-mode tab in the renderer.
|
|
17
26
|
onCloseTab: (cb) => {
|
|
18
27
|
electron_1.ipcRenderer.on("monacori:close-tab", () => cb());
|
package/dist/render.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ export declare function splitDiffForLazy(diffHtml: string, files: DiffFile[]): {
|
|
|
6
6
|
islands: string;
|
|
7
7
|
bodies: string[];
|
|
8
8
|
};
|
|
9
|
+
export declare function renderReviewStatus(input: {
|
|
10
|
+
files: number;
|
|
11
|
+
hunks: number;
|
|
12
|
+
embeddedFiles: number;
|
|
13
|
+
sourceFileCount: number;
|
|
14
|
+
ignoreWhitespace?: boolean;
|
|
15
|
+
watch?: boolean;
|
|
16
|
+
generatedAt?: string;
|
|
17
|
+
}): string;
|
|
9
18
|
export declare function renderDiffHtml(input: {
|
|
10
19
|
files: DiffFile[];
|
|
11
20
|
diffHtml: string;
|
|
@@ -25,6 +34,8 @@ export declare function renderDiffHtml(input: {
|
|
|
25
34
|
signature?: string;
|
|
26
35
|
generatedAt?: string;
|
|
27
36
|
}): string;
|
|
37
|
+
export declare function renderDiffTree(files: DiffFile[]): string;
|
|
38
|
+
export declare function renderSourceTree(files: SourceFile[]): string;
|
|
28
39
|
export declare function diffSubtitle(options: {
|
|
29
40
|
base?: string;
|
|
30
41
|
staged: boolean;
|
package/dist/render.js
CHANGED
|
@@ -82,6 +82,11 @@ export function splitDiffForLazy(diffHtml, files) {
|
|
|
82
82
|
});
|
|
83
83
|
return { container: shells.join("\n"), islands: islands.join("\n"), bodies };
|
|
84
84
|
}
|
|
85
|
+
// The toolbar's review-status row (file/hunk counts, index + live status). Extracted so the in-place
|
|
86
|
+
// update path can re-render just this strip; renderDiffHtml wraps it in <div class="review-status">.
|
|
87
|
+
export function renderReviewStatus(input) {
|
|
88
|
+
return `<span>${input.files} <span data-i18n="status.files">files</span></span><span>${input.hunks} <span data-i18n="status.hunks">hunks</span></span>${input.ignoreWhitespace ? '<span class="ws-ignored" data-i18n="status.wsIgnored" data-i18n-title="status.wsIgnored.title" title="Whitespace ignored — Cmd/Ctrl+Shift+W">ws ignored</span>' : ""}<span class="index-status" id="index-status" data-i18n-title="status.index.title" title="Go-to-definition index">${input.embeddedFiles}/${input.sourceFileCount} indexed</span><span class="index-progress hidden" id="index-progress" aria-hidden="true"><span class="index-progress-bar"></span></span><span class="live-status ${input.watch ? "watching" : ""}" id="live-status"${input.watch ? ' data-i18n="status.watching"' : ""}>${input.watch ? "watching" : escapeHtml(input.generatedAt ?? new Date().toISOString())}</span>`;
|
|
89
|
+
}
|
|
85
90
|
export function renderDiffHtml(input) {
|
|
86
91
|
const totalHunks = input.files.reduce((sum, file) => sum + file.hunks.length, 0);
|
|
87
92
|
const fileNav = renderDiffTree(input.files);
|
|
@@ -102,6 +107,8 @@ export function renderDiffHtml(input) {
|
|
|
102
107
|
"</style>",
|
|
103
108
|
"</head>",
|
|
104
109
|
"<body>",
|
|
110
|
+
// Boot overlay (removed by the renderer once bootstrap has painted) covers the blank gap after loadFile.
|
|
111
|
+
'<div id="boot-overlay"><div class="boot-spinner"></div><div>monacori</div></div>',
|
|
105
112
|
'<aside class="sidebar" aria-label="Review navigation">',
|
|
106
113
|
'<div class="sidebar-scroll">',
|
|
107
114
|
`<div class="sidebar-brand" title="${escapeAttr(input.projectPath)}"><span class="brand-mark">monacori</span><span class="brand-project">${escapeHtml(input.projectName)}</span></div>`,
|
|
@@ -122,7 +129,7 @@ export function renderDiffHtml(input) {
|
|
|
122
129
|
'<section id="diff-view" class="hidden">',
|
|
123
130
|
'<div class="toolbar">',
|
|
124
131
|
'<div class="breadcrumb" id="diff-breadcrumb"></div>',
|
|
125
|
-
`<div class="review-status"
|
|
132
|
+
`<div class="review-status">${renderReviewStatus({ files: input.files.length, hunks: totalHunks, embeddedFiles, sourceFileCount: input.sourceFiles.length, ignoreWhitespace: input.ignoreWhitespace, watch: input.watch, generatedAt: input.generatedAt })}</div>`,
|
|
126
133
|
'<button type="button" id="diff-viewed-toggle" class="diff-viewed-toggle" aria-pressed="false" data-i18n="btn.viewed" data-i18n-title="btn.viewed.title" title="Toggle viewed (<)" hidden>Viewed</button>',
|
|
127
134
|
"</div>",
|
|
128
135
|
`<div id="diff2html-container" class="diff2html-container">${input.diffHtml || '<div class="empty" data-i18n="diff.noDiff">No diff to review.</div>'}</div>`,
|
|
@@ -194,13 +201,14 @@ export function renderDiffHtml(input) {
|
|
|
194
201
|
'<kbd>Cmd/Ctrl+Shift+/ .</kbd><span data-i18n="kbd.allQuestionsChanges">All questions / changes</span>' +
|
|
195
202
|
'<kbd>Cmd/Ctrl+Shift+W</kbd><span data-i18n="kbd.ignoreWhitespace">Ignore whitespace</span>' +
|
|
196
203
|
'<kbd>Cmd/Ctrl+Enter</kbd><span data-i18n="kbd.saveComment">Save comment</span>' +
|
|
204
|
+
'<kbd>Cmd/Ctrl+Shift+N</kbd><span data-i18n="kbd.promptMemo">Prompt memo</span>' +
|
|
197
205
|
'</div>' +
|
|
198
206
|
'<div class="keys-cat" data-i18n="settings.kbd.cat.terminal">Terminal</div>' +
|
|
199
207
|
'<div class="keys-grid">' +
|
|
200
208
|
'<kbd>Ctrl+`</kbd><span data-i18n="kbd.toggleTerminal">Toggle terminal</span>' +
|
|
201
209
|
'<kbd>Cmd/Ctrl+D</kbd><span data-i18n="kbd.splitPane">Split pane</span>' +
|
|
202
210
|
'<kbd>Cmd/Ctrl+Alt+[ / ]</kbd><span data-i18n="kbd.focusPane">Focus prev / next pane</span>' +
|
|
203
|
-
'<kbd>
|
|
211
|
+
'<kbd>Cmd/Ctrl+Alt+R</kbd><span data-i18n="kbd.renamePane">Rename pane</span>' +
|
|
204
212
|
'<kbd>Cmd/Ctrl+W</kbd><span data-i18n="kbd.closeTerminal">Close terminal (when focused)</span>' +
|
|
205
213
|
'</div>' +
|
|
206
214
|
'</div>',
|
|
@@ -234,7 +242,7 @@ export function renderDiffHtml(input) {
|
|
|
234
242
|
"</html>",
|
|
235
243
|
].join("\n");
|
|
236
244
|
}
|
|
237
|
-
function renderDiffTree(files) {
|
|
245
|
+
export function renderDiffTree(files) {
|
|
238
246
|
if (files.length === 0) {
|
|
239
247
|
return '<div class="empty-nav">No changed files</div>';
|
|
240
248
|
}
|
|
@@ -266,7 +274,7 @@ function renderDiffTree(files) {
|
|
|
266
274
|
});
|
|
267
275
|
return `<nav class="tree changes-flat">${rows.join("")}</nav>`;
|
|
268
276
|
}
|
|
269
|
-
function renderSourceTree(files) {
|
|
277
|
+
export function renderSourceTree(files) {
|
|
270
278
|
if (files.length === 0) {
|
|
271
279
|
return '<div class="empty-nav">No source files indexed</div>';
|
|
272
280
|
}
|
|
@@ -394,7 +402,7 @@ function renderSourceNode(node, depth) {
|
|
|
394
402
|
}
|
|
395
403
|
return [
|
|
396
404
|
`<details class="tree-dir source-dir" data-dir="${escapeAttr(labelNode.path)}" style="--depth:${depth}">`,
|
|
397
|
-
`<summary><span class="folder-icon"
|
|
405
|
+
`<summary><span class="folder-icon"><svg class="folder-ic fi-closed" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg><svg class="folder-ic fi-open" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 14 1.45-2.9A2 2 0 0 1 9.24 10H21a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg></span><span class="path">${escapeHtml(names.join("/"))}</span></summary>`,
|
|
398
406
|
renderSourceChildren(labelNode, depth + 1),
|
|
399
407
|
"</details>",
|
|
400
408
|
].join("\n");
|
package/dist/server.js
CHANGED
|
@@ -108,6 +108,12 @@ export function serveDiffWatch(input) {
|
|
|
108
108
|
});
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
|
+
// Compact in-place refresh payload — the poller fetches this only when the signature changed.
|
|
112
|
+
if (requestUrl.pathname === "/__ai_flow_update") {
|
|
113
|
+
const latest = lastBuild ?? build();
|
|
114
|
+
writeHttpJson(response, latest.update ?? {});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
111
117
|
if (requestUrl.pathname === "/__http_send" && request.method === "POST") {
|
|
112
118
|
void handleHttpProxy(request, response);
|
|
113
119
|
return;
|
package/dist/types.d.ts
CHANGED
|
@@ -78,12 +78,24 @@ export type DiffReviewResult = {
|
|
|
78
78
|
files: number;
|
|
79
79
|
hunks: number;
|
|
80
80
|
};
|
|
81
|
+
export type DiffReviewUpdate = {
|
|
82
|
+
signature: string;
|
|
83
|
+
generatedAt: string;
|
|
84
|
+
diffContainer: string;
|
|
85
|
+
changesPanel: string;
|
|
86
|
+
filesTree: string;
|
|
87
|
+
reviewStatus: string;
|
|
88
|
+
fileStates: ReviewFileState[];
|
|
89
|
+
sourceFilesMeta: SourceFile[];
|
|
90
|
+
httpEnvironments: Record<string, Record<string, string>>;
|
|
91
|
+
};
|
|
81
92
|
export type DiffReviewBuild = {
|
|
82
93
|
html: string;
|
|
83
94
|
files: number;
|
|
84
95
|
hunks: number;
|
|
85
96
|
signature: string;
|
|
86
97
|
generatedAt: string;
|
|
98
|
+
update?: DiffReviewUpdate;
|
|
87
99
|
lazyBodies?: string[];
|
|
88
100
|
lazySourceData?: string;
|
|
89
101
|
};
|
package/dist/viewer.client.js
CHANGED
|
@@ -113,9 +113,9 @@ function setupLazyDiff() {
|
|
|
113
113
|
if (wrappers[0]) ensureFileReady(wrappers[0]); // first file ready so the initial caret has a row to land on
|
|
114
114
|
}
|
|
115
115
|
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
116
|
-
|
|
116
|
+
let links = Array.from(document.querySelectorAll('#changes-panel .file-link')); // re-captured on in-place diff update
|
|
117
117
|
let sourceLinks = Array.from(document.querySelectorAll('.source-link')); // re-captured when a deferred tree materializes
|
|
118
|
-
|
|
118
|
+
let sourceFiles = JSON.parse(document.getElementById('source-files-data')?.textContent || '[]');
|
|
119
119
|
// i18n: the message catalog (en + ko) is emitted server-side; the locale lives in localStorage and the
|
|
120
120
|
// whole UI switches live (no reload). t() feeds dynamically-built text; applyI18n() rewrites the static
|
|
121
121
|
// chrome (data-i18n / -ph / -title / -aria). English is the first-paint default.
|
|
@@ -147,13 +147,13 @@ function applyI18n() {
|
|
|
147
147
|
var sel = document.getElementById('settings-language');
|
|
148
148
|
if (sel) sel.value = locale;
|
|
149
149
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
let fileStates = JSON.parse(document.getElementById('file-state-data')?.textContent || '[]');
|
|
151
|
+
let httpEnvironments = JSON.parse(document.getElementById('http-env-data')?.textContent || '{}');
|
|
152
|
+
let httpEnvNames = Object.keys(httpEnvironments);
|
|
153
153
|
const httpEnvKey = 'monacori-http-env:' + location.pathname;
|
|
154
154
|
const httpRequestsByPath = new Map();
|
|
155
155
|
const httpVarsByPath = new Map();
|
|
156
|
-
|
|
156
|
+
let sourceByPath = new Map(sourceFiles.map((file) => [file.path, file]));
|
|
157
157
|
// Phase 2b lazy-LOAD: source content is fetched once after first paint (serve /source-data or the
|
|
158
158
|
// Electron bridge) and merged into the metadata-only source records; until then sourceLoaded is false
|
|
159
159
|
// and the source view shows a brief loading state. Non-lazy-load modes embed source -> already loaded.
|
|
@@ -184,16 +184,16 @@ function loadSourceData() {
|
|
|
184
184
|
}
|
|
185
185
|
sourceLoaded = true;
|
|
186
186
|
sourceLoading = false;
|
|
187
|
-
|
|
187
|
+
scheduleSymbolIndex();
|
|
188
188
|
if (pendingSourceOpen) { var po = pendingSourceOpen; pendingSourceOpen = null; openSourceFile(po.path, po.shouldSwitch); }
|
|
189
189
|
else if (isSourceViewerVisible() && document.getElementById('source-viewer').dataset.openPath) { openSourceFile(document.getElementById('source-viewer').dataset.openPath, false); }
|
|
190
190
|
if (pendingSymbol) { var s = pendingSymbol; pendingSymbol = null; goToDefOrUsages(s); }
|
|
191
191
|
}, function () { sourceLoaded = true; sourceLoading = false; });
|
|
192
192
|
}
|
|
193
|
-
|
|
193
|
+
let fileSignatureByPath = new Map(fileStates.map((file) => [file.path, file.signature]));
|
|
194
194
|
const reviewMeta = document.getElementById('review-meta');
|
|
195
195
|
const watchEnabled = reviewMeta?.dataset.watch === 'true';
|
|
196
|
-
|
|
196
|
+
let currentSignature = reviewMeta?.dataset.signature || '';
|
|
197
197
|
const uiStateKey = 'monacori-diff-ui:' + location.pathname;
|
|
198
198
|
const recentKey = 'monacori-diff-recent:' + location.pathname;
|
|
199
199
|
const viewedKey = 'monacori-diff-viewed:' + location.pathname;
|
|
@@ -455,14 +455,26 @@ function scheduleDiffScroll(row) {
|
|
|
455
455
|
});
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
var setActiveRaf = 0, setActiveScrollPending = true;
|
|
458
459
|
function setActive(index, shouldScroll = true) {
|
|
459
460
|
if (hunkTotal() === 0) return;
|
|
460
461
|
current = ((index % hunkTotal()) + hunkTotal()) % hunkTotal();
|
|
462
|
+
// Coalesce rapid presses (holding/spamming F7 or Shift+F7) into one DOM apply per animation frame. The
|
|
463
|
+
// key handler returns immediately and `current` updates synchronously (so next()/nav math stays correct),
|
|
464
|
+
// while the heavy DOM work (full link/wrapper sweeps, body materialize) runs at most once per frame
|
|
465
|
+
// instead of once per keystroke — the input queue never blocks and can't pile up on big repos.
|
|
466
|
+
setActiveScrollPending = shouldScroll;
|
|
467
|
+
if (setActiveRaf) return;
|
|
468
|
+
setActiveRaf = requestAnimationFrame(function () {
|
|
469
|
+
setActiveRaf = 0;
|
|
470
|
+
applySetActive(current, setActiveScrollPending);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function applySetActive(idx, shouldScroll) {
|
|
461
474
|
document.getElementById('source-viewer')?.classList.add('hidden');
|
|
462
475
|
document.getElementById('diff-view')?.classList.remove('hidden');
|
|
463
476
|
setTab('changes');
|
|
464
|
-
const file = hunkPathAt(
|
|
465
|
-
const idx = current;
|
|
477
|
+
const file = hunkPathAt(idx);
|
|
466
478
|
links.forEach((link) => link.classList.toggle('active', link.dataset.file === file));
|
|
467
479
|
renderBreadcrumb(document.getElementById('diff-breadcrumb'), file);
|
|
468
480
|
var dvt = document.getElementById('diff-viewed-toggle');
|
|
@@ -973,6 +985,13 @@ document.addEventListener('keydown', (event) => {
|
|
|
973
985
|
openMergedView((event.code === 'Slash' || event.key === '?') ? 'q' : 'c');
|
|
974
986
|
return;
|
|
975
987
|
}
|
|
988
|
+
// Cmd/Ctrl+Shift+N opens/closes the prompt memo. Electron also routes this via the Review menu; in the
|
|
989
|
+
// browser/serve build (no menu) this keydown is the only path. Match the physical key so layout/IME never swallows it.
|
|
990
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
991
|
+
event.preventDefault();
|
|
992
|
+
openMemoView();
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
976
995
|
// "?" = question, ">" = change-request composer on the current line/selection (no modifier).
|
|
977
996
|
if (!event.altKey && !event.metaKey && !event.ctrlKey && (event.key === '?' || event.key === '>')) {
|
|
978
997
|
const ce = document.activeElement;
|
|
@@ -1215,7 +1234,7 @@ document.addEventListener('copy', handleSourceCopy);
|
|
|
1215
1234
|
|
|
1216
1235
|
applyI18n(); // first paint already shows English (inline); this swaps to the saved locale before the rest of init renders dynamic text
|
|
1217
1236
|
populateHttpEnvSelect();
|
|
1218
|
-
if (!REVIEW_LAZY_LOAD)
|
|
1237
|
+
if (!REVIEW_LAZY_LOAD) scheduleSymbolIndex(); // non-lazy indexes when idle; lazy-LOAD defers the (large) source blob + index to the first source-view open / go-to-def
|
|
1219
1238
|
const restored = restoreUiState();
|
|
1220
1239
|
if (!restored) {
|
|
1221
1240
|
const initial = location.hash.match(/^#hunk-(\d+)$/);
|
|
@@ -1227,6 +1246,19 @@ initSourceTreeFolds();
|
|
|
1227
1246
|
if (watchEnabled) setInterval(checkForLiveUpdate, 1500);
|
|
1228
1247
|
window.addEventListener('beforeunload', saveUiState);
|
|
1229
1248
|
|
|
1249
|
+
// First render has painted — drop the boot overlay (it bridged the blank gap right after loadFile). Two
|
|
1250
|
+
// rAFs so the spinner stays until the diff/tree are actually on screen, then a short fade-out.
|
|
1251
|
+
(function () {
|
|
1252
|
+
var ov = document.getElementById('boot-overlay');
|
|
1253
|
+
if (!ov) return;
|
|
1254
|
+
requestAnimationFrame(function () {
|
|
1255
|
+
requestAnimationFrame(function () {
|
|
1256
|
+
ov.classList.add('hide');
|
|
1257
|
+
setTimeout(function () { ov.remove(); }, 240);
|
|
1258
|
+
});
|
|
1259
|
+
});
|
|
1260
|
+
})();
|
|
1261
|
+
|
|
1230
1262
|
(function setupSidebarResize() {
|
|
1231
1263
|
const resizer = document.querySelector('.sidebar-resizer');
|
|
1232
1264
|
if (!resizer) return;
|
|
@@ -1943,6 +1975,96 @@ function openMergedView(kind) {
|
|
|
1943
1975
|
}
|
|
1944
1976
|
}
|
|
1945
1977
|
|
|
1978
|
+
// Prompt memo (Cmd/Ctrl+Shift+N): one freeform Markdown scratchpad with a live split preview, persisted
|
|
1979
|
+
// across reopens via the same store as comments/locale. "Send to terminal" hands the current draft to the
|
|
1980
|
+
// same pane-pick mode the merged views use, so a half-formed prompt can target any live claude/codex session.
|
|
1981
|
+
var memoKey = 'monacori-memo';
|
|
1982
|
+
function loadMemo() {
|
|
1983
|
+
var v = persistRead(memoKey);
|
|
1984
|
+
if (typeof v === 'string') return v;
|
|
1985
|
+
try { var s = localStorage.getItem(memoKey); return typeof s === 'string' ? s : ''; } catch (e) { return ''; }
|
|
1986
|
+
}
|
|
1987
|
+
function saveMemo(text) { persistSave(memoKey, text || ''); }
|
|
1988
|
+
function renderMemoMd(text) {
|
|
1989
|
+
if (!text || !text.trim()) return '<div class="mc-memo-empty" data-i18n="memo.previewEmpty">' + escapeHtml(t('memo.previewEmpty')) + '</div>';
|
|
1990
|
+
return renderMarkdownBlocks(text).map(function (b) { return b.html; }).join('');
|
|
1991
|
+
}
|
|
1992
|
+
function openMemoView() {
|
|
1993
|
+
var existing = document.getElementById('mc-memo');
|
|
1994
|
+
if (existing) { existing.remove(); return; } // the shortcut toggles: a second press closes the memo
|
|
1995
|
+
var modal = document.createElement('div');
|
|
1996
|
+
modal.id = 'mc-memo';
|
|
1997
|
+
modal.className = 'mc-modal';
|
|
1998
|
+
var panel = document.createElement('div');
|
|
1999
|
+
panel.className = 'mc-modal-panel mc-memo-panel';
|
|
2000
|
+
var head = document.createElement('div');
|
|
2001
|
+
head.className = 'mc-modal-head';
|
|
2002
|
+
var title = document.createElement('span');
|
|
2003
|
+
title.setAttribute('data-i18n', 'memo.title');
|
|
2004
|
+
title.textContent = t('memo.title');
|
|
2005
|
+
var closeBtn = document.createElement('button');
|
|
2006
|
+
closeBtn.type = 'button';
|
|
2007
|
+
closeBtn.className = 'mc-btn mc-ghost';
|
|
2008
|
+
closeBtn.setAttribute('data-i18n', 'merged.close');
|
|
2009
|
+
closeBtn.textContent = t('merged.close');
|
|
2010
|
+
closeBtn.addEventListener('click', function () { modal.remove(); });
|
|
2011
|
+
|
|
2012
|
+
var body = document.createElement('div');
|
|
2013
|
+
body.className = 'mc-memo-body';
|
|
2014
|
+
var area = document.createElement('textarea');
|
|
2015
|
+
area.className = 'mc-modal-text mc-memo-edit';
|
|
2016
|
+
area.spellcheck = false;
|
|
2017
|
+
area.setAttribute('data-i18n-ph', 'memo.placeholder');
|
|
2018
|
+
area.placeholder = t('memo.placeholder');
|
|
2019
|
+
area.value = loadMemo();
|
|
2020
|
+
var preview = document.createElement('div');
|
|
2021
|
+
preview.className = 'md-cell mc-memo-preview';
|
|
2022
|
+
preview.innerHTML = renderMemoMd(area.value);
|
|
2023
|
+
area.addEventListener('input', function () {
|
|
2024
|
+
saveMemo(area.value);
|
|
2025
|
+
preview.innerHTML = renderMemoMd(area.value);
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
// Terminal send: hand the current draft to pane-pick mode (arrows choose the session, Enter sends). Shown
|
|
2029
|
+
// only once a terminal pane exists; enterSendMode reopens the panel if it was closed.
|
|
2030
|
+
var sendBtn = null;
|
|
2031
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.paneCount === 'function' && window.__monacoriTerminal.paneCount() > 0) {
|
|
2032
|
+
sendBtn = document.createElement('button');
|
|
2033
|
+
sendBtn.type = 'button';
|
|
2034
|
+
sendBtn.className = 'mc-btn mc-send-term';
|
|
2035
|
+
sendBtn.setAttribute('data-i18n', 'merged.sendToTerminal');
|
|
2036
|
+
sendBtn.textContent = t('merged.sendToTerminal');
|
|
2037
|
+
sendBtn.addEventListener('click', function () {
|
|
2038
|
+
var text = area.value;
|
|
2039
|
+
modal.remove();
|
|
2040
|
+
window.__monacoriTerminal.enterSendMode(text);
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
head.appendChild(title);
|
|
2045
|
+
if (sendBtn) head.appendChild(sendBtn);
|
|
2046
|
+
head.appendChild(closeBtn);
|
|
2047
|
+
body.appendChild(area);
|
|
2048
|
+
body.appendChild(preview);
|
|
2049
|
+
panel.appendChild(head);
|
|
2050
|
+
panel.appendChild(body);
|
|
2051
|
+
modal.appendChild(panel);
|
|
2052
|
+
modal.addEventListener('mousedown', function (e) { if (e.target === modal) modal.remove(); });
|
|
2053
|
+
modal.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); modal.remove(); } });
|
|
2054
|
+
document.body.appendChild(modal);
|
|
2055
|
+
// Focus the editor; Electron async-restores focus to <body>, so retry briefly (same as the composer/merged view).
|
|
2056
|
+
var memoFocusTries = 0;
|
|
2057
|
+
var tryFocusMemo = function () {
|
|
2058
|
+
if (!document.getElementById('mc-memo')) return true;
|
|
2059
|
+
if (document.activeElement === area) return true;
|
|
2060
|
+
try { area.focus(); } catch (e) {}
|
|
2061
|
+
return document.activeElement === area;
|
|
2062
|
+
};
|
|
2063
|
+
if (!tryFocusMemo()) {
|
|
2064
|
+
var memoFocusIv = setInterval(function () { if (tryFocusMemo() || ++memoFocusTries > 12) clearInterval(memoFocusIv); }, 25);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1946
2068
|
document.addEventListener('click', function (event) {
|
|
1947
2069
|
var t = event.target;
|
|
1948
2070
|
if (!t || !t.closest) return;
|
|
@@ -2011,7 +2133,11 @@ refreshComments();
|
|
|
2011
2133
|
|
|
2012
2134
|
function setActive(p) {
|
|
2013
2135
|
active = p;
|
|
2014
|
-
panes.forEach(function (q) {
|
|
2136
|
+
panes.forEach(function (q) {
|
|
2137
|
+
q.el.classList.toggle('is-active', q === p);
|
|
2138
|
+
// 2+ panes: dim every pane but the active one (no border, just a clean focus cue). A lone pane stays full.
|
|
2139
|
+
q.el.classList.toggle('is-inactive', panes.length > 1 && q !== p);
|
|
2140
|
+
});
|
|
2015
2141
|
if (p) requestAnimationFrame(function () { try { p.term.focus(); } catch (e) {} });
|
|
2016
2142
|
}
|
|
2017
2143
|
|
|
@@ -2044,7 +2170,13 @@ refreshComments();
|
|
|
2044
2170
|
term.attachCustomKeyEventHandler(function (e) {
|
|
2045
2171
|
if (e.type === 'keydown' && e.metaKey) {
|
|
2046
2172
|
var k = (e.key || '').toLowerCase();
|
|
2047
|
-
|
|
2173
|
+
// The bare modifier press (Cmd goes down BEFORE the letter on macOS) must not blur — blurring
|
|
2174
|
+
// here drops the textarea focus the upcoming Cmd+V paste / Cmd+C copy needs, which broke them.
|
|
2175
|
+
if (k === 'meta' || k === 'control' || k === 'alt' || k === 'shift') return true;
|
|
2176
|
+
// Match the PHYSICAL key (e.code), not e.key: under a non-Latin layout/IME (e.g. Korean 한글)
|
|
2177
|
+
// Cmd+V reports e.key as 'ㅍ', so a key-based check misses it — blurring the terminal and
|
|
2178
|
+
// breaking paste/copy/cut/select-all whenever the Korean input source is active.
|
|
2179
|
+
if (e.code === 'KeyC' || e.code === 'KeyV' || e.code === 'KeyX' || e.code === 'KeyA') return true;
|
|
2048
2180
|
try { term.blur(); } catch (x) {}
|
|
2049
2181
|
return false;
|
|
2050
2182
|
}
|
|
@@ -2067,8 +2199,18 @@ refreshComments();
|
|
|
2067
2199
|
if (el.getAttribute('contenteditable') === 'true') return;
|
|
2068
2200
|
setActive(pane);
|
|
2069
2201
|
el.contentEditable = 'true';
|
|
2070
|
-
|
|
2071
|
-
|
|
2202
|
+
// Electron asynchronously restores focus to <body> after the keydown, so a one-shot focus loses the
|
|
2203
|
+
// race and the label turns editable but never gets the caret — retry until it sticks, then select all
|
|
2204
|
+
// (same pattern as the composer/memo). This is why rename "did nothing" before.
|
|
2205
|
+
var renameTries = 0;
|
|
2206
|
+
var focusLabel = function () {
|
|
2207
|
+
if (el.getAttribute('contenteditable') !== 'true') return true; // finished/cancelled meanwhile
|
|
2208
|
+
try { el.focus(); } catch (e) {}
|
|
2209
|
+
if (document.activeElement !== el) return false;
|
|
2210
|
+
try { var range = document.createRange(); range.selectNodeContents(el); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } catch (e) {}
|
|
2211
|
+
return true;
|
|
2212
|
+
};
|
|
2213
|
+
if (!focusLabel()) { var renameIv = setInterval(function () { if (focusLabel() || ++renameTries > 12) clearInterval(renameIv); }, 25); }
|
|
2072
2214
|
function finish(commit) {
|
|
2073
2215
|
el.removeEventListener('keydown', onKey);
|
|
2074
2216
|
el.removeEventListener('blur', onBlur);
|
|
@@ -2239,6 +2381,15 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onMergedView === 'function
|
|
|
2239
2381
|
// split), so the user can pick which claude/codex session receives the prompt.
|
|
2240
2382
|
window.monacoriMenu.onMergedView(function (kind) { openMergedView(kind); });
|
|
2241
2383
|
}
|
|
2384
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onOpenMemo === 'function') {
|
|
2385
|
+
// Cmd/Ctrl+Shift+N from the Review menu -> open/close the prompt memo.
|
|
2386
|
+
window.monacoriMenu.onOpenMemo(function () { openMemoView(); });
|
|
2387
|
+
}
|
|
2388
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function') {
|
|
2389
|
+
// Electron watch: main rebuilds on working-tree changes and pushes the new HTML so we refresh the diff
|
|
2390
|
+
// in place — NO window reload — keeping the integrated terminal's pty sessions (claude/codex) alive.
|
|
2391
|
+
window.monacoriMenu.onDiffUpdate(function (html) { try { applyDiffUpdate(html); } catch (e) {} });
|
|
2392
|
+
}
|
|
2242
2393
|
if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function') {
|
|
2243
2394
|
// Cmd/Ctrl+W: close the active Files-mode tab (no-op outside the source viewer).
|
|
2244
2395
|
window.monacoriMenu.onCloseTab(function () {
|
|
@@ -2471,6 +2622,71 @@ function restoreUiState() {
|
|
|
2471
2622
|
return false;
|
|
2472
2623
|
}
|
|
2473
2624
|
|
|
2625
|
+
// In-place diff refresh (instead of a full window reload): apply a compact payload of just the changed
|
|
2626
|
+
// regions (diff container, sidebar trees, status, data) and re-run the bootstrap steps. The window never
|
|
2627
|
+
// reloads, so the integrated terminal's pty sessions (claude/codex) survive a watch refresh. Electron's
|
|
2628
|
+
// main pushes the payload over IPC (monacori:diff-update); serve mode's poller fetches /__ai_flow_update.
|
|
2629
|
+
function applyDiffUpdate(u) {
|
|
2630
|
+
if (!u || !u.signature || u.signature === currentSignature) return false; // unchanged — nothing to do
|
|
2631
|
+
|
|
2632
|
+
// Remember what to restore after the swap (comments/viewed persist on their own; these don't).
|
|
2633
|
+
var sv = document.getElementById('source-viewer');
|
|
2634
|
+
var openPath = (sv && sv.dataset.openPath) || '';
|
|
2635
|
+
var wasSource = isSourceViewerVisible();
|
|
2636
|
+
var container = document.getElementById('diff2html-container');
|
|
2637
|
+
var diffScrollTop = container ? container.scrollTop : 0;
|
|
2638
|
+
|
|
2639
|
+
// 1) Replace the visible regions straight from the payload (no full-HTML parse).
|
|
2640
|
+
if (container) container.innerHTML = u.diffContainer || '';
|
|
2641
|
+
var changesPanel = document.getElementById('changes-panel');
|
|
2642
|
+
if (changesPanel) changesPanel.innerHTML = u.changesPanel || '';
|
|
2643
|
+
// Files tree: keep the inert island (lazy, not yet opened) in sync, and refresh the live panel when it's
|
|
2644
|
+
// already materialized — or always, in eager mode where the panel holds the tree directly.
|
|
2645
|
+
var filesIsland = document.getElementById('files-tree-html');
|
|
2646
|
+
if (filesIsland) filesIsland.textContent = u.filesTree || '';
|
|
2647
|
+
var filesPanel = document.getElementById('files-panel');
|
|
2648
|
+
if (filesPanel && (!REVIEW_LAZY || filesPanel.innerHTML.trim())) filesPanel.innerHTML = u.filesTree || '';
|
|
2649
|
+
var statusEl = document.querySelector('.review-status');
|
|
2650
|
+
if (statusEl) statusEl.innerHTML = u.reviewStatus || '';
|
|
2651
|
+
if (reviewMeta) { reviewMeta.setAttribute('data-signature', u.signature); if (u.generatedAt) reviewMeta.setAttribute('data-generated-at', u.generatedAt); }
|
|
2652
|
+
|
|
2653
|
+
// 2) Re-derive module-level state directly from the payload objects.
|
|
2654
|
+
fileStates = u.fileStates || [];
|
|
2655
|
+
fileSignatureByPath = new Map(fileStates.map(function (f) { return [f.path, f.signature]; }));
|
|
2656
|
+
sourceFiles = u.sourceFilesMeta || [];
|
|
2657
|
+
sourceByPath = new Map(sourceFiles.map(function (f) { return [f.path, f]; }));
|
|
2658
|
+
httpEnvironments = u.httpEnvironments || {};
|
|
2659
|
+
httpEnvNames = Object.keys(httpEnvironments);
|
|
2660
|
+
currentSignature = u.signature;
|
|
2661
|
+
links = Array.from(document.querySelectorAll('#changes-panel .file-link'));
|
|
2662
|
+
sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
2663
|
+
|
|
2664
|
+
// 3) Reset lazy-materialize + index state so the new diff bodies / source / symbols rebuild on demand.
|
|
2665
|
+
bodyPromise = {};
|
|
2666
|
+
diffBootDone = false;
|
|
2667
|
+
sourceLoaded = !REVIEW_LAZY_LOAD; // lazyLoad: re-fetch source content on next use
|
|
2668
|
+
sourceLoading = false;
|
|
2669
|
+
symbolIndex = null;
|
|
2670
|
+
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
2671
|
+
else { prepareDiff2HtmlHunks(); diffBootDone = true; }
|
|
2672
|
+
if (!REVIEW_LAZY_LOAD) setTimeout(startSymbolIndex, 0);
|
|
2673
|
+
|
|
2674
|
+
// 4) Re-run the DOM-dependent bootstrap steps.
|
|
2675
|
+
applyI18n();
|
|
2676
|
+
populateHttpEnvSelect();
|
|
2677
|
+
initSourceTreeFolds();
|
|
2678
|
+
refreshComments();
|
|
2679
|
+
|
|
2680
|
+
// 5) Best-effort restore of what the user was looking at.
|
|
2681
|
+
if (wasSource && openPath && sourceByPath.has(openPath)) {
|
|
2682
|
+
openSourceFile(openPath, false);
|
|
2683
|
+
} else if (container) {
|
|
2684
|
+
showDiffView(false);
|
|
2685
|
+
container.scrollTop = diffScrollTop;
|
|
2686
|
+
}
|
|
2687
|
+
return true;
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2474
2690
|
async function checkForLiveUpdate() {
|
|
2475
2691
|
if (checkingForUpdates) return;
|
|
2476
2692
|
checkingForUpdates = true;
|
|
@@ -2483,8 +2699,12 @@ async function checkForLiveUpdate() {
|
|
|
2483
2699
|
liveStatus.textContent = t('status.live.updated') + ' ' + new Date(state.generatedAt).toLocaleTimeString();
|
|
2484
2700
|
}
|
|
2485
2701
|
if (state.signature && state.signature !== currentSignature) {
|
|
2486
|
-
|
|
2487
|
-
|
|
2702
|
+
// serve mode: fetch just the compact update payload and refresh in place (same path Electron uses
|
|
2703
|
+
// over IPC) rather than reloading — so an open integrated terminal keeps its sessions.
|
|
2704
|
+
try {
|
|
2705
|
+
var fresh = await fetch('__ai_flow_update', { cache: 'no-store' });
|
|
2706
|
+
if (fresh.ok) applyDiffUpdate(await fresh.json());
|
|
2707
|
+
} catch (e) {}
|
|
2488
2708
|
}
|
|
2489
2709
|
} catch {
|
|
2490
2710
|
if (liveStatus) liveStatus.textContent = t('status.live.waiting');
|
|
@@ -3104,6 +3324,14 @@ function symbolIndexWorker() {
|
|
|
3104
3324
|
self.postMessage({ index: index, total: total });
|
|
3105
3325
|
};
|
|
3106
3326
|
}
|
|
3327
|
+
// Run symbol indexing off the critical path: requestIdleCallback so the heavy postMessage of the whole
|
|
3328
|
+
// source blob to the worker (structured-clone serialization is synchronous on the main thread) never
|
|
3329
|
+
// competes with key handling — especially on big repos right after the diff/tree first paints.
|
|
3330
|
+
function scheduleSymbolIndex() {
|
|
3331
|
+
var run = function () { try { startSymbolIndex(); } catch (e) {} };
|
|
3332
|
+
if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') window.requestIdleCallback(run, { timeout: 3000 });
|
|
3333
|
+
else setTimeout(run, 0);
|
|
3334
|
+
}
|
|
3107
3335
|
function startSymbolIndex() {
|
|
3108
3336
|
try {
|
|
3109
3337
|
if (typeof Worker === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined' || !URL.createObjectURL) return;
|
|
@@ -3810,16 +4038,21 @@ function populateHttpEnvSelect() {
|
|
|
3810
4038
|
opts += '<option value="' + escapeHtml(name) + '"' + (name === currentHttpEnvName ? ' selected' : '') + '>' + escapeHtml(name) + '</option>';
|
|
3811
4039
|
});
|
|
3812
4040
|
select.innerHTML = opts;
|
|
3813
|
-
select
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
4041
|
+
// The <select> lives in the toolbar (not swapped on in-place diff updates), so wire the change handler
|
|
4042
|
+
// exactly once — populateHttpEnvSelect is re-called by applyDiffUpdate to refresh the options.
|
|
4043
|
+
if (!select.dataset.wired) {
|
|
4044
|
+
select.dataset.wired = '1';
|
|
4045
|
+
select.addEventListener('change', function () {
|
|
4046
|
+
currentHttpEnvName = select.value;
|
|
4047
|
+
try { localStorage.setItem(httpEnvKey, currentHttpEnvName); } catch (error) {}
|
|
4048
|
+
const path = document.getElementById('source-viewer')?.dataset.openPath || '';
|
|
4049
|
+
if (path && isHttpFile(path)) {
|
|
4050
|
+
const file = sourceByPath.get(path);
|
|
4051
|
+
const body = document.getElementById('source-body');
|
|
4052
|
+
if (file && body) body.innerHTML = renderHttpTable(file);
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
3823
4056
|
}
|
|
3824
4057
|
|
|
3825
4058
|
function renderSourceTable(file, query) {
|