@happy-nut/monacori 0.1.23 → 0.1.26

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.
@@ -918,10 +918,18 @@ function allQuickItems() {
918
918
  }));
919
919
  }
920
920
 
921
+ // The agent's plan file, pinned to the top of Recent so a freshly-written plan is one ⌘E away — loadRecent
922
+ // only tracks files you've opened, so a brand-new plan would otherwise be absent until first opened.
923
+ var PLAN_PATH = '.monacori/plan.md';
924
+ function planQuickItem() {
925
+ var f = sourceByPath.get(PLAN_PATH);
926
+ if (!f) return null;
927
+ return { path: f.path, name: baseName(f.path), detail: 'plan', kind: 'source', recent: true, recentRank: -1 };
928
+ }
921
929
  function recentItems() {
922
930
  const all = allQuickItems();
923
931
  const byPath = new Map(all.map((item) => [item.path, item]));
924
- return loadRecent()
932
+ const items = loadRecent()
925
933
  .map((item) => byPath.get(item.path) || {
926
934
  path: item.path,
927
935
  name: baseName(item.path),
@@ -931,6 +939,9 @@ function recentItems() {
931
939
  recentRank: 0,
932
940
  })
933
941
  .map((item, index) => ({ ...item, recent: true, recentRank: index }));
942
+ const plan = planQuickItem();
943
+ if (plan) return [plan].concat(items.filter((it) => it.path !== plan.path));
944
+ return items;
934
945
  }
935
946
 
936
947
  function scoreQuickItem(item, query) {
@@ -1184,11 +1195,22 @@ document.addEventListener('keydown', (event) => {
1184
1195
  toggleHistory();
1185
1196
  return;
1186
1197
  }
1198
+ if (typeof isHistoryOpen === 'function' && isHistoryOpen() && typeof handleHistoryKey === 'function' && handleHistoryKey(event)) return;
1187
1199
 
1188
1200
  // Settings overlay (or a focused merged/memo dock) captures keys: stand down the rest of the global
1189
1201
  // shortcuts (Cmd+1, F7, Cmd+[/], Cmd+B, …). Each has its own Esc + editing handlers.
1190
1202
  if (isFloatingModalOpen()) return;
1191
1203
 
1204
+ // Cmd/Ctrl+A in the diff/source view selects ONLY that view's content (the browser default reached into
1205
+ // the sidebar + terminal). In an editable field, let the default select-within-field stand.
1206
+ if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.key === 'a' || event.key === 'A')) {
1207
+ var aae = document.activeElement;
1208
+ if (!(aae && (aae.tagName === 'INPUT' || aae.tagName === 'TEXTAREA' || aae.tagName === 'SELECT')) && selectAllInView()) {
1209
+ event.preventDefault();
1210
+ return;
1211
+ }
1212
+ }
1213
+
1192
1214
  if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '1') {
1193
1215
  event.preventDefault();
1194
1216
  // Coming from the diff: open the file you were viewing as source so Cmd+1 lands ON it (not a stale/blank
@@ -1636,7 +1658,6 @@ window.addEventListener('beforeunload', saveUiState);
1636
1658
  });
1637
1659
  ensureDiffCursor();
1638
1660
  })();
1639
-
1640
1661
  // ===== Side-by-side diff caret (keyboard navigation across the old/new panes) =====
1641
1662
  function isDiffViewVisible() {
1642
1663
  var d = document.getElementById('diff-view');
@@ -2078,6 +2099,19 @@ function commentsAt(path, line) {
2078
2099
  function commentKindLabel(kind) {
2079
2100
  return kind === 'q' ? t('comment.kind.q') : t('comment.kind.c');
2080
2101
  }
2102
+ // Monochrome inline icons for the two comment kinds: a help-circle for questions, a pencil for change
2103
+ // requests (the pencil path matches the activity-rail "c" button). No emoji, no color — stroke=currentColor
2104
+ // so the kind pill stays monotone (.mc-kind in viewer.css); the icon, not the color, distinguishes q vs c.
2105
+ function commentKindIcon(kind) {
2106
+ var path = kind === 'q'
2107
+ ? '<circle cx="12" cy="12" r="9"/><path d="M9.4 9.3a2.7 2.7 0 0 1 5.2 1c0 1.8-2.6 2.4-2.6 2.4"/><line x1="12" y1="16.7" x2="12.01" y2="16.7"/>'
2108
+ : '<path d="M14.5 5.5l4 4"/><path d="M4.5 19.5l1-4 10-10 3 3-10 10z"/>';
2109
+ return '<svg class="mc-kind-ic" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + path + '</svg>';
2110
+ }
2111
+ // Full inner HTML for a .mc-kind pill: monochrome icon + the (localized) label.
2112
+ function commentKindHtml(kind) {
2113
+ return commentKindIcon(kind) + '<span class="mc-kind-text">' + escapeHtml(commentKindLabel(kind)) + '</span>';
2114
+ }
2081
2115
  function relevantLines(path) {
2082
2116
  var set = {};
2083
2117
  reviewComments.forEach(function (c) { if (c.path === path) set[c.line] = true; });
@@ -2181,14 +2215,14 @@ function threadHtml(path, line) {
2181
2215
  commentsAt(path, line).forEach(function (c) {
2182
2216
  if (composerState && composerState.editSeq === c.seq) return; // being edited -> rendered as the composer below
2183
2217
  html += '<div class="mc-card mc-' + c.kind + '">'
2184
- + '<div class="mc-card-head"><span class="mc-kind">' + commentKindLabel(c.kind) + '</span>'
2218
+ + '<div class="mc-card-head"><span class="mc-kind">' + commentKindHtml(c.kind) + '</span>'
2185
2219
  + '<button type="button" class="mc-del" data-seq="' + c.seq + '" title="' + escapeHtml(t('composer.delete')) + '">×</button></div>'
2186
2220
  + '<div class="mc-card-body">' + escapeHtml(c.text) + '</div></div>';
2187
2221
  });
2188
2222
  if (composerState && composerState.path === path && composerState.line === line) {
2189
2223
  var ph = composerState.kind === 'q' ? t('composer.question') : t('composer.changeRequest');
2190
2224
  html += '<div class="mc-card mc-' + composerState.kind + ' mc-composer">'
2191
- + '<div class="mc-card-head"><span class="mc-kind">' + commentKindLabel(composerState.kind) + '</span><span class="mc-target" title="' + escapeHtml(composerState.path || '') + '">' + escapeHtml(composerTargetLabel(composerState)) + '</span></div>'
2225
+ + '<div class="mc-card-head"><span class="mc-kind">' + commentKindHtml(composerState.kind) + '</span><span class="mc-target" title="' + escapeHtml(composerState.path || '') + '">' + escapeHtml(composerTargetLabel(composerState)) + '</span></div>'
2192
2226
  + '<textarea class="mc-input" rows="3" placeholder="' + escapeHtml(ph) + '">' + escapeHtml(composerState.editText || '') + '</textarea>'
2193
2227
  + '<div class="mc-actions"><button type="button" class="mc-btn mc-save">' + escapeHtml(t('composer.save')) + '</button>'
2194
2228
  + '<button type="button" class="mc-btn mc-ghost mc-cancel">' + escapeHtml(t('composer.cancel')) + '</button>'
@@ -2380,7 +2414,7 @@ function saveComposer(ta) {
2380
2414
  // Settings → Merge prompts (stored per browser in localStorage); buildMergedText + the textarea
2381
2415
  // placeholders fall back to these when the stored value is empty.
2382
2416
  function defaultMergePrompt(kind) {
2383
- return t(kind === 'q' ? 'mergePrompt.default.q' : 'mergePrompt.default.c');
2417
+ return t(kind === 'q' ? 'mergePrompt.default.q' : kind === 'plan' ? 'plan.contract' : 'mergePrompt.default.c');
2384
2418
  }
2385
2419
  var mergePromptsKey = 'monacori-merge-prompts';
2386
2420
  function loadMergePrompts() {
@@ -2501,6 +2535,10 @@ function buildMergedText(kind) {
2501
2535
  var items = reviewComments.filter(function (c) { return c.kind === kind; });
2502
2536
  var nl = String.fromCharCode(10);
2503
2537
  var lines = [];
2538
+ // Change requests are task instructions, so they lead with the plan contract: plan first, decompose into
2539
+ // verifiable steps, write the plan to .monacori/plan.md (editable in Settings → Merge prompts). Questions
2540
+ // are read-only clarifications, so they skip it.
2541
+ if (kind === 'c') { lines.push(mergePromptFor('plan')); lines.push(''); }
2504
2542
  // Per-kind agent contract heading (editable in Settings → Merge prompts; default otherwise).
2505
2543
  lines.push(mergePromptFor(kind));
2506
2544
  lines.push('');
@@ -2741,9 +2779,13 @@ function openMemoView() {
2741
2779
  sendBtn.setAttribute('data-i18n', 'merged.sendToTerminal');
2742
2780
  sendBtn.textContent = t('merged.sendToTerminal');
2743
2781
  sendBtn.addEventListener('click', function () {
2782
+ // The memo is a task instruction, so it leads with the plan contract (plan first, decompose into
2783
+ // verifiable steps, write the plan to .monacori/plan.md) — same prefix change requests get. Empty memo
2784
+ // sends nothing extra.
2744
2785
  var text = area.value;
2786
+ var planned = text.trim() ? mergePromptFor('plan') + '\n\n' + text : text;
2745
2787
  dock.close();
2746
- window.__monacoriTerminal.enterSendMode(text);
2788
+ window.__monacoriTerminal.enterSendMode(planned);
2747
2789
  });
2748
2790
  dock.bar.insertBefore(sendBtn, dock.bar.querySelector('.dock-max'));
2749
2791
  }
@@ -2867,6 +2909,10 @@ refreshComments();
2867
2909
  // Exception: keep focus for clipboard/selection combos (Cmd+C/V/X/A) so the terminal's own copy &
2868
2910
  // paste keep working — blurring on Cmd+V drops the textarea focus the paste event needs.
2869
2911
  term.attachCustomKeyEventHandler(function (e) {
2912
+ // F7 / Shift+F7 are diff prev/next-change nav. Don't let the terminal eat them (it would send an
2913
+ // escape sequence to the shell); return false so xterm ignores the key and it bubbles to the document
2914
+ // handler. We DON'T blur — the diff caret is a JS cursor, so nav runs while the terminal keeps focus.
2915
+ if (e.type === 'keydown' && e.key === 'F7' && !e.metaKey && !e.ctrlKey && !e.altKey) return false;
2870
2916
  if (e.type === 'keydown' && e.metaKey) {
2871
2917
  var k = (e.key || '').toLowerCase();
2872
2918
  // The bare modifier press (Cmd goes down BEFORE the letter on macOS) must not blur — blurring
@@ -2879,6 +2925,10 @@ refreshComments();
2879
2925
  // copy misses xterm's own selection, so Cmd+C silently did nothing. No selection -> fall through.
2880
2926
  if (e.code === 'KeyC' && term.hasSelection && term.hasSelection()) { copyToClipboard(term.getSelection()); return false; }
2881
2927
  if (e.code === 'KeyC' || e.code === 'KeyV' || e.code === 'KeyX' || e.code === 'KeyA') return true;
2928
+ // Cmd/Ctrl+W is the close-pane menu accelerator. onCloseTab closes the FOCUSED pane only if the
2929
+ // terminal still has focus — blurring here first made hasFocus() false, so the focused split pane
2930
+ // never closed. Release the key WITHOUT blurring so focus stays and onCloseTab can close it.
2931
+ if (e.code === 'KeyW') return false;
2882
2932
  try { term.blur(); } catch (x) {}
2883
2933
  return false;
2884
2934
  }
@@ -3193,6 +3243,7 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
3193
3243
  var gearBtn = document.getElementById('app-info-btn');
3194
3244
  var flag = document.getElementById('app-update-flag');
3195
3245
  var updateBtn = document.getElementById('app-info-update');
3246
+ var pta = document.getElementById('settings-prompt-plan');
3196
3247
  var qta = document.getElementById('settings-prompt-q');
3197
3248
  var cta = document.getElementById('settings-prompt-c');
3198
3249
  var resetBtn = document.getElementById('settings-reset');
@@ -3205,6 +3256,7 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
3205
3256
  }
3206
3257
  function fill() {
3207
3258
  var s = loadMergePrompts();
3259
+ if (pta) { pta.value = typeof s.plan === 'string' ? s.plan : ''; pta.placeholder = defaultMergePrompt('plan'); }
3208
3260
  if (qta) { qta.value = typeof s.q === 'string' ? s.q : ''; qta.placeholder = defaultMergePrompt('q'); }
3209
3261
  if (cta) { cta.value = typeof s.c === 'string' ? s.c : ''; cta.placeholder = defaultMergePrompt('c'); }
3210
3262
  }
@@ -3240,9 +3292,10 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
3240
3292
  }).catch(function () { updateBtn.disabled = false; if (status) status.textContent = t('settings.updateFailed'); });
3241
3293
  });
3242
3294
  }
3295
+ if (pta) pta.addEventListener('input', function () { saveMergePrompt('plan', pta.value); flash(); });
3243
3296
  if (qta) qta.addEventListener('input', function () { saveMergePrompt('q', qta.value); flash(); });
3244
3297
  if (cta) cta.addEventListener('input', function () { saveMergePrompt('c', cta.value); flash(); });
3245
- if (resetBtn) resetBtn.addEventListener('click', function () { saveMergePrompt('q', ''); saveMergePrompt('c', ''); fill(); flash(); });
3298
+ if (resetBtn) resetBtn.addEventListener('click', function () { saveMergePrompt('plan', ''); saveMergePrompt('q', ''); saveMergePrompt('c', ''); fill(); flash(); });
3246
3299
  // Terminal-bell notification toggle (default ON — persistRead returns undefined when never set).
3247
3300
  var bellCb = document.getElementById('set-bell-notify');
3248
3301
  if (bellCb) {
@@ -3383,15 +3436,21 @@ function restoreUiState() {
3383
3436
  }
3384
3437
  return true;
3385
3438
  }
3386
- if (state.sourcePath && sourceByPath.has(state.sourcePath)) {
3387
- openSourceFile(state.sourcePath);
3439
+ // Source view. Open the saved file — or fall back to the first restored tab when that file is gone
3440
+ // (filtered out above) or wasn't recorded. Otherwise we'd render the tab bar but leave the body on its
3441
+ // "select a file" placeholder, which looks broken (a tab is clearly open). No openable tab → drop the
3442
+ // stale tabs and let the init fallback pick a sensible default.
3443
+ var openPath = (state.sourcePath && sourceByPath.has(state.sourcePath)) ? state.sourcePath : (sourceTabs[0] || '');
3444
+ if (openPath) {
3445
+ openSourceFile(openPath);
3388
3446
  // Restore the exact source caret/scroll (openSourceFile alone resets it to the top).
3389
- if (state.viewerCursor && state.viewerCursor.path === state.sourcePath) {
3447
+ if (state.viewerCursor && state.viewerCursor.path === openPath) {
3390
3448
  var vc = state.viewerCursor;
3391
- setTimeout(function () { try { setSourceCursor(state.sourcePath, vc.lineIndex, vc.column, true, -1); } catch (e) {} }, 60);
3449
+ setTimeout(function () { try { setSourceCursor(openPath, vc.lineIndex, vc.column, true, -1); } catch (e) {} }, 60);
3392
3450
  }
3393
3451
  return true;
3394
3452
  }
3453
+ sourceTabs = [];
3395
3454
  } catch {
3396
3455
  sessionStorage.removeItem(uiStateKey);
3397
3456
  }
@@ -3412,6 +3471,45 @@ function flushPendingDiffUpdate() {
3412
3471
  pendingDiffUpdate = null;
3413
3472
  try { applyDiffUpdate(u); } catch (e) {}
3414
3473
  }
3474
+ // Flicker-saver for the live-watch refresh. The default path replaces the WHOLE diff DOM
3475
+ // (container.innerHTML = …), which re-renders the file you're looking at even when only an OFF-SCREEN file
3476
+ // changed. When the file set AND order are identical, reconcile per-file instead: keep every unchanged
3477
+ // wrapper's DOM node untouched (no flicker — including the visible one) and swap only the changed wrappers,
3478
+ // which are off-screen so the swap is invisible. Returns false (caller does the full innerHTML swap) for the
3479
+ // risky cases — files added/removed/reordered — so that proven path still handles index-shift correctly.
3480
+ function reconcileDiffWrappers(container, newDiffHtml, oldSigByPath, newSigByPath) {
3481
+ var oldW = Array.prototype.slice.call(container.querySelectorAll('.d2h-file-wrapper'));
3482
+ if (!oldW.length) return false;
3483
+ var tmp = document.createElement('div');
3484
+ tmp.innerHTML = newDiffHtml;
3485
+ var newW = Array.prototype.slice.call(tmp.querySelectorAll('.d2h-file-wrapper'));
3486
+ if (newW.length !== oldW.length) return false; // add/remove → full swap (global hunk indices shift)
3487
+ for (var i = 0; i < oldW.length; i++) {
3488
+ if (diffWrapperPathKey(oldW[i]) !== diffWrapperPathKey(newW[i])) return false; // reordered → full swap
3489
+ }
3490
+ for (var j = 0; j < oldW.length; j++) {
3491
+ var ow = oldW[j], nw = newW[j], p = diffWrapperPathKey(ow);
3492
+ if ((oldSigByPath.get(p) || '') !== (newSigByPath.get(p) || '')) {
3493
+ // Changed file (off-screen): drop in the fresh shell and clear its cached body so it refetches the
3494
+ // new content when it next scrolls into view. Replacing an off-screen node is invisible.
3495
+ var nidx = (nw.id || '').replace('file-', '');
3496
+ delete bodyCache[nidx];
3497
+ delete bodyPromise[nidx];
3498
+ ow.parentNode.replaceChild(nw, ow);
3499
+ } else {
3500
+ // Unchanged file: keep its DOM node (no flicker). An earlier file's changed hunk count can shift the
3501
+ // global numbering, so sync the index attrs and renumber a materialized body's hunk ids (id/class
3502
+ // changes only — invisible, no flicker).
3503
+ var baseChanged = ow.getAttribute('data-first-hunk') !== nw.getAttribute('data-first-hunk');
3504
+ ow.id = nw.id;
3505
+ if (nw.hasAttribute('data-first-hunk')) ow.setAttribute('data-first-hunk', nw.getAttribute('data-first-hunk'));
3506
+ if (nw.hasAttribute('data-hunk-count')) ow.setAttribute('data-hunk-count', nw.getAttribute('data-hunk-count'));
3507
+ var body = ow.querySelector('.d2h-files-diff');
3508
+ if (baseChanged && body && !body.hasAttribute('data-lazy')) markWrapperHunks(ow);
3509
+ }
3510
+ }
3511
+ return true;
3512
+ }
3415
3513
  function applyDiffUpdate(u) {
3416
3514
  if (!u || !u.signature || u.signature === currentSignature) return false; // unchanged — nothing to do
3417
3515
  if (composerState) { pendingDiffUpdate = u; return false; } // composing a comment — hold the refresh until close/save
@@ -3431,11 +3529,19 @@ function applyDiffUpdate(u) {
3431
3529
  // open file's signature BEFORE fileSignatureByPath is rebuilt below.
3432
3530
  var prevOpenSig = openPath ? (fileSignatureByPath.get(openPath) || '') : '';
3433
3531
 
3434
- // Snapshot already-materialized file bodies (keyed by path + current signature) BEFORE the swap, so an
3435
- // UNCHANGED file can be re-filled synchronously afterwards. Without this, the swap turns every wrapper into
3436
- // an empty lazy shell that blanks until its body re-loads over IPC — the visible "flicker" on a watch tick.
3532
+ // Fast-path: when the file set + order are unchanged, reconcile per-file so an off-screen change never
3533
+ // flickers the file you're viewing. Falls back to the full swap (below) for add/remove/reorder or eager.
3534
+ var newSigByPath = new Map((u.fileStates || []).map(function (f) { return [f.path, f.signature]; }));
3535
+ var fastPath = false;
3536
+ if (REVIEW_LAZY && container && u.diffContainer) {
3537
+ fastPath = reconcileDiffWrappers(container, u.diffContainer, fileSignatureByPath, newSigByPath);
3538
+ }
3539
+
3540
+ // Full-swap path only: snapshot already-materialized bodies (keyed by path + signature) BEFORE the swap so
3541
+ // UNCHANGED files re-fill synchronously afterwards — otherwise the swap blanks every wrapper into an empty
3542
+ // lazy shell until its body reloads over IPC (the "flicker"). The fast-path keeps those nodes, so skip it.
3437
3543
  var prevBodies = {};
3438
- if (REVIEW_LAZY && container) {
3544
+ if (!fastPath && REVIEW_LAZY && container) {
3439
3545
  container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
3440
3546
  var b = w.querySelector('.d2h-files-diff');
3441
3547
  if (!b || b.hasAttribute('data-lazy')) return; // only bodies that are actually materialized
@@ -3444,8 +3550,9 @@ function applyDiffUpdate(u) {
3444
3550
  });
3445
3551
  }
3446
3552
 
3447
- // 1) Replace the visible regions straight from the payload (no full-HTML parse).
3448
- if (container) container.innerHTML = u.diffContainer || '';
3553
+ // 1) Replace the visible regions straight from the payload (no full-HTML parse) — unless the fast-path
3554
+ // already reconciled the diff DOM in place.
3555
+ if (container && !fastPath) container.innerHTML = u.diffContainer || '';
3449
3556
  var changesPanel = document.getElementById('changes-panel');
3450
3557
  if (changesPanel) changesPanel.innerHTML = u.changesPanel || '';
3451
3558
  // Files tree: keep the inert island (lazy, not yet opened) in sync, and refresh the live panel when it's
@@ -3507,7 +3614,7 @@ function applyDiffUpdate(u) {
3507
3614
  // flicker). Runs BEFORE setupLazyDiff so the IntersectionObserver sees them already materialized and never
3508
3615
  // re-fetches them. The fresh wrapper carries the correct data-first-hunk + file index, so materializeBody
3509
3616
  // numbers hunks exactly as a normal lazy load would. Changed/new files stay shells and lazy-load as usual.
3510
- if (REVIEW_LAZY && container) {
3617
+ if (!fastPath && REVIEW_LAZY && container) {
3511
3618
  container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
3512
3619
  var p = diffWrapperPathKey(w);
3513
3620
  var prev = p ? prevBodies[p] : null;
@@ -3894,6 +4001,26 @@ function isSourceViewerVisible() {
3894
4001
  return Boolean(viewer && !viewer.classList.contains('hidden'));
3895
4002
  }
3896
4003
 
4004
+ // Cmd/Ctrl+A scoped to the current view: select the source body, or the active diff file's content —
4005
+ // NOT the whole page (the browser default reached into the sidebar/terminal). Returns false if there's
4006
+ // no view target so the caller can fall back to the default.
4007
+ function selectAllInView() {
4008
+ var target = null;
4009
+ if (isSourceViewerVisible()) target = document.getElementById('source-body');
4010
+ else if (typeof isDiffViewVisible === 'function' && isDiffViewVisible()) {
4011
+ target = document.querySelector('#diff2html-container .d2h-file-wrapper:not(.df-inactive)') || document.getElementById('diff2html-container');
4012
+ }
4013
+ if (!target) return false;
4014
+ try {
4015
+ var sel = window.getSelection();
4016
+ var range = document.createRange();
4017
+ range.selectNodeContents(target);
4018
+ sel.removeAllRanges();
4019
+ sel.addRange(range);
4020
+ } catch (e) { return false; }
4021
+ return true;
4022
+ }
4023
+
3897
4024
  function openDiffFileAtCaret() {
3898
4025
  if (diffCursor && isDiffViewVisible()) {
3899
4026
  const dwrap = diffWrapperByPath(diffCursor.path);
@@ -5221,6 +5348,8 @@ var historyGraph = [];
5221
5348
  var historyMaxLane = 0;
5222
5349
  var historyActiveSha = '';
5223
5350
  var historyLoading = false;
5351
+ var historyFocus = 'commits'; // commits | files | diff
5352
+ var historyDiffState = null;
5224
5353
 
5225
5354
  // Lane layout. Walks commits newest-first, tracking open edges (lanes) by the hash each expects next.
5226
5355
  // Returns per-row { hash, myLane, color, topEdges, bottomEdges } using LANE INDICES + COLOR INDICES (px-free,
@@ -5334,6 +5463,33 @@ function renderHistoryList() {
5334
5463
  }).join('');
5335
5464
  }
5336
5465
 
5466
+ function historyVisibleRows() {
5467
+ var list = document.getElementById('history-list');
5468
+ return list ? Array.prototype.slice.call(list.querySelectorAll('.hrow')).filter(function (r) { return !r.classList.contains('hidden'); }) : [];
5469
+ }
5470
+ function selectHistoryCommit(sha, shouldScroll) {
5471
+ if (!sha) return;
5472
+ historyActiveSha = sha;
5473
+ var list = document.getElementById('history-list');
5474
+ if (!list) return;
5475
+ var active = null;
5476
+ list.querySelectorAll('.hrow').forEach(function (r) {
5477
+ var on = r.dataset.sha === sha;
5478
+ r.classList.toggle('active', on);
5479
+ if (on) active = r;
5480
+ });
5481
+ if (shouldScroll !== false && active && active.scrollIntoView) active.scrollIntoView({ block: 'nearest' });
5482
+ }
5483
+ function moveHistoryCommit(delta) {
5484
+ var rows = historyVisibleRows();
5485
+ if (!rows.length) return;
5486
+ var idx = rows.findIndex(function (r) { return r.dataset.sha === historyActiveSha; });
5487
+ if (idx < 0) idx = delta > 0 ? -1 : 0;
5488
+ idx = Math.max(0, Math.min(rows.length - 1, idx + delta));
5489
+ historyFocus = 'commits';
5490
+ selectHistoryCommit(rows[idx].dataset.sha, true);
5491
+ }
5492
+
5337
5493
  // Text filter (subject / author). The graph only reads right on the full contiguous history, so filtering
5338
5494
  // hides the graph column (IntelliJ does the same) and just shows matching rows.
5339
5495
  function applyHistoryFilter() {
@@ -5348,13 +5504,15 @@ function applyHistoryFilter() {
5348
5504
  var hit = !q || (c.subject + '\n' + c.author + '\n' + c.hash).toLowerCase().indexOf(q) !== -1;
5349
5505
  rows[i].classList.toggle('hidden', !hit);
5350
5506
  }
5507
+ var visible = historyVisibleRows();
5508
+ if (visible.length && !visible.some(function (r) { return r.dataset.sha === historyActiveSha; })) {
5509
+ selectHistoryCommit(visible[0].dataset.sha, false);
5510
+ }
5351
5511
  }
5352
5512
 
5353
5513
  function openHistoryCommit(sha) {
5354
5514
  if (!sha || !window.monacoriGit) return;
5355
- historyActiveSha = sha;
5356
- var list = document.getElementById('history-list');
5357
- if (list) list.querySelectorAll('.hrow').forEach(function (r) { r.classList.toggle('active', r.dataset.sha === sha); });
5515
+ selectHistoryCommit(sha, true);
5358
5516
  var detail = document.getElementById('history-detail');
5359
5517
  if (detail) detail.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t('history.loading')) + '</div>';
5360
5518
  Promise.resolve(window.monacoriGit.commitDiff(sha)).then(function (d) {
@@ -5373,9 +5531,335 @@ function renderHistoryDetail(d) {
5373
5531
  + '<span class="hd-date">' + escapeHtml(historyShortDate(d.date)) + '</span>'
5374
5532
  + historyRefBadges(d.refs) + '</div></div>';
5375
5533
  var body = (d.diffHtml && d.diffHtml.trim())
5376
- ? '<div class="history-diff diff2html-container">' + d.diffHtml + '</div>'
5534
+ ? '<div class="history-workspace"><aside id="history-files" class="history-files"></aside><div id="history-diff-container" class="history-diff diff2html-container" tabindex="0" aria-readonly="true">' + d.diffHtml + '</div></div>'
5377
5535
  : '<div class="quick-open-empty">' + escapeHtml(t(d.isMerge ? 'history.merge' : 'history.noDiff')) + '</div>';
5378
5536
  detail.innerHTML = head + body;
5537
+ setupHistoryDiffWorkspace(d.hash || historyActiveSha);
5538
+ }
5539
+
5540
+ function historyWrapperPathKey(w) {
5541
+ return (w.dataset && w.dataset.path) || ((w.querySelector('.d2h-file-name') || {}).textContent || '').trim();
5542
+ }
5543
+ function historyWrapperByPath(path) {
5544
+ if (!historyDiffState || !path) return null;
5545
+ for (var i = 0; i < historyDiffState.wrappers.length; i++) {
5546
+ if (historyWrapperPathKey(historyDiffState.wrappers[i]) === path) return historyDiffState.wrappers[i];
5547
+ }
5548
+ return null;
5549
+ }
5550
+ function historySideTables(wrapper) {
5551
+ var sides = wrapper ? wrapper.querySelectorAll('.d2h-file-side-diff') : [];
5552
+ return { left: sides[0] || null, right: sides[sides.length - 1] || null };
5553
+ }
5554
+ function historySideTable(wrapper, side) {
5555
+ var t = historySideTables(wrapper);
5556
+ return side === 'old' ? t.left : t.right;
5557
+ }
5558
+ function historyRowsOf(sideTable) {
5559
+ return diffRowsOf(sideTable);
5560
+ }
5561
+ function historyRowAt(wrapper, side, rowIndex) {
5562
+ return historyRowsOf(historySideTable(wrapper, side))[rowIndex] || null;
5563
+ }
5564
+ function historyDiffRowInfoFromNode(node) {
5565
+ var el = node ? (node.nodeType === 1 ? node : node.parentElement) : null;
5566
+ if (!el || !el.closest) return null;
5567
+ var wrapper = el.closest('.d2h-file-wrapper');
5568
+ var sideEl = el.closest('.d2h-file-side-diff');
5569
+ var row = el.closest('tr');
5570
+ if (!wrapper || !sideEl || !row || !isDiffCodeRow(row)) return null;
5571
+ var path = historyWrapperPathKey(wrapper);
5572
+ var t = historySideTables(wrapper);
5573
+ var rowIndex = historyRowsOf(sideEl).indexOf(row);
5574
+ if (!path || rowIndex < 0) return null;
5575
+ return { path: path, side: sideEl === t.left ? 'old' : 'new', rowIndex: rowIndex };
5576
+ }
5577
+ function historyFirstDiffCodeRow(wrapper, side) {
5578
+ var rows = historyRowsOf(historySideTable(wrapper, side));
5579
+ for (var i = 0; i < rows.length; i++) if (isDiffCodeRow(rows[i])) return i;
5580
+ return -1;
5581
+ }
5582
+ function historyFirstCodeRowOfHunk(hunkRow) {
5583
+ var row = hunkRow.nextElementSibling;
5584
+ var firstRow = null;
5585
+ while (row && !row.classList.contains('history-hunk') && !row.classList.contains('history-hunk-peer')) {
5586
+ if (row.querySelector && row.querySelector('.d2h-code-side-line')) {
5587
+ if (!firstRow) firstRow = row;
5588
+ if (isChangeCodeRow(row)) return row;
5589
+ }
5590
+ row = row.nextElementSibling;
5591
+ }
5592
+ return firstRow || hunkRow;
5593
+ }
5594
+ function historyFirstChangeRowForCaret(hunkRow) {
5595
+ var wrapper = hunkRow.closest('.d2h-file-wrapper');
5596
+ var sides = wrapper ? wrapper.querySelectorAll('.d2h-file-side-diff') : [];
5597
+ var hunkSideEl = hunkRow.closest('.d2h-file-side-diff');
5598
+ if (sides.length >= 2 && hunkSideEl) {
5599
+ var hunkRows = Array.prototype.slice.call(hunkSideEl.querySelectorAll('tr'));
5600
+ var otherEl = hunkSideEl === sides[0] ? sides[1] : sides[0];
5601
+ var otherRows = Array.prototype.slice.call(otherEl.querySelectorAll('tr'));
5602
+ var fallbackOld = null;
5603
+ for (var i = hunkRows.indexOf(hunkRow) + 1; i < hunkRows.length; i++) {
5604
+ var hr = hunkRows[i];
5605
+ if (hr.classList.contains('history-hunk') || hr.classList.contains('history-hunk-peer')) break;
5606
+ if (isChangeCodeRow(otherRows[i])) return otherRows[i];
5607
+ if (fallbackOld === null && isChangeCodeRow(hr)) fallbackOld = hr;
5608
+ }
5609
+ if (fallbackOld) return fallbackOld;
5610
+ }
5611
+ return historyFirstCodeRowOfHunk(hunkRow);
5612
+ }
5613
+ function setupHistoryDiffWorkspace(sha) {
5614
+ var container = document.getElementById('history-diff-container');
5615
+ var filesEl = document.getElementById('history-files');
5616
+ if (!container || !filesEl) { historyDiffState = null; return; }
5617
+ container.querySelectorAll('.d2h-code-side-linenumber, .d2h-code-linenumber, .d2h-code-line-prefix').forEach(function (el) { el.setAttribute('contenteditable', 'false'); });
5618
+ var wrappers = Array.prototype.slice.call(container.querySelectorAll('.d2h-file-wrapper'));
5619
+ var files = [], hunks = [];
5620
+ var hunkIndex = 0;
5621
+ wrappers.forEach(function (wrapper, fileIndex) {
5622
+ var path = historyWrapperPathKey(wrapper);
5623
+ if (path) wrapper.dataset.path = path;
5624
+ wrapper.dataset.historyFileIndex = String(fileIndex);
5625
+ var first = hunkIndex;
5626
+ var headerToIndex = new Map();
5627
+ Array.prototype.forEach.call(wrapper.querySelectorAll('tr'), function (row) {
5628
+ var header = (row.textContent || '').trim();
5629
+ if (header.indexOf('@@') !== 0) return;
5630
+ var idx = headerToIndex.get(header);
5631
+ if (idx === undefined) {
5632
+ idx = hunkIndex++;
5633
+ headerToIndex.set(header, idx);
5634
+ row.classList.add('history-hunk');
5635
+ hunks[idx] = { path: path, row: row };
5636
+ } else {
5637
+ row.classList.add('history-hunk-peer');
5638
+ }
5639
+ row.dataset.historyHunkIndex = String(idx);
5640
+ row.dataset.historyFile = path;
5641
+ });
5642
+ files.push({ path: path, hunk: first, count: hunkIndex - first });
5643
+ });
5644
+ historyDiffState = { sha: sha, container: container, filesEl: filesEl, wrappers: wrappers, files: files, hunks: hunks, currentHunk: -1, cursor: null, fileFocusIndex: 0 };
5645
+ filesEl.innerHTML = files.map(function (file, i) {
5646
+ var slash = file.path.lastIndexOf('/');
5647
+ var name = slash >= 0 ? file.path.slice(slash + 1) : file.path;
5648
+ var dir = slash >= 0 ? file.path.slice(0, slash) : '';
5649
+ return '<button type="button" class="file-link history-file" data-index="' + i + '" data-file="' + escapeHtml(file.path) + '" data-hunk="' + file.hunk + '">'
5650
+ + '<span class="status status-modified">M</span><span class="change-name"><span class="path" title="' + escapeHtml(file.path) + '">' + escapeHtml(name) + '</span>'
5651
+ + (dir ? '<span class="change-dir">' + escapeHtml(dir) + '</span>' : '') + '</span></button>';
5652
+ }).join('');
5653
+ container.addEventListener('click', function (event) {
5654
+ var info = historyDiffRowInfoFromNode(event.target);
5655
+ if (info && info.path) {
5656
+ historyFocus = 'diff';
5657
+ historySetDiffCursor(info.path, info.side, info.rowIndex, 0, false);
5658
+ }
5659
+ });
5660
+ if (files[0]) historyShowFile(files[0].path, files[0].hunk, false);
5661
+ focusHistoryDiff();
5662
+ }
5663
+ function historySetFileFocus(index) {
5664
+ if (!historyDiffState || !historyDiffState.files.length) return;
5665
+ var max = historyDiffState.files.length - 1;
5666
+ historyDiffState.fileFocusIndex = Math.max(0, Math.min(max, index));
5667
+ historyFocus = 'files';
5668
+ var btns = historyDiffState.filesEl.querySelectorAll('.history-file');
5669
+ btns.forEach(function (b, i) {
5670
+ b.classList.toggle('tree-focus', i === historyDiffState.fileFocusIndex);
5671
+ if (i === historyDiffState.fileFocusIndex && b.scrollIntoView) b.scrollIntoView({ block: 'nearest' });
5672
+ });
5673
+ }
5674
+ function focusHistoryFiles() {
5675
+ if (!historyDiffState) return;
5676
+ var active = historyDiffState.files.findIndex(function (f) { return f.path === historyCurrentFile(); });
5677
+ historySetFileFocus(active >= 0 ? active : 0);
5678
+ }
5679
+ function focusHistoryDiff() {
5680
+ if (!historyDiffState) return;
5681
+ historyFocus = 'diff';
5682
+ historyDiffState.filesEl.querySelectorAll('.history-file').forEach(function (b) { b.classList.remove('tree-focus'); });
5683
+ try { historyDiffState.container.focus(); } catch (e) {}
5684
+ }
5685
+ function historyCurrentFile() {
5686
+ if (!historyDiffState) return '';
5687
+ var active = historyDiffState.filesEl.querySelector('.history-file.active');
5688
+ return active ? active.dataset.file || '' : '';
5689
+ }
5690
+ function historyShowFile(path, hunkIndex, shouldScroll) {
5691
+ if (!historyDiffState || !path) return;
5692
+ historyDiffState.wrappers.forEach(function (wrapper) {
5693
+ wrapper.classList.toggle('df-inactive', historyWrapperPathKey(wrapper) !== path);
5694
+ });
5695
+ historyDiffState.filesEl.querySelectorAll('.history-file').forEach(function (button, i) {
5696
+ var on = button.dataset.file === path;
5697
+ button.classList.toggle('active', on);
5698
+ if (on) historyDiffState.fileFocusIndex = i;
5699
+ });
5700
+ var file = historyDiffState.files.find(function (f) { return f.path === path; });
5701
+ var target = typeof hunkIndex === 'number' && hunkIndex >= 0 ? hunkIndex : (file ? file.hunk : -1);
5702
+ if (target >= 0 && historyDiffState.hunks[target]) historySetActiveHunk(target, shouldScroll !== false);
5703
+ else historyEnsureDiffCursor(path, shouldScroll !== false);
5704
+ }
5705
+ function historyHunkPathAt(index) {
5706
+ return historyDiffState && historyDiffState.hunks[index] ? historyDiffState.hunks[index].path : '';
5707
+ }
5708
+ function historySetActiveHunk(index, shouldScroll) {
5709
+ if (!historyDiffState || !historyDiffState.hunks.length) return;
5710
+ var len = historyDiffState.hunks.length;
5711
+ var idx = ((index % len) + len) % len;
5712
+ var h = historyDiffState.hunks[idx];
5713
+ if (!h || !h.path) return;
5714
+ historyDiffState.currentHunk = idx;
5715
+ historyDiffState.wrappers.forEach(function (wrapper) {
5716
+ wrapper.classList.toggle('df-inactive', historyWrapperPathKey(wrapper) !== h.path);
5717
+ });
5718
+ historyDiffState.filesEl.querySelectorAll('.history-file').forEach(function (button, i) {
5719
+ var on = button.dataset.file === h.path;
5720
+ button.classList.toggle('active', on);
5721
+ if (on) historyDiffState.fileFocusIndex = i;
5722
+ });
5723
+ historyDiffState.container.querySelectorAll('.history-hunk.active, .history-hunk-peer.active').forEach(function (row) { row.classList.remove('active'); });
5724
+ historyDiffState.container.querySelectorAll('[data-history-hunk-index="' + idx + '"]').forEach(function (row) { row.classList.add('active'); });
5725
+ var targetRow = historyFirstChangeRowForCaret(h.row);
5726
+ if (targetRow) {
5727
+ var info = historyDiffRowInfoFromNode(targetRow);
5728
+ if (info) historySetDiffCursor(info.path, info.side, info.rowIndex, 0, shouldScroll);
5729
+ }
5730
+ }
5731
+ function historyEnsureDiffCursor(path, reveal) {
5732
+ var wrapper = historyWrapperByPath(path);
5733
+ var ri = historyFirstDiffCodeRow(wrapper, 'new');
5734
+ if (ri >= 0) historySetDiffCursor(path, 'new', ri, 0, reveal);
5735
+ }
5736
+ function historyClearDiffCaret() {
5737
+ if (!historyDiffState) return;
5738
+ historyDiffState.container.querySelectorAll('.mc-diff-cursor-row').forEach(function (r) { r.classList.remove('mc-diff-cursor-row'); });
5739
+ historyDiffState.container.querySelectorAll('.code-cursor').forEach(function (s) { var p = s.parentNode; if (p) { p.removeChild(s); if (p.normalize) p.normalize(); } });
5740
+ }
5741
+ function historyRenderDiffCaret() {
5742
+ historyClearDiffCaret();
5743
+ if (!historyDiffState || !historyDiffState.cursor) return;
5744
+ var c = historyDiffState.cursor;
5745
+ var wrapper = historyWrapperByPath(c.path);
5746
+ var row = wrapper ? historyRowAt(wrapper, c.side, c.rowIndex) : null;
5747
+ if (!row) return;
5748
+ row.classList.add('mc-diff-cursor-row');
5749
+ var ctn = diffCellCtn(row);
5750
+ if (!ctn) return;
5751
+ if ((ctn.textContent || '').length === 0) {
5752
+ var emptySpan = document.createElement('span');
5753
+ emptySpan.className = 'code-cursor';
5754
+ emptySpan.setAttribute('aria-hidden', 'true');
5755
+ emptySpan.style.position = 'absolute';
5756
+ ctn.appendChild(emptySpan);
5757
+ return;
5758
+ }
5759
+ var pos = diffCaretDomPosition(ctn, c.column);
5760
+ if (!pos) return;
5761
+ var span = document.createElement('span');
5762
+ span.className = 'code-cursor';
5763
+ span.setAttribute('aria-hidden', 'true');
5764
+ try {
5765
+ var off = pos.node.nodeType === 3 ? Math.min(pos.offset, (pos.node.textContent || '').length) : pos.offset;
5766
+ var range = document.createRange();
5767
+ range.setStart(pos.node, off);
5768
+ range.collapse(true);
5769
+ range.insertNode(span);
5770
+ } catch (e) {}
5771
+ }
5772
+ function historySetDiffCursor(path, side, rowIndex, column, reveal) {
5773
+ if (!historyDiffState) return;
5774
+ var wrapper = historyWrapperByPath(path);
5775
+ if (!wrapper) return;
5776
+ var rows = historyRowsOf(historySideTable(wrapper, side));
5777
+ if (!rows.length) return;
5778
+ var ri = Math.max(0, Math.min(rowIndex, rows.length - 1));
5779
+ var col = Math.max(0, Math.min(column, diffLineText(rows[ri]).length));
5780
+ historyDiffState.cursor = { path: path, side: side, rowIndex: ri, column: col };
5781
+ historyRenderDiffCaret();
5782
+ if (reveal) scrolloffReveal(rows[ri], historyDiffState.container, 0.15);
5783
+ }
5784
+ function historyMoveDiffCursor(dLine, dColumn) {
5785
+ if (!historyDiffState || !historyDiffState.cursor) return;
5786
+ var c = historyDiffState.cursor;
5787
+ var wrapper = historyWrapperByPath(c.path);
5788
+ if (!wrapper) return;
5789
+ var rows = historyRowsOf(historySideTable(wrapper, c.side));
5790
+ var ri = c.rowIndex, col = c.column;
5791
+ var text = diffLineText(rows[ri]);
5792
+ if (dColumn < 0) {
5793
+ if (col > 0) col -= 1;
5794
+ else { var p = ri - 1; while (p >= 0 && !isDiffCodeRow(rows[p])) p -= 1; if (p >= 0) { ri = p; col = diffLineText(rows[p]).length; } }
5795
+ } else if (dColumn > 0) {
5796
+ if (col < text.length) col += 1;
5797
+ else { var n = ri + 1; while (n < rows.length && !isDiffCodeRow(rows[n])) n += 1; if (n < rows.length) { ri = n; col = 0; } }
5798
+ }
5799
+ if (dLine !== 0) {
5800
+ var step = dLine > 0 ? 1 : -1;
5801
+ var cand = ri + step;
5802
+ while (cand >= 0 && cand < rows.length && !isDiffCodeRow(rows[cand])) cand += step;
5803
+ if (cand >= 0 && cand < rows.length) { ri = cand; col = Math.min(col, diffLineText(rows[ri]).length); }
5804
+ }
5805
+ historySetDiffCursor(c.path, c.side, ri, col, true);
5806
+ }
5807
+ function historyNextHunk(delta) {
5808
+ if (!historyDiffState || !historyDiffState.hunks.length) return;
5809
+ var base = historyDiffState.currentHunk >= 0 ? historyDiffState.currentHunk : 0;
5810
+ historySetActiveHunk(base + delta, true);
5811
+ focusHistoryDiff();
5812
+ }
5813
+ function historySelectAllDiff() {
5814
+ if (!historyDiffState) return;
5815
+ var sel = window.getSelection();
5816
+ if (!sel) return;
5817
+ var range = document.createRange();
5818
+ range.selectNodeContents(historyDiffState.container);
5819
+ sel.removeAllRanges();
5820
+ sel.addRange(range);
5821
+ }
5822
+ function handleHistoryDiffKey(event) {
5823
+ if (!historyDiffState || !historyDiffState.cursor) return false;
5824
+ if (event.key === 'Tab' && event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
5825
+ var tc = historyDiffState.cursor;
5826
+ var tw = historyWrapperByPath(tc.path);
5827
+ var otherSide = tc.side === 'new' ? 'old' : 'new';
5828
+ var otherRow = tw ? historyRowAt(tw, otherSide, tc.rowIndex) : null;
5829
+ if (isDiffCodeRow(otherRow)) historySetDiffCursor(tc.path, otherSide, tc.rowIndex, Math.min(tc.column, diffLineText(otherRow).length), true);
5830
+ return true;
5831
+ }
5832
+ if (!event.metaKey && !event.ctrlKey && !event.altKey) {
5833
+ if (event.key === 'ArrowDown') { historyMoveDiffCursor(1, 0); return true; }
5834
+ if (event.key === 'ArrowUp') { historyMoveDiffCursor(-1, 0); return true; }
5835
+ if (event.key === 'ArrowLeft') { historyMoveDiffCursor(0, -1); return true; }
5836
+ if (event.key === 'ArrowRight') { historyMoveDiffCursor(0, 1); return true; }
5837
+ }
5838
+ if (event.altKey && !event.metaKey && !event.ctrlKey && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
5839
+ var wc = historyDiffState.cursor;
5840
+ var ww = historyWrapperByPath(wc.path);
5841
+ var wr = ww ? historyRowAt(ww, wc.side, wc.rowIndex) : null;
5842
+ var nextCol = typeof nextWordBoundary === 'function'
5843
+ ? nextWordBoundary(diffLineText(wr), wc.column, event.key === 'ArrowRight' ? 1 : -1)
5844
+ : wc.column;
5845
+ historySetDiffCursor(wc.path, wc.side, wc.rowIndex, nextCol, true);
5846
+ return true;
5847
+ }
5848
+ if ((event.metaKey || event.ctrlKey) && !event.altKey && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
5849
+ var c = historyDiffState.cursor;
5850
+ var wrapper = historyWrapperByPath(c.path);
5851
+ var row = wrapper ? historyRowAt(wrapper, c.side, c.rowIndex) : null;
5852
+ var len = diffLineText(row).length;
5853
+ if (event.key === 'ArrowLeft') {
5854
+ if (c.column > 0) historySetDiffCursor(c.path, c.side, c.rowIndex, 0, true);
5855
+ else if (c.side === 'new') { var oldRow = historyRowAt(wrapper, 'old', c.rowIndex); if (isDiffCodeRow(oldRow)) historySetDiffCursor(c.path, 'old', c.rowIndex, diffLineText(oldRow).length, true); }
5856
+ } else {
5857
+ if (c.column < len) historySetDiffCursor(c.path, c.side, c.rowIndex, len, true);
5858
+ else if (c.side === 'old') { var newRow = historyRowAt(wrapper, 'new', c.rowIndex); if (isDiffCodeRow(newRow)) historySetDiffCursor(c.path, 'new', c.rowIndex, 0, true); }
5859
+ }
5860
+ return true;
5861
+ }
5862
+ return false;
5379
5863
  }
5380
5864
 
5381
5865
  function isHistoryOpen() {
@@ -5385,6 +5869,7 @@ function isHistoryOpen() {
5385
5869
  function closeHistory() {
5386
5870
  var v = document.getElementById('history-view');
5387
5871
  if (v) v.classList.add('hidden');
5872
+ historyFocus = 'commits';
5388
5873
  if (typeof syncRail === 'function') syncRail();
5389
5874
  }
5390
5875
  function openHistory() {
@@ -5397,6 +5882,8 @@ function openHistory() {
5397
5882
  if (search) { search.value = ''; }
5398
5883
  applyHistoryFilter();
5399
5884
  historyLoading = true;
5885
+ historyFocus = 'commits';
5886
+ historyDiffState = null;
5400
5887
  renderHistoryList();
5401
5888
  Promise.resolve(window.monacoriGit.log({ limit: 300 })).then(function (commits) {
5402
5889
  historyLoading = false;
@@ -5406,13 +5893,64 @@ function openHistory() {
5406
5893
  renderHistoryList();
5407
5894
  var detail = document.getElementById('history-detail');
5408
5895
  if (detail) detail.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t('history.selectCommit')) + '</div>';
5409
- if (historyCommits[0]) openHistoryCommit(historyCommits[0].hash); // preview the newest commit
5410
- if (search) setTimeout(function () { try { search.focus(); } catch (e) {} }, 0);
5896
+ if (historyCommits[0]) selectHistoryCommit(historyCommits[0].hash, false);
5897
+ setTimeout(function () { try { v.focus(); } catch (e) {} }, 0);
5411
5898
  }, function () { historyLoading = false; renderHistoryList(); });
5412
5899
  }
5413
5900
  function toggleHistory() { if (isHistoryOpen()) closeHistory(); else openHistory(); }
5414
5901
  if (typeof window !== 'undefined') window.__monacoriHistory = { open: openHistory, close: closeHistory, toggle: toggleHistory, isOpen: isHistoryOpen };
5415
5902
 
5903
+ function handleHistoryKey(e) {
5904
+ if (!isHistoryOpen()) return false;
5905
+ var ae = document.activeElement;
5906
+ var inSearch = ae && ae.id === 'history-search';
5907
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && (e.code === 'Digit9' || e.key === '9')) {
5908
+ e.preventDefault(); e.stopPropagation(); closeHistory(); return true;
5909
+ }
5910
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeHistory(); return true; }
5911
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key === '0') {
5912
+ if (historyDiffState) { e.preventDefault(); e.stopPropagation(); focusHistoryFiles(); return true; }
5913
+ }
5914
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && (e.key === 'a' || e.key === 'A') && historyFocus === 'diff') {
5915
+ e.preventDefault(); e.stopPropagation(); historySelectAllDiff(); return true;
5916
+ }
5917
+ if (e.key === 'PageDown' || e.key === 'PageUp') {
5918
+ var scroller = historyFocus === 'diff' && historyDiffState ? historyDiffState.container : document.getElementById('history-list');
5919
+ if (scroller) { e.preventDefault(); e.stopPropagation(); scroller.scrollTop += (e.key === 'PageDown' ? 0.9 : -0.9) * scroller.clientHeight; return true; }
5920
+ }
5921
+ if (e.key === 'F7' && !e.metaKey && !e.ctrlKey && !e.altKey) {
5922
+ e.preventDefault(); e.stopPropagation(); historyNextHunk(e.shiftKey ? -1 : 1); return true;
5923
+ }
5924
+ if (!e.metaKey && !e.ctrlKey && !e.altKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
5925
+ e.preventDefault(); e.stopPropagation();
5926
+ var delta = e.key === 'ArrowDown' ? 1 : -1;
5927
+ if (historyFocus === 'files') historySetFileFocus((historyDiffState ? historyDiffState.fileFocusIndex : 0) + delta);
5928
+ else if (historyFocus === 'diff' && historyDiffState) historyMoveDiffCursor(delta, 0);
5929
+ else moveHistoryCommit(delta);
5930
+ return true;
5931
+ }
5932
+ if (!e.metaKey && !e.ctrlKey && !e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight') && historyFocus === 'diff') {
5933
+ e.preventDefault(); e.stopPropagation(); historyMoveDiffCursor(0, e.key === 'ArrowRight' ? 1 : -1); return true;
5934
+ }
5935
+ if (!e.metaKey && !e.ctrlKey && !e.altKey && e.key === 'Enter') {
5936
+ e.preventDefault(); e.stopPropagation();
5937
+ if (historyFocus === 'diff') {
5938
+ return true;
5939
+ } else if (historyFocus === 'files' && historyDiffState) {
5940
+ var file = historyDiffState.files[historyDiffState.fileFocusIndex];
5941
+ if (file) historyShowFile(file.path, file.hunk, true);
5942
+ focusHistoryDiff();
5943
+ } else {
5944
+ var rows = historyVisibleRows();
5945
+ var row = rows.find(function (r) { return r.dataset.sha === historyActiveSha; }) || rows[0];
5946
+ if (row) openHistoryCommit(row.dataset.sha);
5947
+ }
5948
+ return true;
5949
+ }
5950
+ if (historyFocus === 'diff' && handleHistoryDiffKey(e)) { e.preventDefault(); e.stopPropagation(); return true; }
5951
+ return !inSearch && historyFocus !== 'commits';
5952
+ }
5953
+
5416
5954
  (function wireHistory() {
5417
5955
  var list = document.getElementById('history-list');
5418
5956
  if (list) list.addEventListener('click', function (e) {
@@ -5424,8 +5962,18 @@ if (typeof window !== 'undefined') window.__monacoriHistory = { open: openHistor
5424
5962
  var closeBtn = document.getElementById('history-close');
5425
5963
  if (closeBtn) closeBtn.addEventListener('click', closeHistory);
5426
5964
  var view = document.getElementById('history-view');
5965
+ if (view) view.setAttribute('tabindex', '-1');
5427
5966
  if (view) view.addEventListener('keydown', function (e) {
5428
- if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeHistory(); }
5967
+ handleHistoryKey(e);
5968
+ });
5969
+ var detail = document.getElementById('history-detail');
5970
+ if (detail) detail.addEventListener('click', function (e) {
5971
+ var file = e.target.closest && e.target.closest('.history-file[data-file]');
5972
+ if (file && historyDiffState) {
5973
+ e.preventDefault();
5974
+ historyShowFile(file.dataset.file, Number(file.dataset.hunk), true);
5975
+ focusHistoryDiff();
5976
+ }
5429
5977
  });
5430
5978
  })();
5431
5979
  // ===== Go-to-line (Cmd/Ctrl+L), copy caret location (Cmd/Ctrl+K), and the sidebar row action menu. =====