@happy-nut/monacori 0.1.12 → 0.1.14

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/dist/i18n.js CHANGED
@@ -131,6 +131,7 @@ export const MESSAGES = {
131
131
  "merged.close": "Close",
132
132
  "dropdown.navigate": "Go to comment",
133
133
  "dropdown.remove": "Remove",
134
+ "toast.commentsDropped": "Removed {n} comment(s) on {file} — the file changed too much to track them",
134
135
  "merged.qHeading": "# Questions",
135
136
  "merged.cHeading": "# Change requests",
136
137
  // Prompt memo (Cmd/Ctrl+Shift+N) — a single freeform Markdown scratchpad with a live split preview.
@@ -262,6 +263,7 @@ export const MESSAGES = {
262
263
  "merged.close": "닫기",
263
264
  "dropdown.navigate": "코멘트로 이동",
264
265
  "dropdown.remove": "지우기",
266
+ "toast.commentsDropped": "{file}이(가) 변경되어 추적할 수 없는 코멘트 {n}개를 제거했습니다",
265
267
  // Structural markers stay English in both locales (the preamble prose below follows the locale).
266
268
  "merged.qHeading": "# Questions",
267
269
  "merged.cHeading": "# Change requests",
package/dist/render.js CHANGED
@@ -180,9 +180,9 @@ export function renderDiffHtml(input) {
180
180
  '<div id="app-info-status" class="app-info-status" data-i18n="settings.checkingUpdates">Checking for updates…</div>',
181
181
  '<button type="button" id="app-info-update" class="plain-button app-info-update hidden" data-i18n="settings.updateRestart">Update &amp; Restart</button>',
182
182
  '<label class="settings-label" for="settings-language" data-i18n="settings.language">Language</label>',
183
- '<select id="settings-language" class="settings-select"><option value="en">English</option><option value="ko">한국어</option></select>',
183
+ '<button type="button" id="settings-language" class="settings-select mc-select" data-i18n-aria="settings.language"></button>',
184
184
  '<label class="settings-label" for="settings-theme" data-i18n="settings.theme">Theme</label>',
185
- '<select id="settings-theme" class="settings-select"><option value="dark" data-i18n="theme.dark">Dark</option><option value="light" data-i18n="theme.light">Light</option></select>',
185
+ '<button type="button" id="settings-theme" class="settings-select mc-select" data-i18n-aria="settings.theme"></button>',
186
186
  '<div class="app-info-keys">' +
187
187
  '<div class="app-info-keys-h" data-i18n="settings.kbd.title">Keyboard shortcuts</div>' +
188
188
  '<div class="keys-cat" data-i18n="settings.kbd.cat.nav">Navigation</div>' +
@@ -147,14 +147,36 @@ var locale = (function () {
147
147
  return (v === 'ko' || v === 'en') ? v : 'en';
148
148
  })();
149
149
  function t(key) { var m = (I18N[locale] || I18N.en || {}); return (m && key in m) ? m[key] : ((I18N.en && I18N.en[key]) || key); }
150
+ var langSelectRef = null, themeSelectRef = null;
151
+ // Replace a native <select> with a button that opens our custom dropdown (consistent with the comment
152
+ // dropdown + themable; native <select> popups ignore the app theme). getOptions() -> [{value,label}];
153
+ // returns { render } so localized labels can be refreshed on a language switch.
154
+ function setupCustomSelect(id, getOptions, getCurrent, onPick) {
155
+ var el = document.getElementById(id);
156
+ if (!el) return null;
157
+ function render() {
158
+ var cur = getCurrent();
159
+ var match = getOptions().filter(function (o) { return o.value === cur; })[0];
160
+ el.textContent = match ? match.label : cur;
161
+ }
162
+ el.addEventListener('click', function (e) {
163
+ e.preventDefault();
164
+ var r = el.getBoundingClientRect(), cur = getCurrent();
165
+ showCustomDropdown(r.left, r.bottom + 4, getOptions().map(function (o) {
166
+ return { label: (o.value === cur ? '✓ ' : '  ') + o.label, onSelect: function () { onPick(o.value); render(); } };
167
+ }), r.top - 4);
168
+ });
169
+ render();
170
+ return { render: render };
171
+ }
150
172
  function applyI18n() {
151
173
  document.querySelectorAll('[data-i18n]').forEach(function (el) { el.textContent = t(el.getAttribute('data-i18n')); });
152
174
  document.querySelectorAll('[data-i18n-ph]').forEach(function (el) { el.setAttribute('placeholder', t(el.getAttribute('data-i18n-ph'))); });
153
175
  document.querySelectorAll('[data-i18n-title]').forEach(function (el) { el.setAttribute('title', t(el.getAttribute('data-i18n-title'))); });
154
176
  document.querySelectorAll('[data-i18n-aria]').forEach(function (el) { el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria'))); });
155
177
  document.documentElement.lang = locale;
156
- var sel = document.getElementById('settings-language');
157
- if (sel) sel.value = locale;
178
+ if (langSelectRef) langSelectRef.render();
179
+ if (themeSelectRef) themeSelectRef.render(); // theme labels are localized — refresh on a language switch too
158
180
  }
159
181
  // Theme mirrors the locale pattern: persisted choice, applied by toggling data-theme on <html> so the
160
182
  // :root[data-theme="light"] palette takes over. Dark is the default (matches the inline :root). Applied
@@ -167,8 +189,7 @@ var theme = (function () {
167
189
  })();
168
190
  function applyTheme() {
169
191
  document.documentElement.setAttribute('data-theme', theme);
170
- var sel = document.getElementById('settings-theme');
171
- if (sel) sel.value = theme;
192
+ if (themeSelectRef) themeSelectRef.render();
172
193
  }
173
194
  applyTheme();
174
195
  let fileStates = JSON.parse(document.getElementById('file-state-data')?.textContent || '[]');
@@ -213,6 +234,7 @@ function loadSourceData() {
213
234
  sourceLoaded = true;
214
235
  sourceLoading = false;
215
236
  scheduleSymbolIndex();
237
+ remapComments(); // content just arrived — reconcile comment anchors against it
216
238
  if (pendingSourceOpen) { var po = pendingSourceOpen; pendingSourceOpen = null; openSourceFile(po.path, po.shouldSwitch); }
217
239
  else if (isSourceViewerVisible() && document.getElementById('source-viewer').dataset.openPath) { openSourceFile(document.getElementById('source-viewer').dataset.openPath, false); }
218
240
  if (pendingSymbol) { var s = pendingSymbol; pendingSymbol = null; goToDefOrUsages(s); }
@@ -1706,6 +1728,52 @@ function handleDiffCaretKey(event) {
1706
1728
 
1707
1729
  // ===== Review comments: questions ("?") and change-requests (">") =====
1708
1730
  // (COMMENTS_KEY / reviewComments / commentSeq / composerState are declared near the top of the script)
1731
+ // Bottom-left, non-blocking toast stack; each toast auto-dismisses. Used to tell the user when a file
1732
+ // change made some comments untrackable (they were removed).
1733
+ function showToast(message) {
1734
+ var stack = document.getElementById('mc-toasts');
1735
+ if (!stack) { stack = document.createElement('div'); stack.id = 'mc-toasts'; document.body.appendChild(stack); }
1736
+ var el = document.createElement('div');
1737
+ el.className = 'mc-toast';
1738
+ el.textContent = message;
1739
+ stack.appendChild(el);
1740
+ requestAnimationFrame(function () { el.classList.add('show'); });
1741
+ setTimeout(function () {
1742
+ el.classList.add('hide');
1743
+ setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
1744
+ }, 4500);
1745
+ }
1746
+ // When a file changes, follow each comment to its snapshot line (c.code) in the new content: same line if
1747
+ // unchanged, else the nearest exact match of that line. If the line can't be found the change is too large
1748
+ // to trust — drop the comment and toast. Files whose content isn't loaded yet (lazy) are skipped here and
1749
+ // reconciled once loadSourceData brings the content in.
1750
+ function remapComments() {
1751
+ if (!reviewComments.length) return;
1752
+ var dropped = [], moved = 0;
1753
+ reviewComments = reviewComments.filter(function (c) {
1754
+ var file = sourceByPath.get(c.path);
1755
+ if (!file || !file.embedded || typeof file.content !== 'string' || !file.content) return true;
1756
+ var code = c.code == null ? '' : String(c.code);
1757
+ if (!code.trim()) return true;
1758
+ var lines = file.content.split(/\r?\n/);
1759
+ if (lines[c.line - 1] === code) return true;
1760
+ var best = -1, bestDist = Infinity;
1761
+ for (var i = 0; i < lines.length; i++) {
1762
+ if (lines[i] === code) { var d = Math.abs(i - (c.line - 1)); if (d < bestDist) { bestDist = d; best = i; } }
1763
+ }
1764
+ if (best >= 0) { if (c.line !== best + 1) moved++; c.line = best + 1; return true; }
1765
+ dropped.push(c);
1766
+ return false;
1767
+ });
1768
+ if (!dropped.length && !moved) return; // nothing changed — skip the save/re-render
1769
+ saveComments();
1770
+ var byPath = {};
1771
+ dropped.forEach(function (c) { byPath[c.path] = (byPath[c.path] || 0) + 1; });
1772
+ Object.keys(byPath).forEach(function (p) {
1773
+ showToast(t('toast.commentsDropped').replace('{n}', byPath[p]).replace('{file}', String(p).split('/').pop()));
1774
+ });
1775
+ refreshComments();
1776
+ }
1709
1777
  function saveComments() {
1710
1778
  persistSave(COMMENTS_KEY, reviewComments);
1711
1779
  }
@@ -1756,14 +1824,16 @@ function currentCommentTarget() {
1756
1824
  var f = Math.min(sa, sb), t = Math.max(sa, sb);
1757
1825
  return { path: viewerCursor.path, line: t + 1, code: selText, from: f + 1, to: t + 1, side: null };
1758
1826
  }
1759
- return { path: viewerCursor.path, line: viewerCursor.lineIndex + 1, code: '', from: null, to: null, side: null };
1827
+ var scaretFile = sourceByPath.get(viewerCursor.path);
1828
+ var scaretCode = (scaretFile && typeof scaretFile.content === 'string') ? (scaretFile.content.split(/\r?\n/)[viewerCursor.lineIndex] || '') : '';
1829
+ return { path: viewerCursor.path, line: viewerCursor.lineIndex + 1, code: scaretCode, from: null, to: null, side: null };
1760
1830
  }
1761
1831
  // Diff view: prefer the explicit diff caret when there is no text selection.
1762
1832
  if (!hasSel && diffCursor && isDiffViewVisible()) {
1763
1833
  var dwrap = diffWrapperByPath(diffCursor.path);
1764
1834
  var drow = dwrap ? diffRowAt(dwrap, diffCursor.side, diffCursor.rowIndex) : null;
1765
1835
  var dline = drow ? diffLineNumber(drow) : null;
1766
- if (dline != null) return { path: diffCursor.path, line: dline, code: '', from: null, to: null, side: null };
1836
+ if (dline != null) return { path: diffCursor.path, line: dline, code: diffLineText(drow) || '', from: null, to: null, side: null };
1767
1837
  }
1768
1838
  // Diff view with a selection (or click): anchor at the LAST line so the composer drops BELOW the
1769
1839
  // drag; capture the selected code + line span (used to keep the drag highlighted via .mc-sel-line).
@@ -2021,7 +2091,28 @@ function saveMergePrompt(kind, text) {
2021
2091
 
2022
2092
  // Reusable custom dropdown (keyboard + mouse). options: [{ label, onSelect }]. First item is pre-selected;
2023
2093
  // Arrow keys move, Enter chooses, Esc / click-outside dismiss. Replaces native <select>/menus everywhere.
2024
- function showCustomDropdown(x, y, options) {
2094
+ // Approximate the caret's pixel position in the (monospace) merged textarea so the dropdown can open right
2095
+ // under it — and flip above when there isn't room below. Returns { x, top (caret line top), below }.
2096
+ function mergedCaretXY(area) {
2097
+ var pos = area.selectionStart || 0;
2098
+ var nl = area.value.slice(0, pos).split('\n');
2099
+ var lineNum = nl.length - 1;
2100
+ var col = nl[lineNum].length;
2101
+ var cs = getComputedStyle(area);
2102
+ var lineH = parseFloat(cs.lineHeight) || 18;
2103
+ var rect = area.getBoundingClientRect();
2104
+ var span = document.createElement('span');
2105
+ span.style.cssText = 'position:absolute;visibility:hidden;white-space:pre';
2106
+ span.style.font = cs.font;
2107
+ span.textContent = 'MMMMMMMMMMMMMMMMMMMM';
2108
+ document.body.appendChild(span);
2109
+ var charW = span.getBoundingClientRect().width / 20;
2110
+ span.remove();
2111
+ var caretTop = rect.top + (parseFloat(cs.paddingTop) || 0) + lineNum * lineH - area.scrollTop;
2112
+ var x = Math.min(rect.left + (parseFloat(cs.paddingLeft) || 0) + col * charW, rect.right - 24);
2113
+ return { x: x, top: caretTop, below: caretTop + lineH + 2 };
2114
+ }
2115
+ function showCustomDropdown(x, y, options, flipTop) {
2025
2116
  var existing = document.getElementById('mc-dropdown');
2026
2117
  if (existing) existing.remove();
2027
2118
  var dd = document.createElement('div');
@@ -2046,9 +2137,16 @@ function showCustomDropdown(x, y, options) {
2046
2137
  item.addEventListener('mousemove', function () { setActive(i); });
2047
2138
  dd.appendChild(item);
2048
2139
  });
2049
- dd.style.left = Math.round(x) + 'px';
2050
- dd.style.top = Math.round(y) + 'px';
2051
2140
  document.body.appendChild(dd);
2141
+ // Position after measuring: open at (x, y); flip above (flipTop) when it would overflow the bottom,
2142
+ // and nudge in from the right/bottom edges so it never clips offscreen.
2143
+ var ddr = dd.getBoundingClientRect();
2144
+ var top = y, left = x;
2145
+ if (typeof flipTop === 'number' && top + ddr.height > window.innerHeight - 8) top = Math.max(8, flipTop - ddr.height);
2146
+ else if (top + ddr.height > window.innerHeight - 8) top = Math.max(8, window.innerHeight - ddr.height - 8);
2147
+ if (left + ddr.width > window.innerWidth - 8) left = Math.max(8, window.innerWidth - ddr.width - 8);
2148
+ dd.style.left = Math.round(left) + 'px';
2149
+ dd.style.top = Math.round(top) + 'px';
2052
2150
  document.addEventListener('keydown', onKey, true);
2053
2151
  document.addEventListener('mousedown', onOutside, true);
2054
2152
  }
@@ -2151,20 +2249,20 @@ function openMergedView(kind) {
2151
2249
  e.stopPropagation();
2152
2250
  var seqs = mergedCommentSeqs(kind, area.selectionStart, area.selectionEnd);
2153
2251
  if (!seqs.length) return;
2154
- var rect = area.getBoundingClientRect();
2155
- var x = rect.left + 24, y = rect.top + 48;
2252
+ var cxy = mergedCaretXY(area);
2253
+ var x = cxy.x, y = cxy.below, flipTop = cxy.top;
2156
2254
  var rerender = function () {
2157
2255
  if (!reviewComments.filter(function (c) { return c.kind === kind; }).length) { modal.remove(); return; }
2158
2256
  area.value = buildMergedText(kind);
2159
2257
  };
2160
2258
  if (area.selectionStart !== area.selectionEnd || seqs.length > 1) {
2161
- showCustomDropdown(x, y, [{ label: t('dropdown.remove'), onSelect: function () { seqs.forEach(deleteComment); rerender(); } }]);
2259
+ showCustomDropdown(x, y, [{ label: t('dropdown.remove'), onSelect: function () { seqs.forEach(deleteComment); rerender(); } }], flipTop);
2162
2260
  } else {
2163
2261
  var seq = seqs[0];
2164
2262
  showCustomDropdown(x, y, [
2165
2263
  { label: t('dropdown.navigate'), onSelect: function () { modal.remove(); navigateToComment(seq); } },
2166
2264
  { label: t('dropdown.remove'), onSelect: function () { deleteComment(seq); rerender(); } },
2167
- ]);
2265
+ ], flipTop);
2168
2266
  }
2169
2267
  });
2170
2268
  closeBtn.addEventListener('click', function () { modal.remove(); });
@@ -2194,12 +2292,12 @@ function openMergedView(kind) {
2194
2292
  document.body.appendChild(modal);
2195
2293
  // Focus the send button (Enter starts pane-pick) when present, else the read-only text. Electron
2196
2294
  // async-restores focus to <body>, so retry briefly (same as the composer).
2197
- var modalFocusTarget = sendBtn || area;
2295
+ var modalFocusTarget = area; // focus the text (not the send button) so the caret is visible and Opt+Arrow/Enter work; Send-to-terminal is a click
2198
2296
  var modalFocusTries = 0;
2199
2297
  var tryFocusModal = function () {
2200
2298
  if (!document.getElementById('mc-modal')) return true;
2201
2299
  if (document.activeElement === modalFocusTarget) return true;
2202
- try { modalFocusTarget.focus(); if (modalFocusTarget === area) modalFocusTarget.select(); } catch (e) {}
2300
+ try { modalFocusTarget.focus(); modalFocusTarget.selectionStart = modalFocusTarget.selectionEnd = 0; } catch (e) {}
2203
2301
  return document.activeElement === modalFocusTarget;
2204
2302
  };
2205
2303
  if (!tryFocusModal()) {
@@ -2751,35 +2849,28 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
2751
2849
  if (resetBtn) resetBtn.addEventListener('click', function () { saveMergePrompt('q', ''); saveMergePrompt('c', ''); fill(); flash(); });
2752
2850
  // Language: live-switch the whole UI (no reload). Persist, re-apply the static chrome, then re-render
2753
2851
  // any currently-shown dynamic text (open composer / merged modal / index status) so it follows too.
2754
- var langSel = document.getElementById('settings-language');
2755
- if (langSel) {
2756
- langSel.value = locale;
2757
- langSel.addEventListener('change', function () {
2758
- var next = langSel.value === 'ko' ? 'ko' : 'en';
2852
+ langSelectRef = setupCustomSelect('settings-language',
2853
+ function () { return [{ value: 'en', label: 'English' }, { value: 'ko', label: '한국어' }]; },
2854
+ function () { return locale; },
2855
+ function (next) {
2759
2856
  if (next === locale) return;
2760
2857
  locale = next;
2761
2858
  persistSave(LOCALE_KEY, locale);
2762
2859
  applyI18n();
2763
- // Merge-prompt placeholders are locale-dependent defaults; refresh them while the panel is open.
2764
- fill();
2765
- // Re-render dynamic, currently-visible text in the new locale.
2860
+ fill(); // merge-prompt placeholders are locale-dependent defaults
2766
2861
  try { if (typeof refreshComments === 'function') refreshComments(); } catch (e) {}
2767
2862
  var mergedModal = document.getElementById('mc-modal');
2768
2863
  if (mergedModal) { var mk = mergedModal.dataset.kind || 'q'; mergedModal.remove(); openMergedView(mk); }
2769
2864
  });
2770
- }
2771
- // Theme: flip data-theme on <html> live (no reload) and persist the choice.
2772
- var themeSel = document.getElementById('settings-theme');
2773
- if (themeSel) {
2774
- themeSel.value = theme;
2775
- themeSel.addEventListener('change', function () {
2776
- var next = themeSel.value === 'light' ? 'light' : 'dark';
2865
+ themeSelectRef = setupCustomSelect('settings-theme',
2866
+ function () { return [{ value: 'dark', label: t('theme.dark') }, { value: 'light', label: t('theme.light') }]; },
2867
+ function () { return theme; },
2868
+ function (next) {
2777
2869
  if (next === theme) return;
2778
2870
  theme = next;
2779
2871
  persistSave(THEME_KEY, theme);
2780
2872
  applyTheme();
2781
2873
  });
2782
- }
2783
2874
  })();
2784
2875
 
2785
2876
  function setTab(name) {
@@ -2934,6 +3025,7 @@ function applyDiffUpdate(u) {
2934
3025
  applyI18n();
2935
3026
  populateHttpEnvSelect();
2936
3027
  initSourceTreeFolds();
3028
+ remapComments(); // follow/drop comments whose anchor line moved or vanished in the new build
2937
3029
  refreshComments();
2938
3030
 
2939
3031
  // 5) Best-effort restore of what the user was looking at.
package/dist/viewer.css CHANGED
@@ -262,6 +262,10 @@ body {
262
262
  font: 12px/1.55 ui-sans-serif, system-ui, sans-serif;
263
263
  }
264
264
  .settings-select:focus { outline: none; border-color: var(--active); }
265
+ /* The settings dropdowns are buttons (custom dropdown), not native <select> — style them to read like one. */
266
+ .settings-select.mc-select { text-align: left; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; min-width: 130px; }
267
+ .settings-select.mc-select::after { content: '▾'; margin-left: auto; opacity: 0.55; }
268
+ .settings-select.mc-select:hover { border-color: var(--active); }
265
269
  .settings-actions { display: flex; align-items: center; gap: 12px; margin-top: 18px; }
266
270
  .settings-saved { font-size: 12px; color: var(--active); }
267
271
  .app-info-keys-h { font-weight: 600; color: var(--text); margin-bottom: 8px; }
@@ -856,9 +860,13 @@ body.mc-composing .source-row.cursor-line .num { color: inherit; }
856
860
  .mc-modal-head span { margin-right: auto; }
857
861
  .mc-modal-text { width: 100%; height: 100%; box-sizing: border-box; resize: none; border: 0; padding: 12px; background: var(--bg); color: var(--text); caret-color: var(--text); font: 12px/1.55 Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
858
862
  .mc-modal-text:focus { outline: none; }
859
- .mc-dropdown { position: fixed; z-index: 70; min-width: 150px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); padding: 4px; }
860
- .mc-dropdown-item { display: block; width: 100%; text-align: left; padding: 7px 12px; border: 0; background: transparent; color: var(--text); border-radius: 6px; font-size: 13px; cursor: pointer; white-space: nowrap; }
863
+ .mc-dropdown { position: fixed; z-index: 70; min-width: 0; background: var(--panel); border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); padding: 3px; }
864
+ .mc-dropdown-item { display: block; width: 100%; text-align: left; padding: 4px 10px; border: 0; background: transparent; color: var(--text); border-radius: 4px; font-size: 12px; line-height: 1.5; cursor: pointer; white-space: nowrap; }
861
865
  .mc-dropdown-item.active, .mc-dropdown-item:hover { background: var(--active); color: #fff; }
866
+ #mc-toasts { position: fixed; left: 16px; bottom: 16px; z-index: 80; display: flex; flex-direction: column; gap: 8px; max-width: 360px; pointer-events: none; }
867
+ .mc-toast { background: var(--panel); color: var(--text); border: 1px solid var(--border); border-left: 3px solid var(--active); border-radius: 8px; padding: 10px 14px; font-size: 13px; line-height: 1.45; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); opacity: 0; transform: translateY(8px); transition: opacity .25s ease, transform .25s ease; }
868
+ .mc-toast.show { opacity: 1; transform: translateY(0); }
869
+ .mc-toast.hide { opacity: 0; transform: translateY(8px); }
862
870
  /* Prompt memo: split editor | live Markdown preview inside the standard modal shell. */
863
871
  .mc-memo-body { display: grid; grid-template-columns: 1fr 1fr; min-height: 0; height: 100%; }
864
872
  .mc-memo-edit { height: 100%; border-right: 1px solid var(--border); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happy-nut/monacori",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Validation control plane for AI-generated code changes.",
5
5
  "type": "module",
6
6
  "repository": {