@youtyan/code-viewer 0.4.1 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -658,6 +658,13 @@
658
658
  lang: params.get("lang") || "en",
659
659
  section: params.get("section") || "overview"
660
660
  };
661
+ case "/doctor":
662
+ return {
663
+ screen: "repo",
664
+ ref: params.get("ref") || params.get("target") || "worktree",
665
+ path: params.get("path") || "",
666
+ range
667
+ };
661
668
  case "/history": {
662
669
  const commit = params.get("commit") || "";
663
670
  return {
@@ -757,6 +764,24 @@
757
764
  function buildRawFileUrl(target) {
758
765
  return "/_file?path=" + encodeURIComponent(target.path) + "&ref=" + encodeURIComponent(target.ref || "worktree");
759
766
  }
767
+ function parseDoctorOverlay(pathname, search) {
768
+ if (pathname === "/doctor")
769
+ return true;
770
+ const params = new URLSearchParams(search);
771
+ return params.get("doctor") === "open";
772
+ }
773
+ function withDoctorOverlay(url, open) {
774
+ const queryIdx = url.indexOf("?");
775
+ const base = queryIdx >= 0 ? url.slice(0, queryIdx) : url;
776
+ const query = queryIdx >= 0 ? url.slice(queryIdx + 1) : "";
777
+ const params = new URLSearchParams(query);
778
+ if (open)
779
+ params.set("doctor", "open");
780
+ else
781
+ params.delete("doctor");
782
+ const qs = params.toString();
783
+ return qs ? `${base}?${qs}` : base;
784
+ }
760
785
 
761
786
  // web-src/core/annotation-player-core.ts
762
787
  function createAnnotationPlayerCore(deps) {
@@ -846,7 +871,9 @@
846
871
  emit();
847
872
  const item = deps.items()[clamped];
848
873
  if (item)
849
- deps.jump(item.entryId).catch(() => {});
874
+ deps.jump(item.entryId).catch(() => {
875
+ return;
876
+ });
850
877
  }
851
878
  }
852
879
  function jumpTo(at) {
@@ -10897,7 +10924,9 @@ ${frontmatter.yaml}
10897
10924
  setTimeout(() => {
10898
10925
  copyBtn.textContent = text2().er.copyMermaid;
10899
10926
  }, 1500);
10900
- }, () => {});
10927
+ }, () => {
10928
+ return;
10929
+ });
10901
10930
  }
10902
10931
  });
10903
10932
  let dragState = null;
@@ -11749,7 +11778,9 @@ ${frontmatter.yaml}
11749
11778
  setTimeout(() => {
11750
11779
  copyBtn.textContent = text2().history.copySql;
11751
11780
  }, 1500);
11752
- }, () => {});
11781
+ }, () => {
11782
+ return;
11783
+ });
11753
11784
  });
11754
11785
  const deleteBtn = document.createElement("button");
11755
11786
  deleteBtn.className = "db-btn db-query-history-danger";
@@ -12599,7 +12630,9 @@ ${frontmatter.yaml}
12599
12630
  keyList.innerHTML = "";
12600
12631
  keyRowsByName.clear();
12601
12632
  setPaneEmpty(mainPane, text2().redis.selectKey);
12602
- const valuePromise = initial.key ? selectKey(initial.key).catch(() => {}) : null;
12633
+ const valuePromise = initial.key ? selectKey(initial.key).catch(() => {
12634
+ return;
12635
+ }) : null;
12603
12636
  await loadKeys(false);
12604
12637
  if (currentDbId !== dbId)
12605
12638
  return;
@@ -14405,7 +14438,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14405
14438
  setTimeout(() => {
14406
14439
  copyBtn.textContent = text2().sessionLog.copy;
14407
14440
  }, 1200);
14408
- }, () => {});
14441
+ }, () => {
14442
+ return;
14443
+ });
14409
14444
  });
14410
14445
  actions.append(useBtn, copyBtn);
14411
14446
  detailCol.appendChild(actions);
@@ -18551,7 +18586,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18551
18586
  if (!res.ok)
18552
18587
  return;
18553
18588
  applyDbUiState(await res.json());
18554
- }).catch(() => {});
18589
+ }).catch(() => {
18590
+ return;
18591
+ });
18555
18592
  return dbUiLoadPromise;
18556
18593
  }
18557
18594
  function getColumnWidths(dbId, table2) {
@@ -18569,7 +18606,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18569
18606
  method: "PATCH",
18570
18607
  headers: actionHeaders(),
18571
18608
  body: JSON.stringify({ columnWidths: { [dbId]: { [table2]: widths } } })
18572
- }).catch(() => {});
18609
+ }).catch(() => {
18610
+ return;
18611
+ });
18573
18612
  }
18574
18613
  function getExpandedTables(scopeKey) {
18575
18614
  return [...dbUiState.expandedTables?.[scopeKey] || []];
@@ -18601,7 +18640,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18601
18640
  [scopeKey]: nextTables.length > 0 ? nextTables : null
18602
18641
  }
18603
18642
  })
18604
- }).catch(() => {});
18643
+ }).catch(() => {
18644
+ return;
18645
+ });
18605
18646
  }
18606
18647
  const dbUiPrefListeners = new Set;
18607
18648
  function applyDbUiState(next) {
@@ -18620,7 +18661,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18620
18661
  method: "PATCH",
18621
18662
  headers: actionHeaders(),
18622
18663
  body: JSON.stringify({ prefs: { [key]: value } })
18623
- }).catch(() => {});
18664
+ }).catch(() => {
18665
+ return;
18666
+ });
18624
18667
  }
18625
18668
  function onDbUiPrefChange(listener) {
18626
18669
  dbUiPrefListeners.add(listener);
@@ -18733,7 +18776,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18733
18776
  }
18734
18777
  return;
18735
18778
  }
18736
- saveChain = saveChain.catch(() => {}).then(async () => {
18779
+ saveChain = saveChain.catch(() => {
18780
+ return;
18781
+ }).then(async () => {
18737
18782
  const controller = new AbortController;
18738
18783
  saveController = controller;
18739
18784
  try {
@@ -18808,7 +18853,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18808
18853
  syncActiveRoute();
18809
18854
  scheduleSave();
18810
18855
  if (mounted && !isRestoring()) {
18811
- ensureInitialEnter(id)?.catch(() => {});
18856
+ ensureInitialEnter(id)?.catch(() => {
18857
+ return;
18858
+ });
18812
18859
  }
18813
18860
  }
18814
18861
  function syncActiveRoute() {
@@ -18871,7 +18918,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18871
18918
  "Content-Type": "application/json",
18872
18919
  "X-Code-Viewer-Action": "1"
18873
18920
  };
18874
- fetch("/_db/close", { method: "POST", headers, body }).catch(() => {});
18921
+ fetch("/_db/close", { method: "POST", headers, body }).catch(() => {
18922
+ return;
18923
+ });
18875
18924
  }
18876
18925
  function clearDropTarget() {
18877
18926
  if (!dropTargetId)
@@ -19381,7 +19430,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19381
19430
  }
19382
19431
  async function enter(db, schema, table2, view, options = {}) {
19383
19432
  const seq = lifecycleSeq;
19384
- enterQueue = enterQueue.catch(() => {}).then(() => doEnter(seq, db, schema, table2, view, options));
19433
+ enterQueue = enterQueue.catch(() => {
19434
+ return;
19435
+ }).then(() => doEnter(seq, db, schema, table2, view, options));
19385
19436
  await enterQueue;
19386
19437
  }
19387
19438
  function leave() {
@@ -20855,6 +20906,196 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20855
20906
  };
20856
20907
  }
20857
20908
 
20909
+ // web-src/views/doctor-view.ts
20910
+ var TEXT = {
20911
+ en: {
20912
+ title: "Environment doctor",
20913
+ refresh: "Re-run checks",
20914
+ refreshing: "Running checks…",
20915
+ loadFailed: "Failed to load doctor report",
20916
+ worstOk: "All checks passed.",
20917
+ worstWarn: "Some checks reported warnings.",
20918
+ worstError: "Some checks failed. See the items marked ERROR below.",
20919
+ empty: "No checks ran.",
20920
+ close: "Close",
20921
+ statusLabels: { ok: "OK", warn: "WARN", error: "ERROR" }
20922
+ },
20923
+ ja: {
20924
+ title: "環境ドクター",
20925
+ refresh: "再診断",
20926
+ refreshing: "診断中…",
20927
+ loadFailed: "doctor の結果を取得できませんでした",
20928
+ worstOk: "すべての診断項目が OK です。",
20929
+ worstWarn: "一部の項目に注意があります (WARN)。",
20930
+ worstError: "失敗している項目があります。下の ERROR を確認してください。",
20931
+ empty: "診断項目がありません。",
20932
+ close: "閉じる",
20933
+ statusLabels: { ok: "OK", warn: "警告", error: "エラー" }
20934
+ }
20935
+ };
20936
+ var viewGeneration = 0;
20937
+ function doctorText(lang) {
20938
+ return TEXT[lang];
20939
+ }
20940
+ function statusLabel(text2, status) {
20941
+ return text2.statusLabels[status];
20942
+ }
20943
+ function renderRow(text2, esc, row) {
20944
+ const pill = `<span class="doctor-row-pill doctor-row-pill-${row.status}">` + `${esc(statusLabel(text2, row.status))}</span>`;
20945
+ const body = `<div class="doctor-row-body">` + `<div class="doctor-row-title">${esc(row.title)}</div>` + (row.detail ? `<div class="doctor-row-detail">${esc(row.detail)}</div>` : "") + (row.hint ? `<div class="doctor-row-hint">${esc(row.hint)}</div>` : "") + `</div>`;
20946
+ return `<div class="doctor-row" data-status="${esc(row.status)}">${pill}${body}</div>`;
20947
+ }
20948
+ function renderGroup(text2, esc, group) {
20949
+ if (group.rows.length === 0)
20950
+ return "";
20951
+ const rows = group.rows.map((row) => renderRow(text2, esc, row)).join("");
20952
+ return `<section class="doctor-group" data-group="${esc(group.id)}">` + `<h2 class="doctor-group-title">${esc(group.title)}</h2>` + rows + `</section>`;
20953
+ }
20954
+ function renderReport(mount, report, text2, esc) {
20955
+ const summary = mount.querySelector(".doctor-summary");
20956
+ if (summary) {
20957
+ const message = report.worstStatus === "error" ? text2.worstError : report.worstStatus === "warn" ? text2.worstWarn : text2.worstOk;
20958
+ summary.textContent = message;
20959
+ }
20960
+ const content = mount.querySelector(".doctor-content");
20961
+ if (!content)
20962
+ return;
20963
+ if (!report.groups.length) {
20964
+ content.innerHTML = `<div class="doctor-empty">${esc(text2.empty)}</div>`;
20965
+ return;
20966
+ }
20967
+ content.innerHTML = report.groups.map((group) => renderGroup(text2, esc, group)).join("");
20968
+ }
20969
+ function ensureSkeleton(mount, text2, esc) {
20970
+ if (mount.querySelector(".doctor-header"))
20971
+ return;
20972
+ mount.innerHTML = `<div class="doctor-header">` + `<h1>${esc(text2.title)}</h1>` + `<button type="button" class="doctor-refresh">${esc(text2.refresh)}</button>` + `<button type="button" class="doctor-close" aria-label="${esc(text2.close)}" title="${esc(text2.close)}">×</button>` + `</div>` + `<div class="doctor-summary"></div>` + `<div class="doctor-content"></div>`;
20973
+ }
20974
+ function createDoctorView(deps) {
20975
+ function getMount() {
20976
+ return deps.$("#doctor-sheet");
20977
+ }
20978
+ function getOverlay() {
20979
+ return deps.$("#doctor-sheet-overlay");
20980
+ }
20981
+ function setRefreshState(mount, busy) {
20982
+ const btn = mount.querySelector(".doctor-refresh");
20983
+ if (!btn)
20984
+ return;
20985
+ btn.disabled = busy;
20986
+ }
20987
+ async function load(mount) {
20988
+ viewGeneration += 1;
20989
+ const myGen = viewGeneration;
20990
+ const lang = deps.getLanguage();
20991
+ const text2 = doctorText(lang);
20992
+ setRefreshState(mount, true);
20993
+ const content = mount.querySelector(".doctor-content");
20994
+ if (content)
20995
+ content.innerHTML = `<div class="doctor-empty">${deps.escapeHtml(text2.refreshing)}</div>`;
20996
+ const summary = mount.querySelector(".doctor-summary");
20997
+ if (summary)
20998
+ summary.textContent = "";
20999
+ try {
21000
+ const res = await deps.trackLoad(fetch("/_doctor"));
21001
+ if (myGen !== viewGeneration)
21002
+ return;
21003
+ if (!res.ok) {
21004
+ throw new Error(`HTTP ${res.status}`);
21005
+ }
21006
+ const data = await res.json();
21007
+ if (myGen !== viewGeneration)
21008
+ return;
21009
+ renderReport(mount, data, text2, deps.escapeHtml);
21010
+ deps.onWorstStatusChange?.(data.worstStatus);
21011
+ } catch (err) {
21012
+ if (myGen !== viewGeneration)
21013
+ return;
21014
+ if (content) {
21015
+ const message = err instanceof Error ? err.message : String(err);
21016
+ content.innerHTML = `<div class="doctor-empty">${deps.escapeHtml(`${text2.loadFailed}: ${message}`)}</div>`;
21017
+ }
21018
+ deps.onWorstStatusChange?.(null);
21019
+ } finally {
21020
+ if (myGen === viewGeneration)
21021
+ setRefreshState(mount, false);
21022
+ }
21023
+ }
21024
+ function applyLocalizedLabels(mount, text2) {
21025
+ const title = mount.querySelector(".doctor-header h1");
21026
+ if (title)
21027
+ title.textContent = text2.title;
21028
+ const refreshBtn = mount.querySelector(".doctor-refresh");
21029
+ if (refreshBtn)
21030
+ refreshBtn.textContent = text2.refresh;
21031
+ const closeBtn = mount.querySelector(".doctor-close");
21032
+ if (closeBtn) {
21033
+ closeBtn.setAttribute("aria-label", text2.close);
21034
+ closeBtn.setAttribute("title", text2.close);
21035
+ }
21036
+ }
21037
+ function bindOnce(mount) {
21038
+ if (mount.dataset.bound)
21039
+ return;
21040
+ mount.dataset.bound = "1";
21041
+ const text2 = doctorText(deps.getLanguage());
21042
+ ensureSkeleton(mount, text2, deps.escapeHtml);
21043
+ const refreshBtn = mount.querySelector(".doctor-refresh");
21044
+ refreshBtn?.addEventListener("click", (event) => {
21045
+ event.preventDefault();
21046
+ load(mount);
21047
+ });
21048
+ const closeBtn = mount.querySelector(".doctor-close");
21049
+ closeBtn?.addEventListener("click", (event) => {
21050
+ event.preventDefault();
21051
+ deps.onCloseRequest?.();
21052
+ });
21053
+ }
21054
+ function isOpen() {
21055
+ const mount = getMount();
21056
+ return mount ? !mount.hidden : false;
21057
+ }
21058
+ async function open() {
21059
+ const mount = getMount();
21060
+ if (!mount)
21061
+ return;
21062
+ bindOnce(mount);
21063
+ applyLocalizedLabels(mount, doctorText(deps.getLanguage()));
21064
+ mount.hidden = false;
21065
+ mount.setAttribute("aria-hidden", "false");
21066
+ mount.removeAttribute("inert");
21067
+ const overlay = getOverlay();
21068
+ if (overlay) {
21069
+ overlay.hidden = false;
21070
+ overlay.setAttribute("aria-hidden", "false");
21071
+ }
21072
+ document.body.classList.add("doctor-sheet-open");
21073
+ await load(mount);
21074
+ }
21075
+ function close() {
21076
+ const mount = getMount();
21077
+ if (!mount)
21078
+ return;
21079
+ mount.hidden = true;
21080
+ mount.setAttribute("aria-hidden", "true");
21081
+ mount.setAttribute("inert", "");
21082
+ const overlay = getOverlay();
21083
+ if (overlay) {
21084
+ overlay.hidden = true;
21085
+ overlay.setAttribute("aria-hidden", "true");
21086
+ }
21087
+ document.body.classList.remove("doctor-sheet-open");
21088
+ viewGeneration += 1;
21089
+ }
21090
+ async function refresh() {
21091
+ const mount = getMount();
21092
+ if (!mount)
21093
+ return;
21094
+ await load(mount);
21095
+ }
21096
+ return { open, close, isOpen, refresh };
21097
+ }
21098
+
20858
21099
  // web-src/views/empty-diff-pane.ts
20859
21100
  function showEmptyHistoryDiffPane(deps) {
20860
21101
  if (deps.diff)
@@ -21501,7 +21742,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21501
21742
  const force = options.force === true;
21502
21743
  const mount = options.mount || (force ? activeMount : defaultMount);
21503
21744
  activateMount(mount);
21504
- entering = entering.then(() => doEnterHistory(force)).catch(() => {});
21745
+ entering = entering.then(() => doEnterHistory(force)).catch(() => {
21746
+ return;
21747
+ });
21505
21748
  return entering;
21506
21749
  }
21507
21750
  async function doEnterHistory(force = false) {
@@ -21835,6 +22078,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21835
22078
  text: "In large repositories the sidebar loads folder children on demand. Folders you open are remembered and automatically re-expanded after a reload."
21836
22079
  }
21837
22080
  ]
22081
+ },
22082
+ {
22083
+ title: "Environment doctor",
22084
+ blocks: [
22085
+ {
22086
+ kind: "paragraph",
22087
+ text: "Toggle the \uD83E\uDE7A icon in the header to slide in a diagnostic sheet from the right. It works on top of any screen (Repository, Diff, History, Datastores) and the open state is preserved in the URL as ?doctor=open so links are reproducible."
22088
+ },
22089
+ {
22090
+ kind: "paragraph",
22091
+ 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)."
22092
+ }
22093
+ ]
21838
22094
  }
21839
22095
  ]
21840
22096
  },
@@ -22398,6 +22654,19 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
22398
22654
  text: "大きいリポジトリではサイドバーがフォルダの中身を必要に応じて読み込みます。開いたフォルダは記憶され、次回のリロード時に同じ状態で展開し直されます。"
22399
22655
  }
22400
22656
  ]
22657
+ },
22658
+ {
22659
+ title: "環境ドクター",
22660
+ blocks: [
22661
+ {
22662
+ kind: "paragraph",
22663
+ text: "ヘッダ右の \uD83E\uDE7A アイコンで、右からスライドする診断シートを開きます。Repository / Diff / History / Datastores などどの画面の上にも重ねて表示でき、開閉状態は URL の ?doctor=open に同期されるのでリンク共有で復元できます。"
22664
+ },
22665
+ {
22666
+ kind: "paragraph",
22667
+ 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 を再実行する手順を表示します。"
22668
+ }
22669
+ ]
22401
22670
  }
22402
22671
  ]
22403
22672
  },
@@ -23386,7 +23655,9 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23386
23655
  if (!GdpExpandLogic.shouldAttachTrailingExpand(data?.lines?.length || 0))
23387
23656
  return;
23388
23657
  attachTrailingExpandControls(item, file, ref, refPath);
23389
- }).catch(() => {});
23658
+ }).catch(() => {
23659
+ return;
23660
+ });
23390
23661
  }
23391
23662
  function insertContextRows(targetTr, lines, newStart, oldStart, dir, sideIndex) {
23392
23663
  const tbody = targetTr.parentElement;
@@ -23525,7 +23796,9 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23525
23796
  function fetchRefs() {
23526
23797
  return fetch("/_refs").then((r2) => r2.json()).then((refs) => {
23527
23798
  Object.assign(REFS, refs);
23528
- }).catch(() => {});
23799
+ }).catch(() => {
23800
+ return;
23801
+ });
23529
23802
  }
23530
23803
  fetchRefs();
23531
23804
  let popTab = "commits";
@@ -28581,7 +28854,9 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
28581
28854
  headers: actionHeaders(),
28582
28855
  body,
28583
28856
  keepalive: options.keepalive
28584
- }).catch(() => {});
28857
+ }).catch(() => {
28858
+ return;
28859
+ });
28585
28860
  }
28586
28861
  let pendingViewPatch = null;
28587
28862
  let pendingViewTimer = null;
@@ -28654,7 +28929,9 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
28654
28929
  headers: actionHeaders(),
28655
28930
  body,
28656
28931
  keepalive
28657
- }).catch(() => {});
28932
+ }).catch(() => {
28933
+ return;
28934
+ });
28658
28935
  };
28659
28936
  if (options.keepalive) {
28660
28937
  if (pendingViewTimer !== null)
@@ -28690,7 +28967,9 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
28690
28967
  headers: actionHeaders(),
28691
28968
  body,
28692
28969
  keepalive
28693
- }).catch(() => {});
28970
+ }).catch(() => {
28971
+ return;
28972
+ });
28694
28973
  }
28695
28974
  function savedScopeOmitDirs() {
28696
28975
  return APP_SETTINGS.scopeOmitDirs ? normalizeScopeOmitDirs(APP_SETTINGS.scopeOmitDirs) : null;
@@ -29886,7 +30165,8 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
29886
30165
  } : { view: route.screen };
29887
30166
  }
29888
30167
  function replaceUrlWithCurrentRoute() {
29889
- const url = withAnnotationSessionParam(buildRoute(STATE.route));
30168
+ const base2 = withAnnotationSessionParam(buildRoute(STATE.route));
30169
+ const url = withDoctorOverlay(base2, parseDoctorOverlay(window.location.pathname, window.location.search));
29890
30170
  const current = window.location.pathname + window.location.search;
29891
30171
  if (url !== current) {
29892
30172
  history.replaceState(historyStateForRoute(STATE.route), "", url + window.location.hash);
@@ -29950,7 +30230,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
29950
30230
  if (nextRoute.screen === "repo" || nextRoute.screen === "file" && (nextRoute.view === "blob" || nextRoute.view === "blame" || nextRoute.view === "history")) {
29951
30231
  STATE.repoRef = nextRoute.ref || "worktree";
29952
30232
  }
29953
- const url = withAnnotationSessionParam(buildRoute(nextRoute));
30233
+ const url = withDoctorOverlay(withAnnotationSessionParam(buildRoute(nextRoute)), parseDoctorOverlay(window.location.pathname, window.location.search));
29954
30234
  const state = historyStateForRoute(nextRoute);
29955
30235
  if (replace2)
29956
30236
  history.replaceState(state, "", url);
@@ -30225,6 +30505,10 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30225
30505
  $("#sb-expand-all").addEventListener("click", () => setAllSidebarDirsCollapsed(false));
30226
30506
  $("#sb-collapse-all").addEventListener("click", () => setAllSidebarDirsCollapsed(true));
30227
30507
  $("#viewer-settings")?.addEventListener("click", toggleScopeSettings);
30508
+ $("#doctor-btn")?.addEventListener("click", (event) => {
30509
+ event.preventDefault();
30510
+ toggleDoctorSheet();
30511
+ });
30228
30512
  $("#scope-settings-close")?.addEventListener("click", closeScopeSettings);
30229
30513
  $("#scope-omit-reset")?.addEventListener("click", resetScopeSettings);
30230
30514
  $("#viewer-language")?.addEventListener("change", (event) => {
@@ -30704,6 +30988,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30704
30988
  } else
30705
30989
  load();
30706
30990
  syncLineRefPill();
30991
+ syncDoctorSheetFromUrl();
30707
30992
  });
30708
30993
  function syncRefInputs() {
30709
30994
  const fi = $("#ref-from"), ti = $("#ref-to");
@@ -30750,7 +31035,9 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30750
31035
  STATE.to = range.to;
30751
31036
  activeHistoryPathFilter = pathFilter || null;
30752
31037
  syncRefInputs();
30753
- return load().then(() => {});
31038
+ return load().then(() => {
31039
+ return;
31040
+ });
30754
31041
  },
30755
31042
  showEmptyDiffPane: () => {
30756
31043
  if (activeFileHistoryDiffHost || activeFileHistoryEmptyHost) {
@@ -30792,6 +31079,69 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30792
31079
  getLanguage: () => STATE.language,
30793
31080
  trackLoad
30794
31081
  });
31082
+ const DOCTOR_VIEW = createDoctorView({
31083
+ $: (sel) => document.querySelector(sel),
31084
+ escapeHtml: escapeHtml3,
31085
+ trackLoad,
31086
+ getLanguage: () => STATE.language,
31087
+ onWorstStatusChange: (status) => {
31088
+ const badge = document.getElementById("doctor-badge");
31089
+ if (!badge)
31090
+ return;
31091
+ if (status === "error") {
31092
+ badge.hidden = false;
31093
+ badge.dataset.level = "error";
31094
+ } else if (status === "warn") {
31095
+ badge.hidden = false;
31096
+ badge.dataset.level = "warn";
31097
+ } else {
31098
+ badge.hidden = true;
31099
+ badge.removeAttribute("data-level");
31100
+ }
31101
+ },
31102
+ onCloseRequest: () => closeDoctorSheet()
31103
+ });
31104
+ function isDoctorOverlayOpen() {
31105
+ return parseDoctorOverlay(window.location.pathname, window.location.search);
31106
+ }
31107
+ function updateUrlForDoctorOverlay(open) {
31108
+ const current = window.location.pathname + window.location.search;
31109
+ const next = withDoctorOverlay(current, open);
31110
+ if (next !== current) {
31111
+ history.replaceState(history.state, "", next + window.location.hash);
31112
+ }
31113
+ }
31114
+ function openDoctorSheet() {
31115
+ updateUrlForDoctorOverlay(true);
31116
+ DOCTOR_VIEW.open();
31117
+ }
31118
+ function closeDoctorSheet() {
31119
+ DOCTOR_VIEW.close();
31120
+ updateUrlForDoctorOverlay(false);
31121
+ }
31122
+ function toggleDoctorSheet() {
31123
+ if (isDoctorOverlayOpen() || DOCTOR_VIEW.isOpen())
31124
+ closeDoctorSheet();
31125
+ else
31126
+ openDoctorSheet();
31127
+ }
31128
+ function syncDoctorSheetFromUrl() {
31129
+ const shouldOpen = isDoctorOverlayOpen();
31130
+ const open = DOCTOR_VIEW.isOpen();
31131
+ if (shouldOpen && !open)
31132
+ DOCTOR_VIEW.open();
31133
+ else if (!shouldOpen && open)
31134
+ DOCTOR_VIEW.close();
31135
+ }
31136
+ document.getElementById("doctor-sheet-overlay")?.addEventListener("click", () => closeDoctorSheet());
31137
+ document.addEventListener("keydown", (event) => {
31138
+ if (event.key !== "Escape")
31139
+ return;
31140
+ if (!DOCTOR_VIEW.isOpen())
31141
+ return;
31142
+ event.preventDefault();
31143
+ closeDoctorSheet();
31144
+ });
30795
31145
  const DATABASE_VIEW = createDatabaseView({
30796
31146
  setRoute,
30797
31147
  setPageMode,
@@ -30852,6 +31202,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30852
31202
  syncRefInputs();
30853
31203
  syncHeaderMenu();
30854
31204
  syncLineRefPill();
31205
+ syncDoctorSheetFromUrl();
30855
31206
  if (isSameBlobFileRoute(previousRoute, STATE.route) && routeBlobPreview(previousRoute) !== routeBlobPreview(STATE.route) && switchSourceTab(routeBlobPreview(STATE.route) ? "preview" : "code", {
30856
31207
  updateRoute: false
30857
31208
  })) {
package/web/index.html CHANGED
@@ -33,6 +33,7 @@
33
33
  <div class="global-actions">
34
34
  <button id="annotations-toggle" class="global-icon-action" title="code annotations" aria-label="code annotations">💬<span id="annotations-count" hidden></span></button>
35
35
  <button id="viewer-settings" class="global-icon-action" title="viewer settings" aria-label="viewer settings"></button>
36
+ <button id="doctor-btn" class="global-icon-action" title="environment doctor" aria-label="environment doctor">🩺<span id="doctor-badge" class="doctor-badge" hidden></span></button>
36
37
  <button id="auto-update" class="global-icon-action active" title="auto update on file change">auto</button>
37
38
  <button id="cancel-requests" class="global-icon-action" title="no in-flight requests" aria-label="cancel in-flight requests" disabled>stop</button>
38
39
  <button id="theme" title="toggle theme">🌗</button>
@@ -251,6 +252,8 @@
251
252
  <div id="query-history-content"></div>
252
253
  </aside>
253
254
 
255
+ <div id="doctor-sheet-overlay" class="doctor-sheet-overlay" hidden aria-hidden="true"></div>
256
+ <aside id="doctor-sheet" class="doctor-sheet" hidden aria-hidden="true" inert aria-label="environment doctor"></aside>
254
257
  <main id="content">
255
258
  <section id="history-commit-info" hidden></section>
256
259
  <div id="empty" class="empty hidden">