@happy-nut/monacori 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -53
- package/assets/monacori-demo.gif +0 -0
- package/dist/app-main.js +47 -7
- package/dist/build.js +23 -1
- package/dist/commands.js +12 -326
- package/dist/i18n.js +10 -0
- package/dist/preload.cjs +9 -0
- package/dist/render.d.ts +11 -0
- package/dist/render.js +13 -5
- package/dist/server.js +6 -0
- package/dist/types.d.ts +12 -0
- package/dist/viewer.client.js +267 -29
- package/dist/viewer.css +56 -7
- package/package.json +1 -1
- package/assets/screenshots/diff-review.png +0 -0
- package/assets/screenshots/terminal.png +0 -0
package/dist/viewer.client.js
CHANGED
|
@@ -113,9 +113,9 @@ function setupLazyDiff() {
|
|
|
113
113
|
if (wrappers[0]) ensureFileReady(wrappers[0]); // first file ready so the initial caret has a row to land on
|
|
114
114
|
}
|
|
115
115
|
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
116
|
-
|
|
116
|
+
let links = Array.from(document.querySelectorAll('#changes-panel .file-link')); // re-captured on in-place diff update
|
|
117
117
|
let sourceLinks = Array.from(document.querySelectorAll('.source-link')); // re-captured when a deferred tree materializes
|
|
118
|
-
|
|
118
|
+
let sourceFiles = JSON.parse(document.getElementById('source-files-data')?.textContent || '[]');
|
|
119
119
|
// i18n: the message catalog (en + ko) is emitted server-side; the locale lives in localStorage and the
|
|
120
120
|
// whole UI switches live (no reload). t() feeds dynamically-built text; applyI18n() rewrites the static
|
|
121
121
|
// chrome (data-i18n / -ph / -title / -aria). English is the first-paint default.
|
|
@@ -147,13 +147,13 @@ function applyI18n() {
|
|
|
147
147
|
var sel = document.getElementById('settings-language');
|
|
148
148
|
if (sel) sel.value = locale;
|
|
149
149
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
let fileStates = JSON.parse(document.getElementById('file-state-data')?.textContent || '[]');
|
|
151
|
+
let httpEnvironments = JSON.parse(document.getElementById('http-env-data')?.textContent || '{}');
|
|
152
|
+
let httpEnvNames = Object.keys(httpEnvironments);
|
|
153
153
|
const httpEnvKey = 'monacori-http-env:' + location.pathname;
|
|
154
154
|
const httpRequestsByPath = new Map();
|
|
155
155
|
const httpVarsByPath = new Map();
|
|
156
|
-
|
|
156
|
+
let sourceByPath = new Map(sourceFiles.map((file) => [file.path, file]));
|
|
157
157
|
// Phase 2b lazy-LOAD: source content is fetched once after first paint (serve /source-data or the
|
|
158
158
|
// Electron bridge) and merged into the metadata-only source records; until then sourceLoaded is false
|
|
159
159
|
// and the source view shows a brief loading state. Non-lazy-load modes embed source -> already loaded.
|
|
@@ -184,16 +184,16 @@ function loadSourceData() {
|
|
|
184
184
|
}
|
|
185
185
|
sourceLoaded = true;
|
|
186
186
|
sourceLoading = false;
|
|
187
|
-
|
|
187
|
+
scheduleSymbolIndex();
|
|
188
188
|
if (pendingSourceOpen) { var po = pendingSourceOpen; pendingSourceOpen = null; openSourceFile(po.path, po.shouldSwitch); }
|
|
189
189
|
else if (isSourceViewerVisible() && document.getElementById('source-viewer').dataset.openPath) { openSourceFile(document.getElementById('source-viewer').dataset.openPath, false); }
|
|
190
190
|
if (pendingSymbol) { var s = pendingSymbol; pendingSymbol = null; goToDefOrUsages(s); }
|
|
191
191
|
}, function () { sourceLoaded = true; sourceLoading = false; });
|
|
192
192
|
}
|
|
193
|
-
|
|
193
|
+
let fileSignatureByPath = new Map(fileStates.map((file) => [file.path, file.signature]));
|
|
194
194
|
const reviewMeta = document.getElementById('review-meta');
|
|
195
195
|
const watchEnabled = reviewMeta?.dataset.watch === 'true';
|
|
196
|
-
|
|
196
|
+
let currentSignature = reviewMeta?.dataset.signature || '';
|
|
197
197
|
const uiStateKey = 'monacori-diff-ui:' + location.pathname;
|
|
198
198
|
const recentKey = 'monacori-diff-recent:' + location.pathname;
|
|
199
199
|
const viewedKey = 'monacori-diff-viewed:' + location.pathname;
|
|
@@ -455,14 +455,26 @@ function scheduleDiffScroll(row) {
|
|
|
455
455
|
});
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
var setActiveRaf = 0, setActiveScrollPending = true;
|
|
458
459
|
function setActive(index, shouldScroll = true) {
|
|
459
460
|
if (hunkTotal() === 0) return;
|
|
460
461
|
current = ((index % hunkTotal()) + hunkTotal()) % hunkTotal();
|
|
462
|
+
// Coalesce rapid presses (holding/spamming F7 or Shift+F7) into one DOM apply per animation frame. The
|
|
463
|
+
// key handler returns immediately and `current` updates synchronously (so next()/nav math stays correct),
|
|
464
|
+
// while the heavy DOM work (full link/wrapper sweeps, body materialize) runs at most once per frame
|
|
465
|
+
// instead of once per keystroke — the input queue never blocks and can't pile up on big repos.
|
|
466
|
+
setActiveScrollPending = shouldScroll;
|
|
467
|
+
if (setActiveRaf) return;
|
|
468
|
+
setActiveRaf = requestAnimationFrame(function () {
|
|
469
|
+
setActiveRaf = 0;
|
|
470
|
+
applySetActive(current, setActiveScrollPending);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function applySetActive(idx, shouldScroll) {
|
|
461
474
|
document.getElementById('source-viewer')?.classList.add('hidden');
|
|
462
475
|
document.getElementById('diff-view')?.classList.remove('hidden');
|
|
463
476
|
setTab('changes');
|
|
464
|
-
const file = hunkPathAt(
|
|
465
|
-
const idx = current;
|
|
477
|
+
const file = hunkPathAt(idx);
|
|
466
478
|
links.forEach((link) => link.classList.toggle('active', link.dataset.file === file));
|
|
467
479
|
renderBreadcrumb(document.getElementById('diff-breadcrumb'), file);
|
|
468
480
|
var dvt = document.getElementById('diff-viewed-toggle');
|
|
@@ -973,6 +985,13 @@ document.addEventListener('keydown', (event) => {
|
|
|
973
985
|
openMergedView((event.code === 'Slash' || event.key === '?') ? 'q' : 'c');
|
|
974
986
|
return;
|
|
975
987
|
}
|
|
988
|
+
// Cmd/Ctrl+Shift+N opens/closes the prompt memo. Electron also routes this via the Review menu; in the
|
|
989
|
+
// browser/serve build (no menu) this keydown is the only path. Match the physical key so layout/IME never swallows it.
|
|
990
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
991
|
+
event.preventDefault();
|
|
992
|
+
openMemoView();
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
976
995
|
// "?" = question, ">" = change-request composer on the current line/selection (no modifier).
|
|
977
996
|
if (!event.altKey && !event.metaKey && !event.ctrlKey && (event.key === '?' || event.key === '>')) {
|
|
978
997
|
const ce = document.activeElement;
|
|
@@ -1215,7 +1234,7 @@ document.addEventListener('copy', handleSourceCopy);
|
|
|
1215
1234
|
|
|
1216
1235
|
applyI18n(); // first paint already shows English (inline); this swaps to the saved locale before the rest of init renders dynamic text
|
|
1217
1236
|
populateHttpEnvSelect();
|
|
1218
|
-
if (!REVIEW_LAZY_LOAD)
|
|
1237
|
+
if (!REVIEW_LAZY_LOAD) scheduleSymbolIndex(); // non-lazy indexes when idle; lazy-LOAD defers the (large) source blob + index to the first source-view open / go-to-def
|
|
1219
1238
|
const restored = restoreUiState();
|
|
1220
1239
|
if (!restored) {
|
|
1221
1240
|
const initial = location.hash.match(/^#hunk-(\d+)$/);
|
|
@@ -1227,6 +1246,19 @@ initSourceTreeFolds();
|
|
|
1227
1246
|
if (watchEnabled) setInterval(checkForLiveUpdate, 1500);
|
|
1228
1247
|
window.addEventListener('beforeunload', saveUiState);
|
|
1229
1248
|
|
|
1249
|
+
// First render has painted — drop the boot overlay (it bridged the blank gap right after loadFile). Two
|
|
1250
|
+
// rAFs so the spinner stays until the diff/tree are actually on screen, then a short fade-out.
|
|
1251
|
+
(function () {
|
|
1252
|
+
var ov = document.getElementById('boot-overlay');
|
|
1253
|
+
if (!ov) return;
|
|
1254
|
+
requestAnimationFrame(function () {
|
|
1255
|
+
requestAnimationFrame(function () {
|
|
1256
|
+
ov.classList.add('hide');
|
|
1257
|
+
setTimeout(function () { ov.remove(); }, 240);
|
|
1258
|
+
});
|
|
1259
|
+
});
|
|
1260
|
+
})();
|
|
1261
|
+
|
|
1230
1262
|
(function setupSidebarResize() {
|
|
1231
1263
|
const resizer = document.querySelector('.sidebar-resizer');
|
|
1232
1264
|
if (!resizer) return;
|
|
@@ -1943,6 +1975,96 @@ function openMergedView(kind) {
|
|
|
1943
1975
|
}
|
|
1944
1976
|
}
|
|
1945
1977
|
|
|
1978
|
+
// Prompt memo (Cmd/Ctrl+Shift+N): one freeform Markdown scratchpad with a live split preview, persisted
|
|
1979
|
+
// across reopens via the same store as comments/locale. "Send to terminal" hands the current draft to the
|
|
1980
|
+
// same pane-pick mode the merged views use, so a half-formed prompt can target any live claude/codex session.
|
|
1981
|
+
var memoKey = 'monacori-memo';
|
|
1982
|
+
function loadMemo() {
|
|
1983
|
+
var v = persistRead(memoKey);
|
|
1984
|
+
if (typeof v === 'string') return v;
|
|
1985
|
+
try { var s = localStorage.getItem(memoKey); return typeof s === 'string' ? s : ''; } catch (e) { return ''; }
|
|
1986
|
+
}
|
|
1987
|
+
function saveMemo(text) { persistSave(memoKey, text || ''); }
|
|
1988
|
+
function renderMemoMd(text) {
|
|
1989
|
+
if (!text || !text.trim()) return '<div class="mc-memo-empty" data-i18n="memo.previewEmpty">' + escapeHtml(t('memo.previewEmpty')) + '</div>';
|
|
1990
|
+
return renderMarkdownBlocks(text).map(function (b) { return b.html; }).join('');
|
|
1991
|
+
}
|
|
1992
|
+
function openMemoView() {
|
|
1993
|
+
var existing = document.getElementById('mc-memo');
|
|
1994
|
+
if (existing) { existing.remove(); return; } // the shortcut toggles: a second press closes the memo
|
|
1995
|
+
var modal = document.createElement('div');
|
|
1996
|
+
modal.id = 'mc-memo';
|
|
1997
|
+
modal.className = 'mc-modal';
|
|
1998
|
+
var panel = document.createElement('div');
|
|
1999
|
+
panel.className = 'mc-modal-panel mc-memo-panel';
|
|
2000
|
+
var head = document.createElement('div');
|
|
2001
|
+
head.className = 'mc-modal-head';
|
|
2002
|
+
var title = document.createElement('span');
|
|
2003
|
+
title.setAttribute('data-i18n', 'memo.title');
|
|
2004
|
+
title.textContent = t('memo.title');
|
|
2005
|
+
var closeBtn = document.createElement('button');
|
|
2006
|
+
closeBtn.type = 'button';
|
|
2007
|
+
closeBtn.className = 'mc-btn mc-ghost';
|
|
2008
|
+
closeBtn.setAttribute('data-i18n', 'merged.close');
|
|
2009
|
+
closeBtn.textContent = t('merged.close');
|
|
2010
|
+
closeBtn.addEventListener('click', function () { modal.remove(); });
|
|
2011
|
+
|
|
2012
|
+
var body = document.createElement('div');
|
|
2013
|
+
body.className = 'mc-memo-body';
|
|
2014
|
+
var area = document.createElement('textarea');
|
|
2015
|
+
area.className = 'mc-modal-text mc-memo-edit';
|
|
2016
|
+
area.spellcheck = false;
|
|
2017
|
+
area.setAttribute('data-i18n-ph', 'memo.placeholder');
|
|
2018
|
+
area.placeholder = t('memo.placeholder');
|
|
2019
|
+
area.value = loadMemo();
|
|
2020
|
+
var preview = document.createElement('div');
|
|
2021
|
+
preview.className = 'md-cell mc-memo-preview';
|
|
2022
|
+
preview.innerHTML = renderMemoMd(area.value);
|
|
2023
|
+
area.addEventListener('input', function () {
|
|
2024
|
+
saveMemo(area.value);
|
|
2025
|
+
preview.innerHTML = renderMemoMd(area.value);
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
// Terminal send: hand the current draft to pane-pick mode (arrows choose the session, Enter sends). Shown
|
|
2029
|
+
// only once a terminal pane exists; enterSendMode reopens the panel if it was closed.
|
|
2030
|
+
var sendBtn = null;
|
|
2031
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.paneCount === 'function' && window.__monacoriTerminal.paneCount() > 0) {
|
|
2032
|
+
sendBtn = document.createElement('button');
|
|
2033
|
+
sendBtn.type = 'button';
|
|
2034
|
+
sendBtn.className = 'mc-btn mc-send-term';
|
|
2035
|
+
sendBtn.setAttribute('data-i18n', 'merged.sendToTerminal');
|
|
2036
|
+
sendBtn.textContent = t('merged.sendToTerminal');
|
|
2037
|
+
sendBtn.addEventListener('click', function () {
|
|
2038
|
+
var text = area.value;
|
|
2039
|
+
modal.remove();
|
|
2040
|
+
window.__monacoriTerminal.enterSendMode(text);
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
head.appendChild(title);
|
|
2045
|
+
if (sendBtn) head.appendChild(sendBtn);
|
|
2046
|
+
head.appendChild(closeBtn);
|
|
2047
|
+
body.appendChild(area);
|
|
2048
|
+
body.appendChild(preview);
|
|
2049
|
+
panel.appendChild(head);
|
|
2050
|
+
panel.appendChild(body);
|
|
2051
|
+
modal.appendChild(panel);
|
|
2052
|
+
modal.addEventListener('mousedown', function (e) { if (e.target === modal) modal.remove(); });
|
|
2053
|
+
modal.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); modal.remove(); } });
|
|
2054
|
+
document.body.appendChild(modal);
|
|
2055
|
+
// Focus the editor; Electron async-restores focus to <body>, so retry briefly (same as the composer/merged view).
|
|
2056
|
+
var memoFocusTries = 0;
|
|
2057
|
+
var tryFocusMemo = function () {
|
|
2058
|
+
if (!document.getElementById('mc-memo')) return true;
|
|
2059
|
+
if (document.activeElement === area) return true;
|
|
2060
|
+
try { area.focus(); } catch (e) {}
|
|
2061
|
+
return document.activeElement === area;
|
|
2062
|
+
};
|
|
2063
|
+
if (!tryFocusMemo()) {
|
|
2064
|
+
var memoFocusIv = setInterval(function () { if (tryFocusMemo() || ++memoFocusTries > 12) clearInterval(memoFocusIv); }, 25);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1946
2068
|
document.addEventListener('click', function (event) {
|
|
1947
2069
|
var t = event.target;
|
|
1948
2070
|
if (!t || !t.closest) return;
|
|
@@ -2011,8 +2133,17 @@ refreshComments();
|
|
|
2011
2133
|
|
|
2012
2134
|
function setActive(p) {
|
|
2013
2135
|
active = p;
|
|
2014
|
-
panes.forEach(function (q) {
|
|
2015
|
-
|
|
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 () {
|
|
2142
|
+
try {
|
|
2143
|
+
if (p.labelEl && p.labelEl.getAttribute('contenteditable') === 'true') return;
|
|
2144
|
+
p.term.focus();
|
|
2145
|
+
} catch (e) {}
|
|
2146
|
+
});
|
|
2016
2147
|
}
|
|
2017
2148
|
|
|
2018
2149
|
function makePane() {
|
|
@@ -2044,7 +2175,13 @@ refreshComments();
|
|
|
2044
2175
|
term.attachCustomKeyEventHandler(function (e) {
|
|
2045
2176
|
if (e.type === 'keydown' && e.metaKey) {
|
|
2046
2177
|
var k = (e.key || '').toLowerCase();
|
|
2047
|
-
|
|
2178
|
+
// The bare modifier press (Cmd goes down BEFORE the letter on macOS) must not blur — blurring
|
|
2179
|
+
// here drops the textarea focus the upcoming Cmd+V paste / Cmd+C copy needs, which broke them.
|
|
2180
|
+
if (k === 'meta' || k === 'control' || k === 'alt' || k === 'shift') return true;
|
|
2181
|
+
// Match the PHYSICAL key (e.code), not e.key: under a non-Latin layout/IME (e.g. Korean 한글)
|
|
2182
|
+
// Cmd+V reports e.key as 'ㅍ', so a key-based check misses it — blurring the terminal and
|
|
2183
|
+
// breaking paste/copy/cut/select-all whenever the Korean input source is active.
|
|
2184
|
+
if (e.code === 'KeyC' || e.code === 'KeyV' || e.code === 'KeyX' || e.code === 'KeyA') return true;
|
|
2048
2185
|
try { term.blur(); } catch (x) {}
|
|
2049
2186
|
return false;
|
|
2050
2187
|
}
|
|
@@ -2067,8 +2204,18 @@ refreshComments();
|
|
|
2067
2204
|
if (el.getAttribute('contenteditable') === 'true') return;
|
|
2068
2205
|
setActive(pane);
|
|
2069
2206
|
el.contentEditable = 'true';
|
|
2070
|
-
|
|
2071
|
-
|
|
2207
|
+
// Electron asynchronously restores focus to <body> after the keydown, so a one-shot focus loses the
|
|
2208
|
+
// race and the label turns editable but never gets the caret — retry until it sticks, then select all
|
|
2209
|
+
// (same pattern as the composer/memo). This is why rename "did nothing" before.
|
|
2210
|
+
var renameTries = 0;
|
|
2211
|
+
var focusLabel = function () {
|
|
2212
|
+
if (el.getAttribute('contenteditable') !== 'true') return true; // finished/cancelled meanwhile
|
|
2213
|
+
try { el.focus(); } catch (e) {}
|
|
2214
|
+
if (document.activeElement !== el) return false;
|
|
2215
|
+
try { var range = document.createRange(); range.selectNodeContents(el); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } catch (e) {}
|
|
2216
|
+
return true;
|
|
2217
|
+
};
|
|
2218
|
+
if (!focusLabel()) { var renameIv = setInterval(function () { if (focusLabel() || ++renameTries > 12) clearInterval(renameIv); }, 25); }
|
|
2072
2219
|
function finish(commit) {
|
|
2073
2220
|
el.removeEventListener('keydown', onKey);
|
|
2074
2221
|
el.removeEventListener('blur', onBlur);
|
|
@@ -2239,6 +2386,15 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onMergedView === 'function
|
|
|
2239
2386
|
// split), so the user can pick which claude/codex session receives the prompt.
|
|
2240
2387
|
window.monacoriMenu.onMergedView(function (kind) { openMergedView(kind); });
|
|
2241
2388
|
}
|
|
2389
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onOpenMemo === 'function') {
|
|
2390
|
+
// Cmd/Ctrl+Shift+N from the Review menu -> open/close the prompt memo.
|
|
2391
|
+
window.monacoriMenu.onOpenMemo(function () { openMemoView(); });
|
|
2392
|
+
}
|
|
2393
|
+
if (window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function') {
|
|
2394
|
+
// Electron watch: main rebuilds on working-tree changes and pushes the new HTML so we refresh the diff
|
|
2395
|
+
// in place — NO window reload — keeping the integrated terminal's pty sessions (claude/codex) alive.
|
|
2396
|
+
window.monacoriMenu.onDiffUpdate(function (html) { try { applyDiffUpdate(html); } catch (e) {} });
|
|
2397
|
+
}
|
|
2242
2398
|
if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function') {
|
|
2243
2399
|
// Cmd/Ctrl+W: close the active Files-mode tab (no-op outside the source viewer).
|
|
2244
2400
|
window.monacoriMenu.onCloseTab(function () {
|
|
@@ -2471,6 +2627,71 @@ function restoreUiState() {
|
|
|
2471
2627
|
return false;
|
|
2472
2628
|
}
|
|
2473
2629
|
|
|
2630
|
+
// In-place diff refresh (instead of a full window reload): apply a compact payload of just the changed
|
|
2631
|
+
// regions (diff container, sidebar trees, status, data) and re-run the bootstrap steps. The window never
|
|
2632
|
+
// reloads, so the integrated terminal's pty sessions (claude/codex) survive a watch refresh. Electron's
|
|
2633
|
+
// main pushes the payload over IPC (monacori:diff-update); serve mode's poller fetches /__ai_flow_update.
|
|
2634
|
+
function applyDiffUpdate(u) {
|
|
2635
|
+
if (!u || !u.signature || u.signature === currentSignature) return false; // unchanged — nothing to do
|
|
2636
|
+
|
|
2637
|
+
// Remember what to restore after the swap (comments/viewed persist on their own; these don't).
|
|
2638
|
+
var sv = document.getElementById('source-viewer');
|
|
2639
|
+
var openPath = (sv && sv.dataset.openPath) || '';
|
|
2640
|
+
var wasSource = isSourceViewerVisible();
|
|
2641
|
+
var container = document.getElementById('diff2html-container');
|
|
2642
|
+
var diffScrollTop = container ? container.scrollTop : 0;
|
|
2643
|
+
|
|
2644
|
+
// 1) Replace the visible regions straight from the payload (no full-HTML parse).
|
|
2645
|
+
if (container) container.innerHTML = u.diffContainer || '';
|
|
2646
|
+
var changesPanel = document.getElementById('changes-panel');
|
|
2647
|
+
if (changesPanel) changesPanel.innerHTML = u.changesPanel || '';
|
|
2648
|
+
// Files tree: keep the inert island (lazy, not yet opened) in sync, and refresh the live panel when it's
|
|
2649
|
+
// already materialized — or always, in eager mode where the panel holds the tree directly.
|
|
2650
|
+
var filesIsland = document.getElementById('files-tree-html');
|
|
2651
|
+
if (filesIsland) filesIsland.textContent = u.filesTree || '';
|
|
2652
|
+
var filesPanel = document.getElementById('files-panel');
|
|
2653
|
+
if (filesPanel && (!REVIEW_LAZY || filesPanel.innerHTML.trim())) filesPanel.innerHTML = u.filesTree || '';
|
|
2654
|
+
var statusEl = document.querySelector('.review-status');
|
|
2655
|
+
if (statusEl) statusEl.innerHTML = u.reviewStatus || '';
|
|
2656
|
+
if (reviewMeta) { reviewMeta.setAttribute('data-signature', u.signature); if (u.generatedAt) reviewMeta.setAttribute('data-generated-at', u.generatedAt); }
|
|
2657
|
+
|
|
2658
|
+
// 2) Re-derive module-level state directly from the payload objects.
|
|
2659
|
+
fileStates = u.fileStates || [];
|
|
2660
|
+
fileSignatureByPath = new Map(fileStates.map(function (f) { return [f.path, f.signature]; }));
|
|
2661
|
+
sourceFiles = u.sourceFilesMeta || [];
|
|
2662
|
+
sourceByPath = new Map(sourceFiles.map(function (f) { return [f.path, f]; }));
|
|
2663
|
+
httpEnvironments = u.httpEnvironments || {};
|
|
2664
|
+
httpEnvNames = Object.keys(httpEnvironments);
|
|
2665
|
+
currentSignature = u.signature;
|
|
2666
|
+
links = Array.from(document.querySelectorAll('#changes-panel .file-link'));
|
|
2667
|
+
sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
2668
|
+
|
|
2669
|
+
// 3) Reset lazy-materialize + index state so the new diff bodies / source / symbols rebuild on demand.
|
|
2670
|
+
bodyPromise = {};
|
|
2671
|
+
diffBootDone = false;
|
|
2672
|
+
sourceLoaded = !REVIEW_LAZY_LOAD; // lazyLoad: re-fetch source content on next use
|
|
2673
|
+
sourceLoading = false;
|
|
2674
|
+
symbolIndex = null;
|
|
2675
|
+
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
2676
|
+
else { prepareDiff2HtmlHunks(); diffBootDone = true; }
|
|
2677
|
+
if (!REVIEW_LAZY_LOAD) setTimeout(startSymbolIndex, 0);
|
|
2678
|
+
|
|
2679
|
+
// 4) Re-run the DOM-dependent bootstrap steps.
|
|
2680
|
+
applyI18n();
|
|
2681
|
+
populateHttpEnvSelect();
|
|
2682
|
+
initSourceTreeFolds();
|
|
2683
|
+
refreshComments();
|
|
2684
|
+
|
|
2685
|
+
// 5) Best-effort restore of what the user was looking at.
|
|
2686
|
+
if (wasSource && openPath && sourceByPath.has(openPath)) {
|
|
2687
|
+
openSourceFile(openPath, false);
|
|
2688
|
+
} else if (container) {
|
|
2689
|
+
showDiffView(false);
|
|
2690
|
+
container.scrollTop = diffScrollTop;
|
|
2691
|
+
}
|
|
2692
|
+
return true;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2474
2695
|
async function checkForLiveUpdate() {
|
|
2475
2696
|
if (checkingForUpdates) return;
|
|
2476
2697
|
checkingForUpdates = true;
|
|
@@ -2483,8 +2704,12 @@ async function checkForLiveUpdate() {
|
|
|
2483
2704
|
liveStatus.textContent = t('status.live.updated') + ' ' + new Date(state.generatedAt).toLocaleTimeString();
|
|
2484
2705
|
}
|
|
2485
2706
|
if (state.signature && state.signature !== currentSignature) {
|
|
2486
|
-
|
|
2487
|
-
|
|
2707
|
+
// serve mode: fetch just the compact update payload and refresh in place (same path Electron uses
|
|
2708
|
+
// over IPC) rather than reloading — so an open integrated terminal keeps its sessions.
|
|
2709
|
+
try {
|
|
2710
|
+
var fresh = await fetch('__ai_flow_update', { cache: 'no-store' });
|
|
2711
|
+
if (fresh.ok) applyDiffUpdate(await fresh.json());
|
|
2712
|
+
} catch (e) {}
|
|
2488
2713
|
}
|
|
2489
2714
|
} catch {
|
|
2490
2715
|
if (liveStatus) liveStatus.textContent = t('status.live.waiting');
|
|
@@ -3104,6 +3329,14 @@ function symbolIndexWorker() {
|
|
|
3104
3329
|
self.postMessage({ index: index, total: total });
|
|
3105
3330
|
};
|
|
3106
3331
|
}
|
|
3332
|
+
// Run symbol indexing off the critical path: requestIdleCallback so the heavy postMessage of the whole
|
|
3333
|
+
// source blob to the worker (structured-clone serialization is synchronous on the main thread) never
|
|
3334
|
+
// competes with key handling — especially on big repos right after the diff/tree first paints.
|
|
3335
|
+
function scheduleSymbolIndex() {
|
|
3336
|
+
var run = function () { try { startSymbolIndex(); } catch (e) {} };
|
|
3337
|
+
if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') window.requestIdleCallback(run, { timeout: 3000 });
|
|
3338
|
+
else setTimeout(run, 0);
|
|
3339
|
+
}
|
|
3107
3340
|
function startSymbolIndex() {
|
|
3108
3341
|
try {
|
|
3109
3342
|
if (typeof Worker === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined' || !URL.createObjectURL) return;
|
|
@@ -3810,16 +4043,21 @@ function populateHttpEnvSelect() {
|
|
|
3810
4043
|
opts += '<option value="' + escapeHtml(name) + '"' + (name === currentHttpEnvName ? ' selected' : '') + '>' + escapeHtml(name) + '</option>';
|
|
3811
4044
|
});
|
|
3812
4045
|
select.innerHTML = opts;
|
|
3813
|
-
select
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
4046
|
+
// The <select> lives in the toolbar (not swapped on in-place diff updates), so wire the change handler
|
|
4047
|
+
// exactly once — populateHttpEnvSelect is re-called by applyDiffUpdate to refresh the options.
|
|
4048
|
+
if (!select.dataset.wired) {
|
|
4049
|
+
select.dataset.wired = '1';
|
|
4050
|
+
select.addEventListener('change', function () {
|
|
4051
|
+
currentHttpEnvName = select.value;
|
|
4052
|
+
try { localStorage.setItem(httpEnvKey, currentHttpEnvName); } catch (error) {}
|
|
4053
|
+
const path = document.getElementById('source-viewer')?.dataset.openPath || '';
|
|
4054
|
+
if (path && isHttpFile(path)) {
|
|
4055
|
+
const file = sourceByPath.get(path);
|
|
4056
|
+
const body = document.getElementById('source-body');
|
|
4057
|
+
if (file && body) body.innerHTML = renderHttpTable(file);
|
|
4058
|
+
}
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
3823
4061
|
}
|
|
3824
4062
|
|
|
3825
4063
|
function renderSourceTable(file, query) {
|
package/dist/viewer.css
CHANGED
|
@@ -465,14 +465,16 @@ td.d2h-del:not(.d2h-code-side-linenumber) { color: #d8e0e8; }
|
|
|
465
465
|
}
|
|
466
466
|
.tree-dir summary::-webkit-details-marker { display: none; }
|
|
467
467
|
.tree-dir summary:hover { background: var(--bg); }
|
|
468
|
-
.tree-dir:not([open]) .folder-icon { transform: rotate(-90deg); }
|
|
469
468
|
.folder-icon {
|
|
470
469
|
display: inline-grid;
|
|
471
470
|
place-items: center;
|
|
472
|
-
font-size: 9px;
|
|
473
471
|
color: var(--muted);
|
|
474
|
-
transition: transform 120ms ease;
|
|
475
472
|
}
|
|
473
|
+
.folder-icon .folder-ic { width: 14px; height: 14px; display: block; }
|
|
474
|
+
/* Closed vs open folder glyph (replaces the old rotated "v" chevron). */
|
|
475
|
+
.tree-dir > summary .fi-open { display: none; }
|
|
476
|
+
.tree-dir[open] > summary .fi-closed { display: none; }
|
|
477
|
+
.tree-dir[open] > summary .fi-open { display: block; }
|
|
476
478
|
.file-link.tree-file { padding-left: calc(8px + (var(--depth) * 14px)); }
|
|
477
479
|
.tree-focus { box-shadow: inset 0 0 0 1px var(--active); border-radius: 6px; }
|
|
478
480
|
summary.tree-focus { background: var(--bg); }
|
|
@@ -524,7 +526,20 @@ summary.tree-focus { background: var(--bg); }
|
|
|
524
526
|
.status-deleted { background: var(--del); color: #cf222e; }
|
|
525
527
|
.status-renamed { background: #fff8c5; color: #9a6700; }
|
|
526
528
|
.status-source { background: var(--line); color: var(--muted); }
|
|
527
|
-
.content { min-width: 0; padding: 0; }
|
|
529
|
+
.content { min-width: 0; padding: 0; display: flex; flex-direction: column; min-height: 100vh; }
|
|
530
|
+
/* Pin the diff's horizontal scrollbar to the viewport bottom instead of letting it float
|
|
531
|
+
mid-screen when a file's diff is short: fill the content column vertically so the last
|
|
532
|
+
file's diff body extends all the way down. */
|
|
533
|
+
#diff-view:not(.hidden) { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
|
|
534
|
+
#diff-view .diff2html-container { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
|
|
535
|
+
.diff2html-container .d2h-wrapper { flex: 1 1 auto; display: flex; flex-direction: column; }
|
|
536
|
+
.diff2html-container .d2h-file-wrapper:last-child { flex: 1 1 auto; }
|
|
537
|
+
.diff2html-container .d2h-file-wrapper:last-child .d2h-files-diff { height: 100%; }
|
|
538
|
+
/* Slimmer scrollbars — the default overlay bars read as chunky on the dark UI. */
|
|
539
|
+
::-webkit-scrollbar { width: 9px; height: 9px; }
|
|
540
|
+
::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--muted) 32%, transparent); border-radius: 5px; }
|
|
541
|
+
::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--muted) 52%, transparent); }
|
|
542
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
528
543
|
.toolbar {
|
|
529
544
|
position: sticky;
|
|
530
545
|
top: 0;
|
|
@@ -629,6 +644,31 @@ h1 { margin: 0; font-size: 18px; }
|
|
|
629
644
|
background: var(--panel);
|
|
630
645
|
user-select: text;
|
|
631
646
|
}
|
|
647
|
+
/* Extend the line-number gutter (and its divider) to the bottom of the panel so it never stops at the
|
|
648
|
+
last line with an empty strip + cut-off border below it — one continuous gutter. The .num cell is 58px
|
|
649
|
+
wide + 8px padding each side + 1px border ≈ 75px; the gradient mirrors that. (pre-wrap = no h-scroll, so
|
|
650
|
+
a fixed background stays aligned.) */
|
|
651
|
+
.source-body:not(.empty):not(.image-body) {
|
|
652
|
+
background-image: linear-gradient(to right, var(--line) 0 74px, var(--border) 74px 75px, var(--panel) 75px);
|
|
653
|
+
background-repeat: no-repeat;
|
|
654
|
+
}
|
|
655
|
+
/* Boot overlay: painted the instant the review HTML loads, removed once the renderer's bootstrap has drawn
|
|
656
|
+
the diff/tree — so there's no blank screen between the startup spinner and first render. Mirrors
|
|
657
|
+
app-main's LOADING_HTML spinner exactly so the hand-off from the loading screen is seamless. */
|
|
658
|
+
#boot-overlay {
|
|
659
|
+
position: fixed; inset: 0; z-index: 200;
|
|
660
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px;
|
|
661
|
+
background: #2b2b2b; color: #9aa4af;
|
|
662
|
+
font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
663
|
+
transition: opacity 0.22s ease;
|
|
664
|
+
}
|
|
665
|
+
#boot-overlay.hide { opacity: 0; pointer-events: none; }
|
|
666
|
+
.boot-spinner {
|
|
667
|
+
width: 34px; height: 34px;
|
|
668
|
+
border: 3px solid #3a3a3a; border-top-color: #4a9eff; border-radius: 50%;
|
|
669
|
+
animation: boot-spin 0.8s linear infinite;
|
|
670
|
+
}
|
|
671
|
+
@keyframes boot-spin { to { transform: rotate(360deg); } }
|
|
632
672
|
/* Empty state ("Select a file…") centered in the available space, not top-left. */
|
|
633
673
|
.source-body.empty {
|
|
634
674
|
display: flex;
|
|
@@ -688,7 +728,8 @@ h1 { margin: 0; font-size: 18px; }
|
|
|
688
728
|
.source-row.md-row.cursor-line .num,
|
|
689
729
|
.source-row.csv-row.cursor-line .num { color: var(--active); }
|
|
690
730
|
.num {
|
|
691
|
-
width:
|
|
731
|
+
width: 75px;
|
|
732
|
+
box-sizing: border-box; /* fixed 75px total so the .source-body gutter gradient lines up exactly (no bleed) */
|
|
692
733
|
user-select: none;
|
|
693
734
|
text-align: right;
|
|
694
735
|
color: var(--muted);
|
|
@@ -760,6 +801,12 @@ h1 { margin: 0; font-size: 18px; }
|
|
|
760
801
|
.mc-modal-head span { margin-right: auto; }
|
|
761
802
|
.mc-modal-text { width: 100%; height: 100%; box-sizing: border-box; resize: none; border: 0; padding: 12px; background: var(--bg); color: var(--text); font: 12px/1.55 Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
762
803
|
.mc-modal-text:focus { outline: none; }
|
|
804
|
+
/* Prompt memo: split editor | live Markdown preview inside the standard modal shell. */
|
|
805
|
+
.mc-memo-body { display: grid; grid-template-columns: 1fr 1fr; min-height: 0; height: 100%; }
|
|
806
|
+
.mc-memo-edit { height: 100%; border-right: 1px solid var(--border); }
|
|
807
|
+
.mc-memo-preview { height: 100%; overflow: auto; padding: 12px 16px; background: var(--panel); color: var(--text); }
|
|
808
|
+
.mc-memo-preview > :first-child { margin-top: 0; }
|
|
809
|
+
.mc-memo-empty { color: var(--muted); font-size: 12px; font-style: italic; }
|
|
763
810
|
.tok-comment { color: var(--token-comment); font-style: italic; }
|
|
764
811
|
.tok-keyword { color: var(--token-keyword); font-weight: 650; }
|
|
765
812
|
.tok-string { color: var(--token-string); }
|
|
@@ -1074,9 +1121,11 @@ h1 { margin: 0; font-size: 18px; }
|
|
|
1074
1121
|
.terminal-pane-label[contenteditable="true"] { color: var(--text); background: var(--bg); outline: none; }
|
|
1075
1122
|
.terminal-pane.is-active .terminal-pane-label { color: var(--text); }
|
|
1076
1123
|
.terminal-pane-host { flex: 1 1 auto; min-width: 0; min-height: 0; padding: 4px 0 4px 8px; }
|
|
1077
|
-
|
|
1124
|
+
/* No border on the active pane — the inactive panes dim back instead, so the focused one stands out
|
|
1125
|
+
cleanly without an outline. Only applies with 2+ panes (a lone pane is never dimmed; see setActive). */
|
|
1126
|
+
.terminal-pane { transition: opacity 120ms ease; }
|
|
1127
|
+
.terminal-panel:not(.send-mode) .terminal-pane.is-inactive { opacity: 0.4; }
|
|
1078
1128
|
/* Pane-pick mode (merged "Send to terminal"): chosen pane ringed + full opacity, the rest dimmed. */
|
|
1079
|
-
.terminal-panel.send-mode .terminal-pane { transition: opacity 120ms ease; }
|
|
1080
1129
|
.terminal-pane.is-dimmed { opacity: 0.3; }
|
|
1081
1130
|
.terminal-pane.is-send-target { box-shadow: inset 0 0 0 2px var(--active); opacity: 1; position: relative; }
|
|
1082
1131
|
/* Faint ⏎ hint floating over the chosen pane; it vanishes the instant Enter exits send mode. */
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|