@happy-nut/monacori 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -132
- package/assets/screenshots/diff-review.png +0 -0
- package/assets/screenshots/terminal.png +0 -0
- package/dist/app-main.js +163 -12
- package/dist/assets.d.ts +2 -0
- package/dist/assets.js +21 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +7 -4
- package/dist/diff.js +41 -0
- package/dist/i18n.d.ts +1 -0
- package/dist/i18n.js +256 -0
- package/dist/preload.cjs +61 -0
- package/dist/render.d.ts +1 -0
- package/dist/render.js +91 -19
- package/dist/types.d.ts +2 -0
- package/dist/viewer.client.js +693 -101
- package/dist/viewer.css +194 -39
- package/package.json +6 -2
- package/scripts/patch-electron-name.mjs +8 -0
package/dist/viewer.client.js
CHANGED
|
@@ -116,6 +116,37 @@ if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true
|
|
|
116
116
|
const links = Array.from(document.querySelectorAll('#changes-panel .file-link'));
|
|
117
117
|
let sourceLinks = Array.from(document.querySelectorAll('.source-link')); // re-captured when a deferred tree materializes
|
|
118
118
|
const 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
|
+
}
|
|
119
150
|
const fileStates = JSON.parse(document.getElementById('file-state-data')?.textContent || '[]');
|
|
120
151
|
const httpEnvironments = JSON.parse(document.getElementById('http-env-data')?.textContent || '{}');
|
|
121
152
|
const httpEnvNames = Object.keys(httpEnvironments);
|
|
@@ -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() {
|
|
@@ -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,6 +439,22 @@ 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
|
+
|
|
410
458
|
function setActive(index, shouldScroll = true) {
|
|
411
459
|
if (hunkTotal() === 0) return;
|
|
412
460
|
current = ((index % hunkTotal()) + hunkTotal()) % hunkTotal();
|
|
@@ -438,15 +486,14 @@ function setActive(index, shouldScroll = true) {
|
|
|
438
486
|
// F7/change navigation moves the caret but must NOT pollute the Cmd+[/] cursor history.
|
|
439
487
|
navSuppress = true;
|
|
440
488
|
try { focusDiffRow(targetRow); } finally { navSuppress = false; }
|
|
441
|
-
if (shouldScroll && targetRow) targetRow
|
|
489
|
+
if (shouldScroll && targetRow) scheduleDiffScroll(targetRow);
|
|
442
490
|
});
|
|
443
491
|
}
|
|
444
492
|
|
|
445
493
|
function showOnlyFile(fileName) {
|
|
446
494
|
if (REVIEW_LAZY) ensureFileReady(diffWrapperByPath(fileName));
|
|
447
495
|
document.querySelectorAll('.d2h-file-wrapper').forEach((wrapper) => {
|
|
448
|
-
|
|
449
|
-
wrapper.classList.toggle('df-inactive', name !== fileName);
|
|
496
|
+
wrapper.classList.toggle('df-inactive', diffWrapperPathKey(wrapper) !== fileName);
|
|
450
497
|
});
|
|
451
498
|
ensureDiffCursor();
|
|
452
499
|
}
|
|
@@ -474,8 +521,10 @@ function hunkIndexAtCaret() {
|
|
|
474
521
|
// New-side row indices, one per change block — a run of change rows (ins/del) separated by context.
|
|
475
522
|
// A wide context window merges several edits into one @@ hunk; stepping by these stops at each edit.
|
|
476
523
|
function changeBlockAnchors(wrapper) {
|
|
524
|
+
if (!wrapper) return [];
|
|
525
|
+
if (wrapper.__anchors) return wrapper.__anchors;
|
|
477
526
|
var right = diffSideTables(wrapper).right;
|
|
478
|
-
if (!right) return [];
|
|
527
|
+
if (!right) return []; // body not materialized yet — don't cache an empty result
|
|
479
528
|
var rows = diffRowsOf(right);
|
|
480
529
|
var anchors = [];
|
|
481
530
|
var prev = false;
|
|
@@ -484,6 +533,7 @@ function changeBlockAnchors(wrapper) {
|
|
|
484
533
|
if (chg && !prev) anchors.push(i);
|
|
485
534
|
prev = chg;
|
|
486
535
|
}
|
|
536
|
+
wrapper.__anchors = anchors; // change-block layout is static once materialized
|
|
487
537
|
return anchors;
|
|
488
538
|
}
|
|
489
539
|
|
|
@@ -501,7 +551,7 @@ function next(delta) {
|
|
|
501
551
|
else { for (let b = anchors.length - 1; b >= 0; b--) { if (anchors[b] < cur) { target = anchors[b]; break; } } }
|
|
502
552
|
if (target != null) {
|
|
503
553
|
const row = diffRowAt(w, 'new', target);
|
|
504
|
-
if (row) { navSuppress = true; try { focusDiffRow(row); } finally { navSuppress = false; } row
|
|
554
|
+
if (row) { navSuppress = true; try { focusDiffRow(row); } finally { navSuppress = false; } scheduleDiffScroll(row); return; }
|
|
505
555
|
}
|
|
506
556
|
}
|
|
507
557
|
}
|
|
@@ -535,7 +585,7 @@ function firstHunkForPath(path) {
|
|
|
535
585
|
function openQuickOpen(mode) {
|
|
536
586
|
if (!quickOpen || !quickInput || !quickModeLabel) return;
|
|
537
587
|
quickMode = mode;
|
|
538
|
-
quickModeLabel.textContent = mode === 'recent' ? '
|
|
588
|
+
quickModeLabel.textContent = mode === 'recent' ? t('quickopen.recent') : mode === 'content' ? t('quickopen.findInFiles') : t('quickopen.searchFiles');
|
|
539
589
|
quickOpen.classList.remove('hidden');
|
|
540
590
|
quickInput.value = '';
|
|
541
591
|
renderQuickOpenResults();
|
|
@@ -590,7 +640,7 @@ function renderQuickOpenResults() {
|
|
|
590
640
|
.slice(0, 80);
|
|
591
641
|
quickActive = Math.min(quickActive, Math.max(quickItems.length - 1, 0));
|
|
592
642
|
if (quickItems.length === 0) {
|
|
593
|
-
quickResults.innerHTML = '<div class="quick-open-empty">
|
|
643
|
+
quickResults.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t('quickopen.noFiles')) + '</div>';
|
|
594
644
|
return;
|
|
595
645
|
}
|
|
596
646
|
quickResults.innerHTML = quickItems.map((item, index) => [
|
|
@@ -1063,6 +1113,9 @@ document.addEventListener('keydown', (event) => {
|
|
|
1063
1113
|
}
|
|
1064
1114
|
|
|
1065
1115
|
// Cmd/Ctrl+[ / ] walk the cursor-position history (back / forward), like an editor's Go Back/Forward.
|
|
1116
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && (event.key === '[' || event.key === ']' || event.key === '{' || event.key === '}')) {
|
|
1117
|
+
if (isSourceViewerVisible() && sourceTabs.length > 1) { event.preventDefault(); cycleSourceTab((event.key === '[' || event.key === '{') ? -1 : 1); return; }
|
|
1118
|
+
}
|
|
1066
1119
|
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && (event.key === '[' || event.key === ']')) {
|
|
1067
1120
|
var navEl = document.activeElement;
|
|
1068
1121
|
var navInField = navEl && (navEl.tagName === 'INPUT' || navEl.tagName === 'TEXTAREA' || navEl.tagName === 'SELECT');
|
|
@@ -1139,6 +1192,12 @@ document.querySelectorAll('.tab').forEach((button) => {
|
|
|
1139
1192
|
});
|
|
1140
1193
|
|
|
1141
1194
|
document.getElementById('back-to-diff')?.addEventListener('click', () => showDiffView(true));
|
|
1195
|
+
document.getElementById('source-tabs')?.addEventListener('click', function (event) {
|
|
1196
|
+
var closeBtn = event.target && event.target.closest && event.target.closest('.source-tab-close');
|
|
1197
|
+
if (closeBtn) { event.stopPropagation(); event.preventDefault(); closeSourceTab(closeBtn.getAttribute('data-close-path')); return; }
|
|
1198
|
+
var tab = event.target && event.target.closest && event.target.closest('.source-tab');
|
|
1199
|
+
if (tab) openSourceFile(tab.getAttribute('data-tab-path'));
|
|
1200
|
+
});
|
|
1142
1201
|
document.getElementById('diff-viewed-toggle')?.addEventListener('click', function () {
|
|
1143
1202
|
var btn = document.getElementById('diff-viewed-toggle');
|
|
1144
1203
|
var path = btn ? (btn.dataset.file || '') : '';
|
|
@@ -1154,11 +1213,12 @@ document.addEventListener('keydown', function (event) {
|
|
|
1154
1213
|
}, true);
|
|
1155
1214
|
document.addEventListener('copy', handleSourceCopy);
|
|
1156
1215
|
|
|
1216
|
+
applyI18n(); // first paint already shows English (inline); this swaps to the saved locale before the rest of init renders dynamic text
|
|
1157
1217
|
populateHttpEnvSelect();
|
|
1158
1218
|
if (!REVIEW_LAZY_LOAD) setTimeout(startSymbolIndex, 0); // non-lazy indexes now; lazy-LOAD defers the (large) source blob + index to the first source-view open / go-to-def
|
|
1159
1219
|
const restored = restoreUiState();
|
|
1160
1220
|
if (!restored) {
|
|
1161
|
-
const initial = location.hash.match(/^#hunk-(
|
|
1221
|
+
const initial = location.hash.match(/^#hunk-(\d+)$/);
|
|
1162
1222
|
if (initial) setActive(Number(initial[1]), false);
|
|
1163
1223
|
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
1224
|
else openDefaultSourceFile();
|
|
@@ -1234,13 +1294,26 @@ function diffActiveWrapper() {
|
|
|
1234
1294
|
return document.querySelector('#diff2html-container .d2h-file-wrapper:not(.df-inactive)')
|
|
1235
1295
|
|| document.querySelector('#diff2html-container .d2h-file-wrapper');
|
|
1236
1296
|
}
|
|
1297
|
+
// path -> wrapper, O(1) after the first build. Rebuilt only on a miss/disconnect
|
|
1298
|
+
// (the wrapper set is stable; only bodies materialize). This is called several times
|
|
1299
|
+
// per F7 press, so the old O(files) querySelector scan made each keystroke cost scale
|
|
1300
|
+
// with the file count — the main source of cross-file nav stutter on big diffs.
|
|
1301
|
+
var wrapperPathMap = null;
|
|
1302
|
+
function diffWrapperPathKey(w) {
|
|
1303
|
+
return (w.dataset && w.dataset.path) || ((w.querySelector('.d2h-file-name') || {}).textContent || '').trim();
|
|
1304
|
+
}
|
|
1237
1305
|
function diffWrapperByPath(path) {
|
|
1306
|
+
if (wrapperPathMap) {
|
|
1307
|
+
var hit = wrapperPathMap.get(path);
|
|
1308
|
+
if (hit && hit.isConnected) return hit;
|
|
1309
|
+
}
|
|
1310
|
+
wrapperPathMap = new Map();
|
|
1238
1311
|
var ws = document.querySelectorAll('#diff2html-container .d2h-file-wrapper');
|
|
1239
1312
|
for (var i = 0; i < ws.length; i++) {
|
|
1240
|
-
var
|
|
1241
|
-
if (
|
|
1313
|
+
var key = diffWrapperPathKey(ws[i]);
|
|
1314
|
+
if (key) wrapperPathMap.set(key, ws[i]);
|
|
1242
1315
|
}
|
|
1243
|
-
return null;
|
|
1316
|
+
return wrapperPathMap.get(path) || null;
|
|
1244
1317
|
}
|
|
1245
1318
|
function diffSideTables(wrapper) {
|
|
1246
1319
|
var sides = wrapper ? wrapper.querySelectorAll('.d2h-file-side-diff') : [];
|
|
@@ -1338,6 +1411,7 @@ function renderDiffCaret() {
|
|
|
1338
1411
|
} catch (e) { diffCaretSpan = null; }
|
|
1339
1412
|
}
|
|
1340
1413
|
function setDiffCursor(path, side, rowIndex, column, reveal) {
|
|
1414
|
+
markCaretBusy();
|
|
1341
1415
|
var wrapper = diffWrapperByPath(path);
|
|
1342
1416
|
if (!wrapper) return;
|
|
1343
1417
|
var rows = diffRowsOf(diffSideTable(wrapper, side));
|
|
@@ -1513,13 +1587,13 @@ function handleDiffCaretKey(event) {
|
|
|
1513
1587
|
// ===== Review comments: questions ("?") and change-requests (">") =====
|
|
1514
1588
|
// (COMMENTS_KEY / reviewComments / commentSeq / composerState are declared near the top of the script)
|
|
1515
1589
|
function saveComments() {
|
|
1516
|
-
|
|
1590
|
+
persistSave(COMMENTS_KEY, reviewComments);
|
|
1517
1591
|
}
|
|
1518
1592
|
function commentsAt(path, line) {
|
|
1519
1593
|
return reviewComments.filter(function (c) { return c.path === path && c.line === line; });
|
|
1520
1594
|
}
|
|
1521
1595
|
function commentKindLabel(kind) {
|
|
1522
|
-
return kind === 'q' ? '
|
|
1596
|
+
return kind === 'q' ? t('comment.kind.q') : t('comment.kind.c');
|
|
1523
1597
|
}
|
|
1524
1598
|
function relevantLines(path) {
|
|
1525
1599
|
var set = {};
|
|
@@ -1607,17 +1681,17 @@ function threadHtml(path, line) {
|
|
|
1607
1681
|
commentsAt(path, line).forEach(function (c) {
|
|
1608
1682
|
html += '<div class="mc-card mc-' + c.kind + '">'
|
|
1609
1683
|
+ '<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="
|
|
1684
|
+
+ '<button type="button" class="mc-del" data-seq="' + c.seq + '" title="' + escapeHtml(t('composer.delete')) + '">×</button></div>'
|
|
1611
1685
|
+ '<div class="mc-card-body">' + escapeHtml(c.text) + '</div></div>';
|
|
1612
1686
|
});
|
|
1613
1687
|
if (composerState && composerState.path === path && composerState.line === line) {
|
|
1614
|
-
var ph = composerState.kind === 'q' ? '
|
|
1688
|
+
var ph = composerState.kind === 'q' ? t('composer.question') : t('composer.changeRequest');
|
|
1615
1689
|
html += '<div class="mc-card mc-' + composerState.kind + ' mc-composer">'
|
|
1616
1690
|
+ '<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">
|
|
1691
|
+
+ '<textarea class="mc-input" rows="3" placeholder="' + escapeHtml(ph) + '"></textarea>'
|
|
1692
|
+
+ '<div class="mc-actions"><button type="button" class="mc-btn mc-save">' + escapeHtml(t('composer.save')) + '</button>'
|
|
1693
|
+
+ '<button type="button" class="mc-btn mc-ghost mc-cancel">' + escapeHtml(t('composer.cancel')) + '</button>'
|
|
1694
|
+
+ '<span class="mc-hint">' + escapeHtml(t('composer.hint')) + '</span></div></div>';
|
|
1621
1695
|
}
|
|
1622
1696
|
return html;
|
|
1623
1697
|
}
|
|
@@ -1684,8 +1758,8 @@ function renderCommentBadges() {
|
|
|
1684
1758
|
var badge = document.createElement('span');
|
|
1685
1759
|
badge.className = 'mc-file-badge';
|
|
1686
1760
|
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 + '
|
|
1761
|
+
if (k.q) html += '<span class="mc-fb mc-fb-q" title="' + k.q + ' ' + escapeHtml(t('badge.questions')) + '">' + k.q + '</span>';
|
|
1762
|
+
if (k.c) html += '<span class="mc-fb mc-fb-c" title="' + k.c + ' ' + escapeHtml(t('badge.changeRequests')) + '">' + k.c + '</span>';
|
|
1689
1763
|
badge.innerHTML = html;
|
|
1690
1764
|
return badge;
|
|
1691
1765
|
}
|
|
@@ -1727,18 +1801,23 @@ function refreshComments() {
|
|
|
1727
1801
|
renderCommentBadges();
|
|
1728
1802
|
applyCommentSelectionHighlight();
|
|
1729
1803
|
if (composerState) {
|
|
1730
|
-
var
|
|
1804
|
+
var composerFocusTries = 0;
|
|
1805
|
+
var tryFocusComposer = function () {
|
|
1731
1806
|
var ta = document.querySelector('.mc-composer .mc-input');
|
|
1732
|
-
if (ta
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
}
|
|
1807
|
+
if (!ta) return true; // composer gone — stop retrying
|
|
1808
|
+
if (document.activeElement === ta) return true; // already focused — done
|
|
1809
|
+
try { ta.focus({ preventScroll: true }); } catch (e) { try { ta.focus(); } catch (e2) {} }
|
|
1810
|
+
try { ta.selectionStart = ta.selectionEnd = ta.value.length; } catch (e3) {}
|
|
1811
|
+
return document.activeElement === ta;
|
|
1736
1812
|
};
|
|
1737
|
-
//
|
|
1738
|
-
// the
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1813
|
+
// A one-shot focus works in a plain browser, but Electron asynchronously restores focus to <body>
|
|
1814
|
+
// after the keydown, so the textarea loses that race. Retry on a short interval until it wins (or the
|
|
1815
|
+
// composer closes), capped at ~300ms so it never fights real user focus once they start typing.
|
|
1816
|
+
if (!tryFocusComposer()) {
|
|
1817
|
+
var composerFocusIv = setInterval(function () {
|
|
1818
|
+
if (tryFocusComposer() || ++composerFocusTries > 12) clearInterval(composerFocusIv);
|
|
1819
|
+
}, 25);
|
|
1820
|
+
}
|
|
1742
1821
|
}
|
|
1743
1822
|
}
|
|
1744
1823
|
|
|
@@ -1765,18 +1844,34 @@ function saveComposer(ta) {
|
|
|
1765
1844
|
refreshComments();
|
|
1766
1845
|
}
|
|
1767
1846
|
|
|
1847
|
+
// Default merge-prompt headings, localized: a Korean user gets Korean defaults. Editable in
|
|
1848
|
+
// Settings → Merge prompts (stored per browser in localStorage); buildMergedText + the textarea
|
|
1849
|
+
// placeholders fall back to these when the stored value is empty.
|
|
1850
|
+
function defaultMergePrompt(kind) {
|
|
1851
|
+
return t(kind === 'q' ? 'mergePrompt.default.q' : 'mergePrompt.default.c');
|
|
1852
|
+
}
|
|
1853
|
+
var mergePromptsKey = 'monacori-merge-prompts';
|
|
1854
|
+
function loadMergePrompts() {
|
|
1855
|
+
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 {}; }
|
|
1856
|
+
}
|
|
1857
|
+
function mergePromptFor(kind) {
|
|
1858
|
+
var v = loadMergePrompts()[kind];
|
|
1859
|
+
return (typeof v === 'string' && v.trim()) ? v : defaultMergePrompt(kind);
|
|
1860
|
+
}
|
|
1861
|
+
function saveMergePrompt(kind, text) {
|
|
1862
|
+
var saved = loadMergePrompts();
|
|
1863
|
+
if (text && text.trim()) saved[kind] = text; else delete saved[kind];
|
|
1864
|
+
persistSave(mergePromptsKey, saved);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1768
1867
|
function buildMergedText(kind) {
|
|
1769
1868
|
var items = reviewComments.filter(function (c) { return c.kind === kind; });
|
|
1770
1869
|
var nl = String.fromCharCode(10);
|
|
1771
1870
|
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.');
|
|
1871
|
+
// Per-kind agent contract heading (editable in Settings → Merge prompts; default otherwise).
|
|
1872
|
+
lines.push(mergePromptFor(kind));
|
|
1778
1873
|
lines.push('');
|
|
1779
|
-
lines.push((kind === 'q' ? '
|
|
1874
|
+
lines.push((kind === 'q' ? t('merged.qHeading') : t('merged.cHeading')) + ' (' + items.length + ')');
|
|
1780
1875
|
lines.push('');
|
|
1781
1876
|
items.forEach(function (c) {
|
|
1782
1877
|
lines.push('### ' + c.path + ':' + c.line);
|
|
@@ -1793,35 +1888,39 @@ function openMergedView(kind) {
|
|
|
1793
1888
|
var modal = document.createElement('div');
|
|
1794
1889
|
modal.id = 'mc-modal';
|
|
1795
1890
|
modal.className = 'mc-modal';
|
|
1891
|
+
modal.dataset.kind = kind; // remembered so a live locale switch can re-render this same view
|
|
1796
1892
|
var panel = document.createElement('div');
|
|
1797
1893
|
panel.className = 'mc-modal-panel';
|
|
1798
1894
|
var head = document.createElement('div');
|
|
1799
1895
|
head.className = 'mc-modal-head';
|
|
1800
1896
|
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';
|
|
1897
|
+
title.textContent = kind === 'q' ? t('merged.qTitle') : t('merged.cTitle');
|
|
1806
1898
|
var closeBtn = document.createElement('button');
|
|
1807
1899
|
closeBtn.type = 'button';
|
|
1808
1900
|
closeBtn.className = 'mc-btn mc-ghost';
|
|
1809
|
-
closeBtn.textContent = '
|
|
1901
|
+
closeBtn.textContent = t('merged.close');
|
|
1810
1902
|
var area = document.createElement('textarea');
|
|
1811
1903
|
area.className = 'mc-modal-text';
|
|
1812
1904
|
area.readOnly = true;
|
|
1813
1905
|
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
1906
|
closeBtn.addEventListener('click', function () { modal.remove(); });
|
|
1907
|
+
// Terminal send (Electron, terminal open): close the modal and hand off to pane-pick mode ON the
|
|
1908
|
+
// terminal — the chosen pane is highlighted, the rest dimmed, arrows change the choice, Enter sends.
|
|
1909
|
+
// One button here; the actual pick happens visually over the live claude/codex sessions.
|
|
1910
|
+
var sendBtn = null;
|
|
1911
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.isOpen === 'function' && window.__monacoriTerminal.isOpen()) {
|
|
1912
|
+
sendBtn = document.createElement('button');
|
|
1913
|
+
sendBtn.type = 'button';
|
|
1914
|
+
sendBtn.className = 'mc-btn mc-send-term';
|
|
1915
|
+
sendBtn.textContent = t('merged.sendToTerminal');
|
|
1916
|
+
sendBtn.addEventListener('click', function () {
|
|
1917
|
+
var text = buildMergedText(kind);
|
|
1918
|
+
modal.remove();
|
|
1919
|
+
window.__monacoriTerminal.enterSendMode(text);
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1823
1922
|
head.appendChild(title);
|
|
1824
|
-
head.appendChild(
|
|
1923
|
+
if (sendBtn) head.appendChild(sendBtn);
|
|
1825
1924
|
head.appendChild(closeBtn);
|
|
1826
1925
|
panel.appendChild(head);
|
|
1827
1926
|
panel.appendChild(area);
|
|
@@ -1829,7 +1928,19 @@ function openMergedView(kind) {
|
|
|
1829
1928
|
modal.addEventListener('mousedown', function (e) { if (e.target === modal) modal.remove(); });
|
|
1830
1929
|
modal.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); modal.remove(); } });
|
|
1831
1930
|
document.body.appendChild(modal);
|
|
1832
|
-
|
|
1931
|
+
// Focus the send button (Enter starts pane-pick) when present, else the read-only text. Electron
|
|
1932
|
+
// async-restores focus to <body>, so retry briefly (same as the composer).
|
|
1933
|
+
var modalFocusTarget = sendBtn || area;
|
|
1934
|
+
var modalFocusTries = 0;
|
|
1935
|
+
var tryFocusModal = function () {
|
|
1936
|
+
if (!document.getElementById('mc-modal')) return true;
|
|
1937
|
+
if (document.activeElement === modalFocusTarget) return true;
|
|
1938
|
+
try { modalFocusTarget.focus(); if (modalFocusTarget === area) modalFocusTarget.select(); } catch (e) {}
|
|
1939
|
+
return document.activeElement === modalFocusTarget;
|
|
1940
|
+
};
|
|
1941
|
+
if (!tryFocusModal()) {
|
|
1942
|
+
var modalFocusIv = setInterval(function () { if (tryFocusModal() || ++modalFocusTries > 12) clearInterval(modalFocusIv); }, 25);
|
|
1943
|
+
}
|
|
1833
1944
|
}
|
|
1834
1945
|
|
|
1835
1946
|
document.addEventListener('click', function (event) {
|
|
@@ -1849,11 +1960,293 @@ document.addEventListener('keydown', function (event) {
|
|
|
1849
1960
|
|
|
1850
1961
|
refreshComments();
|
|
1851
1962
|
|
|
1963
|
+
|
|
1964
|
+
// Integrated terminal (Electron only): xterm panes wired to node-pty sessions in the main process.
|
|
1965
|
+
// Toggle with Ctrl+` / Opt+F12 / the footer ⌗ button; Cmd/Ctrl+D splits the active pane (side by side,
|
|
1966
|
+
// no tabs); drag the top edge to resize. window.__monacoriTerminal pipes the merged prompt into the
|
|
1967
|
+
// active pane. Cmd combos are released back to the app so shortcuts like Cmd+1 don't get stuck typing.
|
|
1968
|
+
(function setupTerminal() {
|
|
1969
|
+
if (!window.monacoriPty) return; // xterm (window.Terminal) is loaded lazily on first open
|
|
1970
|
+
var panel = document.getElementById('terminal-panel');
|
|
1971
|
+
var host = document.getElementById('terminal-host');
|
|
1972
|
+
var toggleBtn = document.getElementById('terminal-toggle');
|
|
1973
|
+
var closeBtn = document.getElementById('terminal-close');
|
|
1974
|
+
var resizer = panel ? panel.querySelector('.terminal-resizer') : null;
|
|
1975
|
+
if (!panel || !host) return;
|
|
1976
|
+
if (toggleBtn) toggleBtn.classList.remove('hidden'); // reveal the footer toggle in Electron
|
|
1977
|
+
|
|
1978
|
+
// xterm ships as an inert island (id=xterm-code) so ~490KB isn't parsed at startup. Inject it on the
|
|
1979
|
+
// first open; returns false if unavailable (e.g. the island is absent), so callers can bail gracefully.
|
|
1980
|
+
function ensureXterm() {
|
|
1981
|
+
if (typeof window.Terminal === 'function') return true;
|
|
1982
|
+
var code = document.getElementById('xterm-code');
|
|
1983
|
+
if (!code) return false;
|
|
1984
|
+
try {
|
|
1985
|
+
var s = document.createElement('script');
|
|
1986
|
+
s.textContent = code.textContent;
|
|
1987
|
+
document.head.appendChild(s);
|
|
1988
|
+
code.remove(); // free the inert text once compiled
|
|
1989
|
+
} catch (e) { return false; }
|
|
1990
|
+
return typeof window.Terminal === 'function';
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
var panes = []; // { id, term, fit, el }
|
|
1994
|
+
var active = null;
|
|
1995
|
+
var MAX_PANES = 4;
|
|
1996
|
+
var heightKey = 'monacori-terminal-height';
|
|
1997
|
+
var openKey = 'monacori-terminal-open:' + location.pathname;
|
|
1998
|
+
|
|
1999
|
+
function applyHeight(px) {
|
|
2000
|
+
var h = Math.max(120, Math.min(px, window.innerHeight - 120));
|
|
2001
|
+
document.documentElement.style.setProperty('--terminal-height', h + 'px');
|
|
2002
|
+
}
|
|
2003
|
+
var savedH = parseInt(localStorage.getItem(heightKey) || '', 10);
|
|
2004
|
+
if (savedH) applyHeight(savedH);
|
|
2005
|
+
|
|
2006
|
+
function fitPane(p) {
|
|
2007
|
+
if (!p) return;
|
|
2008
|
+
try { p.fit.fit(); if (p.id != null) window.monacoriPty.resize({ id: p.id, cols: p.term.cols, rows: p.term.rows }); } catch (e) {}
|
|
2009
|
+
}
|
|
2010
|
+
function fitAll() { panes.forEach(fitPane); }
|
|
2011
|
+
|
|
2012
|
+
function setActive(p) {
|
|
2013
|
+
active = p;
|
|
2014
|
+
panes.forEach(function (q) { q.el.classList.toggle('is-active', q === p); });
|
|
2015
|
+
if (p) requestAnimationFrame(function () { try { p.term.focus(); } catch (e) {} });
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function makePane() {
|
|
2019
|
+
if (!ensureXterm()) return null; // xterm unavailable — leave the panel empty rather than throw
|
|
2020
|
+
var el = document.createElement('div');
|
|
2021
|
+
el.className = 'terminal-pane';
|
|
2022
|
+
var labelEl = document.createElement('div');
|
|
2023
|
+
labelEl.className = 'terminal-pane-label';
|
|
2024
|
+
var paneHost = document.createElement('div');
|
|
2025
|
+
paneHost.className = 'terminal-pane-host';
|
|
2026
|
+
el.appendChild(labelEl);
|
|
2027
|
+
el.appendChild(paneHost);
|
|
2028
|
+
host.appendChild(el);
|
|
2029
|
+
var term = new window.Terminal({
|
|
2030
|
+
fontSize: 12,
|
|
2031
|
+
fontFamily: 'Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
2032
|
+
theme: { background: '#161616', foreground: '#a9b7c6', cursor: '#a9b7c6', selectionBackground: '#214283' },
|
|
2033
|
+
cursorBlink: true,
|
|
2034
|
+
});
|
|
2035
|
+
var fit = new window.FitAddon.FitAddon();
|
|
2036
|
+
term.loadAddon(fit);
|
|
2037
|
+
term.open(paneHost);
|
|
2038
|
+
var pane = { id: null, term: term, fit: fit, el: el, labelEl: labelEl, name: 'Terminal ' + (panes.length + 1) };
|
|
2039
|
+
labelEl.textContent = pane.name;
|
|
2040
|
+
// Cmd combos are app shortcuts (Cmd+1/0 tab switch, Cmd+B go-to-def, …). Release the terminal and let
|
|
2041
|
+
// them bubble to the document handler instead of typing into the shell (fixes "Cmd+1 stuck in term").
|
|
2042
|
+
// Exception: keep focus for clipboard/selection combos (Cmd+C/V/X/A) so the terminal's own copy &
|
|
2043
|
+
// paste keep working — blurring on Cmd+V drops the textarea focus the paste event needs.
|
|
2044
|
+
term.attachCustomKeyEventHandler(function (e) {
|
|
2045
|
+
if (e.type === 'keydown' && e.metaKey) {
|
|
2046
|
+
var k = (e.key || '').toLowerCase();
|
|
2047
|
+
if (k === 'c' || k === 'v' || k === 'x' || k === 'a') return true;
|
|
2048
|
+
try { term.blur(); } catch (x) {}
|
|
2049
|
+
return false;
|
|
2050
|
+
}
|
|
2051
|
+
return true;
|
|
2052
|
+
});
|
|
2053
|
+
term.onData(function (d) { if (pane.id != null) window.monacoriPty.write({ id: pane.id, data: d }); });
|
|
2054
|
+
el.addEventListener('mousedown', function (e) { if (e.target !== labelEl) setActive(pane); });
|
|
2055
|
+
labelEl.addEventListener('dblclick', function () { renamePane(pane); });
|
|
2056
|
+
panes.push(pane);
|
|
2057
|
+
try { fit.fit(); } catch (e) {}
|
|
2058
|
+
window.monacoriPty.spawn({ cols: term.cols || 80, rows: term.rows || 24 }).then(function (r) { pane.id = r && r.id; });
|
|
2059
|
+
setActive(pane);
|
|
2060
|
+
return pane;
|
|
2061
|
+
}
|
|
2062
|
+
// Rename a pane inline: the label becomes editable, Enter commits, Esc/blur reverts to the last name.
|
|
2063
|
+
function renamePane(pane) {
|
|
2064
|
+
if (!pane) { pane = active; }
|
|
2065
|
+
if (!pane) return;
|
|
2066
|
+
var el = pane.labelEl;
|
|
2067
|
+
if (el.getAttribute('contenteditable') === 'true') return;
|
|
2068
|
+
setActive(pane);
|
|
2069
|
+
el.contentEditable = 'true';
|
|
2070
|
+
el.focus();
|
|
2071
|
+
try { var range = document.createRange(); range.selectNodeContents(el); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } catch (e) {}
|
|
2072
|
+
function finish(commit) {
|
|
2073
|
+
el.removeEventListener('keydown', onKey);
|
|
2074
|
+
el.removeEventListener('blur', onBlur);
|
|
2075
|
+
el.contentEditable = 'false';
|
|
2076
|
+
if (commit) pane.name = (el.textContent || '').trim() || pane.name;
|
|
2077
|
+
el.textContent = pane.name;
|
|
2078
|
+
try { if (pane.term) pane.term.focus(); } catch (e) {}
|
|
2079
|
+
}
|
|
2080
|
+
function onKey(e) {
|
|
2081
|
+
e.stopPropagation();
|
|
2082
|
+
if (e.key === 'Enter') { e.preventDefault(); finish(true); }
|
|
2083
|
+
else if (e.key === 'Escape') { e.preventDefault(); finish(false); }
|
|
2084
|
+
}
|
|
2085
|
+
function onBlur() { finish(true); }
|
|
2086
|
+
el.addEventListener('keydown', onKey);
|
|
2087
|
+
el.addEventListener('blur', onBlur);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function removePane(id) {
|
|
2091
|
+
var i = -1;
|
|
2092
|
+
for (var k = 0; k < panes.length; k++) { if (panes[k].id === id) { i = k; break; } }
|
|
2093
|
+
if (i < 0) return;
|
|
2094
|
+
var p = panes[i];
|
|
2095
|
+
try { p.term.dispose(); } catch (e) {}
|
|
2096
|
+
if (p.el.parentNode) p.el.parentNode.removeChild(p.el);
|
|
2097
|
+
panes.splice(i, 1);
|
|
2098
|
+
if (active === p) setActive(panes[panes.length - 1] || null);
|
|
2099
|
+
if (panes.length === 0) setOpen(false);
|
|
2100
|
+
else fitAll();
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function split() {
|
|
2104
|
+
if (panes.length >= MAX_PANES) return;
|
|
2105
|
+
makePane();
|
|
2106
|
+
fitAll();
|
|
2107
|
+
}
|
|
2108
|
+
// Move active focus between split panes (menu accelerators Cmd/Ctrl+Alt+[ and ]).
|
|
2109
|
+
function focusPaneByDelta(delta) {
|
|
2110
|
+
if (panes.length < 2) return;
|
|
2111
|
+
var i = panes.indexOf(active);
|
|
2112
|
+
if (i < 0) i = 0;
|
|
2113
|
+
setActive(panes[(i + delta + panes.length) % panes.length]);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// Route per-pane pty output / exit by id (registered once for the window).
|
|
2117
|
+
window.monacoriPty.onData(function (msg) {
|
|
2118
|
+
for (var k = 0; k < panes.length; k++) { if (panes[k].id === msg.id) { panes[k].term.write(msg.data); return; } }
|
|
2119
|
+
});
|
|
2120
|
+
window.monacoriPty.onExit(function (msg) { removePane(msg.id); });
|
|
2121
|
+
|
|
2122
|
+
function isOpen() { return !panel.classList.contains('hidden'); }
|
|
2123
|
+
function setOpen(open) {
|
|
2124
|
+
panel.classList.toggle('hidden', !open);
|
|
2125
|
+
document.body.classList.toggle('terminal-open', open);
|
|
2126
|
+
if (toggleBtn) toggleBtn.classList.toggle('is-active', open);
|
|
2127
|
+
try { sessionStorage.setItem(openKey, open ? '1' : '0'); } catch (e) {}
|
|
2128
|
+
if (open) {
|
|
2129
|
+
if (panes.length === 0) makePane();
|
|
2130
|
+
requestAnimationFrame(function () { fitAll(); if (active) try { active.term.focus(); } catch (e) {} });
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
function toggle() { setOpen(!isOpen()); }
|
|
2134
|
+
|
|
2135
|
+
if (toggleBtn) toggleBtn.addEventListener('click', toggle);
|
|
2136
|
+
if (closeBtn) closeBtn.addEventListener('click', function () { setOpen(false); });
|
|
2137
|
+
// Toggle (Ctrl+`/Alt+F12) and split (Cmd+D) arrive from the Terminal menu accelerators (app-main),
|
|
2138
|
+
// because Chromium swallows Cmd+D before a renderer keydown would ever see it.
|
|
2139
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onTerminalToggle === 'function') window.monacoriMenu.onTerminalToggle(toggle);
|
|
2140
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onTerminalSplit === 'function') window.monacoriMenu.onTerminalSplit(split);
|
|
2141
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onTerminalPaneFocus === 'function') window.monacoriMenu.onTerminalPaneFocus(focusPaneByDelta);
|
|
2142
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onTerminalPaneRename === 'function') window.monacoriMenu.onTerminalPaneRename(function () { renamePane(active); });
|
|
2143
|
+
|
|
2144
|
+
var ro = (typeof ResizeObserver === 'function') ? new ResizeObserver(function () { if (isOpen()) fitAll(); }) : null;
|
|
2145
|
+
if (ro) ro.observe(host);
|
|
2146
|
+
window.addEventListener('resize', function () { if (isOpen()) fitAll(); });
|
|
2147
|
+
|
|
2148
|
+
if (resizer) {
|
|
2149
|
+
resizer.addEventListener('mousedown', function (e) {
|
|
2150
|
+
e.preventDefault();
|
|
2151
|
+
resizer.classList.add('resizing');
|
|
2152
|
+
function move(ev) { applyHeight(window.innerHeight - ev.clientY); }
|
|
2153
|
+
function up() {
|
|
2154
|
+
resizer.classList.remove('resizing');
|
|
2155
|
+
document.removeEventListener('mousemove', move);
|
|
2156
|
+
document.removeEventListener('mouseup', up);
|
|
2157
|
+
var cur = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--terminal-height'), 10);
|
|
2158
|
+
if (cur) { try { localStorage.setItem(heightKey, String(cur)); } catch (e) {} }
|
|
2159
|
+
fitAll();
|
|
2160
|
+
}
|
|
2161
|
+
document.addEventListener('mousemove', move);
|
|
2162
|
+
document.addEventListener('mouseup', up);
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// Kill this window's ptys on unload so a reload/close doesn't leak them in the main process.
|
|
2167
|
+
window.addEventListener('beforeunload', function () {
|
|
2168
|
+
panes.forEach(function (p) { if (p.id != null) { try { window.monacoriPty.kill({ id: p.id }); } catch (e) {} } });
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
// Hook for the merged-prompt modal: pipe the combined text into a chosen pane (no trailing Enter —
|
|
2172
|
+
// the user reviews in the live session, then presses Enter, so multiline prompts stay intact).
|
|
2173
|
+
function writeToPane(p, text) {
|
|
2174
|
+
if (!p) return;
|
|
2175
|
+
setOpen(true);
|
|
2176
|
+
if (p.id != null) window.monacoriPty.write({ id: p.id, data: text });
|
|
2177
|
+
setActive(p);
|
|
2178
|
+
requestAnimationFrame(function () { try { p.term.focus(); } catch (e) {} });
|
|
2179
|
+
}
|
|
2180
|
+
// Pane-pick mode: triggered from the merged modal's "Send to terminal". The chosen pane is highlighted,
|
|
2181
|
+
// the rest are dimmed; arrows change the pick, Enter sends, Esc cancels. Single pane → send at once.
|
|
2182
|
+
var sendModeText = null, sendModeIdx = 0;
|
|
2183
|
+
function paintSendMode() {
|
|
2184
|
+
panes.forEach(function (p, i) {
|
|
2185
|
+
p.el.classList.toggle('is-send-target', i === sendModeIdx);
|
|
2186
|
+
p.el.classList.toggle('is-dimmed', i !== sendModeIdx);
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
function exitSendMode() {
|
|
2190
|
+
if (sendModeText == null) return;
|
|
2191
|
+
sendModeText = null;
|
|
2192
|
+
panel.classList.remove('send-mode');
|
|
2193
|
+
document.body.classList.remove('terminal-send-mode'); // un-dim the rest of the app
|
|
2194
|
+
panes.forEach(function (p) { p.el.classList.remove('is-send-target', 'is-dimmed'); });
|
|
2195
|
+
}
|
|
2196
|
+
function enterSendMode(text) {
|
|
2197
|
+
if (panes.length === 0) return;
|
|
2198
|
+
setOpen(true);
|
|
2199
|
+
sendModeText = text;
|
|
2200
|
+
sendModeIdx = Math.max(0, panes.indexOf(active));
|
|
2201
|
+
panel.classList.add('send-mode');
|
|
2202
|
+
document.body.classList.add('terminal-send-mode'); // dim sidebar + file/diff view; only the terminal pops
|
|
2203
|
+
paintSendMode();
|
|
2204
|
+
}
|
|
2205
|
+
// Capture phase so the pick keys win over the focused xterm; while picking, every key is swallowed.
|
|
2206
|
+
document.addEventListener('keydown', function (e) {
|
|
2207
|
+
if (sendModeText == null) return;
|
|
2208
|
+
e.preventDefault(); e.stopPropagation();
|
|
2209
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
2210
|
+
var d = (e.key === 'ArrowRight' || e.key === 'ArrowDown') ? 1 : -1;
|
|
2211
|
+
sendModeIdx = (sendModeIdx + d + panes.length) % panes.length;
|
|
2212
|
+
paintSendMode();
|
|
2213
|
+
} else if (e.key === 'Enter') {
|
|
2214
|
+
var p = panes[sendModeIdx], text = sendModeText;
|
|
2215
|
+
exitSendMode();
|
|
2216
|
+
writeToPane(p, text);
|
|
2217
|
+
} else if (e.key === 'Escape') {
|
|
2218
|
+
exitSendMode();
|
|
2219
|
+
}
|
|
2220
|
+
}, true);
|
|
2221
|
+
window.__monacoriTerminal = {
|
|
2222
|
+
isOpen: isOpen,
|
|
2223
|
+
open: function () { setOpen(true); },
|
|
2224
|
+
paneCount: function () { return panes.length; },
|
|
2225
|
+
enterSendMode: enterSendMode,
|
|
2226
|
+
send: function (text) { writeToPane(active || panes[0], text); },
|
|
2227
|
+
sendToPane: function (i, text) { writeToPane(panes[i] || active || panes[0], text); },
|
|
2228
|
+
close: function () { setOpen(false); },
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
// Restore the open state across reloads.
|
|
2232
|
+
try { if (sessionStorage.getItem(openKey) === '1') setOpen(true); } catch (e) {}
|
|
2233
|
+
})();
|
|
2234
|
+
|
|
1852
2235
|
// In Electron, the Review menu's Cmd/Ctrl+Shift+/ and +. accelerators arrive here via IPC
|
|
1853
2236
|
// (macOS reserves Cmd+? for its Help search, so the menu claims it and routes to these views).
|
|
1854
2237
|
if (window.monacoriMenu && typeof window.monacoriMenu.onMergedView === 'function') {
|
|
2238
|
+
// Always open the merged-view modal; sending to a terminal pane is a button inside it (per-pane when
|
|
2239
|
+
// split), so the user can pick which claude/codex session receives the prompt.
|
|
1855
2240
|
window.monacoriMenu.onMergedView(function (kind) { openMergedView(kind); });
|
|
1856
2241
|
}
|
|
2242
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function') {
|
|
2243
|
+
// Cmd/Ctrl+W: close the active Files-mode tab (no-op outside the source viewer).
|
|
2244
|
+
window.monacoriMenu.onCloseTab(function () {
|
|
2245
|
+
// Cmd/Ctrl+W closes the terminal panel first when it's open, otherwise the active Files-mode tab.
|
|
2246
|
+
if (window.__monacoriTerminal && window.__monacoriTerminal.isOpen()) { window.__monacoriTerminal.close(); return; }
|
|
2247
|
+
if (isSourceViewerVisible()) closeActiveSourceTab();
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
1857
2250
|
|
|
1858
2251
|
(function checkForUpdate() {
|
|
1859
2252
|
var current = window.__MONACORI_VERSION__ || '';
|
|
@@ -1873,9 +2266,19 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onMergedView === 'function
|
|
|
1873
2266
|
if (isNewer(latest, current)) {
|
|
1874
2267
|
var flag = document.getElementById('app-update-flag');
|
|
1875
2268
|
if (flag) flag.classList.remove('hidden');
|
|
1876
|
-
|
|
2269
|
+
// One-click auto-update needs the Electron main process (it spawns npm). When available, reveal the
|
|
2270
|
+
// button so a click installs + restarts; otherwise (browser/static export) name the command instead.
|
|
2271
|
+
var ub = document.getElementById('app-info-update');
|
|
2272
|
+
if (ub && window.monacoriUpdate && typeof window.monacoriUpdate.run === 'function') {
|
|
2273
|
+
ub.textContent = t('settings.updateRestart') + ' (v' + latest + ')';
|
|
2274
|
+
ub.classList.remove('hidden');
|
|
2275
|
+
if (status) { status.textContent = t('settings.updateAvailable') + ': v' + latest; status.classList.add('has-update'); }
|
|
2276
|
+
} else if (status) {
|
|
2277
|
+
status.textContent = t('settings.updateAvailable') + ': v' + latest + ' — npm i -g @happy-nut/monacori';
|
|
2278
|
+
status.classList.add('has-update');
|
|
2279
|
+
}
|
|
1877
2280
|
} else if (status) {
|
|
1878
|
-
status.textContent = '
|
|
2281
|
+
status.textContent = t('settings.upToDate') + ' (v' + current + ')';
|
|
1879
2282
|
}
|
|
1880
2283
|
};
|
|
1881
2284
|
// Cache the npm result for the session so watch-mode reloads reuse it instead of refetching.
|
|
@@ -1893,28 +2296,81 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onMergedView === 'function
|
|
|
1893
2296
|
.catch(function () {});
|
|
1894
2297
|
})();
|
|
1895
2298
|
|
|
1896
|
-
(
|
|
1897
|
-
|
|
1898
|
-
|
|
2299
|
+
// Unified settings modal: the sidebar-footer gear opens it (General category by default), with
|
|
2300
|
+
// About/update/shortcuts under General and the merge-prompt editor under Merge prompts.
|
|
2301
|
+
(function setupSettings() {
|
|
2302
|
+
var modal = document.getElementById('settings-modal');
|
|
2303
|
+
if (!modal) return;
|
|
2304
|
+
var gearBtn = document.getElementById('app-info-btn');
|
|
1899
2305
|
var flag = document.getElementById('app-update-flag');
|
|
1900
|
-
var
|
|
1901
|
-
|
|
1902
|
-
var
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2306
|
+
var updateBtn = document.getElementById('app-info-update');
|
|
2307
|
+
var qta = document.getElementById('settings-prompt-q');
|
|
2308
|
+
var cta = document.getElementById('settings-prompt-c');
|
|
2309
|
+
var resetBtn = document.getElementById('settings-reset');
|
|
2310
|
+
var savedMsg = document.getElementById('settings-saved');
|
|
2311
|
+
var cats = Array.prototype.slice.call(modal.querySelectorAll('.settings-cat'));
|
|
2312
|
+
var secs = Array.prototype.slice.call(modal.querySelectorAll('.settings-section'));
|
|
2313
|
+
function showCat(cat) {
|
|
2314
|
+
cats.forEach(function (c) { c.classList.toggle('active', c.dataset.cat === cat); });
|
|
2315
|
+
secs.forEach(function (s) { s.classList.toggle('hidden', s.dataset.cat !== cat); });
|
|
2316
|
+
}
|
|
2317
|
+
function fill() {
|
|
2318
|
+
var s = loadMergePrompts();
|
|
2319
|
+
if (qta) { qta.value = typeof s.q === 'string' ? s.q : ''; qta.placeholder = defaultMergePrompt('q'); }
|
|
2320
|
+
if (cta) { cta.value = typeof s.c === 'string' ? s.c : ''; cta.placeholder = defaultMergePrompt('c'); }
|
|
2321
|
+
}
|
|
2322
|
+
function open(cat) { fill(); if (cat) showCat(cat); modal.classList.remove('hidden'); }
|
|
2323
|
+
function close() { modal.classList.add('hidden'); }
|
|
2324
|
+
var flashTimer = null;
|
|
2325
|
+
function flash() { if (!savedMsg) return; savedMsg.textContent = 'Saved'; if (flashTimer) clearTimeout(flashTimer); flashTimer = setTimeout(function () { savedMsg.textContent = ''; }, 1200); }
|
|
2326
|
+
if (gearBtn) gearBtn.addEventListener('click', function (e) { e.stopPropagation(); if (modal.classList.contains('hidden')) open('general'); else close(); });
|
|
2327
|
+
if (flag) flag.addEventListener('click', function (e) { e.stopPropagation(); open('general'); });
|
|
2328
|
+
cats.forEach(function (c) { c.addEventListener('click', function () { showCat(c.dataset.cat); }); });
|
|
2329
|
+
modal.addEventListener('click', function (e) { if (e.target === modal) close(); });
|
|
2330
|
+
// Capture so closing settings wins over other Escape handlers (lightbox / composer).
|
|
1915
2331
|
document.addEventListener('keydown', function (e) {
|
|
1916
|
-
if (e.key === 'Escape' && !
|
|
1917
|
-
|
|
2332
|
+
if (e.key === 'Escape' && !modal.classList.contains('hidden')) { e.stopPropagation(); e.preventDefault(); close(); return; }
|
|
2333
|
+
// Cmd/Ctrl+, (the standard "Preferences" accelerator) toggles the settings panel from anywhere.
|
|
2334
|
+
if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && (e.key === ',' || e.code === 'Comma')) {
|
|
2335
|
+
e.preventDefault(); e.stopPropagation();
|
|
2336
|
+
if (modal.classList.contains('hidden')) open('general'); else close();
|
|
2337
|
+
}
|
|
2338
|
+
}, true);
|
|
2339
|
+
// One-click self-update (Electron only): install latest globally via the main process, then relaunch.
|
|
2340
|
+
if (updateBtn && window.monacoriUpdate && typeof window.monacoriUpdate.run === 'function') {
|
|
2341
|
+
updateBtn.addEventListener('click', function () {
|
|
2342
|
+
if (updateBtn.disabled) return;
|
|
2343
|
+
updateBtn.disabled = true;
|
|
2344
|
+
var status = document.getElementById('app-info-status');
|
|
2345
|
+
if (status) { status.textContent = t('settings.updating'); status.classList.add('has-update'); }
|
|
2346
|
+
window.monacoriUpdate.run().then(function (r) {
|
|
2347
|
+
if (r && r.ok) { if (status) status.textContent = t('settings.updated'); }
|
|
2348
|
+
else { updateBtn.disabled = false; if (status) status.textContent = t('settings.updateFailed'); }
|
|
2349
|
+
}).catch(function () { updateBtn.disabled = false; if (status) status.textContent = t('settings.updateFailed'); });
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
if (qta) qta.addEventListener('input', function () { saveMergePrompt('q', qta.value); flash(); });
|
|
2353
|
+
if (cta) cta.addEventListener('input', function () { saveMergePrompt('c', cta.value); flash(); });
|
|
2354
|
+
if (resetBtn) resetBtn.addEventListener('click', function () { saveMergePrompt('q', ''); saveMergePrompt('c', ''); fill(); flash(); });
|
|
2355
|
+
// Language: live-switch the whole UI (no reload). Persist, re-apply the static chrome, then re-render
|
|
2356
|
+
// any currently-shown dynamic text (open composer / merged modal / index status) so it follows too.
|
|
2357
|
+
var langSel = document.getElementById('settings-language');
|
|
2358
|
+
if (langSel) {
|
|
2359
|
+
langSel.value = locale;
|
|
2360
|
+
langSel.addEventListener('change', function () {
|
|
2361
|
+
var next = langSel.value === 'ko' ? 'ko' : 'en';
|
|
2362
|
+
if (next === locale) return;
|
|
2363
|
+
locale = next;
|
|
2364
|
+
persistSave(LOCALE_KEY, locale);
|
|
2365
|
+
applyI18n();
|
|
2366
|
+
// Merge-prompt placeholders are locale-dependent defaults; refresh them while the panel is open.
|
|
2367
|
+
fill();
|
|
2368
|
+
// Re-render dynamic, currently-visible text in the new locale.
|
|
2369
|
+
try { if (typeof refreshComments === 'function') refreshComments(); } catch (e) {}
|
|
2370
|
+
var mergedModal = document.getElementById('mc-modal');
|
|
2371
|
+
if (mergedModal) { var mk = mergedModal.dataset.kind || 'q'; mergedModal.remove(); openMergedView(mk); }
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
1918
2374
|
})();
|
|
1919
2375
|
|
|
1920
2376
|
function setTab(name) {
|
|
@@ -1933,7 +2389,7 @@ function ensureTreeRendered() {
|
|
|
1933
2389
|
if (!panel || !island) return;
|
|
1934
2390
|
var html = island.textContent || '';
|
|
1935
2391
|
island.parentNode && island.parentNode.removeChild(island);
|
|
1936
|
-
panel.innerHTML = '<div class="empty-nav">
|
|
2392
|
+
panel.innerHTML = '<div class="empty-nav">' + escapeHtml(t('source.buildingTree')) + '</div>';
|
|
1937
2393
|
setTimeout(function () { // let "Building…" paint before the heavy innerHTML
|
|
1938
2394
|
panel.innerHTML = html;
|
|
1939
2395
|
sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
@@ -1975,6 +2431,11 @@ function saveUiState() {
|
|
|
1975
2431
|
view: document.getElementById('source-viewer')?.classList.contains('hidden') ? 'diff' : 'source',
|
|
1976
2432
|
sourcePath,
|
|
1977
2433
|
hash: location.hash,
|
|
2434
|
+
// Preserve open tabs + the exact caret across watch reloads (otherwise the caret resets to the
|
|
2435
|
+
// hunk's first change / file top every time the working tree changes).
|
|
2436
|
+
tabs: sourceTabs,
|
|
2437
|
+
diffCursor: diffCursor,
|
|
2438
|
+
viewerCursor: viewerCursor,
|
|
1978
2439
|
}));
|
|
1979
2440
|
}
|
|
1980
2441
|
|
|
@@ -1983,13 +2444,25 @@ function restoreUiState() {
|
|
|
1983
2444
|
if (!raw) return false;
|
|
1984
2445
|
try {
|
|
1985
2446
|
const state = JSON.parse(raw);
|
|
2447
|
+
// Restore Files-mode tabs first so a watch reload doesn't drop the open tabs.
|
|
2448
|
+
if (Array.isArray(state.tabs)) sourceTabs = state.tabs.filter(function (p) { return sourceByPath.has(p); });
|
|
1986
2449
|
if (state.view === 'diff') {
|
|
1987
|
-
const match = String(state.hash || location.hash || '').match(/^#hunk-(
|
|
2450
|
+
const match = String(state.hash || location.hash || '').match(/^#hunk-(\d+)$/);
|
|
1988
2451
|
setActive(match ? Number(match[1]) : current >= 0 ? current : 0, false);
|
|
2452
|
+
// Restore the exact diff caret (setActive only lands on the hunk's first change).
|
|
2453
|
+
if (state.diffCursor && state.diffCursor.path) {
|
|
2454
|
+
var dc = state.diffCursor;
|
|
2455
|
+
setTimeout(function () { try { setDiffCursor(dc.path, dc.side, dc.rowIndex, dc.column, true); } catch (e) {} }, 60);
|
|
2456
|
+
}
|
|
1989
2457
|
return true;
|
|
1990
2458
|
}
|
|
1991
2459
|
if (state.sourcePath && sourceByPath.has(state.sourcePath)) {
|
|
1992
2460
|
openSourceFile(state.sourcePath);
|
|
2461
|
+
// Restore the exact source caret/scroll (openSourceFile alone resets it to the top).
|
|
2462
|
+
if (state.viewerCursor && state.viewerCursor.path === state.sourcePath) {
|
|
2463
|
+
var vc = state.viewerCursor;
|
|
2464
|
+
setTimeout(function () { try { setSourceCursor(state.sourcePath, vc.lineIndex, vc.column, true, -1); } catch (e) {} }, 60);
|
|
2465
|
+
}
|
|
1993
2466
|
return true;
|
|
1994
2467
|
}
|
|
1995
2468
|
} catch {
|
|
@@ -2007,14 +2480,14 @@ async function checkForLiveUpdate() {
|
|
|
2007
2480
|
if (!response.ok) return;
|
|
2008
2481
|
const state = await response.json();
|
|
2009
2482
|
if (liveStatus && state.generatedAt) {
|
|
2010
|
-
liveStatus.textContent = '
|
|
2483
|
+
liveStatus.textContent = t('status.live.updated') + ' ' + new Date(state.generatedAt).toLocaleTimeString();
|
|
2011
2484
|
}
|
|
2012
2485
|
if (state.signature && state.signature !== currentSignature) {
|
|
2013
2486
|
saveUiState();
|
|
2014
2487
|
location.reload();
|
|
2015
2488
|
}
|
|
2016
2489
|
} catch {
|
|
2017
|
-
if (liveStatus) liveStatus.textContent = '
|
|
2490
|
+
if (liveStatus) liveStatus.textContent = t('status.live.waiting');
|
|
2018
2491
|
} finally {
|
|
2019
2492
|
checkingForUpdates = false;
|
|
2020
2493
|
}
|
|
@@ -2190,7 +2663,18 @@ function measureCharWidth(element) {
|
|
|
2190
2663
|
return measuredCharWidth;
|
|
2191
2664
|
}
|
|
2192
2665
|
|
|
2666
|
+
var caretBusyTimer = null;
|
|
2667
|
+
// While the caret is actively moving (held arrow key, typing), keep it solid and only resume the
|
|
2668
|
+
// blink animation after a short idle. Otherwise key-repeat exposes the blink's "off" frames between
|
|
2669
|
+
// moves and the caret appears to vanish intermittently.
|
|
2670
|
+
function markCaretBusy() {
|
|
2671
|
+
document.body.classList.add('caret-busy');
|
|
2672
|
+
if (caretBusyTimer) clearTimeout(caretBusyTimer);
|
|
2673
|
+
caretBusyTimer = setTimeout(function () { document.body.classList.remove('caret-busy'); }, 650);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2193
2676
|
function setSourceCursor(path, lineIndex, column, shouldReveal = false, targetLine = -1) {
|
|
2677
|
+
markCaretBusy();
|
|
2194
2678
|
selectedCommentRow = null; // any explicit caret placement (click/move) ends a comment-box selection
|
|
2195
2679
|
const file = sourceByPath.get(path);
|
|
2196
2680
|
if (!file || !file.embedded) return;
|
|
@@ -2229,14 +2713,19 @@ function setSourceCursor(path, lineIndex, column, shouldReveal = false, targetLi
|
|
|
2229
2713
|
function updateSourceCaret(prev, lines, language) {
|
|
2230
2714
|
const body = document.getElementById('source-body');
|
|
2231
2715
|
if (!body) return;
|
|
2716
|
+
// Markdown/CSV render to HTML cells (.rendered-body): the caret is a whole-row highlight there,
|
|
2717
|
+
// so never rewrite a cell's innerHTML (that would replace the rendered block with raw text).
|
|
2718
|
+
const rendered = body.classList.contains('rendered-body');
|
|
2232
2719
|
const rowFor = (idx) => body.querySelector('.source-row[data-line-index="' + idx + '"]');
|
|
2233
2720
|
// Restore the line the caret left: drop the caret span, re-highlight the full line.
|
|
2234
2721
|
if (prev && prev.lineIndex !== viewerCursor.lineIndex) {
|
|
2235
2722
|
const prevRow = rowFor(prev.lineIndex);
|
|
2236
2723
|
if (prevRow) {
|
|
2237
2724
|
prevRow.classList.remove('cursor-line');
|
|
2238
|
-
|
|
2239
|
-
|
|
2725
|
+
if (!rendered) {
|
|
2726
|
+
const prevCell = prevRow.querySelector('.source-code');
|
|
2727
|
+
if (prevCell) prevCell.innerHTML = highlightLine(lines[prev.lineIndex] || '', language);
|
|
2728
|
+
}
|
|
2240
2729
|
}
|
|
2241
2730
|
}
|
|
2242
2731
|
// Reconcile the go-to-definition highlight (set only on symbol jumps, cleared on plain moves).
|
|
@@ -2244,10 +2733,12 @@ function updateSourceCaret(prev, lines, language) {
|
|
|
2244
2733
|
if (viewerCursor.targetLine >= 0) rowFor(viewerCursor.targetLine)?.classList.add('symbol-target');
|
|
2245
2734
|
// Rebuild the new caret line with the caret span.
|
|
2246
2735
|
const row = rowFor(viewerCursor.lineIndex);
|
|
2247
|
-
if (!row) { openSourceFile(viewerCursor.path, false); return; } // line not in the DOM —
|
|
2736
|
+
if (!row) { if (!rendered) openSourceFile(viewerCursor.path, false); return; } // line not in the DOM — full re-render (eager source only)
|
|
2248
2737
|
row.classList.add('cursor-line');
|
|
2249
|
-
|
|
2250
|
-
|
|
2738
|
+
if (!rendered) {
|
|
2739
|
+
const cell = row.querySelector('.source-code');
|
|
2740
|
+
if (cell) cell.innerHTML = renderLineWithCursor(lines[viewerCursor.lineIndex] || '', language, viewerCursor.column);
|
|
2741
|
+
}
|
|
2251
2742
|
}
|
|
2252
2743
|
|
|
2253
2744
|
function openSourceAt(path, lineIndex, column) {
|
|
@@ -2351,6 +2842,20 @@ function moveSourceCursor(dLine, dColumn, extend) {
|
|
|
2351
2842
|
if (!viewerCursor) return;
|
|
2352
2843
|
const file = sourceByPath.get(viewerCursor.path);
|
|
2353
2844
|
if (!file || !file.embedded) return;
|
|
2845
|
+
// Markdown/CSV rendered view: rows are blocks (sparse data-line-index), so any arrow steps to the
|
|
2846
|
+
// adjacent block row rather than into a (non-existent) raw line. No text column / selection there.
|
|
2847
|
+
const renderedBody = document.getElementById('source-body');
|
|
2848
|
+
if (renderedBody && renderedBody.classList.contains('rendered-body')) {
|
|
2849
|
+
const rows = Array.from(renderedBody.querySelectorAll('.source-row'));
|
|
2850
|
+
if (!rows.length) return;
|
|
2851
|
+
let ci = rows.indexOf(renderedBody.querySelector('.source-row[data-line-index="' + viewerCursor.lineIndex + '"]'));
|
|
2852
|
+
if (ci < 0) ci = 0;
|
|
2853
|
+
const step = (dLine || 0) + (dColumn > 0 ? 1 : dColumn < 0 ? -1 : 0);
|
|
2854
|
+
const ni = Math.max(0, Math.min(rows.length - 1, ci + (step || 0)));
|
|
2855
|
+
selectionAnchor = null;
|
|
2856
|
+
setSourceCursor(viewerCursor.path, Number(rows[ni].dataset.lineIndex) || 0, 0, true, -1);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2354
2859
|
const lines = file.content.split(/\r?\n/);
|
|
2355
2860
|
let line = viewerCursor.lineIndex;
|
|
2356
2861
|
let col = viewerCursor.column;
|
|
@@ -2631,11 +3136,11 @@ function setIndexProgress(done, total) {
|
|
|
2631
3136
|
var bar = document.getElementById('index-progress');
|
|
2632
3137
|
if (!el) return;
|
|
2633
3138
|
if (!total || done >= total) {
|
|
2634
|
-
el.textContent = (total || 0) + ' indexed';
|
|
3139
|
+
el.textContent = (total || 0) + ' ' + t('status.indexed');
|
|
2635
3140
|
if (bar) bar.classList.add('hidden');
|
|
2636
3141
|
return;
|
|
2637
3142
|
}
|
|
2638
|
-
el.textContent = 'indexing ' + done + '/' + total + '…';
|
|
3143
|
+
el.textContent = t('status.indexing') + ' ' + done + '/' + total + '…';
|
|
2639
3144
|
if (bar) {
|
|
2640
3145
|
bar.classList.remove('hidden');
|
|
2641
3146
|
var fill = bar.firstElementChild;
|
|
@@ -2712,9 +3217,56 @@ function setSourceTypeIcon(path) {
|
|
|
2712
3217
|
var icon = link ? link.querySelector('.ftype') : null;
|
|
2713
3218
|
holder.innerHTML = icon ? icon.outerHTML : '';
|
|
2714
3219
|
}
|
|
3220
|
+
// Files-mode tabs: each distinct file opened in the source viewer becomes a tab (session-only).
|
|
3221
|
+
// Cmd/Ctrl+W closes the active tab; Cmd/Ctrl+Shift+[ / ] cycle tabs; the × button closes one.
|
|
3222
|
+
// (sourceTabs is declared near the other source state up top so early restore-state openSourceFile
|
|
3223
|
+
// calls run before this block don't see an undefined array.)
|
|
3224
|
+
function addSourceTab(path) { if (path && sourceTabs.indexOf(path) < 0) sourceTabs.push(path); }
|
|
3225
|
+
function sourceTabLabel(path) { var p = String(path || ''); var s = p.lastIndexOf('/'); return s >= 0 ? p.slice(s + 1) : p; }
|
|
3226
|
+
function currentSourceTabPath() { var v = document.getElementById('source-viewer'); return (v && v.dataset.openPath) || ''; }
|
|
3227
|
+
function renderSourceTabs(activePath) {
|
|
3228
|
+
var bar = document.getElementById('source-tabs');
|
|
3229
|
+
if (!bar) return;
|
|
3230
|
+
if (!sourceTabs.length) { bar.classList.add('hidden'); bar.innerHTML = ''; return; }
|
|
3231
|
+
bar.classList.remove('hidden');
|
|
3232
|
+
bar.innerHTML = sourceTabs.map(function (p) {
|
|
3233
|
+
var active = p === activePath;
|
|
3234
|
+
return '<div class="source-tab' + (active ? ' active' : '') + '" data-tab-path="' + escapeHtml(p) + '" title="' + escapeHtml(p) + '">'
|
|
3235
|
+
+ '<span class="source-tab-name">' + escapeHtml(sourceTabLabel(p)) + '</span>'
|
|
3236
|
+
+ '<button type="button" class="source-tab-close" data-close-path="' + escapeHtml(p) + '" aria-label="Close tab" title="Close (Cmd/Ctrl+W)">×</button>'
|
|
3237
|
+
+ '</div>';
|
|
3238
|
+
}).join('');
|
|
3239
|
+
var act = bar.querySelector('.source-tab.active');
|
|
3240
|
+
if (act && act.scrollIntoView) act.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
3241
|
+
}
|
|
3242
|
+
function closeSourceTab(path) {
|
|
3243
|
+
var idx = sourceTabs.indexOf(path);
|
|
3244
|
+
if (idx < 0) return;
|
|
3245
|
+
var wasActive = path === currentSourceTabPath();
|
|
3246
|
+
sourceTabs.splice(idx, 1);
|
|
3247
|
+
if (!wasActive) { renderSourceTabs(currentSourceTabPath()); return; }
|
|
3248
|
+
var nextPath = sourceTabs[idx] || sourceTabs[idx - 1] || '';
|
|
3249
|
+
if (nextPath) { openSourceFile(nextPath); return; }
|
|
3250
|
+
// No tabs left: reset the source view to its empty state.
|
|
3251
|
+
var v = document.getElementById('source-viewer'); if (v) v.dataset.openPath = '';
|
|
3252
|
+
var body = document.getElementById('source-body');
|
|
3253
|
+
if (body) { body.className = 'source-body empty'; body.textContent = t('source.selectFile'); }
|
|
3254
|
+
sourceLinks.forEach(function (l) { l.classList.remove('active'); });
|
|
3255
|
+
renderSourceTabs('');
|
|
3256
|
+
}
|
|
3257
|
+
function closeActiveSourceTab() { var p = currentSourceTabPath(); if (p) { closeSourceTab(p); return true; } return false; }
|
|
3258
|
+
function cycleSourceTab(dir) {
|
|
3259
|
+
if (sourceTabs.length < 2) return;
|
|
3260
|
+
var cur = sourceTabs.indexOf(currentSourceTabPath());
|
|
3261
|
+
if (cur < 0) cur = 0;
|
|
3262
|
+
openSourceFile(sourceTabs[(cur + dir + sourceTabs.length) % sourceTabs.length]);
|
|
3263
|
+
}
|
|
3264
|
+
|
|
2715
3265
|
function openSourceFile(path, shouldSwitch = true) {
|
|
2716
3266
|
const file = sourceByPath.get(path);
|
|
2717
3267
|
if (!file) return;
|
|
3268
|
+
addSourceTab(path);
|
|
3269
|
+
renderSourceTabs(path);
|
|
2718
3270
|
// lazy-LOAD: source content not fetched yet -> show a loading state; loadSourceData re-opens it.
|
|
2719
3271
|
if (REVIEW_LAZY_LOAD && !sourceLoaded && file.embedded) {
|
|
2720
3272
|
pendingSourceOpen = { path: path, shouldSwitch: shouldSwitch };
|
|
@@ -2726,7 +3278,7 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2726
3278
|
revealTreeFor(path);
|
|
2727
3279
|
var lb = document.getElementById('source-body');
|
|
2728
3280
|
lb.className = 'source-body empty';
|
|
2729
|
-
lb.textContent = '
|
|
3281
|
+
lb.textContent = t('source.loading');
|
|
2730
3282
|
if (shouldSwitch) showSourceView();
|
|
2731
3283
|
return;
|
|
2732
3284
|
}
|
|
@@ -2736,12 +3288,9 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2736
3288
|
renderBreadcrumb(document.getElementById('source-title'), path);
|
|
2737
3289
|
setSourceTypeIcon(path);
|
|
2738
3290
|
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(' | ');
|
|
3291
|
+
const meta = file.embedded
|
|
3292
|
+
? formatBytes(file.size || 0)
|
|
3293
|
+
: formatBytes(file.size || 0) + ' · ' + (file.skippedReason || 'not embedded');
|
|
2745
3294
|
document.getElementById('source-meta').textContent = meta;
|
|
2746
3295
|
const body = document.getElementById('source-body');
|
|
2747
3296
|
// Image files carry a data: URI preview instead of text — render inline (click to zoom).
|
|
@@ -2755,7 +3304,7 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2755
3304
|
}
|
|
2756
3305
|
if (!file.embedded) {
|
|
2757
3306
|
body.className = 'source-body empty';
|
|
2758
|
-
body.textContent = file.skippedReason ? '
|
|
3307
|
+
body.textContent = file.skippedReason ? t('source.previewUnavailable').replace(/\.$/, '') + ': ' + file.skippedReason + '.' : t('source.previewUnavailable');
|
|
2759
3308
|
document.getElementById('http-env-select')?.classList.add('hidden');
|
|
2760
3309
|
updateRenderToggle(path);
|
|
2761
3310
|
if (shouldSwitch) showSourceView();
|
|
@@ -2770,17 +3319,27 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2770
3319
|
// is a .source-row keyed by its start line, so the gutter shows line numbers and line/block comments
|
|
2771
3320
|
// work exactly as in the plain source view (renderSourceComments anchors on .source-row[data-line-index]).
|
|
2772
3321
|
if (isMarkdownPath(path)) {
|
|
2773
|
-
|
|
2774
|
-
|
|
3322
|
+
if (renderRawMode) {
|
|
3323
|
+
body.innerHTML = renderSourceTable(file, '');
|
|
3324
|
+
} else {
|
|
3325
|
+
body.classList.add('rendered-body');
|
|
3326
|
+
body.innerHTML = renderMarkdownRows(file.content);
|
|
3327
|
+
}
|
|
2775
3328
|
if (httpEnvSelect) httpEnvSelect.classList.add('hidden');
|
|
3329
|
+
updateRenderToggle(path);
|
|
2776
3330
|
renderSourceComments();
|
|
2777
3331
|
if (shouldSwitch) showSourceView();
|
|
2778
3332
|
return;
|
|
2779
3333
|
}
|
|
2780
3334
|
if (isCsvPath(path)) {
|
|
2781
|
-
|
|
2782
|
-
|
|
3335
|
+
if (renderRawMode) {
|
|
3336
|
+
body.innerHTML = renderSourceTable(file, '');
|
|
3337
|
+
} else {
|
|
3338
|
+
body.classList.add('rendered-body');
|
|
3339
|
+
body.innerHTML = renderCsvRows(file.content, path);
|
|
3340
|
+
}
|
|
2783
3341
|
if (httpEnvSelect) httpEnvSelect.classList.add('hidden');
|
|
3342
|
+
updateRenderToggle(path);
|
|
2784
3343
|
renderSourceComments();
|
|
2785
3344
|
if (shouldSwitch) showSourceView();
|
|
2786
3345
|
return;
|
|
@@ -2792,12 +3351,45 @@ function openSourceFile(path, shouldSwitch = true) {
|
|
|
2792
3351
|
body.innerHTML = renderSourceTable(file, '');
|
|
2793
3352
|
if (httpEnvSelect) httpEnvSelect.classList.add('hidden');
|
|
2794
3353
|
}
|
|
3354
|
+
updateRenderToggle(path);
|
|
2795
3355
|
renderSourceComments();
|
|
2796
3356
|
if (shouldSwitch) showSourceView();
|
|
2797
3357
|
}
|
|
2798
3358
|
|
|
2799
3359
|
function isMarkdownPath(p) { return /\.(md|mdx|markdown)$/i.test(p || ''); }
|
|
2800
3360
|
function isCsvPath(p) { return /\.(csv|tsv)$/i.test(p || ''); }
|
|
3361
|
+
function isRenderToggleable(p) { return isMarkdownPath(p) || isCsvPath(p); }
|
|
3362
|
+
|
|
3363
|
+
// Markdown/CSV open rendered by default; this flips the open file to raw line-numbered text and back.
|
|
3364
|
+
// Session-global so the choice carries across files. The toolbar button + Cmd/Ctrl+Shift+M both call it.
|
|
3365
|
+
var renderRawMode = false;
|
|
3366
|
+
function updateRenderToggle(path) {
|
|
3367
|
+
var btn = document.getElementById('render-toggle');
|
|
3368
|
+
if (!btn) return;
|
|
3369
|
+
var on = isRenderToggleable(path);
|
|
3370
|
+
btn.classList.toggle('hidden', !on);
|
|
3371
|
+
if (!on) return;
|
|
3372
|
+
btn.textContent = renderRawMode ? t('source.viewRendered') : t('source.viewRaw'); // label = the mode you switch TO
|
|
3373
|
+
btn.setAttribute('aria-pressed', renderRawMode ? 'true' : 'false');
|
|
3374
|
+
}
|
|
3375
|
+
function toggleRenderMode() {
|
|
3376
|
+
var sv = document.getElementById('source-viewer');
|
|
3377
|
+
var open = sv && sv.dataset.openPath;
|
|
3378
|
+
if (!open || !isRenderToggleable(open)) return;
|
|
3379
|
+
renderRawMode = !renderRawMode;
|
|
3380
|
+
openSourceFile(open, false); // re-render the current file in the new mode
|
|
3381
|
+
}
|
|
3382
|
+
(function wireRenderToggle() {
|
|
3383
|
+
var btn = document.getElementById('render-toggle');
|
|
3384
|
+
if (btn) btn.addEventListener('click', function () { toggleRenderMode(); });
|
|
3385
|
+
document.addEventListener('keydown', function (e) {
|
|
3386
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && !e.altKey && (e.key === 'M' || e.key === 'm' || e.code === 'KeyM')) {
|
|
3387
|
+
var sv = document.getElementById('source-viewer');
|
|
3388
|
+
var open = sv && sv.dataset.openPath;
|
|
3389
|
+
if (open && isRenderToggleable(open) && isSourceViewerVisible()) { e.preventDefault(); toggleRenderMode(); }
|
|
3390
|
+
}
|
|
3391
|
+
});
|
|
3392
|
+
})();
|
|
2801
3393
|
|
|
2802
3394
|
function renderImageView(file) {
|
|
2803
3395
|
return '<div class="image-view">'
|