@happy-nut/monacori 0.1.12 → 0.1.13

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
+ }));
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).
@@ -2751,35 +2821,28 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
2751
2821
  if (resetBtn) resetBtn.addEventListener('click', function () { saveMergePrompt('q', ''); saveMergePrompt('c', ''); fill(); flash(); });
2752
2822
  // Language: live-switch the whole UI (no reload). Persist, re-apply the static chrome, then re-render
2753
2823
  // 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';
2824
+ langSelectRef = setupCustomSelect('settings-language',
2825
+ function () { return [{ value: 'en', label: 'English' }, { value: 'ko', label: '한국어' }]; },
2826
+ function () { return locale; },
2827
+ function (next) {
2759
2828
  if (next === locale) return;
2760
2829
  locale = next;
2761
2830
  persistSave(LOCALE_KEY, locale);
2762
2831
  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.
2832
+ fill(); // merge-prompt placeholders are locale-dependent defaults
2766
2833
  try { if (typeof refreshComments === 'function') refreshComments(); } catch (e) {}
2767
2834
  var mergedModal = document.getElementById('mc-modal');
2768
2835
  if (mergedModal) { var mk = mergedModal.dataset.kind || 'q'; mergedModal.remove(); openMergedView(mk); }
2769
2836
  });
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';
2837
+ themeSelectRef = setupCustomSelect('settings-theme',
2838
+ function () { return [{ value: 'dark', label: t('theme.dark') }, { value: 'light', label: t('theme.light') }]; },
2839
+ function () { return theme; },
2840
+ function (next) {
2777
2841
  if (next === theme) return;
2778
2842
  theme = next;
2779
2843
  persistSave(THEME_KEY, theme);
2780
2844
  applyTheme();
2781
2845
  });
2782
- }
2783
2846
  })();
2784
2847
 
2785
2848
  function setTab(name) {
@@ -2934,6 +2997,7 @@ function applyDiffUpdate(u) {
2934
2997
  applyI18n();
2935
2998
  populateHttpEnvSelect();
2936
2999
  initSourceTreeFolds();
3000
+ remapComments(); // follow/drop comments whose anchor line moved or vanished in the new build
2937
3001
  refreshComments();
2938
3002
 
2939
3003
  // 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; }
@@ -859,6 +863,10 @@ body.mc-composing .source-row.cursor-line .num { color: inherit; }
859
863
  .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
864
  .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; }
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.13",
4
4
  "description": "Validation control plane for AI-generated code changes.",
5
5
  "type": "module",
6
6
  "repository": {