@youtyan/code-viewer 0.5.5 → 0.6.0

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/web/app.js CHANGED
@@ -400,7 +400,8 @@
400
400
  { action: "start-g-sequence", key: "g", scope: "main" },
401
401
  { action: "layout-unified", key: "u" },
402
402
  { action: "layout-split", key: "s" },
403
- { action: "toggle-theme", key: "t" }
403
+ { action: "toggle-theme", key: "t" },
404
+ { action: "open-help", key: "?", shift: true }
404
405
  ];
405
406
  function resolveKeymapAction(event, context) {
406
407
  const key = event.key.toLowerCase();
@@ -20235,6 +20236,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20235
20236
  const b2 = Math.max(1, Math.floor(Math.max(start, end)));
20236
20237
  return a2 === b2 ? `@${path}#${a2}` : `@${path}#${a2}-${b2}`;
20237
20238
  }
20239
+ function fileReferenceWithCodeClipboardText(path, start, end, lines, lang) {
20240
+ const ref = fileReferenceClipboardText(path, start, end);
20241
+ if (!ref)
20242
+ return "";
20243
+ if (!lines || lines.length === 0)
20244
+ return ref;
20245
+ const fence2 = (lang || "").trim();
20246
+ return `${ref}
20247
+
20248
+ \`\`\`${fence2}
20249
+ ${lines.join(`
20250
+ `)}
20251
+ \`\`\``;
20252
+ }
20238
20253
 
20239
20254
  // web-src/core/ws-highlight.ts
20240
20255
  function isWhitespaceOnlyInlineHighlight(text2) {
@@ -20265,6 +20280,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20265
20280
  function shouldRenderDiffSidebar(listSame, domIntact) {
20266
20281
  return !listSame || !domIntact;
20267
20282
  }
20283
+ function findDirectDiffCardByKey(target, key) {
20284
+ for (const child of Array.from(target.children)) {
20285
+ if (!child.classList.contains("gdp-file-shell"))
20286
+ continue;
20287
+ const card = child;
20288
+ if (card.dataset.key === key)
20289
+ return card;
20290
+ }
20291
+ return null;
20292
+ }
20268
20293
  function createDiffView(deps) {
20269
20294
  const {
20270
20295
  $,
@@ -20480,11 +20505,17 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20480
20505
  let CLIENT_REQ_SEQ = 0;
20481
20506
  const LOAD_QUEUE = [];
20482
20507
  let ACTIVE_LOADS = 0;
20508
+ let LOAD_EPOCH = 0;
20483
20509
  const MAX_PARALLEL = 2;
20484
20510
  let lazyObserver = null;
20485
20511
  let scrollSpyInstalled = false;
20486
20512
  let prevListSignature = "";
20487
20513
  let prevCardSignatures = new Map;
20514
+ function resetLoadScheduling() {
20515
+ LOAD_EPOCH++;
20516
+ LOAD_QUEUE.length = 0;
20517
+ ACTIVE_LOADS = 0;
20518
+ }
20488
20519
  function fileKey(f2) {
20489
20520
  return f2.key || f2.path;
20490
20521
  }
@@ -20534,11 +20565,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20534
20565
  }
20535
20566
  }
20536
20567
  function invalidateLoadedCard(card, file, changedPaths) {
20537
- if (!card.classList.contains("loaded"))
20568
+ if (!card.classList.contains("loaded") && !card.classList.contains("loading"))
20538
20569
  return false;
20539
20570
  if (changedPaths && !changedPaths.has(file.path))
20540
20571
  return false;
20541
- card.classList.remove("loaded", "error");
20572
+ card.dataset.reqId = String(++CLIENT_REQ_SEQ);
20573
+ card.classList.remove("loaded", "loading", "error");
20542
20574
  card.classList.add("pending");
20543
20575
  card._diffData = null;
20544
20576
  const body = card.querySelector(".gdp-shell-body");
@@ -20568,6 +20600,25 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20568
20600
  }
20569
20601
  }
20570
20602
  }
20603
+ function resetReusableCard(card, file) {
20604
+ card.classList.remove("loaded", "loading", "error", "manual-load");
20605
+ card.classList.add("pending");
20606
+ card.replaceChildren();
20607
+ const tmp = createPlaceholder(file);
20608
+ while (tmp.firstChild)
20609
+ card.appendChild(tmp.firstChild);
20610
+ card.dataset.path = file.path;
20611
+ card.dataset.key = fileKey(file);
20612
+ card.dataset.sizeClass = file.size_class || "small";
20613
+ card.dataset.status = file.status || "M";
20614
+ card.dataset.reqId = String(++CLIENT_REQ_SEQ);
20615
+ delete card.dataset.manualRendered;
20616
+ delete card.dataset.manualLoad;
20617
+ delete card.dataset.manualMode;
20618
+ card.style.minHeight = `${file.estimated_height_px || 80}px`;
20619
+ card._diffData = null;
20620
+ card._file = null;
20621
+ }
20571
20622
  function renderShell(meta, changedPaths) {
20572
20623
  const newFiles = meta.files || [];
20573
20624
  const newListSig = computeListSignature(newFiles);
@@ -20596,7 +20647,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20596
20647
  empty.classList.remove("hidden");
20597
20648
  target.replaceChildren();
20598
20649
  }
20599
- LOAD_QUEUE.length = 0;
20650
+ resetLoadScheduling();
20600
20651
  return {
20601
20652
  structureChanged: sidebarNeedsRender,
20602
20653
  invalidatedCards: 0,
@@ -20608,39 +20659,33 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20608
20659
  for (const f2 of newFiles) {
20609
20660
  newCardSigs.set(fileKey(f2), computeCardSignature(f2));
20610
20661
  }
20662
+ const pathsUnknown = !changedPaths;
20663
+ const contentMayNeedReload = !listSame || pathsUnknown || (changedPaths ? newFiles.some((file) => {
20664
+ const key = fileKey(file);
20665
+ return prevCardSignatures.get(key) !== newCardSigs.get(key) || changedPaths.has(file.path);
20666
+ }) : false);
20667
+ if (contentMayNeedReload)
20668
+ resetLoadScheduling();
20611
20669
  let invalidatedCards = 0;
20612
20670
  if (listSame && domIntact) {
20613
- const pathsUnknown = !changedPaths;
20671
+ const pathsUnknown2 = !changedPaths;
20614
20672
  let sidebarNeedsStatsUpdate = false;
20615
20673
  for (const f2 of newFiles) {
20616
20674
  const key = fileKey(f2);
20617
20675
  const oldSig = prevCardSignatures.get(key);
20618
- const newSig = newCardSigs.get(key);
20676
+ const newSig = newCardSigs.get(key) ?? computeCardSignature(f2);
20619
20677
  const sigChanged = oldSig !== newSig;
20620
- const pathHint = pathsUnknown || changedPaths.has(f2.path);
20678
+ const pathHint = pathsUnknown2 || changedPaths.has(f2.path);
20621
20679
  if (!sigChanged && !pathHint) {
20622
20680
  continue;
20623
20681
  }
20624
- const card = target.querySelector(`.gdp-file-shell[data-key="${CSS.escape(key)}"]`);
20682
+ const card = findDirectDiffCardByKey(target, key);
20625
20683
  if (!card)
20626
20684
  continue;
20627
20685
  const sizeChanged = card.dataset.sizeClass !== (f2.size_class || "small");
20628
20686
  const statusChanged = card.dataset.status !== (f2.status || "M");
20629
20687
  if (sizeChanged || statusChanged) {
20630
- card.classList.remove("loaded", "error");
20631
- card.classList.add("pending");
20632
- card.replaceChildren();
20633
- const tmp = createPlaceholder(f2);
20634
- while (tmp.firstChild)
20635
- card.appendChild(tmp.firstChild);
20636
- card.dataset.sizeClass = f2.size_class || "small";
20637
- card.dataset.status = f2.status || "M";
20638
- delete card.dataset.manualRendered;
20639
- delete card.dataset.manualLoad;
20640
- delete card.dataset.manualMode;
20641
- card.style.minHeight = `${f2.estimated_height_px || 80}px`;
20642
- card._diffData = null;
20643
- card._file = null;
20688
+ resetReusableCard(card, f2);
20644
20689
  activatePendingCard(card, f2);
20645
20690
  invalidatedCards++;
20646
20691
  sidebarNeedsStatsUpdate = true;
@@ -20653,6 +20698,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20653
20698
  const didInvalidate = sigChanged ? invalidateLoadedCard(card, f2, null) : invalidateLoadedCard(card, f2, changedPaths);
20654
20699
  if (didInvalidate)
20655
20700
  invalidatedCards++;
20701
+ else if ((sigChanged || pathHint) && card.classList.contains("pending")) {
20702
+ activatePendingCard(card, f2);
20703
+ }
20656
20704
  if (sigChanged)
20657
20705
  sidebarNeedsStatsUpdate = true;
20658
20706
  }
@@ -20687,21 +20735,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20687
20735
  oldByKey.delete(key);
20688
20736
  const sizeChanged = old.dataset.sizeClass !== (f2.size_class || "small");
20689
20737
  const statusChanged = old.dataset.status !== (f2.status || "M");
20690
- if (sizeChanged || statusChanged) {
20691
- old.classList.remove("loaded", "error");
20692
- old.classList.add("pending");
20693
- old.replaceChildren();
20694
- const tmp = createPlaceholder(f2);
20695
- while (tmp.firstChild)
20696
- old.appendChild(tmp.firstChild);
20697
- old.dataset.sizeClass = f2.size_class || "small";
20698
- old.dataset.status = f2.status || "M";
20699
- delete old.dataset.manualRendered;
20700
- delete old.dataset.manualLoad;
20701
- delete old.dataset.manualMode;
20702
- old.style.minHeight = `${f2.estimated_height_px || 80}px`;
20703
- old._diffData = null;
20704
- old._file = null;
20738
+ const oldSig = prevCardSignatures.get(key);
20739
+ const newSig = newCardSigs.get(key) ?? computeCardSignature(f2);
20740
+ const sigChanged = oldSig !== newSig;
20741
+ if (sizeChanged || statusChanged || sigChanged) {
20742
+ resetReusableCard(old, f2);
20705
20743
  invalidatedCards++;
20706
20744
  } else {
20707
20745
  const stats = old.querySelector(".gdp-shell-header .stats");
@@ -20793,20 +20831,29 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20793
20831
  renderManualLoadPlaceholder(card, file);
20794
20832
  return;
20795
20833
  }
20796
- if (LOAD_QUEUE.find((item) => item.card === card))
20834
+ if (LOAD_QUEUE.find((item) => item.card === card && item.epoch === LOAD_EPOCH))
20797
20835
  return;
20798
- LOAD_QUEUE.push({ file, card, priority: priority || 0 });
20836
+ LOAD_QUEUE.push({
20837
+ file,
20838
+ card,
20839
+ priority: priority || 0,
20840
+ epoch: LOAD_EPOCH
20841
+ });
20799
20842
  LOAD_QUEUE.sort((a2, b2) => b2.priority - a2.priority);
20800
20843
  pumpQueue();
20801
20844
  }
20802
20845
  function pumpQueue() {
20803
20846
  while (ACTIVE_LOADS < MAX_PARALLEL && LOAD_QUEUE.length) {
20804
20847
  const item = LOAD_QUEUE.shift();
20848
+ if (item.epoch !== LOAD_EPOCH)
20849
+ continue;
20805
20850
  if (item.card.classList.contains("loaded") || item.card.classList.contains("loading"))
20806
20851
  continue;
20807
20852
  ACTIVE_LOADS++;
20808
20853
  loadFile(item.file, item.card).finally(() => {
20809
- ACTIVE_LOADS--;
20854
+ if (item.epoch === LOAD_EPOCH) {
20855
+ ACTIVE_LOADS = Math.max(0, ACTIVE_LOADS - 1);
20856
+ }
20810
20857
  pumpQueue();
20811
20858
  });
20812
20859
  }
@@ -21451,7 +21498,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21451
21498
  });
21452
21499
  }
21453
21500
  function clearLoadQueue() {
21454
- LOAD_QUEUE.length = 0;
21501
+ resetLoadScheduling();
21455
21502
  }
21456
21503
  return {
21457
21504
  renderMeta,
@@ -21937,6 +21984,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21937
21984
  let hasMore = false;
21938
21985
  let loading = false;
21939
21986
  let generation = 0;
21987
+ let selectionGeneration = 0;
21940
21988
  let selectedSha = "";
21941
21989
  let query = "";
21942
21990
  let mode = "history";
@@ -22197,11 +22245,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22197
22245
  return page.commits.length > 0;
22198
22246
  }
22199
22247
  async function selectCommit(commit, options = {}) {
22248
+ const selectionGen = ++selectionGeneration;
22200
22249
  const gen = generation;
22201
22250
  selectedSha = commit.sha;
22202
22251
  updateActiveRow();
22203
22252
  await updateCommitInfo(commit);
22204
- if (gen !== generation)
22253
+ if (selectionGen !== selectionGeneration || gen !== generation)
22205
22254
  return;
22206
22255
  if (options.updateUrl !== false) {
22207
22256
  const range = commitDiffRange(commit);
@@ -22218,13 +22267,14 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22218
22267
  deps.setRoute({ screen: "history", ref, commit: commit.sha, range }, true);
22219
22268
  }
22220
22269
  }
22221
- if (gen !== generation)
22270
+ if (selectionGen !== selectionGeneration || gen !== generation)
22222
22271
  return;
22223
22272
  await deps.applyCommitRange(commitDiffRange(commit), pathFilter || undefined);
22224
- if (gen !== generation)
22273
+ if (selectionGen !== selectionGeneration || gen !== generation)
22225
22274
  return;
22226
22275
  }
22227
22276
  async function selectWorktree(options = {}) {
22277
+ const selectionGen = ++selectionGeneration;
22228
22278
  const gen = generation;
22229
22279
  selectedSha = HISTORY_WORKTREE_COMMIT;
22230
22280
  updateActiveRow();
@@ -22244,10 +22294,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22244
22294
  deps.setRoute({ screen: "history", ref, commit: selectedSha, range }, true);
22245
22295
  }
22246
22296
  }
22247
- if (gen !== generation)
22297
+ if (selectionGen !== selectionGeneration || gen !== generation)
22248
22298
  return;
22249
22299
  await deps.applyCommitRange(range, pathFilter || undefined);
22250
- if (gen !== generation)
22300
+ if (selectionGen !== selectionGeneration || gen !== generation)
22251
22301
  return;
22252
22302
  }
22253
22303
  let lookupFailed = false;
@@ -22344,6 +22394,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22344
22394
  hasMore = false;
22345
22395
  loading = false;
22346
22396
  inFlight = null;
22397
+ selectionGeneration++;
22347
22398
  selectedSha = "";
22348
22399
  setBanner("");
22349
22400
  await updateCommitInfo(null);
@@ -22360,6 +22411,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22360
22411
  if (scope2.commit) {
22361
22412
  await resolveDeepLink(scope2.commit);
22362
22413
  } else {
22414
+ selectionGeneration++;
22363
22415
  selectedSha = "";
22364
22416
  updateActiveRow();
22365
22417
  await updateCommitInfo(null);
@@ -22389,6 +22441,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22389
22441
  generation++;
22390
22442
  loading = false;
22391
22443
  inFlight = null;
22444
+ selectionGeneration++;
22392
22445
  selectedSha = "";
22393
22446
  setBanner("");
22394
22447
  setStatusText("");
@@ -22413,6 +22466,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22413
22466
  return;
22414
22467
  query = value;
22415
22468
  generation++;
22469
+ selectionGeneration++;
22416
22470
  commits = [];
22417
22471
  hasMore = false;
22418
22472
  loading = false;
@@ -22575,6 +22629,308 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22575
22629
  };
22576
22630
  }
22577
22631
 
22632
+ // web-src/views/help-keybindings.ts
22633
+ var HIDDEN_HELP_KEYBINDING_ACTIONS = new Set([
22634
+ "start-g-sequence"
22635
+ ]);
22636
+ var HELP_KEYBINDING_GROUPS = [
22637
+ {
22638
+ title: { en: "Global", ja: "グローバル" },
22639
+ rows: [
22640
+ {
22641
+ selectors: [{ action: "open-file-palette" }],
22642
+ description: {
22643
+ en: "Open file palette",
22644
+ ja: "ファイルパレットを開く"
22645
+ }
22646
+ },
22647
+ {
22648
+ selectors: [{ action: "open-grep-palette" }],
22649
+ description: {
22650
+ en: "Open grep palette",
22651
+ ja: "grep パレットを開く"
22652
+ }
22653
+ },
22654
+ {
22655
+ selectors: [{ action: "focus-file-filter" }],
22656
+ description: {
22657
+ en: "Focus file filter",
22658
+ ja: "ファイルフィルターへフォーカス"
22659
+ }
22660
+ },
22661
+ {
22662
+ selectors: [{ action: "open-help" }],
22663
+ description: {
22664
+ en: "Open this help (keybindings)",
22665
+ ja: "このヘルプ(キーバインド)を開く"
22666
+ }
22667
+ },
22668
+ {
22669
+ selectors: [{ action: "toggle-theme" }],
22670
+ description: { en: "Toggle theme", ja: "テーマ切り替え" }
22671
+ },
22672
+ {
22673
+ selectors: [
22674
+ { action: "annotation-previous" },
22675
+ { action: "annotation-next" }
22676
+ ],
22677
+ description: {
22678
+ en: "Previous / next annotation",
22679
+ ja: "前 / 次の注釈へ移動"
22680
+ }
22681
+ },
22682
+ {
22683
+ selectors: [{ action: "layout-unified" }, { action: "layout-split" }],
22684
+ description: {
22685
+ en: "Switch unified / split layout",
22686
+ ja: "統合 / 分割レイアウトへ切り替え"
22687
+ }
22688
+ },
22689
+ {
22690
+ selectors: [{ action: "cancel-source-load" }],
22691
+ description: {
22692
+ en: "Cancel active source load",
22693
+ ja: "進行中のソース読み込みを中断"
22694
+ }
22695
+ }
22696
+ ]
22697
+ },
22698
+ {
22699
+ title: { en: "Panels", ja: "パネル" },
22700
+ rows: [
22701
+ {
22702
+ selectors: [{ action: "focus-sidebar" }],
22703
+ description: { en: "Focus sidebar", ja: "サイドバーへフォーカス" }
22704
+ },
22705
+ {
22706
+ selectors: [{ action: "focus-main" }],
22707
+ description: {
22708
+ en: "Focus main panel",
22709
+ ja: "メインパネルへフォーカス"
22710
+ }
22711
+ }
22712
+ ]
22713
+ },
22714
+ {
22715
+ title: { en: "Sidebar", ja: "サイドバー" },
22716
+ rows: [
22717
+ {
22718
+ selectors: [{ action: "sidebar-next" }, { action: "sidebar-previous" }],
22719
+ description: {
22720
+ en: "Move selection down / up",
22721
+ ja: "選択を下 / 上へ移動"
22722
+ }
22723
+ },
22724
+ {
22725
+ selectors: [
22726
+ { action: "sidebar-page-down" },
22727
+ { action: "sidebar-page-up" }
22728
+ ],
22729
+ description: {
22730
+ en: "Move selection by half a page",
22731
+ ja: "半ページ分選択を移動"
22732
+ }
22733
+ },
22734
+ {
22735
+ selectors: [
22736
+ { action: "goto-top", pendingG: true },
22737
+ { action: "goto-bottom", shift: true, pendingG: false }
22738
+ ],
22739
+ description: { en: "Move to top / bottom", ja: "先頭 / 末尾へ移動" }
22740
+ },
22741
+ {
22742
+ selectors: [{ action: "open-sidebar-item" }],
22743
+ description: { en: "Open selected item", ja: "選択項目を開く" }
22744
+ },
22745
+ {
22746
+ selectors: [
22747
+ { action: "sidebar-collapse" },
22748
+ { action: "sidebar-expand" }
22749
+ ],
22750
+ description: {
22751
+ en: "Collapse / expand directory",
22752
+ ja: "ディレクトリを閉じる / 開く"
22753
+ }
22754
+ }
22755
+ ]
22756
+ },
22757
+ {
22758
+ title: { en: "Main Panel", ja: "メインパネル" },
22759
+ rows: [
22760
+ {
22761
+ selectors: [
22762
+ { action: "scroll-main-down", scope: "main" },
22763
+ { action: "scroll-main-up", scope: "main" }
22764
+ ],
22765
+ description: {
22766
+ en: "Move code cursor down / up",
22767
+ ja: "コードカーソルを下 / 上へ移動"
22768
+ }
22769
+ },
22770
+ {
22771
+ selectors: [
22772
+ {
22773
+ action: "scroll-main-page-down",
22774
+ scope: "main",
22775
+ key: "d",
22776
+ ctrl: true
22777
+ },
22778
+ {
22779
+ action: "scroll-main-page-up",
22780
+ scope: "main",
22781
+ key: "u",
22782
+ ctrl: true
22783
+ }
22784
+ ],
22785
+ description: {
22786
+ en: "Move code cursor by half a page",
22787
+ ja: "コードカーソルを半ページ分移動"
22788
+ }
22789
+ },
22790
+ {
22791
+ selectors: [
22792
+ { action: "scroll-main-page-down", scope: "main", key: "pagedown" },
22793
+ { action: "scroll-main-page-up", scope: "main", key: "pageup" }
22794
+ ],
22795
+ description: {
22796
+ en: "Move code cursor by a page",
22797
+ ja: "コードカーソルを1ページ分移動"
22798
+ }
22799
+ },
22800
+ {
22801
+ selectors: [
22802
+ {
22803
+ action: "scroll-main-page-down",
22804
+ scope: "main",
22805
+ key: "arrowdown",
22806
+ ctrl: true
22807
+ },
22808
+ {
22809
+ action: "scroll-main-page-up",
22810
+ scope: "main",
22811
+ key: "arrowup",
22812
+ ctrl: true
22813
+ }
22814
+ ],
22815
+ description: {
22816
+ en: "Move code cursor by a page",
22817
+ ja: "コードカーソルを1ページ分移動"
22818
+ }
22819
+ },
22820
+ {
22821
+ selectors: [
22822
+ { action: "goto-top", pendingG: true },
22823
+ { action: "goto-bottom", shift: true, pendingG: false }
22824
+ ],
22825
+ description: {
22826
+ en: "Move code cursor to top / bottom",
22827
+ ja: "コードカーソルを先頭 / 末尾へ移動"
22828
+ }
22829
+ },
22830
+ {
22831
+ selectors: [
22832
+ { action: "tab-preview", scope: "main", pendingG: true },
22833
+ { action: "tab-code", scope: "main", pendingG: true }
22834
+ ],
22835
+ description: {
22836
+ en: "Switch to Preview / Code tab",
22837
+ ja: "Preview / Code タブへ切り替え"
22838
+ }
22839
+ }
22840
+ ]
22841
+ }
22842
+ ];
22843
+ function matchesSelector(binding, selector) {
22844
+ if (binding.action !== selector.action)
22845
+ return false;
22846
+ if (selector.key !== undefined && binding.key !== selector.key)
22847
+ return false;
22848
+ if (selector.scope !== undefined && binding.scope !== selector.scope)
22849
+ return false;
22850
+ if (selector.ctrl !== undefined && !!binding.ctrl !== selector.ctrl)
22851
+ return false;
22852
+ if (selector.meta !== undefined && !!binding.meta !== selector.meta)
22853
+ return false;
22854
+ if (selector.alt !== undefined && !!binding.alt !== selector.alt)
22855
+ return false;
22856
+ if (selector.shift !== undefined && !!binding.shift !== selector.shift)
22857
+ return false;
22858
+ if (selector.pendingG !== undefined && !!binding.pendingG !== selector.pendingG)
22859
+ return false;
22860
+ return true;
22861
+ }
22862
+ function formatKeyName(key, shifted) {
22863
+ const lower = key.toLowerCase();
22864
+ if (lower === "escape")
22865
+ return "Escape";
22866
+ if (lower === "enter")
22867
+ return "Enter";
22868
+ if (lower === "pagedown")
22869
+ return "PageDown";
22870
+ if (lower === "pageup")
22871
+ return "PageUp";
22872
+ if (lower === "arrowdown")
22873
+ return "ArrowDown";
22874
+ if (lower === "arrowup")
22875
+ return "ArrowUp";
22876
+ if (key === "?")
22877
+ return "?";
22878
+ if (shifted && key.length === 1)
22879
+ return key.toUpperCase();
22880
+ return key;
22881
+ }
22882
+ function formatKeyBinding(binding) {
22883
+ const key = formatKeyName(binding.key, !!binding.shift || !!binding.ctrl || !!binding.meta || !!binding.alt);
22884
+ if (binding.pendingG && !binding.ctrl && !binding.meta && !binding.alt && !binding.shift && binding.key.length === 1)
22885
+ return `g${key}`;
22886
+ const parts = [];
22887
+ if (binding.pendingG)
22888
+ parts.push("g");
22889
+ if (binding.ctrl)
22890
+ parts.push("Ctrl");
22891
+ if (binding.meta)
22892
+ parts.push("Meta");
22893
+ if (binding.alt)
22894
+ parts.push("Alt");
22895
+ if (binding.shift && binding.key !== "?")
22896
+ parts.push("Shift");
22897
+ parts.push(key);
22898
+ return parts.join("+");
22899
+ }
22900
+ function addUnique(values, next) {
22901
+ if (!values.includes(next))
22902
+ values.push(next);
22903
+ }
22904
+ function collectRowCoverage(row, bindings) {
22905
+ const coverage = [];
22906
+ const seen = new Set;
22907
+ for (const selector of row.selectors) {
22908
+ for (const binding of bindings) {
22909
+ if (!matchesSelector(binding, selector))
22910
+ continue;
22911
+ const label = formatKeyBinding(binding);
22912
+ const key = `${binding.action}\x00${binding.scope || ""}\x00${label}`;
22913
+ if (seen.has(key))
22914
+ continue;
22915
+ seen.add(key);
22916
+ coverage.push({ action: selector.action, binding, label });
22917
+ }
22918
+ }
22919
+ return coverage;
22920
+ }
22921
+ function buildRow(row, language, bindings) {
22922
+ const labels = [];
22923
+ for (const item of collectRowCoverage(row, bindings))
22924
+ addUnique(labels, item.label);
22925
+ return [labels.join(" / "), row.description[language]];
22926
+ }
22927
+ function buildHelpKeybindingGroups(language, bindings = DEFAULT_KEY_BINDINGS) {
22928
+ return HELP_KEYBINDING_GROUPS.map((group) => ({
22929
+ title: group.title[language],
22930
+ rows: group.rows.map((row) => buildRow(row, language, bindings))
22931
+ }));
22932
+ }
22933
+
22578
22934
  // web-src/views/help-page.ts
22579
22935
  var HELP_LANGUAGES = ["en", "ja"];
22580
22936
  var HELP_SECTIONS = [
@@ -22583,6 +22939,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22583
22939
  "annotations",
22584
22940
  "database",
22585
22941
  "skills",
22942
+ "mcp",
22586
22943
  "keybindings"
22587
22944
  ];
22588
22945
  var HELP_CONTENT = {
@@ -22670,6 +23027,21 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22670
23027
  {
22671
23028
  kind: "paragraph",
22672
23029
  text: "Each row reports OK / WARN / ERROR with a remediation hint when relevant. The check groups are Runtime (Node / Bun / NODE_MODULE_VERSION), Package (version + execution origin including npx cache), SQLite driver, Snapshot store, Git, Discovery summary, Docker / Compose (CLI / v2 plugin / daemon / compose config dry-parse / compose ps health per discovered service), and Server. The most common hint is the npx cache fix for better-sqlite3 NODE_MODULE_VERSION mismatch (rm -rf ~/.npm/_npx then re-run with npx -y @youtyan/code-viewer@latest)."
23030
+ },
23031
+ {
23032
+ kind: "paragraph",
23033
+ text: 'From the terminal — and for AI agents or CI — the same report is available without a browser. `code-viewer doctor` prints a status summary; add `--json` for the full DoctorReport (matches the /_doctor endpoint). Exit code is 1 when worstStatus is "error", so it doubles as a CI gate.'
23034
+ },
23035
+ {
23036
+ kind: "command",
23037
+ title: "Doctor summary in the terminal",
23038
+ command: "code-viewer doctor"
23039
+ },
23040
+ {
23041
+ kind: "command",
23042
+ title: "Doctor JSON for agents and CI",
23043
+ command: `code-viewer doctor --json
23044
+ code-viewer doctor --cwd /path/to/repo --port 64160 --json`
22673
23045
  }
22674
23046
  ]
22675
23047
  }
@@ -22985,7 +23357,7 @@ code-viewer query clear --db app.db`
22985
23357
  {
22986
23358
  kind: "command",
22987
23359
  title: "Search across all tables",
22988
- command: 'code-viewer query search --db app.db --term "john@example.com" \\\n --tables users,orders --include-non-text --max-hits 20'
23360
+ command: 'code-viewer query search --db app.db --term "sample@example.com" \\\n --tables users,orders --include-non-text --max-hits 20'
22989
23361
  },
22990
23362
  {
22991
23363
  kind: "command",
@@ -23002,16 +23374,12 @@ code-viewer query snapshot delete --id snap-abc123`
23002
23374
  {
23003
23375
  kind: "command",
23004
23376
  title: "Diff two snapshots",
23005
- command: `code-viewer query diff create --before snap-abc123 --after snap-def456 \\
23006
- --note "User registration test"
23007
- code-viewer query diff list --db app.db --json
23008
- code-viewer query diff delete --id diff-xyz789`
23377
+ command: "code-viewer query diff tables --before snap-abc123 --after snap-def456 --json"
23009
23378
  },
23010
23379
  {
23011
23380
  kind: "command",
23012
23381
  title: "Inspect a diff",
23013
- command: `code-viewer query diff tables --id diff-xyz789
23014
- code-viewer query diff rows --id diff-xyz789 --table users --type inserted --limit 50`
23382
+ command: "code-viewer query diff rows --before snap-abc123 --after snap-def456 --table users --limit 50"
23015
23383
  },
23016
23384
  {
23017
23385
  kind: "command",
@@ -23097,63 +23465,120 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23097
23465
  }
23098
23466
  ]
23099
23467
  },
23100
- keybindings: {
23101
- nav: "Keybindings",
23102
- title: "Keyboard Shortcuts",
23103
- intro: "Use these shortcuts to move between panels and navigate files without leaving the keyboard.",
23468
+ mcp: {
23469
+ nav: "MCP Server",
23470
+ title: "MCP Server",
23471
+ intro: "While code-viewer is running, the same server also exposes a local, read-only MCP endpoint at /_mcp so AI agents can call status, file, search, and datastore tools directly over JSON-RPC instead of spawning code-viewer CLI subprocesses.",
23104
23472
  groups: [
23105
23473
  {
23106
- title: "Global",
23474
+ title: "Connect to the endpoint",
23107
23475
  blocks: [
23108
23476
  {
23109
- kind: "table",
23110
- rows: [
23111
- ["Ctrl+K", "Open file palette"],
23112
- ["Ctrl+G", "Open grep palette"],
23113
- ["/", "Focus file filter"],
23114
- ["t", "Toggle theme"],
23115
- ["[ / ]", "Previous / next annotation"]
23116
- ]
23117
- }
23118
- ]
23119
- },
23120
- {
23121
- title: "Panels",
23122
- blocks: [
23477
+ kind: "paragraph",
23478
+ text: "The endpoint speaks JSON-RPC 2.0 over the Streamable HTTP transport (initialize, ping, tools/list, tools/call). It accepts POST requests with an application/json body only, and is guarded by the same localhost / same-origin check as every other route."
23479
+ },
23123
23480
  {
23124
- kind: "table",
23125
- rows: [
23126
- ["Ctrl+H", "Focus sidebar"],
23127
- ["Ctrl+L", "Focus main panel"]
23128
- ]
23481
+ kind: "command",
23482
+ title: "Endpoint URL (port from the printed startup URL)",
23483
+ command: "http://127.0.0.1:<port>/_mcp"
23129
23484
  }
23130
23485
  ]
23131
23486
  },
23132
23487
  {
23133
- title: "Sidebar",
23488
+ title: "Available tools",
23134
23489
  blocks: [
23135
23490
  {
23136
23491
  kind: "table",
23137
23492
  rows: [
23138
- ["j / k", "Move selection down / up"],
23139
- ["Ctrl+D / Ctrl+U", "Move selection by half a page"],
23140
- ["gg / Shift+G", "Move to top / bottom"],
23141
- ["Enter", "Open selected item"],
23142
- ["h / l", "Collapse / expand directory"]
23493
+ [
23494
+ "code_viewer_agent_help",
23495
+ "Index of every AI-facing CLI subcommand."
23496
+ ],
23497
+ [
23498
+ "code_viewer_status",
23499
+ "Branch, remote, changed files, and recent commits."
23500
+ ],
23501
+ [
23502
+ "code_viewer_file_show",
23503
+ "Read a file (optionally a line range) at any ref."
23504
+ ],
23505
+ [
23506
+ "code_viewer_file_blame",
23507
+ "Per-line blame (sha / author / time / summary)."
23508
+ ],
23509
+ [
23510
+ "code_viewer_file_history",
23511
+ "Commit history for one path (follows renames)."
23512
+ ],
23513
+ [
23514
+ "code_viewer_file_diff",
23515
+ "Unified diff for one path (preview-capped by default)."
23516
+ ],
23517
+ [
23518
+ "code_viewer_search_files",
23519
+ "Rank repository paths by fuzzy or glob match."
23520
+ ],
23521
+ [
23522
+ "code_viewer_search_code",
23523
+ "Grep the repository (rg / git grep / fallback)."
23524
+ ],
23525
+ [
23526
+ "code_viewer_datastore_sources",
23527
+ "Discover read-only datastore source ids."
23528
+ ],
23529
+ [
23530
+ "code_viewer_datastore_schemas",
23531
+ "List schemas for one SQL datastore."
23532
+ ],
23533
+ [
23534
+ "code_viewer_datastore_schema",
23535
+ "Inspect tables, indexes, FKs, and columns."
23536
+ ],
23537
+ [
23538
+ "code_viewer_datastore_columns",
23539
+ "Inspect columns for one SQL table."
23540
+ ],
23541
+ [
23542
+ "code_viewer_datastore_ddl",
23543
+ "Inspect the CREATE statement and triggers."
23544
+ ],
23545
+ [
23546
+ "code_viewer_datastore_query",
23547
+ "Run a read-only SELECT / PRAGMA / EXPLAIN / WITH."
23548
+ ],
23549
+ [
23550
+ "code_viewer_datastore_history",
23551
+ "Inspect saved query history."
23552
+ ]
23143
23553
  ]
23144
23554
  }
23145
23555
  ]
23146
- },
23556
+ }
23557
+ ]
23558
+ },
23559
+ keybindings: {
23560
+ nav: "Keybindings",
23561
+ title: "Keyboard Shortcuts",
23562
+ intro: "Use these shortcuts to move between panels and navigate files without leaving the keyboard.",
23563
+ groups: [
23147
23564
  {
23148
- title: "Main Panel",
23565
+ title: "Line selection",
23149
23566
  blocks: [
23150
23567
  {
23151
23568
  kind: "table",
23152
23569
  rows: [
23153
- ["j / k", "Move code cursor down / up"],
23154
- ["Ctrl+D / Ctrl+U", "Move code cursor by half a page"],
23155
- ["gg / Shift+G", "Move code cursor to top / bottom"],
23156
- ["gp / gc", "Switch to Preview / Code tab"]
23570
+ [
23571
+ "Drag on line numbers",
23572
+ "Highlight a range; the floating copy pill prepares @path#L1-L9"
23573
+ ],
23574
+ [
23575
+ "Click the pill",
23576
+ "Copy @path#L1-L9 to the clipboard for pasting into Claude Code / Codex"
23577
+ ],
23578
+ [
23579
+ "Shift+Click the pill",
23580
+ "Copy @path#L1-L9 plus a fenced code block of the selected lines — paste straight to an AI without re-fetching the file"
23581
+ ]
23157
23582
  ]
23158
23583
  }
23159
23584
  ]
@@ -23246,6 +23671,21 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23246
23671
  {
23247
23672
  kind: "paragraph",
23248
23673
  text: "各項目は OK / WARN / ERROR で表示され、必要に応じて対処手順のヒントが付きます。診断グループは Runtime (Node / Bun / NODE_MODULE_VERSION)、Package (バージョン + 実行元: npx cache / global / local / bunx)、SQLite driver、Snapshot store、Git、Discovery summary、Docker / Compose (CLI / v2 plugin / daemon / compose config dry parse / compose ps による各サービスのヘルス)、Server (待ち受けポート) の 8 つ。よくあるヒントは npx キャッシュ起因の better-sqlite3 NODE_MODULE_VERSION 不一致で、rm -rf ~/.npm/_npx の後に npx -y @youtyan/code-viewer@latest を再実行する手順を表示します。"
23674
+ },
23675
+ {
23676
+ kind: "paragraph",
23677
+ text: 'ターミナルからは `code-viewer doctor` で同じレポートを取得できます。AI エージェントや CI からは `--json` で /_doctor と同じ DoctorReport を受け取れます。worstStatus が "error" なら exit code 1 を返すので CI ガードに直接使えます。'
23678
+ },
23679
+ {
23680
+ kind: "command",
23681
+ title: "ターミナル用 doctor サマリー",
23682
+ command: "code-viewer doctor"
23683
+ },
23684
+ {
23685
+ kind: "command",
23686
+ title: "AI / CI 用 doctor JSON",
23687
+ command: `code-viewer doctor --json
23688
+ code-viewer doctor --cwd /path/to/repo --port 64160 --json`
23249
23689
  }
23250
23690
  ]
23251
23691
  }
@@ -23561,7 +24001,7 @@ code-viewer query clear --db app.db`
23561
24001
  {
23562
24002
  kind: "command",
23563
24003
  title: "テーブル横断の全文検索",
23564
- command: 'code-viewer query search --db app.db --term "john@example.com" \\\n --tables users,orders --include-non-text --max-hits 20'
24004
+ command: 'code-viewer query search --db app.db --term "sample@example.com" \\\n --tables users,orders --include-non-text --max-hits 20'
23565
24005
  },
23566
24006
  {
23567
24007
  kind: "command",
@@ -23578,16 +24018,12 @@ code-viewer query snapshot delete --id snap-abc123`
23578
24018
  {
23579
24019
  kind: "command",
23580
24020
  title: "2 つのスナップショットを diff",
23581
- command: `code-viewer query diff create --before snap-abc123 --after snap-def456 \\
23582
- --note "ユーザー登録テストの差分"
23583
- code-viewer query diff list --db app.db --json
23584
- code-viewer query diff delete --id diff-xyz789`
24021
+ command: "code-viewer query diff tables --before snap-abc123 --after snap-def456 --json"
23585
24022
  },
23586
24023
  {
23587
24024
  kind: "command",
23588
24025
  title: "diff の中身を確認",
23589
- command: `code-viewer query diff tables --id diff-xyz789
23590
- code-viewer query diff rows --id diff-xyz789 --table users --type inserted --limit 50`
24026
+ command: "code-viewer query diff rows --before snap-abc123 --after snap-def456 --table users --limit 50"
23591
24027
  },
23592
24028
  {
23593
24029
  kind: "command",
@@ -23673,63 +24109,117 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23673
24109
  }
23674
24110
  ]
23675
24111
  },
23676
- keybindings: {
23677
- nav: "キーバインド",
23678
- title: "キーバインド",
23679
- intro: "キーボードだけでパネル移動、ファイル選択、スクロールを行うためのショートカットです。",
24112
+ mcp: {
24113
+ nav: "MCPサーバー",
24114
+ title: "MCPサーバー",
24115
+ intro: "code-viewer を起動している間、同じサーバーがローカルの read-only な MCP エンドポイント (/_mcp) も公開します。AI エージェントは code-viewer の CLI をサブプロセスとして起動する代わりに、status / file / search / datastore の各ツールを JSON-RPC 経由で直接呼び出せます。",
23680
24116
  groups: [
23681
24117
  {
23682
- title: "グローバル",
24118
+ title: "エンドポイントへの接続",
23683
24119
  blocks: [
23684
24120
  {
23685
- kind: "table",
23686
- rows: [
23687
- ["Ctrl+K", "ファイルパレットを開く"],
23688
- ["Ctrl+G", "grep パレットを開く"],
23689
- ["/", "ファイルフィルターへフォーカス"],
23690
- ["t", "テーマ切り替え"],
23691
- ["[ / ]", "前 / 次の注釈へ移動"]
23692
- ]
23693
- }
23694
- ]
23695
- },
23696
- {
23697
- title: "パネル",
23698
- blocks: [
24121
+ kind: "paragraph",
24122
+ text: "エンドポイントは JSON-RPC 2.0 の Streamable HTTP トランスポート (initialize / ping / tools/list / tools/call) で応答します。受け付けるのは application/json ボディの POST リクエストのみで、他のルートと同じ localhost / 同一オリジンチェックで保護されています。"
24123
+ },
23699
24124
  {
23700
- kind: "table",
23701
- rows: [
23702
- ["Ctrl+H", "サイドバーへフォーカス"],
23703
- ["Ctrl+L", "メインパネルへフォーカス"]
23704
- ]
24125
+ kind: "command",
24126
+ title: "エンドポイント URL (port は起動時に表示される URL と同じ)",
24127
+ command: "http://127.0.0.1:<port>/_mcp"
23705
24128
  }
23706
24129
  ]
23707
24130
  },
23708
24131
  {
23709
- title: "サイドバー",
24132
+ title: "利用できるツール",
23710
24133
  blocks: [
23711
24134
  {
23712
24135
  kind: "table",
23713
24136
  rows: [
23714
- ["j / k", "選択を下 / 上へ移動"],
23715
- ["Ctrl+D / Ctrl+U", "半ページ分選択を移動"],
23716
- ["gg / Shift+G", "先頭 / 末尾へ移動"],
23717
- ["Enter", "選択項目を開く"],
23718
- ["h / l", "ディレクトリを閉じる / 開く"]
24137
+ [
24138
+ "code_viewer_agent_help",
24139
+ "AI 向け CLI サブコマンドの一覧索引。"
24140
+ ],
24141
+ [
24142
+ "code_viewer_status",
24143
+ "ブランチ、remote、変更ファイル、直近のコミット。"
24144
+ ],
24145
+ [
24146
+ "code_viewer_file_show",
24147
+ "任意の ref でファイル(または行範囲)を読む。"
24148
+ ],
24149
+ [
24150
+ "code_viewer_file_blame",
24151
+ "行単位の blame (sha / author / time / summary)。"
24152
+ ],
24153
+ [
24154
+ "code_viewer_file_history",
24155
+ "1ファイルのコミット履歴(リネーム追跡あり)。"
24156
+ ],
24157
+ [
24158
+ "code_viewer_file_diff",
24159
+ "1ファイルの unified diff(既定はプレビュー上限あり)。"
24160
+ ],
24161
+ [
24162
+ "code_viewer_search_files",
24163
+ "ファジー / glob マッチでリポジトリのパスを順位付け。"
24164
+ ],
24165
+ [
24166
+ "code_viewer_search_code",
24167
+ "リポジトリを grep する (rg / git grep / フォールバック)。"
24168
+ ],
24169
+ [
24170
+ "code_viewer_datastore_sources",
24171
+ "read-only なデータストアの source id を発見。"
24172
+ ],
24173
+ [
24174
+ "code_viewer_datastore_schemas",
24175
+ "SQL データストア1件のスキーマ一覧。"
24176
+ ],
24177
+ [
24178
+ "code_viewer_datastore_schema",
24179
+ "テーブル、インデックス、外部キー、カラムを調査。"
24180
+ ],
24181
+ [
24182
+ "code_viewer_datastore_columns",
24183
+ "SQL テーブル1件のカラムを調査。"
24184
+ ],
24185
+ ["code_viewer_datastore_ddl", "CREATE 文とトリガーを調査。"],
24186
+ [
24187
+ "code_viewer_datastore_query",
24188
+ "read-only な SELECT / PRAGMA / EXPLAIN / WITH を実行。"
24189
+ ],
24190
+ [
24191
+ "code_viewer_datastore_history",
24192
+ "保存済みのクエリ履歴を調査。"
24193
+ ]
23719
24194
  ]
23720
24195
  }
23721
24196
  ]
23722
- },
24197
+ }
24198
+ ]
24199
+ },
24200
+ keybindings: {
24201
+ nav: "キーバインド",
24202
+ title: "キーバインド",
24203
+ intro: "キーボードだけでパネル移動、ファイル選択、スクロールを行うためのショートカットです。",
24204
+ groups: [
23723
24205
  {
23724
- title: "メインパネル",
24206
+ title: "行選択",
23725
24207
  blocks: [
23726
24208
  {
23727
24209
  kind: "table",
23728
24210
  rows: [
23729
- ["j / k", "コードカーソルを下 / 上へ移動"],
23730
- ["Ctrl+D / Ctrl+U", "コードカーソルを半ページ分移動"],
23731
- ["gg / Shift+G", "コードカーソルを先頭 / 末尾へ移動"],
23732
- ["gp / gc", "Preview / Code タブへ切り替え"]
24211
+ [
24212
+ "行番号でドラッグ",
24213
+ "範囲をハイライトし、フロートする Copy ピルが @path#L1-L9 を用意"
24214
+ ],
24215
+ [
24216
+ "ピルをクリック",
24217
+ "@path#L1-L9 をコピー(Claude Code / Codex への貼り付け用)"
24218
+ ],
24219
+ [
24220
+ "Shift+ピルをクリック",
24221
+ "@path#L1-L9 と選択行の実コード(フェンス付き)をまとめてコピー — AI へ直接貼れて、ファイル再取得不要"
24222
+ ]
23733
24223
  ]
23734
24224
  }
23735
24225
  ]
@@ -23745,6 +24235,19 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23745
24235
  function helpSectionFromRoute(route) {
23746
24236
  return route.screen === "help" && HELP_SECTIONS.includes(route.section) ? route.section : "overview";
23747
24237
  }
24238
+ function openHelpKeybindings(deps) {
24239
+ const route = deps.getRoute();
24240
+ deps.cancelActiveSourceLoad("navigation");
24241
+ deps.setRoute({
24242
+ screen: "help",
24243
+ lang: route.screen === "help" ? helpLanguageFromRoute(route) : deps.getLanguage(),
24244
+ section: "keybindings",
24245
+ range: deps.currentRange()
24246
+ });
24247
+ deps.setPageMode();
24248
+ deps.renderHelpPage();
24249
+ deps.setStatus("live");
24250
+ }
23748
24251
  function renderHelpCommand(block2) {
23749
24252
  const wrap = document.createElement("div");
23750
24253
  wrap.className = "gdp-help-command";
@@ -23813,6 +24316,13 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23813
24316
  const section = helpSectionFromRoute(deps.getRoute());
23814
24317
  const content = HELP_CONTENT[lang];
23815
24318
  const sectionContent = content.sections[section];
24319
+ const sectionGroups = section === "keybindings" ? [
24320
+ ...buildHelpKeybindingGroups(lang).map((group) => ({
24321
+ title: group.title,
24322
+ blocks: [{ kind: "table", rows: group.rows }]
24323
+ })),
24324
+ ...sectionContent.groups
24325
+ ] : sectionContent.groups;
23816
24326
  const shell = document.createElement("section");
23817
24327
  shell.className = "gdp-help-shell";
23818
24328
  const header = document.createElement("header");
@@ -23862,7 +24372,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23862
24372
  const intro = document.createElement("p");
23863
24373
  intro.textContent = sectionContent.intro;
23864
24374
  article.append(h2, intro);
23865
- sectionContent.groups.forEach((group) => {
24375
+ sectionGroups.forEach((group) => {
23866
24376
  const groupSection = document.createElement("section");
23867
24377
  groupSection.className = "gdp-help-group";
23868
24378
  const groupTitle = document.createElement("h3");
@@ -24270,39 +24780,109 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
24270
24780
  return { setupHunkExpand };
24271
24781
  }
24272
24782
 
24783
+ // web-src/core/html-escape.ts
24784
+ function escapeHtml2(value) {
24785
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
24786
+ }
24787
+
24273
24788
  // web-src/views/line-ref-pill.ts
24274
24789
  var COPY_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="currentColor">' + '<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>' + '<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>' + "</svg>";
24275
24790
  var CHECK_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="currentColor">' + '<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>' + "</svg>";
24276
- function escapeHtml2(value) {
24277
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24791
+ function langFromPath(path) {
24792
+ const m = path.match(/\.([^./]+)$/);
24793
+ if (!m)
24794
+ return "";
24795
+ return EXT_TO_LANG[m[1].toLowerCase()] || "";
24796
+ }
24797
+ function readRenderedLines(path, start, end) {
24798
+ const card = document.querySelector(`.gdp-file-shell[data-path="${CSS.escape(path)}"]`);
24799
+ if (!card)
24800
+ return [];
24801
+ const out = [];
24802
+ for (let line = start;line <= end; line++) {
24803
+ const sourceRow = card.querySelector(`.gdp-source-table tr[data-line="${String(line)}"]`);
24804
+ if (sourceRow) {
24805
+ const code2 = sourceRow.querySelector(".gdp-source-line-code");
24806
+ out.push(code2?.textContent ?? "");
24807
+ }
24808
+ }
24809
+ if (out.length > 0 && out.some((value) => value.length > 0))
24810
+ return out;
24811
+ const diffRows = card.querySelectorAll("table.d2h-diff-table tr");
24812
+ if (diffRows.length === 0)
24813
+ return [];
24814
+ const collected = new Map;
24815
+ diffRows.forEach((row) => {
24816
+ const sideCell = row.querySelector("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
24817
+ if (!sideCell)
24818
+ return;
24819
+ const side = sideCell.closest(".d2h-file-side-diff");
24820
+ const wrapper = sideCell.closest(".d2h-file-wrapper");
24821
+ if (side && wrapper) {
24822
+ const sides = wrapper.querySelectorAll(".d2h-file-side-diff");
24823
+ if (sides.length >= 2 && side !== sides[1])
24824
+ return;
24825
+ }
24826
+ const numText = sideCell.querySelector(".line-num2")?.textContent ?? sideCell.textContent ?? "";
24827
+ const line = Number(numText.trim());
24828
+ if (!Number.isInteger(line) || line < start || line > end)
24829
+ return;
24830
+ const codeCell = row.querySelector(".d2h-code-line-ctn");
24831
+ if (codeCell)
24832
+ collected.set(line, codeCell.textContent ?? "");
24833
+ });
24834
+ if (collected.size === 0)
24835
+ return [];
24836
+ const ordered = [];
24837
+ for (let line = start;line <= end; line++) {
24838
+ ordered.push(collected.get(line) ?? "");
24839
+ }
24840
+ return ordered;
24278
24841
  }
24279
24842
  function createLineRefPill() {
24280
24843
  const pill = document.createElement("button");
24281
24844
  pill.id = "line-ref-pill";
24282
24845
  pill.type = "button";
24283
- pill.title = "選択行の参照をコピー(Claude Code / Codex に貼り付け用)";
24846
+ pill.title = "選択行の参照をコピー(Claude Code / Codex に貼り付け用)。Shift+Click でコード本体も添付。";
24284
24847
  pill.hidden = true;
24285
24848
  document.body.appendChild(pill);
24286
24849
  let refText = "";
24850
+ let currentPath = "";
24851
+ let currentStart = 0;
24852
+ let currentEnd = 0;
24287
24853
  let feedbackTimer = null;
24288
24854
  function render(state) {
24289
- pill.classList.toggle("copied", state === "copied");
24855
+ pill.classList.toggle("copied", state === "copied" || state === "copied-code");
24290
24856
  if (state === "copied") {
24291
24857
  pill.innerHTML = `${CHECK_ICON}<span class="lrp-label">Copied!</span>`;
24292
24858
  return;
24293
24859
  }
24860
+ if (state === "copied-code") {
24861
+ pill.innerHTML = `${CHECK_ICON}<span class="lrp-label">Copied + code</span>`;
24862
+ return;
24863
+ }
24294
24864
  if (state === "failed") {
24295
24865
  pill.innerHTML = `${COPY_ICON}<span class="lrp-label">copy failed</span>`;
24296
24866
  return;
24297
24867
  }
24298
24868
  pill.innerHTML = `${COPY_ICON}<span class="lrp-label">Copy</span>` + `<span class="lrp-ref">${escapeHtml2(refText)}</span>`;
24299
24869
  }
24300
- pill.addEventListener("click", async () => {
24870
+ pill.addEventListener("click", async (event) => {
24301
24871
  if (!refText)
24302
24872
  return;
24873
+ const withCode = event.shiftKey;
24874
+ let payload = refText;
24875
+ let copiedCode = false;
24876
+ if (withCode && currentPath) {
24877
+ const lines = readRenderedLines(currentPath, currentStart, currentEnd);
24878
+ if (lines.length > 0) {
24879
+ payload = fileReferenceWithCodeClipboardText(currentPath, currentStart, currentEnd, lines, langFromPath(currentPath));
24880
+ copiedCode = true;
24881
+ }
24882
+ }
24303
24883
  try {
24304
- await navigator.clipboard.writeText(refText);
24305
- render("copied");
24884
+ await navigator.clipboard.writeText(payload);
24885
+ render(copiedCode ? "copied-code" : "copied");
24306
24886
  } catch {
24307
24887
  render("failed");
24308
24888
  }
@@ -24321,6 +24901,9 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
24321
24901
  return;
24322
24902
  const changed = next !== refText;
24323
24903
  refText = next;
24904
+ currentPath = path;
24905
+ currentStart = Math.max(1, Math.floor(Math.min(start, end)));
24906
+ currentEnd = Math.max(1, Math.floor(Math.max(start, end)));
24324
24907
  if (feedbackTimer) {
24325
24908
  clearTimeout(feedbackTimer);
24326
24909
  feedbackTimer = null;
@@ -24335,6 +24918,9 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
24335
24918
  },
24336
24919
  hide() {
24337
24920
  refText = "";
24921
+ currentPath = "";
24922
+ currentStart = 0;
24923
+ currentEnd = 0;
24338
24924
  pill.hidden = true;
24339
24925
  }
24340
24926
  };
@@ -24600,6 +25186,11 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
24600
25186
  wireRefSelectorInput(refToInput, () => deps.setRange(refFromInput.value, refToInput.value));
24601
25187
  wireRefSelectorInput(deps.$("#repo-target"), (ref) => {
24602
25188
  const route = deps.getRoute();
25189
+ if (route.screen === "repo") {
25190
+ deps.setRoute({ ...route, ref, range: deps.currentRange() });
25191
+ deps.loadRepo();
25192
+ return;
25193
+ }
24603
25194
  if (route.screen !== "file")
24604
25195
  return;
24605
25196
  const nextRoute = {
@@ -25563,6 +26154,13 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
25563
26154
  function renderSidebar(files, onFileClick) {
25564
26155
  const ul = $("#filelist");
25565
26156
  const repoSidebar = !!onFileClick;
26157
+ if (!repoSidebar) {
26158
+ const repoTarget = document.querySelector("#repo-target-wrap");
26159
+ if (repoTarget) {
26160
+ repoTarget.hidden = true;
26161
+ repoTarget.style.display = "none";
26162
+ }
26163
+ }
25566
26164
  const treeMode = STATE.sbView === "tree" || repoSidebar;
25567
26165
  ul.innerHTML = "";
25568
26166
  ul.classList.toggle("tree", treeMode);
@@ -26211,8 +26809,10 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
26211
26809
  const wrap = document.querySelector("#repo-target-wrap");
26212
26810
  if (!input || !wrap)
26213
26811
  return;
26214
- input.value = ref || "worktree";
26215
- wrap.hidden = activeRepoTreeRef() == null;
26812
+ const activeRef = activeRepoTreeRef();
26813
+ input.value = activeRef || ref || "worktree";
26814
+ wrap.hidden = activeRef == null;
26815
+ wrap.style.display = activeRef == null ? "none" : "";
26216
26816
  syncSidebarHeaderHeight();
26217
26817
  }
26218
26818
  function activeRepoTreeRef() {
@@ -29998,8 +30598,8 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
29998
30598
  view: "view",
29999
30599
  tree: "tree",
30000
30600
  flat: "flat",
30001
- filter: "Filter files...",
30002
- filterTitle: "Filter files. Use /pattern/ for regex. Cmd/Ctrl+K focuses this field.",
30601
+ filter: "Filter files… / ⌘K",
30602
+ filterTitle: "Filter files. Use /pattern/ for regex. Press / to focus this field, Cmd/Ctrl+K for the full-file palette, Ctrl+G for grep, ? for help.",
30003
30603
  hide: "hide sidebar"
30004
30604
  },
30005
30605
  history: {
@@ -30104,8 +30704,8 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30104
30704
  view: "表示",
30105
30705
  tree: "ツリー",
30106
30706
  flat: "一覧",
30107
- filter: "ファイルを絞り込み...",
30108
- filterTitle: "ファイルを絞り込みます。正規表現は /pattern/。Cmd/Ctrl+K でフォーカス。",
30707
+ filter: "ファイル絞り込み… / ⌘K",
30708
+ filterTitle: "ファイルを絞り込みます。/pattern/ は正規表現。/ でこの欄にフォーカス、Cmd/Ctrl+K で全ファイルパレット、Ctrl+G で grep、? でヘルプ。",
30109
30709
  hide: "サイドバーを隠す"
30110
30710
  },
30111
30711
  history: {
@@ -30841,13 +31441,21 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30841
31441
  function setPageMode() {
30842
31442
  const historyPanelRoute = STATE.route.screen === "history";
30843
31443
  const fileHistoryRoute = isFileHistoryRoute(STATE.route);
31444
+ const fileRepoBlobRoute = STATE.route.screen === "file" && (STATE.route.view === "blob" || STATE.route.view === "blame" || STATE.route.view === "history");
31445
+ const repoSidebarRoute = STATE.route.screen === "repo" || fileRepoBlobRoute;
30844
31446
  document.body.classList.toggle("gdp-file-detail-page", STATE.route.screen === "file");
30845
- document.body.classList.toggle("gdp-repo-blob-page", STATE.route.screen === "file" && (STATE.route.view === "blob" || STATE.route.view === "blame" || STATE.route.view === "history"));
31447
+ document.body.classList.toggle("gdp-repo-blob-page", fileRepoBlobRoute);
30846
31448
  document.body.classList.toggle("gdp-repo-page", STATE.route.screen === "repo");
31449
+ document.body.classList.toggle("gdp-diff-page", STATE.route.screen === "diff");
30847
31450
  document.body.classList.toggle("gdp-help-page", STATE.route.screen === "help");
30848
31451
  document.body.classList.toggle("gdp-history-page", historyPanelRoute);
30849
31452
  document.body.classList.toggle("gdp-file-history-page", fileHistoryRoute);
30850
31453
  document.body.classList.toggle("gdp-database-page", STATE.route.screen === "database");
31454
+ const repoTargetWrap = document.querySelector("#repo-target-wrap");
31455
+ if (!repoSidebarRoute && repoTargetWrap) {
31456
+ repoTargetWrap.hidden = true;
31457
+ repoTargetWrap.style.display = "none";
31458
+ }
30851
31459
  placeSidebarToggle();
30852
31460
  syncSidebarHeaderHeight();
30853
31461
  const historyPanel = $("#history-panel");
@@ -31442,6 +32050,19 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
31442
32050
  $("#theme").click();
31443
32051
  return true;
31444
32052
  }
32053
+ if (action === "open-help") {
32054
+ openHelpKeybindings({
32055
+ getRoute: () => STATE.route,
32056
+ getLanguage: () => STATE.language,
32057
+ currentRange,
32058
+ setRoute,
32059
+ setPageMode,
32060
+ renderHelpPage,
32061
+ setStatus,
32062
+ cancelActiveSourceLoad
32063
+ });
32064
+ return true;
32065
+ }
31445
32066
  return false;
31446
32067
  }
31447
32068
  document.addEventListener("keydown", handleVirtualSourcePagingKeydown, {
@@ -31612,6 +32233,8 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
31612
32233
  getRoute: () => STATE.route,
31613
32234
  setRoute,
31614
32235
  applyCommitRange: (range, pathFilter) => {
32236
+ cancelInFlightRequests();
32237
+ DIFF_VIEW.clearLoadQueue();
31615
32238
  STATE.from = range.from;
31616
32239
  STATE.to = range.to;
31617
32240
  activeHistoryPathFilter = pathFilter || null;
@@ -31743,6 +32366,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
31743
32366
  currentRange,
31744
32367
  setRange,
31745
32368
  setRoute,
32369
+ loadRepo,
31746
32370
  renderStandaloneSource,
31747
32371
  getFrom: () => STATE.from,
31748
32372
  getTo: () => STATE.to,