@happy-nut/monacori 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -147
- package/dist/app-main.js +205 -14
- package/dist/assets.d.ts +2 -0
- package/dist/assets.js +21 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +30 -5
- package/dist/commands.js +12 -326
- package/dist/diff.js +41 -0
- package/dist/i18n.d.ts +1 -0
- package/dist/i18n.js +266 -0
- package/dist/preload.cjs +70 -0
- package/dist/render.d.ts +12 -0
- package/dist/render.js +102 -22
- package/dist/server.js +6 -0
- package/dist/types.d.ts +14 -0
- package/dist/viewer.client.js +950 -125
- package/dist/viewer.css +248 -44
- package/package.json +6 -2
- package/scripts/patch-electron-name.mjs +8 -0
package/dist/viewer.client.js
CHANGED
|
@@ -113,16 +113,47 @@ 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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
let sourceFiles = JSON.parse(document.getElementById('source-files-data')?.textContent || '[]');
|
|
119
|
+
// i18n: the message catalog (en + ko) is emitted server-side; the locale lives in localStorage and the
|
|
120
|
+
// whole UI switches live (no reload). t() feeds dynamically-built text; applyI18n() rewrites the static
|
|
121
|
+
// chrome (data-i18n / -ph / -title / -aria). English is the first-paint default.
|
|
122
|
+
var I18N = JSON.parse(document.getElementById('i18n-data')?.textContent || '{}');
|
|
123
|
+
// Cross-reopen persistence. Electron persists via the main process (window.monacoriSettings — survives
|
|
124
|
+
// app restart; file:// localStorage doesn't); browser/serve falls back to localStorage. persistRead
|
|
125
|
+
// returns the bridge value (native) if present, else undefined so callers parse localStorage themselves.
|
|
126
|
+
function persistRead(key) {
|
|
127
|
+
try { if (window.monacoriSettings && window.monacoriSettings.all && key in window.monacoriSettings.all) return window.monacoriSettings.all[key]; } catch (e) {}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
function persistSave(key, value) {
|
|
131
|
+
try { localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); } catch (e) {}
|
|
132
|
+
try { if (window.monacoriSettings) window.monacoriSettings.set(key, value); } catch (e2) {}
|
|
133
|
+
}
|
|
134
|
+
var LOCALE_KEY = 'monacori-locale';
|
|
135
|
+
var locale = (function () {
|
|
136
|
+
var v = persistRead(LOCALE_KEY);
|
|
137
|
+
if (v !== 'ko' && v !== 'en') { try { v = localStorage.getItem(LOCALE_KEY); } catch (e) {} }
|
|
138
|
+
return (v === 'ko' || v === 'en') ? v : 'en';
|
|
139
|
+
})();
|
|
140
|
+
function t(key) { var m = (I18N[locale] || I18N.en || {}); return (m && key in m) ? m[key] : ((I18N.en && I18N.en[key]) || key); }
|
|
141
|
+
function applyI18n() {
|
|
142
|
+
document.querySelectorAll('[data-i18n]').forEach(function (el) { el.textContent = t(el.getAttribute('data-i18n')); });
|
|
143
|
+
document.querySelectorAll('[data-i18n-ph]').forEach(function (el) { el.setAttribute('placeholder', t(el.getAttribute('data-i18n-ph'))); });
|
|
144
|
+
document.querySelectorAll('[data-i18n-title]').forEach(function (el) { el.setAttribute('title', t(el.getAttribute('data-i18n-title'))); });
|
|
145
|
+
document.querySelectorAll('[data-i18n-aria]').forEach(function (el) { el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria'))); });
|
|
146
|
+
document.documentElement.lang = locale;
|
|
147
|
+
var sel = document.getElementById('settings-language');
|
|
148
|
+
if (sel) sel.value = locale;
|
|
149
|
+
}
|
|
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);
|
|
122
153
|
const httpEnvKey = 'monacori-http-env:' + location.pathname;
|
|
123
154
|
const httpRequestsByPath = new Map();
|
|
124
155
|
const httpVarsByPath = new Map();
|
|
125
|
-
|
|
156
|
+
let sourceByPath = new Map(sourceFiles.map((file) => [file.path, file]));
|
|
126
157
|
// Phase 2b lazy-LOAD: source content is fetched once after first paint (serve /source-data or the
|
|
127
158
|
// Electron bridge) and merged into the metadata-only source records; until then sourceLoaded is false
|
|
128
159
|
// and the source view shows a brief loading state. Non-lazy-load modes embed source -> already loaded.
|
|
@@ -130,6 +161,7 @@ var sourceLoaded = !REVIEW_LAZY_LOAD;
|
|
|
130
161
|
var pendingSourceOpen = null;
|
|
131
162
|
var sourceLoading = false;
|
|
132
163
|
var pendingSymbol = null;
|
|
164
|
+
var sourceTabs = []; // Files-mode tab paths (session-only); see addSourceTab / renderSourceTabs.
|
|
133
165
|
// The source blob (content + image base64) is large on big repos, so lazy-LOAD fetches it lazily — on
|
|
134
166
|
// the first source-view open or go-to-definition — not eagerly at startup. Idempotent.
|
|
135
167
|
function loadSourceData() {
|
|
@@ -152,16 +184,16 @@ function loadSourceData() {
|
|
|
152
184
|
}
|
|
153
185
|
sourceLoaded = true;
|
|
154
186
|
sourceLoading = false;
|
|
155
|
-
|
|
187
|
+
scheduleSymbolIndex();
|
|
156
188
|
if (pendingSourceOpen) { var po = pendingSourceOpen; pendingSourceOpen = null; openSourceFile(po.path, po.shouldSwitch); }
|
|
157
189
|
else if (isSourceViewerVisible() && document.getElementById('source-viewer').dataset.openPath) { openSourceFile(document.getElementById('source-viewer').dataset.openPath, false); }
|
|
158
190
|
if (pendingSymbol) { var s = pendingSymbol; pendingSymbol = null; goToDefOrUsages(s); }
|
|
159
191
|
}, function () { sourceLoaded = true; sourceLoading = false; });
|
|
160
192
|
}
|
|
161
|
-
|
|
193
|
+
let fileSignatureByPath = new Map(fileStates.map((file) => [file.path, file.signature]));
|
|
162
194
|
const reviewMeta = document.getElementById('review-meta');
|
|
163
195
|
const watchEnabled = reviewMeta?.dataset.watch === 'true';
|
|
164
|
-
|
|
196
|
+
let currentSignature = reviewMeta?.dataset.signature || '';
|
|
165
197
|
const uiStateKey = 'monacori-diff-ui:' + location.pathname;
|
|
166
198
|
const recentKey = 'monacori-diff-recent:' + location.pathname;
|
|
167
199
|
const viewedKey = 'monacori-diff-viewed:' + location.pathname;
|
|
@@ -202,7 +234,7 @@ let measuredCharWidth = 0;
|
|
|
202
234
|
// restoreUiState()/openDefaultSourceFile() run on startup and try to render them.
|
|
203
235
|
var COMMENTS_KEY = 'monacori-comments:' + location.pathname;
|
|
204
236
|
var reviewComments = [];
|
|
205
|
-
|
|
237
|
+
reviewComments = (function () { var b = persistRead(COMMENTS_KEY); if (Array.isArray(b)) return b; try { return JSON.parse(localStorage.getItem(COMMENTS_KEY) || '[]'); } catch (commentsErr) { return []; } })();
|
|
206
238
|
if (!Array.isArray(reviewComments)) reviewComments = [];
|
|
207
239
|
var commentSeq = reviewComments.reduce(function (max, c) { return Math.max(max, c.seq || 0); }, 0);
|
|
208
240
|
var composerState = null;
|
|
@@ -243,7 +275,7 @@ function prepareViewedControls() {
|
|
|
243
275
|
const toggle = wrapper.querySelector('.d2h-file-collapse');
|
|
244
276
|
const input = toggle?.querySelector('input');
|
|
245
277
|
if (!fileName || !toggle || !input) return;
|
|
246
|
-
toggle.title = '
|
|
278
|
+
toggle.title = t('btn.viewed.title');
|
|
247
279
|
input.tabIndex = -1;
|
|
248
280
|
toggle.addEventListener('click', (event) => {
|
|
249
281
|
event.preventDefault();
|
|
@@ -407,14 +439,42 @@ function renderBreadcrumb(container, path) {
|
|
|
407
439
|
});
|
|
408
440
|
}
|
|
409
441
|
|
|
442
|
+
// Coalesce diff-nav scrolls: hammering F7 / [ / ] schedules at most one
|
|
443
|
+
// scrollIntoView per frame (to the latest target) instead of forcing a
|
|
444
|
+
// synchronous reflow on every keystroke.
|
|
445
|
+
var pendingDiffScrollRow = null;
|
|
446
|
+
var diffScrollRaf = 0;
|
|
447
|
+
function scheduleDiffScroll(row) {
|
|
448
|
+
pendingDiffScrollRow = row || null;
|
|
449
|
+
if (diffScrollRaf) return;
|
|
450
|
+
diffScrollRaf = requestAnimationFrame(function () {
|
|
451
|
+
diffScrollRaf = 0;
|
|
452
|
+
var r = pendingDiffScrollRow;
|
|
453
|
+
pendingDiffScrollRow = null;
|
|
454
|
+
if (r && r.scrollIntoView) r.scrollIntoView({ block: 'center' });
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
var setActiveRaf = 0, setActiveScrollPending = true;
|
|
410
459
|
function setActive(index, shouldScroll = true) {
|
|
411
460
|
if (hunkTotal() === 0) return;
|
|
412
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) {
|
|
413
474
|
document.getElementById('source-viewer')?.classList.add('hidden');
|
|
414
475
|
document.getElementById('diff-view')?.classList.remove('hidden');
|
|
415
476
|
setTab('changes');
|
|
416
|
-
const file = hunkPathAt(
|
|
417
|
-
const idx = current;
|
|
477
|
+
const file = hunkPathAt(idx);
|
|
418
478
|
links.forEach((link) => link.classList.toggle('active', link.dataset.file === file));
|
|
419
479
|
renderBreadcrumb(document.getElementById('diff-breadcrumb'), file);
|
|
420
480
|
var dvt = document.getElementById('diff-viewed-toggle');
|
|
@@ -438,15 +498,14 @@ function setActive(index, shouldScroll = true) {
|
|
|
438
498
|
// F7/change navigation moves the caret but must NOT pollute the Cmd+[/] cursor history.
|
|
439
499
|
navSuppress = true;
|
|
440
500
|
try { focusDiffRow(targetRow); } finally { navSuppress = false; }
|
|
441
|
-
if (shouldScroll && targetRow) targetRow
|
|
501
|
+
if (shouldScroll && targetRow) scheduleDiffScroll(targetRow);
|
|
442
502
|
});
|
|
443
503
|
}
|
|
444
504
|
|
|
445
505
|
function showOnlyFile(fileName) {
|
|
446
506
|
if (REVIEW_LAZY) ensureFileReady(diffWrapperByPath(fileName));
|
|
447
507
|
document.querySelectorAll('.d2h-file-wrapper').forEach((wrapper) => {
|
|
448
|
-
|
|
449
|
-
wrapper.classList.toggle('df-inactive', name !== fileName);
|
|
508
|
+
wrapper.classList.toggle('df-inactive', diffWrapperPathKey(wrapper) !== fileName);
|
|
450
509
|
});
|
|
451
510
|
ensureDiffCursor();
|
|
452
511
|
}
|
|
@@ -474,8 +533,10 @@ function hunkIndexAtCaret() {
|
|
|
474
533
|
// New-side row indices, one per change block — a run of change rows (ins/del) separated by context.
|
|
475
534
|
// A wide context window merges several edits into one @@ hunk; stepping by these stops at each edit.
|
|
476
535
|
function changeBlockAnchors(wrapper) {
|
|
536
|
+
if (!wrapper) return [];
|
|
537
|
+
if (wrapper.__anchors) return wrapper.__anchors;
|
|
477
538
|
var right = diffSideTables(wrapper).right;
|
|
478
|
-
if (!right) return [];
|
|
539
|
+
if (!right) return []; // body not materialized yet — don't cache an empty result
|
|
479
540
|
var rows = diffRowsOf(right);
|
|
480
541
|
var anchors = [];
|
|
481
542
|
var prev = false;
|
|
@@ -484,6 +545,7 @@ function changeBlockAnchors(wrapper) {
|
|
|
484
545
|
if (chg && !prev) anchors.push(i);
|
|
485
546
|
prev = chg;
|
|
486
547
|
}
|
|
548
|
+
wrapper.__anchors = anchors; // change-block layout is static once materialized
|
|
487
549
|
return anchors;
|
|
488
550
|
}
|
|
489
551
|
|
|
@@ -501,7 +563,7 @@ function next(delta) {
|
|
|
501
563
|
else { for (let b = anchors.length - 1; b >= 0; b--) { if (anchors[b] < cur) { target = anchors[b]; break; } } }
|
|
502
564
|
if (target != null) {
|
|
503
565
|
const row = diffRowAt(w, 'new', target);
|
|
504
|
-
if (row) { navSuppress = true; try { focusDiffRow(row); } finally { navSuppress = false; } row
|
|
566
|
+
if (row) { navSuppress = true; try { focusDiffRow(row); } finally { navSuppress = false; } scheduleDiffScroll(row); return; }
|
|
505
567
|
}
|
|
506
568
|
}
|
|
507
569
|
}
|
|
@@ -535,7 +597,7 @@ function firstHunkForPath(path) {
|
|
|
535
597
|
function openQuickOpen(mode) {
|
|
536
598
|
if (!quickOpen || !quickInput || !quickModeLabel) return;
|
|
537
599
|
quickMode = mode;
|
|
538
|
-
quickModeLabel.textContent = mode === 'recent' ? '
|
|
600
|
+
quickModeLabel.textContent = mode === 'recent' ? t('quickopen.recent') : mode === 'content' ? t('quickopen.findInFiles') : t('quickopen.searchFiles');
|
|
539
601
|
quickOpen.classList.remove('hidden');
|
|
540
602
|
quickInput.value = '';
|
|
541
603
|
renderQuickOpenResults();
|
|
@@ -590,7 +652,7 @@ function renderQuickOpenResults() {
|
|
|
590
652
|
.slice(0, 80);
|
|
591
653
|
quickActive = Math.min(quickActive, Math.max(quickItems.length - 1, 0));
|
|
592
654
|
if (quickItems.length === 0) {
|
|
593
|
-
quickResults.innerHTML = '<div class="quick-open-empty">
|
|
655
|
+
quickResults.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t('quickopen.noFiles')) + '</div>';
|
|
594
656
|
return;
|
|
595
657
|
}
|
|
596
658
|
quickResults.innerHTML = quickItems.map((item, index) => [
|
|
@@ -923,6 +985,13 @@ document.addEventListener('keydown', (event) => {
|
|
|
923
985
|
openMergedView((event.code === 'Slash' || event.key === '?') ? 'q' : 'c');
|
|
924
986
|
return;
|
|
925
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
|
+
}
|
|
926
995
|
// "?" = question, ">" = change-request composer on the current line/selection (no modifier).
|
|
927
996
|
if (!event.altKey && !event.metaKey && !event.ctrlKey && (event.key === '?' || event.key === '>')) {
|
|
928
997
|
const ce = document.activeElement;
|
|
@@ -1063,6 +1132,9 @@ document.addEventListener('keydown', (event) => {
|
|
|
1063
1132
|
}
|
|
1064
1133
|
|
|
1065
1134
|
// Cmd/Ctrl+[ / ] walk the cursor-position history (back / forward), like an editor's Go Back/Forward.
|
|
1135
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && (event.key === '[' || event.key === ']' || event.key === '{' || event.key === '}')) {
|
|
1136
|
+
if (isSourceViewerVisible() && sourceTabs.length > 1) { event.preventDefault(); cycleSourceTab((event.key === '[' || event.key === '{') ? -1 : 1); return; }
|
|
1137
|
+
}
|
|
1066
1138
|
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && (event.key === '[' || event.key === ']')) {
|
|
1067
1139
|
var navEl = document.activeElement;
|
|
1068
1140
|
var navInField = navEl && (navEl.tagName === 'INPUT' || navEl.tagName === 'TEXTAREA' || navEl.tagName === 'SELECT');
|
|
@@ -1139,6 +1211,12 @@ document.querySelectorAll('.tab').forEach((button) => {
|
|
|
1139
1211
|
});
|
|
1140
1212
|
|
|
1141
1213
|
document.getElementById('back-to-diff')?.addEventListener('click', () => showDiffView(true));
|
|
1214
|
+
document.getElementById('source-tabs')?.addEventListener('click', function (event) {
|
|
1215
|
+
var closeBtn = event.target && event.target.closest && event.target.closest('.source-tab-close');
|
|
1216
|
+
if (closeBtn) { event.stopPropagation(); event.preventDefault(); closeSourceTab(closeBtn.getAttribute('data-close-path')); return; }
|
|
1217
|
+
var tab = event.target && event.target.closest && event.target.closest('.source-tab');
|
|
1218
|
+
if (tab) openSourceFile(tab.getAttribute('data-tab-path'));
|
|
1219
|
+
});
|
|
1142
1220
|
document.getElementById('diff-viewed-toggle')?.addEventListener('click', function () {
|
|
1143
1221
|
var btn = document.getElementById('diff-viewed-toggle');
|
|
1144
1222
|
var path = btn ? (btn.dataset.file || '') : '';
|
|
@@ -1154,11 +1232,12 @@ document.addEventListener('keydown', function (event) {
|
|
|
1154
1232
|
}, true);
|
|
1155
1233
|
document.addEventListener('copy', handleSourceCopy);
|
|
1156
1234
|
|
|
1235
|
+
applyI18n(); // first paint already shows English (inline); this swaps to the saved locale before the rest of init renders dynamic text
|
|
1157
1236
|
populateHttpEnvSelect();
|
|
1158
|
-
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
|
|
1159
1238
|
const restored = restoreUiState();
|
|
1160
1239
|
if (!restored) {
|
|
1161
|
-
const initial = location.hash.match(/^#hunk-(
|
|
1240
|
+
const initial = location.hash.match(/^#hunk-(\d+)$/);
|
|
1162
1241
|
if (initial) setActive(Number(initial[1]), false);
|
|
1163
1242
|
else if (REVIEW_LAZY_LOAD) showDiffView(false); // big repos: open to the diff (Changes); the source tree stays deferred until the Files tab is opened
|
|
1164
1243
|
else openDefaultSourceFile();
|
|
@@ -1167,6 +1246,19 @@ initSourceTreeFolds();
|
|
|
1167
1246
|
if (watchEnabled) setInterval(checkForLiveUpdate, 1500);
|
|
1168
1247
|
window.addEventListener('beforeunload', saveUiState);
|
|
1169
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
|
+
|
|
1170
1262
|
(function setupSidebarResize() {
|
|
1171
1263
|
const resizer = document.querySelector('.sidebar-resizer');
|
|
1172
1264
|
if (!resizer) return;
|
|
@@ -1234,13 +1326,26 @@ function diffActiveWrapper() {
|
|
|
1234
1326
|
return document.querySelector('#diff2html-container .d2h-file-wrapper:not(.df-inactive)')
|
|
1235
1327
|
|| document.querySelector('#diff2html-container .d2h-file-wrapper');
|
|
1236
1328
|
}
|
|
1329
|
+
// path -> wrapper, O(1) after the first build. Rebuilt only on a miss/disconnect
|
|
1330
|
+
// (the wrapper set is stable; only bodies materialize). This is called several times
|
|
1331
|
+
// per F7 press, so the old O(files) querySelector scan made each keystroke cost scale
|
|
1332
|
+
// with the file count — the main source of cross-file nav stutter on big diffs.
|
|
1333
|
+
var wrapperPathMap = null;
|
|
1334
|
+
function diffWrapperPathKey(w) {
|
|
1335
|
+
return (w.dataset && w.dataset.path) || ((w.querySelector('.d2h-file-name') || {}).textContent || '').trim();
|
|
1336
|
+
}
|
|
1237
1337
|
function diffWrapperByPath(path) {
|
|
1338
|
+
if (wrapperPathMap) {
|
|
1339
|
+
var hit = wrapperPathMap.get(path);
|
|
1340
|
+
if (hit && hit.isConnected) return hit;
|
|
1341
|
+
}
|
|
1342
|
+
wrapperPathMap = new Map();
|
|
1238
1343
|
var ws = document.querySelectorAll('#diff2html-container .d2h-file-wrapper');
|
|
1239
1344
|
for (var i = 0; i < ws.length; i++) {
|
|
1240
|
-
var
|
|
1241
|
-
if (
|
|
1345
|
+
var key = diffWrapperPathKey(ws[i]);
|
|
1346
|
+
if (key) wrapperPathMap.set(key, ws[i]);
|
|
1242
1347
|
}
|
|
1243
|
-
return null;
|
|
1348
|
+
return wrapperPathMap.get(path) || null;
|
|
1244
1349
|
}
|
|
1245
1350
|
function diffSideTables(wrapper) {
|
|
1246
1351
|
var sides = wrapper ? wrapper.querySelectorAll('.d2h-file-side-diff') : [];
|
|
@@ -1338,6 +1443,7 @@ function renderDiffCaret() {
|
|
|
1338
1443
|
} catch (e) { diffCaretSpan = null; }
|
|
1339
1444
|
}
|
|
1340
1445
|
function setDiffCursor(path, side, rowIndex, column, reveal) {
|
|
1446
|
+
markCaretBusy();
|
|
1341
1447
|
var wrapper = diffWrapperByPath(path);
|
|
1342
1448
|
if (!wrapper) return;
|
|
1343
1449
|
var rows = diffRowsOf(diffSideTable(wrapper, side));
|
|
@@ -1513,13 +1619,13 @@ function handleDiffCaretKey(event) {
|
|
|
1513
1619
|
// ===== Review comments: questions ("?") and change-requests (">") =====
|
|
1514
1620
|
// (COMMENTS_KEY / reviewComments / commentSeq / composerState are declared near the top of the script)
|
|
1515
1621
|
function saveComments() {
|
|
1516
|
-
|
|
1622
|
+
persistSave(COMMENTS_KEY, reviewComments);
|
|
1517
1623
|
}
|
|
1518
1624
|
function commentsAt(path, line) {
|
|
1519
1625
|
return reviewComments.filter(function (c) { return c.path === path && c.line === line; });
|
|
1520
1626
|
}
|
|
1521
1627
|
function commentKindLabel(kind) {
|
|
1522
|
-
return kind === 'q' ? '
|
|
1628
|
+
return kind === 'q' ? t('comment.kind.q') : t('comment.kind.c');
|
|
1523
1629
|
}
|
|
1524
1630
|
function relevantLines(path) {
|
|
1525
1631
|
var set = {};
|
|
@@ -1607,17 +1713,17 @@ function threadHtml(path, line) {
|
|
|
1607
1713
|
commentsAt(path, line).forEach(function (c) {
|
|
1608
1714
|
html += '<div class="mc-card mc-' + c.kind + '">'
|
|
1609
1715
|
+ '<div class="mc-card-head"><span class="mc-kind">' + commentKindLabel(c.kind) + '</span>'
|
|
1610
|
-
+ '<button type="button" class="mc-del" data-seq="' + c.seq + '" title="
|
|
1716
|
+
+ '<button type="button" class="mc-del" data-seq="' + c.seq + '" title="' + escapeHtml(t('composer.delete')) + '">×</button></div>'
|
|
1611
1717
|
+ '<div class="mc-card-body">' + escapeHtml(c.text) + '</div></div>';
|
|
1612
1718
|
});
|
|
1613
1719
|
if (composerState && composerState.path === path && composerState.line === line) {
|
|
1614
|
-
var ph = composerState.kind === 'q' ? '
|
|
1720
|
+
var ph = composerState.kind === 'q' ? t('composer.question') : t('composer.changeRequest');
|
|
1615
1721
|
html += '<div class="mc-card mc-' + composerState.kind + ' mc-composer">'
|
|
1616
1722
|
+ '<div class="mc-card-head"><span class="mc-kind">' + commentKindLabel(composerState.kind) + '</span></div>'
|
|
1617
|
-
+ '<textarea class="mc-input" rows="3" placeholder="' + ph + '"></textarea>'
|
|
1618
|
-
+ '<div class="mc-actions"><button type="button" class="mc-btn mc-save">
|
|
1619
|
-
+ '<button type="button" class="mc-btn mc-ghost mc-cancel">
|
|
1620
|
-
+ '<span class="mc-hint">
|
|
1723
|
+
+ '<textarea class="mc-input" rows="3" placeholder="' + escapeHtml(ph) + '"></textarea>'
|
|
1724
|
+
+ '<div class="mc-actions"><button type="button" class="mc-btn mc-save">' + escapeHtml(t('composer.save')) + '</button>'
|
|
1725
|
+
+ '<button type="button" class="mc-btn mc-ghost mc-cancel">' + escapeHtml(t('composer.cancel')) + '</button>'
|
|
1726
|
+
+ '<span class="mc-hint">' + escapeHtml(t('composer.hint')) + '</span></div></div>';
|
|
1621
1727
|
}
|
|
1622
1728
|
return html;
|
|
1623
1729
|
}
|
|
@@ -1684,8 +1790,8 @@ function renderCommentBadges() {
|
|
|
1684
1790
|
var badge = document.createElement('span');
|
|
1685
1791
|
badge.className = 'mc-file-badge';
|
|
1686
1792
|
var html = '';
|
|
1687
|
-
if (k.q) html += '<span class="mc-fb mc-fb-q" title="' + k.q + '
|
|
1688
|
-
if (k.c) html += '<span class="mc-fb mc-fb-c" title="' + k.c + '
|
|
1793
|
+
if (k.q) html += '<span class="mc-fb mc-fb-q" title="' + k.q + ' ' + escapeHtml(t('badge.questions')) + '">' + k.q + '</span>';
|
|
1794
|
+
if (k.c) html += '<span class="mc-fb mc-fb-c" title="' + k.c + ' ' + escapeHtml(t('badge.changeRequests')) + '">' + k.c + '</span>';
|
|
1689
1795
|
badge.innerHTML = html;
|
|
1690
1796
|
return badge;
|
|
1691
1797
|
}
|
|
@@ -1727,18 +1833,23 @@ function refreshComments() {
|
|
|
1727
1833
|
renderCommentBadges();
|
|
1728
1834
|
applyCommentSelectionHighlight();
|
|
1729
1835
|
if (composerState) {
|
|
1730
|
-
var
|
|
1836
|
+
var composerFocusTries = 0;
|
|
1837
|
+
var tryFocusComposer = function () {
|
|
1731
1838
|
var ta = document.querySelector('.mc-composer .mc-input');
|
|
1732
|
-
if (ta
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
}
|
|
1839
|
+
if (!ta) return true; // composer gone — stop retrying
|
|
1840
|
+
if (document.activeElement === ta) return true; // already focused — done
|
|
1841
|
+
try { ta.focus({ preventScroll: true }); } catch (e) { try { ta.focus(); } catch (e2) {} }
|
|
1842
|
+
try { ta.selectionStart = ta.selectionEnd = ta.value.length; } catch (e3) {}
|
|
1843
|
+
return document.activeElement === ta;
|
|
1736
1844
|
};
|
|
1737
|
-
//
|
|
1738
|
-
// the
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1845
|
+
// A one-shot focus works in a plain browser, but Electron asynchronously restores focus to <body>
|
|
1846
|
+
// after the keydown, so the textarea loses that race. Retry on a short interval until it wins (or the
|
|
1847
|
+
// composer closes), capped at ~300ms so it never fights real user focus once they start typing.
|
|
1848
|
+
if (!tryFocusComposer()) {
|
|
1849
|
+
var composerFocusIv = setInterval(function () {
|
|
1850
|
+
if (tryFocusComposer() || ++composerFocusTries > 12) clearInterval(composerFocusIv);
|
|
1851
|
+
}, 25);
|
|
1852
|
+
}
|
|
1742
1853
|
}
|
|
1743
1854
|
}
|
|
1744
1855
|
|
|
@@ -1765,18 +1876,34 @@ function saveComposer(ta) {
|
|
|
1765
1876
|
refreshComments();
|
|
1766
1877
|
}
|
|
1767
1878
|
|
|
1879
|
+
// Default merge-prompt headings, localized: a Korean user gets Korean defaults. Editable in
|
|
1880
|
+
// Settings → Merge prompts (stored per browser in localStorage); buildMergedText + the textarea
|
|
1881
|
+
// placeholders fall back to these when the stored value is empty.
|
|
1882
|
+
function defaultMergePrompt(kind) {
|
|
1883
|
+
return t(kind === 'q' ? 'mergePrompt.default.q' : 'mergePrompt.default.c');
|
|
1884
|
+
}
|
|
1885
|
+
var mergePromptsKey = 'monacori-merge-prompts';
|
|
1886
|
+
function loadMergePrompts() {
|
|
1887
|
+
var b = persistRead(mergePromptsKey); if (b && typeof b === 'object') return b; try { var v = JSON.parse(localStorage.getItem(mergePromptsKey) || '{}'); return (v && typeof v === 'object') ? v : {}; } catch (e) { return {}; }
|
|
1888
|
+
}
|
|
1889
|
+
function mergePromptFor(kind) {
|
|
1890
|
+
var v = loadMergePrompts()[kind];
|
|
1891
|
+
return (typeof v === 'string' && v.trim()) ? v : defaultMergePrompt(kind);
|
|
1892
|
+
}
|
|
1893
|
+
function saveMergePrompt(kind, text) {
|
|
1894
|
+
var saved = loadMergePrompts();
|
|
1895
|
+
if (text && text.trim()) saved[kind] = text; else delete saved[kind];
|
|
1896
|
+
persistSave(mergePromptsKey, saved);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1768
1899
|
function buildMergedText(kind) {
|
|
1769
1900
|
var items = reviewComments.filter(function (c) { return c.kind === kind; });
|
|
1770
1901
|
var nl = String.fromCharCode(10);
|
|
1771
1902
|
var lines = [];
|
|
1772
|
-
// Per-kind agent contract
|
|
1773
|
-
|
|
1774
|
-
// reviewer can trim or localize this before copying.
|
|
1775
|
-
lines.push(kind === 'q'
|
|
1776
|
-
? '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.'
|
|
1777
|
-
: '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.');
|
|
1903
|
+
// Per-kind agent contract heading (editable in Settings → Merge prompts; default otherwise).
|
|
1904
|
+
lines.push(mergePromptFor(kind));
|
|
1778
1905
|
lines.push('');
|
|
1779
|
-
lines.push((kind === 'q' ? '
|
|
1906
|
+
lines.push((kind === 'q' ? t('merged.qHeading') : t('merged.cHeading')) + ' (' + items.length + ')');
|
|
1780
1907
|
lines.push('');
|
|
1781
1908
|
items.forEach(function (c) {
|
|
1782
1909
|
lines.push('### ' + c.path + ':' + c.line);
|
|
@@ -1793,35 +1920,39 @@ function openMergedView(kind) {
|
|
|
1793
1920
|
var modal = document.createElement('div');
|
|
1794
1921
|
modal.id = 'mc-modal';
|
|
1795
1922
|
modal.className = 'mc-modal';
|
|
1923
|
+
modal.dataset.kind = kind; // remembered so a live locale switch can re-render this same view
|
|
1796
1924
|
var panel = document.createElement('div');
|
|
1797
1925
|
panel.className = 'mc-modal-panel';
|
|
1798
1926
|
var head = document.createElement('div');
|
|
1799
1927
|
head.className = 'mc-modal-head';
|
|
1800
1928
|
var title = document.createElement('span');
|
|
1801
|
-
title.textContent = kind === 'q' ? '
|
|
1802
|
-
var copyBtn = document.createElement('button');
|
|
1803
|
-
copyBtn.type = 'button';
|
|
1804
|
-
copyBtn.className = 'mc-btn';
|
|
1805
|
-
copyBtn.textContent = 'Copy all';
|
|
1929
|
+
title.textContent = kind === 'q' ? t('merged.qTitle') : t('merged.cTitle');
|
|
1806
1930
|
var closeBtn = document.createElement('button');
|
|
1807
1931
|
closeBtn.type = 'button';
|
|
1808
1932
|
closeBtn.className = 'mc-btn mc-ghost';
|
|
1809
|
-
closeBtn.textContent = '
|
|
1933
|
+
closeBtn.textContent = t('merged.close');
|
|
1810
1934
|
var area = document.createElement('textarea');
|
|
1811
1935
|
area.className = 'mc-modal-text';
|
|
1812
1936
|
area.readOnly = true;
|
|
1813
1937
|
area.value = buildMergedText(kind);
|
|
1814
|
-
copyBtn.addEventListener('click', function () {
|
|
1815
|
-
area.focus(); area.select();
|
|
1816
|
-
var ok = false;
|
|
1817
|
-
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
1818
|
-
if (navigator.clipboard && navigator.clipboard.writeText) { try { navigator.clipboard.writeText(area.value); ok = true; } catch (e) {} }
|
|
1819
|
-
copyBtn.textContent = ok ? 'Copied' : 'Copy failed';
|
|
1820
|
-
setTimeout(function () { copyBtn.textContent = 'Copy all'; }, 1500);
|
|
1821
|
-
});
|
|
1822
1938
|
closeBtn.addEventListener('click', function () { modal.remove(); });
|
|
1939
|
+
// Terminal send (Electron, terminal open): close the modal and hand off to pane-pick mode ON the
|
|
1940
|
+
// terminal — the chosen pane is highlighted, the rest dimmed, arrows change the choice, Enter sends.
|
|
1941
|
+
// One button here; the actual pick happens visually over the live claude/codex sessions.
|
|
1942
|
+
var sendBtn = null;
|
|
1943
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.isOpen === 'function' && window.__monacoriTerminal.isOpen()) {
|
|
1944
|
+
sendBtn = document.createElement('button');
|
|
1945
|
+
sendBtn.type = 'button';
|
|
1946
|
+
sendBtn.className = 'mc-btn mc-send-term';
|
|
1947
|
+
sendBtn.textContent = t('merged.sendToTerminal');
|
|
1948
|
+
sendBtn.addEventListener('click', function () {
|
|
1949
|
+
var text = buildMergedText(kind);
|
|
1950
|
+
modal.remove();
|
|
1951
|
+
window.__monacoriTerminal.enterSendMode(text);
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1823
1954
|
head.appendChild(title);
|
|
1824
|
-
head.appendChild(
|
|
1955
|
+
if (sendBtn) head.appendChild(sendBtn);
|
|
1825
1956
|
head.appendChild(closeBtn);
|
|
1826
1957
|
panel.appendChild(head);
|
|
1827
1958
|
panel.appendChild(area);
|
|
@@ -1829,7 +1960,109 @@ function openMergedView(kind) {
|
|
|
1829
1960
|
modal.addEventListener('mousedown', function (e) { if (e.target === modal) modal.remove(); });
|
|
1830
1961
|
modal.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); modal.remove(); } });
|
|
1831
1962
|
document.body.appendChild(modal);
|
|
1832
|
-
|
|
1963
|
+
// Focus the send button (Enter starts pane-pick) when present, else the read-only text. Electron
|
|
1964
|
+
// async-restores focus to <body>, so retry briefly (same as the composer).
|
|
1965
|
+
var modalFocusTarget = sendBtn || area;
|
|
1966
|
+
var modalFocusTries = 0;
|
|
1967
|
+
var tryFocusModal = function () {
|
|
1968
|
+
if (!document.getElementById('mc-modal')) return true;
|
|
1969
|
+
if (document.activeElement === modalFocusTarget) return true;
|
|
1970
|
+
try { modalFocusTarget.focus(); if (modalFocusTarget === area) modalFocusTarget.select(); } catch (e) {}
|
|
1971
|
+
return document.activeElement === modalFocusTarget;
|
|
1972
|
+
};
|
|
1973
|
+
if (!tryFocusModal()) {
|
|
1974
|
+
var modalFocusIv = setInterval(function () { if (tryFocusModal() || ++modalFocusTries > 12) clearInterval(modalFocusIv); }, 25);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
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
|
+
}
|
|
1833
2066
|
}
|
|
1834
2067
|
|
|
1835
2068
|
document.addEventListener('click', function (event) {
|
|
@@ -1849,11 +2082,322 @@ document.addEventListener('keydown', function (event) {
|
|
|
1849
2082
|
|
|
1850
2083
|
refreshComments();
|
|
1851
2084
|
|
|
2085
|
+
|
|
2086
|
+
// Integrated terminal (Electron only): xterm panes wired to node-pty sessions in the main process.
|
|
2087
|
+
// Toggle with Ctrl+` / Opt+F12 / the footer ⌗ button; Cmd/Ctrl+D splits the active pane (side by side,
|
|
2088
|
+
// no tabs); drag the top edge to resize. window.__monacoriTerminal pipes the merged prompt into the
|
|
2089
|
+
// active pane. Cmd combos are released back to the app so shortcuts like Cmd+1 don't get stuck typing.
|
|
2090
|
+
(function setupTerminal() {
|
|
2091
|
+
if (!window.monacoriPty) return; // xterm (window.Terminal) is loaded lazily on first open
|
|
2092
|
+
var panel = document.getElementById('terminal-panel');
|
|
2093
|
+
var host = document.getElementById('terminal-host');
|
|
2094
|
+
var toggleBtn = document.getElementById('terminal-toggle');
|
|
2095
|
+
var closeBtn = document.getElementById('terminal-close');
|
|
2096
|
+
var resizer = panel ? panel.querySelector('.terminal-resizer') : null;
|
|
2097
|
+
if (!panel || !host) return;
|
|
2098
|
+
if (toggleBtn) toggleBtn.classList.remove('hidden'); // reveal the footer toggle in Electron
|
|
2099
|
+
|
|
2100
|
+
// xterm ships as an inert island (id=xterm-code) so ~490KB isn't parsed at startup. Inject it on the
|
|
2101
|
+
// first open; returns false if unavailable (e.g. the island is absent), so callers can bail gracefully.
|
|
2102
|
+
function ensureXterm() {
|
|
2103
|
+
if (typeof window.Terminal === 'function') return true;
|
|
2104
|
+
var code = document.getElementById('xterm-code');
|
|
2105
|
+
if (!code) return false;
|
|
2106
|
+
try {
|
|
2107
|
+
var s = document.createElement('script');
|
|
2108
|
+
s.textContent = code.textContent;
|
|
2109
|
+
document.head.appendChild(s);
|
|
2110
|
+
code.remove(); // free the inert text once compiled
|
|
2111
|
+
} catch (e) { return false; }
|
|
2112
|
+
return typeof window.Terminal === 'function';
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
var panes = []; // { id, term, fit, el }
|
|
2116
|
+
var active = null;
|
|
2117
|
+
var MAX_PANES = 4;
|
|
2118
|
+
var heightKey = 'monacori-terminal-height';
|
|
2119
|
+
var openKey = 'monacori-terminal-open:' + location.pathname;
|
|
2120
|
+
|
|
2121
|
+
function applyHeight(px) {
|
|
2122
|
+
var h = Math.max(120, Math.min(px, window.innerHeight - 120));
|
|
2123
|
+
document.documentElement.style.setProperty('--terminal-height', h + 'px');
|
|
2124
|
+
}
|
|
2125
|
+
var savedH = parseInt(localStorage.getItem(heightKey) || '', 10);
|
|
2126
|
+
if (savedH) applyHeight(savedH);
|
|
2127
|
+
|
|
2128
|
+
function fitPane(p) {
|
|
2129
|
+
if (!p) return;
|
|
2130
|
+
try { p.fit.fit(); if (p.id != null) window.monacoriPty.resize({ id: p.id, cols: p.term.cols, rows: p.term.rows }); } catch (e) {}
|
|
2131
|
+
}
|
|
2132
|
+
function fitAll() { panes.forEach(fitPane); }
|
|
2133
|
+
|
|
2134
|
+
function setActive(p) {
|
|
2135
|
+
active = p;
|
|
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
|
+
});
|
|
2141
|
+
if (p) requestAnimationFrame(function () { try { p.term.focus(); } catch (e) {} });
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function makePane() {
|
|
2145
|
+
if (!ensureXterm()) return null; // xterm unavailable — leave the panel empty rather than throw
|
|
2146
|
+
var el = document.createElement('div');
|
|
2147
|
+
el.className = 'terminal-pane';
|
|
2148
|
+
var labelEl = document.createElement('div');
|
|
2149
|
+
labelEl.className = 'terminal-pane-label';
|
|
2150
|
+
var paneHost = document.createElement('div');
|
|
2151
|
+
paneHost.className = 'terminal-pane-host';
|
|
2152
|
+
el.appendChild(labelEl);
|
|
2153
|
+
el.appendChild(paneHost);
|
|
2154
|
+
host.appendChild(el);
|
|
2155
|
+
var term = new window.Terminal({
|
|
2156
|
+
fontSize: 12,
|
|
2157
|
+
fontFamily: 'Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
2158
|
+
theme: { background: '#161616', foreground: '#a9b7c6', cursor: '#a9b7c6', selectionBackground: '#214283' },
|
|
2159
|
+
cursorBlink: true,
|
|
2160
|
+
});
|
|
2161
|
+
var fit = new window.FitAddon.FitAddon();
|
|
2162
|
+
term.loadAddon(fit);
|
|
2163
|
+
term.open(paneHost);
|
|
2164
|
+
var pane = { id: null, term: term, fit: fit, el: el, labelEl: labelEl, name: 'Terminal ' + (panes.length + 1) };
|
|
2165
|
+
labelEl.textContent = pane.name;
|
|
2166
|
+
// Cmd combos are app shortcuts (Cmd+1/0 tab switch, Cmd+B go-to-def, …). Release the terminal and let
|
|
2167
|
+
// them bubble to the document handler instead of typing into the shell (fixes "Cmd+1 stuck in term").
|
|
2168
|
+
// Exception: keep focus for clipboard/selection combos (Cmd+C/V/X/A) so the terminal's own copy &
|
|
2169
|
+
// paste keep working — blurring on Cmd+V drops the textarea focus the paste event needs.
|
|
2170
|
+
term.attachCustomKeyEventHandler(function (e) {
|
|
2171
|
+
if (e.type === 'keydown' && e.metaKey) {
|
|
2172
|
+
var k = (e.key || '').toLowerCase();
|
|
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;
|
|
2180
|
+
try { term.blur(); } catch (x) {}
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
return true;
|
|
2184
|
+
});
|
|
2185
|
+
term.onData(function (d) { if (pane.id != null) window.monacoriPty.write({ id: pane.id, data: d }); });
|
|
2186
|
+
el.addEventListener('mousedown', function (e) { if (e.target !== labelEl) setActive(pane); });
|
|
2187
|
+
labelEl.addEventListener('dblclick', function () { renamePane(pane); });
|
|
2188
|
+
panes.push(pane);
|
|
2189
|
+
try { fit.fit(); } catch (e) {}
|
|
2190
|
+
window.monacoriPty.spawn({ cols: term.cols || 80, rows: term.rows || 24 }).then(function (r) { pane.id = r && r.id; });
|
|
2191
|
+
setActive(pane);
|
|
2192
|
+
return pane;
|
|
2193
|
+
}
|
|
2194
|
+
// Rename a pane inline: the label becomes editable, Enter commits, Esc/blur reverts to the last name.
|
|
2195
|
+
function renamePane(pane) {
|
|
2196
|
+
if (!pane) { pane = active; }
|
|
2197
|
+
if (!pane) return;
|
|
2198
|
+
var el = pane.labelEl;
|
|
2199
|
+
if (el.getAttribute('contenteditable') === 'true') return;
|
|
2200
|
+
setActive(pane);
|
|
2201
|
+
el.contentEditable = 'true';
|
|
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); }
|
|
2214
|
+
function finish(commit) {
|
|
2215
|
+
el.removeEventListener('keydown', onKey);
|
|
2216
|
+
el.removeEventListener('blur', onBlur);
|
|
2217
|
+
el.contentEditable = 'false';
|
|
2218
|
+
if (commit) pane.name = (el.textContent || '').trim() || pane.name;
|
|
2219
|
+
el.textContent = pane.name;
|
|
2220
|
+
try { if (pane.term) pane.term.focus(); } catch (e) {}
|
|
2221
|
+
}
|
|
2222
|
+
function onKey(e) {
|
|
2223
|
+
e.stopPropagation();
|
|
2224
|
+
if (e.key === 'Enter') { e.preventDefault(); finish(true); }
|
|
2225
|
+
else if (e.key === 'Escape') { e.preventDefault(); finish(false); }
|
|
2226
|
+
}
|
|
2227
|
+
function onBlur() { finish(true); }
|
|
2228
|
+
el.addEventListener('keydown', onKey);
|
|
2229
|
+
el.addEventListener('blur', onBlur);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function removePane(id) {
|
|
2233
|
+
var i = -1;
|
|
2234
|
+
for (var k = 0; k < panes.length; k++) { if (panes[k].id === id) { i = k; break; } }
|
|
2235
|
+
if (i < 0) return;
|
|
2236
|
+
var p = panes[i];
|
|
2237
|
+
try { p.term.dispose(); } catch (e) {}
|
|
2238
|
+
if (p.el.parentNode) p.el.parentNode.removeChild(p.el);
|
|
2239
|
+
panes.splice(i, 1);
|
|
2240
|
+
if (active === p) setActive(panes[panes.length - 1] || null);
|
|
2241
|
+
if (panes.length === 0) setOpen(false);
|
|
2242
|
+
else fitAll();
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
function split() {
|
|
2246
|
+
if (panes.length >= MAX_PANES) return;
|
|
2247
|
+
makePane();
|
|
2248
|
+
fitAll();
|
|
2249
|
+
}
|
|
2250
|
+
// Move active focus between split panes (menu accelerators Cmd/Ctrl+Alt+[ and ]).
|
|
2251
|
+
function focusPaneByDelta(delta) {
|
|
2252
|
+
if (panes.length < 2) return;
|
|
2253
|
+
var i = panes.indexOf(active);
|
|
2254
|
+
if (i < 0) i = 0;
|
|
2255
|
+
setActive(panes[(i + delta + panes.length) % panes.length]);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// Route per-pane pty output / exit by id (registered once for the window).
|
|
2259
|
+
window.monacoriPty.onData(function (msg) {
|
|
2260
|
+
for (var k = 0; k < panes.length; k++) { if (panes[k].id === msg.id) { panes[k].term.write(msg.data); return; } }
|
|
2261
|
+
});
|
|
2262
|
+
window.monacoriPty.onExit(function (msg) { removePane(msg.id); });
|
|
2263
|
+
|
|
2264
|
+
function isOpen() { return !panel.classList.contains('hidden'); }
|
|
2265
|
+
function setOpen(open) {
|
|
2266
|
+
panel.classList.toggle('hidden', !open);
|
|
2267
|
+
document.body.classList.toggle('terminal-open', open);
|
|
2268
|
+
if (toggleBtn) toggleBtn.classList.toggle('is-active', open);
|
|
2269
|
+
try { sessionStorage.setItem(openKey, open ? '1' : '0'); } catch (e) {}
|
|
2270
|
+
if (open) {
|
|
2271
|
+
if (panes.length === 0) makePane();
|
|
2272
|
+
requestAnimationFrame(function () { fitAll(); if (active) try { active.term.focus(); } catch (e) {} });
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function toggle() { setOpen(!isOpen()); }
|
|
2276
|
+
|
|
2277
|
+
if (toggleBtn) toggleBtn.addEventListener('click', toggle);
|
|
2278
|
+
if (closeBtn) closeBtn.addEventListener('click', function () { setOpen(false); });
|
|
2279
|
+
// Toggle (Ctrl+`/Alt+F12) and split (Cmd+D) arrive from the Terminal menu accelerators (app-main),
|
|
2280
|
+
// because Chromium swallows Cmd+D before a renderer keydown would ever see it.
|
|
2281
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onTerminalToggle === 'function') window.monacoriMenu.onTerminalToggle(toggle);
|
|
2282
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onTerminalSplit === 'function') window.monacoriMenu.onTerminalSplit(split);
|
|
2283
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onTerminalPaneFocus === 'function') window.monacoriMenu.onTerminalPaneFocus(focusPaneByDelta);
|
|
2284
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onTerminalPaneRename === 'function') window.monacoriMenu.onTerminalPaneRename(function () { renamePane(active); });
|
|
2285
|
+
|
|
2286
|
+
var ro = (typeof ResizeObserver === 'function') ? new ResizeObserver(function () { if (isOpen()) fitAll(); }) : null;
|
|
2287
|
+
if (ro) ro.observe(host);
|
|
2288
|
+
window.addEventListener('resize', function () { if (isOpen()) fitAll(); });
|
|
2289
|
+
|
|
2290
|
+
if (resizer) {
|
|
2291
|
+
resizer.addEventListener('mousedown', function (e) {
|
|
2292
|
+
e.preventDefault();
|
|
2293
|
+
resizer.classList.add('resizing');
|
|
2294
|
+
function move(ev) { applyHeight(window.innerHeight - ev.clientY); }
|
|
2295
|
+
function up() {
|
|
2296
|
+
resizer.classList.remove('resizing');
|
|
2297
|
+
document.removeEventListener('mousemove', move);
|
|
2298
|
+
document.removeEventListener('mouseup', up);
|
|
2299
|
+
var cur = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--terminal-height'), 10);
|
|
2300
|
+
if (cur) { try { localStorage.setItem(heightKey, String(cur)); } catch (e) {} }
|
|
2301
|
+
fitAll();
|
|
2302
|
+
}
|
|
2303
|
+
document.addEventListener('mousemove', move);
|
|
2304
|
+
document.addEventListener('mouseup', up);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// Kill this window's ptys on unload so a reload/close doesn't leak them in the main process.
|
|
2309
|
+
window.addEventListener('beforeunload', function () {
|
|
2310
|
+
panes.forEach(function (p) { if (p.id != null) { try { window.monacoriPty.kill({ id: p.id }); } catch (e) {} } });
|
|
2311
|
+
});
|
|
2312
|
+
|
|
2313
|
+
// Hook for the merged-prompt modal: pipe the combined text into a chosen pane (no trailing Enter —
|
|
2314
|
+
// the user reviews in the live session, then presses Enter, so multiline prompts stay intact).
|
|
2315
|
+
function writeToPane(p, text) {
|
|
2316
|
+
if (!p) return;
|
|
2317
|
+
setOpen(true);
|
|
2318
|
+
if (p.id != null) window.monacoriPty.write({ id: p.id, data: text });
|
|
2319
|
+
setActive(p);
|
|
2320
|
+
requestAnimationFrame(function () { try { p.term.focus(); } catch (e) {} });
|
|
2321
|
+
}
|
|
2322
|
+
// Pane-pick mode: triggered from the merged modal's "Send to terminal". The chosen pane is highlighted,
|
|
2323
|
+
// the rest are dimmed; arrows change the pick, Enter sends, Esc cancels. Single pane → send at once.
|
|
2324
|
+
var sendModeText = null, sendModeIdx = 0;
|
|
2325
|
+
function paintSendMode() {
|
|
2326
|
+
panes.forEach(function (p, i) {
|
|
2327
|
+
p.el.classList.toggle('is-send-target', i === sendModeIdx);
|
|
2328
|
+
p.el.classList.toggle('is-dimmed', i !== sendModeIdx);
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
function exitSendMode() {
|
|
2332
|
+
if (sendModeText == null) return;
|
|
2333
|
+
sendModeText = null;
|
|
2334
|
+
panel.classList.remove('send-mode');
|
|
2335
|
+
document.body.classList.remove('terminal-send-mode'); // un-dim the rest of the app
|
|
2336
|
+
panes.forEach(function (p) { p.el.classList.remove('is-send-target', 'is-dimmed'); });
|
|
2337
|
+
}
|
|
2338
|
+
function enterSendMode(text) {
|
|
2339
|
+
if (panes.length === 0) return;
|
|
2340
|
+
setOpen(true);
|
|
2341
|
+
sendModeText = text;
|
|
2342
|
+
sendModeIdx = Math.max(0, panes.indexOf(active));
|
|
2343
|
+
panel.classList.add('send-mode');
|
|
2344
|
+
document.body.classList.add('terminal-send-mode'); // dim sidebar + file/diff view; only the terminal pops
|
|
2345
|
+
paintSendMode();
|
|
2346
|
+
}
|
|
2347
|
+
// Capture phase so the pick keys win over the focused xterm; while picking, every key is swallowed.
|
|
2348
|
+
document.addEventListener('keydown', function (e) {
|
|
2349
|
+
if (sendModeText == null) return;
|
|
2350
|
+
e.preventDefault(); e.stopPropagation();
|
|
2351
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
2352
|
+
var d = (e.key === 'ArrowRight' || e.key === 'ArrowDown') ? 1 : -1;
|
|
2353
|
+
sendModeIdx = (sendModeIdx + d + panes.length) % panes.length;
|
|
2354
|
+
paintSendMode();
|
|
2355
|
+
} else if (e.key === 'Enter') {
|
|
2356
|
+
var p = panes[sendModeIdx], text = sendModeText;
|
|
2357
|
+
exitSendMode();
|
|
2358
|
+
writeToPane(p, text);
|
|
2359
|
+
} else if (e.key === 'Escape') {
|
|
2360
|
+
exitSendMode();
|
|
2361
|
+
}
|
|
2362
|
+
}, true);
|
|
2363
|
+
window.__monacoriTerminal = {
|
|
2364
|
+
isOpen: isOpen,
|
|
2365
|
+
open: function () { setOpen(true); },
|
|
2366
|
+
paneCount: function () { return panes.length; },
|
|
2367
|
+
enterSendMode: enterSendMode,
|
|
2368
|
+
send: function (text) { writeToPane(active || panes[0], text); },
|
|
2369
|
+
sendToPane: function (i, text) { writeToPane(panes[i] || active || panes[0], text); },
|
|
2370
|
+
close: function () { setOpen(false); },
|
|
2371
|
+
};
|
|
2372
|
+
|
|
2373
|
+
// Restore the open state across reloads.
|
|
2374
|
+
try { if (sessionStorage.getItem(openKey) === '1') setOpen(true); } catch (e) {}
|
|
2375
|
+
})();
|
|
2376
|
+
|
|
1852
2377
|
// In Electron, the Review menu's Cmd/Ctrl+Shift+/ and +. accelerators arrive here via IPC
|
|
1853
2378
|
// (macOS reserves Cmd+? for its Help search, so the menu claims it and routes to these views).
|
|
1854
2379
|
if (window.monacoriMenu && typeof window.monacoriMenu.onMergedView === 'function') {
|
|
2380
|
+
// Always open the merged-view modal; sending to a terminal pane is a button inside it (per-pane when
|
|
2381
|
+
// split), so the user can pick which claude/codex session receives the prompt.
|
|
1855
2382
|
window.monacoriMenu.onMergedView(function (kind) { openMergedView(kind); });
|
|
1856
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
|
+
}
|
|
2393
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function') {
|
|
2394
|
+
// Cmd/Ctrl+W: close the active Files-mode tab (no-op outside the source viewer).
|
|
2395
|
+
window.monacoriMenu.onCloseTab(function () {
|
|
2396
|
+
// Cmd/Ctrl+W closes the terminal panel first when it's open, otherwise the active Files-mode tab.
|
|
2397
|
+
if (window.__monacoriTerminal && window.__monacoriTerminal.isOpen()) { window.__monacoriTerminal.close(); return; }
|
|
2398
|
+
if (isSourceViewerVisible()) closeActiveSourceTab();
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
1857
2401
|
|
|
1858
2402
|
(function checkForUpdate() {
|
|
1859
2403
|
var current = window.__MONACORI_VERSION__ || '';
|
|
@@ -1873,9 +2417,19 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onMergedView === 'function
|
|
|
1873
2417
|
if (isNewer(latest, current)) {
|
|
1874
2418
|
var flag = document.getElementById('app-update-flag');
|
|
1875
2419
|
if (flag) flag.classList.remove('hidden');
|
|
1876
|
-
|
|
2420
|
+
// One-click auto-update needs the Electron main process (it spawns npm). When available, reveal the
|
|
2421
|
+
// button so a click installs + restarts; otherwise (browser/static export) name the command instead.
|
|
2422
|
+
var ub = document.getElementById('app-info-update');
|
|
2423
|
+
if (ub && window.monacoriUpdate && typeof window.monacoriUpdate.run === 'function') {
|
|
2424
|
+
ub.textContent = t('settings.updateRestart') + ' (v' + latest + ')';
|
|
2425
|
+
ub.classList.remove('hidden');
|
|
2426
|
+
if (status) { status.textContent = t('settings.updateAvailable') + ': v' + latest; status.classList.add('has-update'); }
|
|
2427
|
+
} else if (status) {
|
|
2428
|
+
status.textContent = t('settings.updateAvailable') + ': v' + latest + ' — npm i -g @happy-nut/monacori';
|
|
2429
|
+
status.classList.add('has-update');
|
|
2430
|
+
}
|
|
1877
2431
|
} else if (status) {
|
|
1878
|
-
status.textContent = '
|
|
2432
|
+
status.textContent = t('settings.upToDate') + ' (v' + current + ')';
|
|
1879
2433
|
}
|
|
1880
2434
|
};
|
|
1881
2435
|
// Cache the npm result for the session so watch-mode reloads reuse it instead of refetching.
|
|
@@ -1893,28 +2447,81 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onMergedView === 'function
|
|
|
1893
2447
|
.catch(function () {});
|
|
1894
2448
|
})();
|
|
1895
2449
|
|
|
1896
|
-
(
|
|
1897
|
-
|
|
1898
|
-
|
|
2450
|
+
// Unified settings modal: the sidebar-footer gear opens it (General category by default), with
|
|
2451
|
+
// About/update/shortcuts under General and the merge-prompt editor under Merge prompts.
|
|
2452
|
+
(function setupSettings() {
|
|
2453
|
+
var modal = document.getElementById('settings-modal');
|
|
2454
|
+
if (!modal) return;
|
|
2455
|
+
var gearBtn = document.getElementById('app-info-btn');
|
|
1899
2456
|
var flag = document.getElementById('app-update-flag');
|
|
1900
|
-
var
|
|
1901
|
-
|
|
1902
|
-
var
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2457
|
+
var updateBtn = document.getElementById('app-info-update');
|
|
2458
|
+
var qta = document.getElementById('settings-prompt-q');
|
|
2459
|
+
var cta = document.getElementById('settings-prompt-c');
|
|
2460
|
+
var resetBtn = document.getElementById('settings-reset');
|
|
2461
|
+
var savedMsg = document.getElementById('settings-saved');
|
|
2462
|
+
var cats = Array.prototype.slice.call(modal.querySelectorAll('.settings-cat'));
|
|
2463
|
+
var secs = Array.prototype.slice.call(modal.querySelectorAll('.settings-section'));
|
|
2464
|
+
function showCat(cat) {
|
|
2465
|
+
cats.forEach(function (c) { c.classList.toggle('active', c.dataset.cat === cat); });
|
|
2466
|
+
secs.forEach(function (s) { s.classList.toggle('hidden', s.dataset.cat !== cat); });
|
|
2467
|
+
}
|
|
2468
|
+
function fill() {
|
|
2469
|
+
var s = loadMergePrompts();
|
|
2470
|
+
if (qta) { qta.value = typeof s.q === 'string' ? s.q : ''; qta.placeholder = defaultMergePrompt('q'); }
|
|
2471
|
+
if (cta) { cta.value = typeof s.c === 'string' ? s.c : ''; cta.placeholder = defaultMergePrompt('c'); }
|
|
2472
|
+
}
|
|
2473
|
+
function open(cat) { fill(); if (cat) showCat(cat); modal.classList.remove('hidden'); }
|
|
2474
|
+
function close() { modal.classList.add('hidden'); }
|
|
2475
|
+
var flashTimer = null;
|
|
2476
|
+
function flash() { if (!savedMsg) return; savedMsg.textContent = 'Saved'; if (flashTimer) clearTimeout(flashTimer); flashTimer = setTimeout(function () { savedMsg.textContent = ''; }, 1200); }
|
|
2477
|
+
if (gearBtn) gearBtn.addEventListener('click', function (e) { e.stopPropagation(); if (modal.classList.contains('hidden')) open('general'); else close(); });
|
|
2478
|
+
if (flag) flag.addEventListener('click', function (e) { e.stopPropagation(); open('general'); });
|
|
2479
|
+
cats.forEach(function (c) { c.addEventListener('click', function () { showCat(c.dataset.cat); }); });
|
|
2480
|
+
modal.addEventListener('click', function (e) { if (e.target === modal) close(); });
|
|
2481
|
+
// Capture so closing settings wins over other Escape handlers (lightbox / composer).
|
|
1915
2482
|
document.addEventListener('keydown', function (e) {
|
|
1916
|
-
if (e.key === 'Escape' && !
|
|
1917
|
-
|
|
2483
|
+
if (e.key === 'Escape' && !modal.classList.contains('hidden')) { e.stopPropagation(); e.preventDefault(); close(); return; }
|
|
2484
|
+
// Cmd/Ctrl+, (the standard "Preferences" accelerator) toggles the settings panel from anywhere.
|
|
2485
|
+
if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && (e.key === ',' || e.code === 'Comma')) {
|
|
2486
|
+
e.preventDefault(); e.stopPropagation();
|
|
2487
|
+
if (modal.classList.contains('hidden')) open('general'); else close();
|
|
2488
|
+
}
|
|
2489
|
+
}, true);
|
|
2490
|
+
// One-click self-update (Electron only): install latest globally via the main process, then relaunch.
|
|
2491
|
+
if (updateBtn && window.monacoriUpdate && typeof window.monacoriUpdate.run === 'function') {
|
|
2492
|
+
updateBtn.addEventListener('click', function () {
|
|
2493
|
+
if (updateBtn.disabled) return;
|
|
2494
|
+
updateBtn.disabled = true;
|
|
2495
|
+
var status = document.getElementById('app-info-status');
|
|
2496
|
+
if (status) { status.textContent = t('settings.updating'); status.classList.add('has-update'); }
|
|
2497
|
+
window.monacoriUpdate.run().then(function (r) {
|
|
2498
|
+
if (r && r.ok) { if (status) status.textContent = t('settings.updated'); }
|
|
2499
|
+
else { updateBtn.disabled = false; if (status) status.textContent = t('settings.updateFailed'); }
|
|
2500
|
+
}).catch(function () { updateBtn.disabled = false; if (status) status.textContent = t('settings.updateFailed'); });
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
if (qta) qta.addEventListener('input', function () { saveMergePrompt('q', qta.value); flash(); });
|
|
2504
|
+
if (cta) cta.addEventListener('input', function () { saveMergePrompt('c', cta.value); flash(); });
|
|
2505
|
+
if (resetBtn) resetBtn.addEventListener('click', function () { saveMergePrompt('q', ''); saveMergePrompt('c', ''); fill(); flash(); });
|
|
2506
|
+
// Language: live-switch the whole UI (no reload). Persist, re-apply the static chrome, then re-render
|
|
2507
|
+
// any currently-shown dynamic text (open composer / merged modal / index status) so it follows too.
|
|
2508
|
+
var langSel = document.getElementById('settings-language');
|
|
2509
|
+
if (langSel) {
|
|
2510
|
+
langSel.value = locale;
|
|
2511
|
+
langSel.addEventListener('change', function () {
|
|
2512
|
+
var next = langSel.value === 'ko' ? 'ko' : 'en';
|
|
2513
|
+
if (next === locale) return;
|
|
2514
|
+
locale = next;
|
|
2515
|
+
persistSave(LOCALE_KEY, locale);
|
|
2516
|
+
applyI18n();
|
|
2517
|
+
// Merge-prompt placeholders are locale-dependent defaults; refresh them while the panel is open.
|
|
2518
|
+
fill();
|
|
2519
|
+
// Re-render dynamic, currently-visible text in the new locale.
|
|
2520
|
+
try { if (typeof refreshComments === 'function') refreshComments(); } catch (e) {}
|
|
2521
|
+
var mergedModal = document.getElementById('mc-modal');
|
|
2522
|
+
if (mergedModal) { var mk = mergedModal.dataset.kind || 'q'; mergedModal.remove(); openMergedView(mk); }
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
1918
2525
|
})();
|
|
1919
2526
|
|
|
1920
2527
|
function setTab(name) {
|
|
@@ -1933,7 +2540,7 @@ function ensureTreeRendered() {
|
|
|
1933
2540
|
if (!panel || !island) return;
|
|
1934
2541
|
var html = island.textContent || '';
|
|
1935
2542
|
island.parentNode && island.parentNode.removeChild(island);
|
|
1936
|
-
panel.innerHTML = '<div class="empty-nav">
|
|
2543
|
+
panel.innerHTML = '<div class="empty-nav">' + escapeHtml(t('source.buildingTree')) + '</div>';
|
|
1937
2544
|
setTimeout(function () { // let "Building…" paint before the heavy innerHTML
|
|
1938
2545
|
panel.innerHTML = html;
|
|
1939
2546
|
sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
@@ -1975,6 +2582,11 @@ function saveUiState() {
|
|
|
1975
2582
|
view: document.getElementById('source-viewer')?.classList.contains('hidden') ? 'diff' : 'source',
|
|
1976
2583
|
sourcePath,
|
|
1977
2584
|
hash: location.hash,
|
|
2585
|
+
// Preserve open tabs + the exact caret across watch reloads (otherwise the caret resets to the
|
|
2586
|
+
// hunk's first change / file top every time the working tree changes).
|
|
2587
|
+
tabs: sourceTabs,
|
|
2588
|
+
diffCursor: diffCursor,
|
|
2589
|
+
viewerCursor: viewerCursor,
|
|
1978
2590
|
}));
|
|
1979
2591
|
}
|
|
1980
2592
|
|
|
@@ -1983,13 +2595,25 @@ function restoreUiState() {
|
|
|
1983
2595
|
if (!raw) return false;
|
|
1984
2596
|
try {
|
|
1985
2597
|
const state = JSON.parse(raw);
|
|
2598
|
+
// Restore Files-mode tabs first so a watch reload doesn't drop the open tabs.
|
|
2599
|
+
if (Array.isArray(state.tabs)) sourceTabs = state.tabs.filter(function (p) { return sourceByPath.has(p); });
|
|
1986
2600
|
if (state.view === 'diff') {
|
|
1987
|
-
const match = String(state.hash || location.hash || '').match(/^#hunk-(
|
|
2601
|
+
const match = String(state.hash || location.hash || '').match(/^#hunk-(\d+)$/);
|
|
1988
2602
|
setActive(match ? Number(match[1]) : current >= 0 ? current : 0, false);
|
|
2603
|
+
// Restore the exact diff caret (setActive only lands on the hunk's first change).
|
|
2604
|
+
if (state.diffCursor && state.diffCursor.path) {
|
|
2605
|
+
var dc = state.diffCursor;
|
|
2606
|
+
setTimeout(function () { try { setDiffCursor(dc.path, dc.side, dc.rowIndex, dc.column, true); } catch (e) {} }, 60);
|
|
2607
|
+
}
|
|
1989
2608
|
return true;
|
|
1990
2609
|
}
|
|
1991
2610
|
if (state.sourcePath && sourceByPath.has(state.sourcePath)) {
|
|
1992
2611
|
openSourceFile(state.sourcePath);
|
|
2612
|
+
// Restore the exact source caret/scroll (openSourceFile alone resets it to the top).
|
|
2613
|
+
if (state.viewerCursor && state.viewerCursor.path === state.sourcePath) {
|
|
2614
|
+
var vc = state.viewerCursor;
|
|
2615
|
+
setTimeout(function () { try { setSourceCursor(state.sourcePath, vc.lineIndex, vc.column, true, -1); } catch (e) {} }, 60);
|
|
2616
|
+
}
|
|
1993
2617
|
return true;
|
|
1994
2618
|
}
|
|
1995
2619
|
} catch {
|
|
@@ -1998,6 +2622,71 @@ function restoreUiState() {
|
|
|
1998
2622
|
return false;
|
|
1999
2623
|
}
|
|
2000
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
|
+
|
|
2001
2690
|
async function checkForLiveUpdate() {
|
|
2002
2691
|
if (checkingForUpdates) return;
|
|
2003
2692
|
checkingForUpdates = true;
|
|
@@ -2007,14 +2696,18 @@ async function checkForLiveUpdate() {
|
|
|
2007
2696
|
if (!response.ok) return;
|
|
2008
2697
|
const state = await response.json();
|
|
2009
2698
|
if (liveStatus && state.generatedAt) {
|
|
2010
|
-
liveStatus.textContent = '
|
|
2699
|
+
liveStatus.textContent = t('status.live.updated') + ' ' + new Date(state.generatedAt).toLocaleTimeString();
|
|
2011
2700
|
}
|
|
2012
2701
|
if (state.signature && state.signature !== currentSignature) {
|
|
2013
|
-
|
|
2014
|
-
|
|
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) {}
|
|
2015
2708
|
}
|
|
2016
2709
|
} catch {
|
|
2017
|
-
if (liveStatus) liveStatus.textContent = '
|
|
2710
|
+
if (liveStatus) liveStatus.textContent = t('status.live.waiting');
|
|
2018
2711
|
} finally {
|
|
2019
2712
|
checkingForUpdates = false;
|
|
2020
2713
|
}
|
|
@@ -2190,7 +2883,18 @@ function measureCharWidth(element) {
|
|
|
2190
2883
|
return measuredCharWidth;
|
|
2191
2884
|
}
|
|
2192
2885
|
|
|
2886
|
+
var caretBusyTimer = null;
|
|
2887
|
+
// While the caret is actively moving (held arrow key, typing), keep it solid and only resume the
|
|
2888
|
+
// blink animation after a short idle. Otherwise key-repeat exposes the blink's "off" frames between
|
|
2889
|
+
// moves and the caret appears to vanish intermittently.
|
|
2890
|
+
function markCaretBusy() {
|
|
2891
|
+
document.body.classList.add('caret-busy');
|
|
2892
|
+
if (caretBusyTimer) clearTimeout(caretBusyTimer);
|
|
2893
|
+
caretBusyTimer = setTimeout(function () { document.body.classList.remove('caret-busy'); }, 650);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2193
2896
|
function setSourceCursor(path, lineIndex, column, shouldReveal = false, targetLine = -1) {
|
|
2897
|
+
markCaretBusy();
|
|
2194
2898
|
selectedCommentRow = null; // any explicit caret placement (click/move) ends a comment-box selection
|
|
2195
2899
|
const file = sourceByPath.get(path);
|
|
2196
2900
|
if (!file || !file.embedded) return;
|
|
@@ -2229,14 +2933,19 @@ function setSourceCursor(path, lineIndex, column, shouldReveal = false, targetLi
|
|
|
2229
2933
|
function updateSourceCaret(prev, lines, language) {
|
|
2230
2934
|
const body = document.getElementById('source-body');
|
|
2231
2935
|
if (!body) return;
|
|
2936
|
+
// Markdown/CSV render to HTML cells (.rendered-body): the caret is a whole-row highlight there,
|
|
2937
|
+
// so never rewrite a cell's innerHTML (that would replace the rendered block with raw text).
|
|
2938
|
+
const rendered = body.classList.contains('rendered-body');
|
|
2232
2939
|
const rowFor = (idx) => body.querySelector('.source-row[data-line-index="' + idx + '"]');
|
|
2233
2940
|
// Restore the line the caret left: drop the caret span, re-highlight the full line.
|
|
2234
2941
|
if (prev && prev.lineIndex !== viewerCursor.lineIndex) {
|
|
2235
2942
|
const prevRow = rowFor(prev.lineIndex);
|
|
2236
2943
|
if (prevRow) {
|
|
2237
2944
|
prevRow.classList.remove('cursor-line');
|
|
2238
|
-
|
|
2239
|
-
|
|
2945
|
+
if (!rendered) {
|
|
2946
|
+
const prevCell = prevRow.querySelector('.source-code');
|
|
2947
|
+
if (prevCell) prevCell.innerHTML = highlightLine(lines[prev.lineIndex] || '', language);
|
|
2948
|
+
}
|
|
2240
2949
|
}
|
|
2241
2950
|
}
|
|
2242
2951
|
// Reconcile the go-to-definition highlight (set only on symbol jumps, cleared on plain moves).
|
|
@@ -2244,10 +2953,12 @@ function updateSourceCaret(prev, lines, language) {
|
|
|
2244
2953
|
if (viewerCursor.targetLine >= 0) rowFor(viewerCursor.targetLine)?.classList.add('symbol-target');
|
|
2245
2954
|
// Rebuild the new caret line with the caret span.
|
|
2246
2955
|
const row = rowFor(viewerCursor.lineIndex);
|
|
2247
|
-
if (!row) { openSourceFile(viewerCursor.path, false); return; } // line not in the DOM —
|
|
2956
|
+
if (!row) { if (!rendered) openSourceFile(viewerCursor.path, false); return; } // line not in the DOM — full re-render (eager source only)
|
|
2248
2957
|
row.classList.add('cursor-line');
|
|
2249
|
-
|
|
2250
|
-
|
|
2958
|
+
if (!rendered) {
|
|
2959
|
+
const cell = row.querySelector('.source-code');
|
|
2960
|
+
if (cell) cell.innerHTML = renderLineWithCursor(lines[viewerCursor.lineIndex] || '', language, viewerCursor.column);
|
|
2961
|
+
}
|
|
2251
2962
|
}
|
|
2252
2963
|
|
|
2253
2964
|
function openSourceAt(path, lineIndex, column) {
|
|
@@ -2351,6 +3062,20 @@ function moveSourceCursor(dLine, dColumn, extend) {
|
|
|
2351
3062
|
if (!viewerCursor) return;
|
|
2352
3063
|
const file = sourceByPath.get(viewerCursor.path);
|
|
2353
3064
|
if (!file || !file.embedded) return;
|
|
3065
|
+
// Markdown/CSV rendered view: rows are blocks (sparse data-line-index), so any arrow steps to the
|
|
3066
|
+
// adjacent block row rather than into a (non-existent) raw line. No text column / selection there.
|
|
3067
|
+
const renderedBody = document.getElementById('source-body');
|
|
3068
|
+
if (renderedBody && renderedBody.classList.contains('rendered-body')) {
|
|
3069
|
+
const rows = Array.from(renderedBody.querySelectorAll('.source-row'));
|
|
3070
|
+
if (!rows.length) return;
|
|
3071
|
+
let ci = rows.indexOf(renderedBody.querySelector('.source-row[data-line-index="' + viewerCursor.lineIndex + '"]'));
|
|
3072
|
+
if (ci < 0) ci = 0;
|
|
3073
|
+
const step = (dLine || 0) + (dColumn > 0 ? 1 : dColumn < 0 ? -1 : 0);
|
|
3074
|
+
const ni = Math.max(0, Math.min(rows.length - 1, ci + (step || 0)));
|
|
3075
|
+
selectionAnchor = null;
|
|
3076
|
+
setSourceCursor(viewerCursor.path, Number(rows[ni].dataset.lineIndex) || 0, 0, true, -1);
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
2354
3079
|
const lines = file.content.split(/\r?\n/);
|
|
2355
3080
|
let line = viewerCursor.lineIndex;
|
|
2356
3081
|
let col = viewerCursor.column;
|
|
@@ -2599,6 +3324,14 @@ function symbolIndexWorker() {
|
|
|
2599
3324
|
self.postMessage({ index: index, total: total });
|
|
2600
3325
|
};
|
|
2601
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
|
+
}
|
|
2602
3335
|
function startSymbolIndex() {
|
|
2603
3336
|
try {
|
|
2604
3337
|
if (typeof Worker === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined' || !URL.createObjectURL) return;
|
|
@@ -2631,11 +3364,11 @@ function setIndexProgress(done, total) {
|
|
|
2631
3364
|
var bar = document.getElementById('index-progress');
|
|
2632
3365
|
if (!el) return;
|
|
2633
3366
|
if (!total || done >= total) {
|
|
2634
|
-
el.textContent = (total || 0) + ' indexed';
|
|
3367
|
+
el.textContent = (total || 0) + ' ' + t('status.indexed');
|
|
2635
3368
|
if (bar) bar.classList.add('hidden');
|
|
2636
3369
|
return;
|
|
2637
3370
|
}
|
|
2638
|
-
el.textContent = 'indexing ' + done + '/' + total + '…';
|
|
3371
|
+
el.textContent = t('status.indexing') + ' ' + done + '/' + total + '…';
|
|
2639
3372
|
if (bar) {
|
|
2640
3373
|
bar.classList.remove('hidden');
|
|
2641
3374
|
var fill = bar.firstElementChild;
|
|
@@ -2712,9 +3445,56 @@ function setSourceTypeIcon(path) {
|
|
|
2712
3445
|
var icon = link ? link.querySelector('.ftype') : null;
|
|
2713
3446
|
holder.innerHTML = icon ? icon.outerHTML : '';
|
|
2714
3447
|
}
|
|
3448
|
+
// Files-mode tabs: each distinct file opened in the source viewer becomes a tab (session-only).
|
|
3449
|
+
// Cmd/Ctrl+W closes the active tab; Cmd/Ctrl+Shift+[ / ] cycle tabs; the × button closes one.
|
|
3450
|
+
// (sourceTabs is declared near the other source state up top so early restore-state openSourceFile
|
|
3451
|
+
// calls run before this block don't see an undefined array.)
|
|
3452
|
+
function addSourceTab(path) { if (path && sourceTabs.indexOf(path) < 0) sourceTabs.push(path); }
|
|
3453
|
+
function sourceTabLabel(path) { var p = String(path || ''); var s = p.lastIndexOf('/'); return s >= 0 ? p.slice(s + 1) : p; }
|
|
3454
|
+
function currentSourceTabPath() { var v = document.getElementById('source-viewer'); return (v && v.dataset.openPath) || ''; }
|
|
3455
|
+
function renderSourceTabs(activePath) {
|
|
3456
|
+
var bar = document.getElementById('source-tabs');
|
|
3457
|
+
if (!bar) return;
|
|
3458
|
+
if (!sourceTabs.length) { bar.classList.add('hidden'); bar.innerHTML = ''; return; }
|
|
3459
|
+
bar.classList.remove('hidden');
|
|
3460
|
+
bar.innerHTML = sourceTabs.map(function (p) {
|
|
3461
|
+
var active = p === activePath;
|
|
3462
|
+
return '<div class="source-tab' + (active ? ' active' : '') + '" data-tab-path="' + escapeHtml(p) + '" title="' + escapeHtml(p) + '">'
|
|
3463
|
+
+ '<span class="source-tab-name">' + escapeHtml(sourceTabLabel(p)) + '</span>'
|
|
3464
|
+
+ '<button type="button" class="source-tab-close" data-close-path="' + escapeHtml(p) + '" aria-label="Close tab" title="Close (Cmd/Ctrl+W)">×</button>'
|
|
3465
|
+
+ '</div>';
|
|
3466
|
+
}).join('');
|
|
3467
|
+
var act = bar.querySelector('.source-tab.active');
|
|
3468
|
+
if (act && act.scrollIntoView) act.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
3469
|
+
}
|
|
3470
|
+
function closeSourceTab(path) {
|
|
3471
|
+
var idx = sourceTabs.indexOf(path);
|
|
3472
|
+
if (idx < 0) return;
|
|
3473
|
+
var wasActive = path === currentSourceTabPath();
|
|
3474
|
+
sourceTabs.splice(idx, 1);
|
|
3475
|
+
if (!wasActive) { renderSourceTabs(currentSourceTabPath()); return; }
|
|
3476
|
+
var nextPath = sourceTabs[idx] || sourceTabs[idx - 1] || '';
|
|
3477
|
+
if (nextPath) { openSourceFile(nextPath); return; }
|
|
3478
|
+
// No tabs left: reset the source view to its empty state.
|
|
3479
|
+
var v = document.getElementById('source-viewer'); if (v) v.dataset.openPath = '';
|
|
3480
|
+
var body = document.getElementById('source-body');
|
|
3481
|
+
if (body) { body.className = 'source-body empty'; body.textContent = t('source.selectFile'); }
|
|
3482
|
+
sourceLinks.forEach(function (l) { l.classList.remove('active'); });
|
|
3483
|
+
renderSourceTabs('');
|
|
3484
|
+
}
|
|
3485
|
+
function closeActiveSourceTab() { var p = currentSourceTabPath(); if (p) { closeSourceTab(p); return true; } return false; }
|
|
3486
|
+
function cycleSourceTab(dir) {
|
|
3487
|
+
if (sourceTabs.length < 2) return;
|
|
3488
|
+
var cur = sourceTabs.indexOf(currentSourceTabPath());
|
|
3489
|
+
if (cur < 0) cur = 0;
|
|
3490
|
+
openSourceFile(sourceTabs[(cur + dir + sourceTabs.length) % sourceTabs.length]);
|
|
3491
|
+
}
|
|
3492
|
+
|
|
2715
3493
|
function openSourceFile(path, shouldSwitch = true) {
|
|
2716
3494
|
const file = sourceByPath.get(path);
|
|
2717
3495
|
if (!file) return;
|
|
3496
|
+
addSourceTab(path);
|
|
3497
|
+
renderSourceTabs(path);
|
|
2718
3498
|
// lazy-LOAD: source content not fetched yet -> show a loading state; loadSourceData re-opens it.
|
|
2719
3499
|
if (REVIEW_LAZY_LOAD && !sourceLoaded && file.embedded) {
|
|
2720
3500
|
pendingSourceOpen = { path: path, shouldSwitch: shouldSwitch };
|
|
@@ -2726,7 +3506,7 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2726
3506
|
revealTreeFor(path);
|
|
2727
3507
|
var lb = document.getElementById('source-body');
|
|
2728
3508
|
lb.className = 'source-body empty';
|
|
2729
|
-
lb.textContent = '
|
|
3509
|
+
lb.textContent = t('source.loading');
|
|
2730
3510
|
if (shouldSwitch) showSourceView();
|
|
2731
3511
|
return;
|
|
2732
3512
|
}
|
|
@@ -2736,12 +3516,9 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2736
3516
|
renderBreadcrumb(document.getElementById('source-title'), path);
|
|
2737
3517
|
setSourceTypeIcon(path);
|
|
2738
3518
|
revealTreeFor(path);
|
|
2739
|
-
const meta =
|
|
2740
|
-
file.
|
|
2741
|
-
formatBytes(file.size || 0)
|
|
2742
|
-
file.changed ? 'changed' : 'unchanged',
|
|
2743
|
-
file.embedded ? 'searchable' : file.skippedReason || 'not embedded',
|
|
2744
|
-
].join(' | ');
|
|
3519
|
+
const meta = file.embedded
|
|
3520
|
+
? formatBytes(file.size || 0)
|
|
3521
|
+
: formatBytes(file.size || 0) + ' · ' + (file.skippedReason || 'not embedded');
|
|
2745
3522
|
document.getElementById('source-meta').textContent = meta;
|
|
2746
3523
|
const body = document.getElementById('source-body');
|
|
2747
3524
|
// Image files carry a data: URI preview instead of text — render inline (click to zoom).
|
|
@@ -2755,7 +3532,7 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2755
3532
|
}
|
|
2756
3533
|
if (!file.embedded) {
|
|
2757
3534
|
body.className = 'source-body empty';
|
|
2758
|
-
body.textContent = file.skippedReason ? '
|
|
3535
|
+
body.textContent = file.skippedReason ? t('source.previewUnavailable').replace(/\.$/, '') + ': ' + file.skippedReason + '.' : t('source.previewUnavailable');
|
|
2759
3536
|
document.getElementById('http-env-select')?.classList.add('hidden');
|
|
2760
3537
|
updateRenderToggle(path);
|
|
2761
3538
|
if (shouldSwitch) showSourceView();
|
|
@@ -2770,17 +3547,27 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2770
3547
|
// is a .source-row keyed by its start line, so the gutter shows line numbers and line/block comments
|
|
2771
3548
|
// work exactly as in the plain source view (renderSourceComments anchors on .source-row[data-line-index]).
|
|
2772
3549
|
if (isMarkdownPath(path)) {
|
|
2773
|
-
|
|
2774
|
-
|
|
3550
|
+
if (renderRawMode) {
|
|
3551
|
+
body.innerHTML = renderSourceTable(file, '');
|
|
3552
|
+
} else {
|
|
3553
|
+
body.classList.add('rendered-body');
|
|
3554
|
+
body.innerHTML = renderMarkdownRows(file.content);
|
|
3555
|
+
}
|
|
2775
3556
|
if (httpEnvSelect) httpEnvSelect.classList.add('hidden');
|
|
3557
|
+
updateRenderToggle(path);
|
|
2776
3558
|
renderSourceComments();
|
|
2777
3559
|
if (shouldSwitch) showSourceView();
|
|
2778
3560
|
return;
|
|
2779
3561
|
}
|
|
2780
3562
|
if (isCsvPath(path)) {
|
|
2781
|
-
|
|
2782
|
-
|
|
3563
|
+
if (renderRawMode) {
|
|
3564
|
+
body.innerHTML = renderSourceTable(file, '');
|
|
3565
|
+
} else {
|
|
3566
|
+
body.classList.add('rendered-body');
|
|
3567
|
+
body.innerHTML = renderCsvRows(file.content, path);
|
|
3568
|
+
}
|
|
2783
3569
|
if (httpEnvSelect) httpEnvSelect.classList.add('hidden');
|
|
3570
|
+
updateRenderToggle(path);
|
|
2784
3571
|
renderSourceComments();
|
|
2785
3572
|
if (shouldSwitch) showSourceView();
|
|
2786
3573
|
return;
|
|
@@ -2792,12 +3579,45 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2792
3579
|
body.innerHTML = renderSourceTable(file, '');
|
|
2793
3580
|
if (httpEnvSelect) httpEnvSelect.classList.add('hidden');
|
|
2794
3581
|
}
|
|
3582
|
+
updateRenderToggle(path);
|
|
2795
3583
|
renderSourceComments();
|
|
2796
3584
|
if (shouldSwitch) showSourceView();
|
|
2797
3585
|
}
|
|
2798
3586
|
|
|
2799
3587
|
function isMarkdownPath(p) { return /\.(md|mdx|markdown)$/i.test(p || ''); }
|
|
2800
3588
|
function isCsvPath(p) { return /\.(csv|tsv)$/i.test(p || ''); }
|
|
3589
|
+
function isRenderToggleable(p) { return isMarkdownPath(p) || isCsvPath(p); }
|
|
3590
|
+
|
|
3591
|
+
// Markdown/CSV open rendered by default; this flips the open file to raw line-numbered text and back.
|
|
3592
|
+
// Session-global so the choice carries across files. The toolbar button + Cmd/Ctrl+Shift+M both call it.
|
|
3593
|
+
var renderRawMode = false;
|
|
3594
|
+
function updateRenderToggle(path) {
|
|
3595
|
+
var btn = document.getElementById('render-toggle');
|
|
3596
|
+
if (!btn) return;
|
|
3597
|
+
var on = isRenderToggleable(path);
|
|
3598
|
+
btn.classList.toggle('hidden', !on);
|
|
3599
|
+
if (!on) return;
|
|
3600
|
+
btn.textContent = renderRawMode ? t('source.viewRendered') : t('source.viewRaw'); // label = the mode you switch TO
|
|
3601
|
+
btn.setAttribute('aria-pressed', renderRawMode ? 'true' : 'false');
|
|
3602
|
+
}
|
|
3603
|
+
function toggleRenderMode() {
|
|
3604
|
+
var sv = document.getElementById('source-viewer');
|
|
3605
|
+
var open = sv && sv.dataset.openPath;
|
|
3606
|
+
if (!open || !isRenderToggleable(open)) return;
|
|
3607
|
+
renderRawMode = !renderRawMode;
|
|
3608
|
+
openSourceFile(open, false); // re-render the current file in the new mode
|
|
3609
|
+
}
|
|
3610
|
+
(function wireRenderToggle() {
|
|
3611
|
+
var btn = document.getElementById('render-toggle');
|
|
3612
|
+
if (btn) btn.addEventListener('click', function () { toggleRenderMode(); });
|
|
3613
|
+
document.addEventListener('keydown', function (e) {
|
|
3614
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && !e.altKey && (e.key === 'M' || e.key === 'm' || e.code === 'KeyM')) {
|
|
3615
|
+
var sv = document.getElementById('source-viewer');
|
|
3616
|
+
var open = sv && sv.dataset.openPath;
|
|
3617
|
+
if (open && isRenderToggleable(open) && isSourceViewerVisible()) { e.preventDefault(); toggleRenderMode(); }
|
|
3618
|
+
}
|
|
3619
|
+
});
|
|
3620
|
+
})();
|
|
2801
3621
|
|
|
2802
3622
|
function renderImageView(file) {
|
|
2803
3623
|
return '<div class="image-view">'
|
|
@@ -3218,16 +4038,21 @@ function populateHttpEnvSelect() {
|
|
|
3218
4038
|
opts += '<option value="' + escapeHtml(name) + '"' + (name === currentHttpEnvName ? ' selected' : '') + '>' + escapeHtml(name) + '</option>';
|
|
3219
4039
|
});
|
|
3220
4040
|
select.innerHTML = opts;
|
|
3221
|
-
select
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
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
|
+
}
|
|
3231
4056
|
}
|
|
3232
4057
|
|
|
3233
4058
|
function renderSourceTable(file, query) {
|