@youtyan/code-viewer 0.1.27 → 0.1.29

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.
@@ -5,7 +5,7 @@ import {
5
5
  closeSync,
6
6
  constants,
7
7
  existsSync as existsSync3,
8
- lstatSync as lstatSync3,
8
+ lstatSync as lstatSync4,
9
9
  mkdirSync,
10
10
  openSync,
11
11
  readFileSync as readFileSync2,
@@ -17,7 +17,7 @@ import {
17
17
  writeFileSync
18
18
  } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { basename as basename2, dirname as dirname2, extname, join as join4, relative } from "node:path";
20
+ import { basename as basename2, dirname as dirname2, extname, join as join5, relative as relative2 } from "node:path";
21
21
 
22
22
  // web-src/directory-name.ts
23
23
  function normalizeNewDirectoryName(name) {
@@ -280,6 +280,8 @@ var DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
280
280
  "out",
281
281
  "target",
282
282
  ".gradle",
283
+ ".pnpm-store",
284
+ ".turbo",
283
285
  "__pycache__",
284
286
  ".pytest_cache",
285
287
  ".tox",
@@ -1240,9 +1242,181 @@ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames =
1240
1242
  return parseRgOutput(normalized, max, omitDirNames, excludeNames);
1241
1243
  }
1242
1244
 
1245
+ // web-src/server/worktree-watcher.ts
1246
+ import {
1247
+ lstatSync as lstatSync3,
1248
+ readdirSync as nodeReaddirSync,
1249
+ watch as nodeWatch
1250
+ } from "node:fs";
1251
+ import { join as join4, relative } from "node:path";
1252
+ function normalizeRelativePath(path) {
1253
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
1254
+ }
1255
+ function isInsideRoot(root, path) {
1256
+ const rel = relative(root, path).replace(/\\/g, "/");
1257
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
1258
+ }
1259
+ function startWorktreeUpdateWatch(options) {
1260
+ const watch = options.watch || nodeWatch;
1261
+ const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
1262
+ const isDirectory = options.isDirectory || ((path) => {
1263
+ try {
1264
+ return lstatSync3(path).isDirectory();
1265
+ } catch {
1266
+ return false;
1267
+ }
1268
+ });
1269
+ const directorySignature = options.directorySignature || ((path) => {
1270
+ try {
1271
+ const stats = lstatSync3(path);
1272
+ if (!stats.isDirectory())
1273
+ return null;
1274
+ return `${stats.dev}:${stats.ino}`;
1275
+ } catch {
1276
+ return null;
1277
+ }
1278
+ });
1279
+ const setTimer = options.setTimeoutFn || setTimeout;
1280
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
1281
+ const debounceMs = options.debounceMs ?? 250;
1282
+ const watchers = new Map;
1283
+ const signatures = new Map;
1284
+ const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
1285
+ const initialScanQueue = [];
1286
+ let initialScanTimer = null;
1287
+ let timer = null;
1288
+ const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
1289
+ const scheduleUpdate = () => {
1290
+ if (timer)
1291
+ clearTimer(timer);
1292
+ timer = setTimer(() => {
1293
+ timer = null;
1294
+ options.onUpdate();
1295
+ }, debounceMs);
1296
+ };
1297
+ const closeSubtree = (dir) => {
1298
+ for (const [watchedDir, watcher] of [...watchers]) {
1299
+ if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
1300
+ continue;
1301
+ try {
1302
+ watcher.close?.();
1303
+ } catch {}
1304
+ watchers.delete(watchedDir);
1305
+ signatures.delete(watchedDir);
1306
+ }
1307
+ };
1308
+ const closeAll = () => {
1309
+ if (initialScanTimer) {
1310
+ clearTimer(initialScanTimer);
1311
+ initialScanTimer = null;
1312
+ }
1313
+ initialScanQueue.length = 0;
1314
+ for (const watcher of [...watchers.values()]) {
1315
+ try {
1316
+ watcher.close?.();
1317
+ } catch {}
1318
+ }
1319
+ watchers.clear();
1320
+ signatures.clear();
1321
+ };
1322
+ const readChildDirectories = (dir) => {
1323
+ let entries;
1324
+ try {
1325
+ entries = readDirs(dir);
1326
+ } catch (error) {
1327
+ options.onError?.(error);
1328
+ return [];
1329
+ }
1330
+ const children = [];
1331
+ for (const entry of entries) {
1332
+ if (!entry.isDirectory())
1333
+ continue;
1334
+ children.push(join4(dir, entry.name));
1335
+ }
1336
+ return children;
1337
+ };
1338
+ const processInitialScanQueue = () => {
1339
+ initialScanTimer = null;
1340
+ const next = initialScanQueue.shift();
1341
+ if (next)
1342
+ watchDirectory(next, true);
1343
+ if (initialScanQueue.length)
1344
+ initialScanTimer = setTimer(processInitialScanQueue, 50);
1345
+ };
1346
+ const queueInitialChildren = (dir) => {
1347
+ initialScanQueue.push(...readChildDirectories(dir));
1348
+ if (!initialScanTimer)
1349
+ initialScanTimer = setTimer(processInitialScanQueue, 5000);
1350
+ };
1351
+ const watchDirectory = (dir, initialScan = false) => {
1352
+ if (watchers.has(dir))
1353
+ return;
1354
+ const rel = normalizeRelativePath(relative(options.root, dir));
1355
+ if (rel && ignored(rel))
1356
+ return;
1357
+ try {
1358
+ const watcher = watch(dir, { persistent: false }, (_event, filename) => {
1359
+ if (!filename) {
1360
+ scheduleUpdate();
1361
+ return;
1362
+ }
1363
+ const changed = normalizeRelativePath(join4(rel, filename.toString()));
1364
+ if (ignored(changed))
1365
+ return;
1366
+ const fullChangedPath = join4(options.root, changed);
1367
+ if (!isInsideRoot(options.root, fullChangedPath))
1368
+ return;
1369
+ const known = watchers.has(fullChangedPath);
1370
+ if (isDirectory(fullChangedPath)) {
1371
+ if (known) {
1372
+ const signature2 = directorySignature(fullChangedPath);
1373
+ if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
1374
+ closeSubtree(fullChangedPath);
1375
+ watchDirectory(fullChangedPath);
1376
+ }
1377
+ scheduleUpdate();
1378
+ return;
1379
+ }
1380
+ watchDirectory(fullChangedPath);
1381
+ } else if (known) {
1382
+ closeSubtree(fullChangedPath);
1383
+ }
1384
+ scheduleUpdate();
1385
+ }) || {};
1386
+ watchers.set(dir, watcher);
1387
+ const signature = directorySignature(dir);
1388
+ if (signature)
1389
+ signatures.set(dir, signature);
1390
+ watcher.on?.("error", () => {
1391
+ if (watchers.get(dir) === watcher) {
1392
+ watchers.delete(dir);
1393
+ signatures.delete(dir);
1394
+ }
1395
+ });
1396
+ watcher.on?.("close", () => {
1397
+ if (watchers.get(dir) === watcher) {
1398
+ watchers.delete(dir);
1399
+ signatures.delete(dir);
1400
+ }
1401
+ });
1402
+ } catch (error) {
1403
+ options.onError?.(error);
1404
+ return;
1405
+ }
1406
+ if (initialScanAsync && initialScan) {
1407
+ queueInitialChildren(dir);
1408
+ return;
1409
+ }
1410
+ for (const child of readChildDirectories(dir))
1411
+ watchDirectory(child);
1412
+ };
1413
+ watchDirectory(options.root, true);
1414
+ return { started: watchers.size > 0, close: closeAll };
1415
+ }
1416
+
1243
1417
  // web-src/server/preview.ts
1244
- var WEB_ROOT = join4(ROOT, "web");
1245
- var VERSION = JSON.parse(readFileSync2(join4(ROOT, "package.json"), "utf8")).version;
1418
+ var WEB_ROOT = join5(ROOT, "web");
1419
+ var VERSION = JSON.parse(readFileSync2(join5(ROOT, "package.json"), "utf8")).version;
1246
1420
  var DEFAULT_ARGS = ["HEAD"];
1247
1421
  var PREVIEW_HUNKS_DEFAULT = 3;
1248
1422
  var PREVIEW_LINES_DEFAULT = 1200;
@@ -1448,7 +1622,7 @@ function staticFile(pathname) {
1448
1622
  const spec = map[pathname];
1449
1623
  if (!spec)
1450
1624
  return null;
1451
- const full = join4(WEB_ROOT, spec[0]);
1625
+ const full = join5(WEB_ROOT, spec[0]);
1452
1626
  if (!existsSync3(full))
1453
1627
  return text("not found", 404);
1454
1628
  return new Response(readFileSync2(full), {
@@ -1682,7 +1856,7 @@ function parseScopeExcludeNamesQuery(value) {
1682
1856
  return normalizeScopeExcludeNames(names);
1683
1857
  }
1684
1858
  function loadProjectConfig() {
1685
- const full = join4(cwd, ".code-viewer.json");
1859
+ const full = join5(cwd, ".code-viewer.json");
1686
1860
  if (!existsSync3(full))
1687
1861
  return null;
1688
1862
  let realCwd;
@@ -1747,7 +1921,7 @@ function safeWorktreePath(path) {
1747
1921
  return null;
1748
1922
  if (isGitInternalPath(path))
1749
1923
  return null;
1750
- const full = join4(cwd, path);
1924
+ const full = join5(cwd, path);
1751
1925
  if (!existsSync3(full))
1752
1926
  return null;
1753
1927
  let realCwd;
@@ -1758,7 +1932,7 @@ function safeWorktreePath(path) {
1758
1932
  } catch {
1759
1933
  return null;
1760
1934
  }
1761
- const rel = relative(realCwd, realFull);
1935
+ const rel = relative2(realCwd, realFull);
1762
1936
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
1763
1937
  return null;
1764
1938
  if (isGitInternalPath(rel))
@@ -1766,7 +1940,7 @@ function safeWorktreePath(path) {
1766
1940
  return realFull;
1767
1941
  }
1768
1942
  function worktreePath(path) {
1769
- return join4(cwd, path);
1943
+ return join5(cwd, path);
1770
1944
  }
1771
1945
  function safeOpenWorktreePath(path) {
1772
1946
  if (path === "") {
@@ -1963,7 +2137,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
1963
2137
  continue;
1964
2138
  let stat;
1965
2139
  try {
1966
- stat = lstatSync3(full);
2140
+ stat = lstatSync4(full);
1967
2141
  } catch {
1968
2142
  continue;
1969
2143
  }
@@ -2529,8 +2703,8 @@ async function handleUploadFiles(req) {
2529
2703
  total += file.size;
2530
2704
  if (total > MAX_UPLOAD_TOTAL_BYTES)
2531
2705
  return text("upload too large", 413);
2532
- const target = join4(realDir, safeName);
2533
- if (relative(realDir, dirname2(target)) !== "")
2706
+ const target = join5(realDir, safeName);
2707
+ if (relative2(realDir, dirname2(target)) !== "")
2534
2708
  return text("invalid filename", 400);
2535
2709
  if (existsSync3(target))
2536
2710
  return text("file exists", 409);
@@ -2557,10 +2731,7 @@ async function handleUploadFiles(req) {
2557
2731
  return text("file exists", 409);
2558
2732
  return text("upload failed", 500);
2559
2733
  }
2560
- generation++;
2561
- fileCache.clear();
2562
- metaCache.clear();
2563
- sendSse("update");
2734
+ triggerUpdate();
2564
2735
  return json({
2565
2736
  ok: true,
2566
2737
  files: uploads.map((upload) => upload.name),
@@ -2647,10 +2818,15 @@ function clearMutableCaches() {
2647
2818
  metaCache.clear();
2648
2819
  fileListCache.clear();
2649
2820
  }
2821
+ function triggerUpdate() {
2822
+ generation++;
2823
+ clearMutableCaches();
2824
+ sendSse("update");
2825
+ }
2650
2826
  function moveMacPathIntoTrash(path) {
2651
- const trashDir = join4(homedir(), ".Trash");
2827
+ const trashDir = join5(homedir(), ".Trash");
2652
2828
  const base = basename2(path) || "code-viewer-trash-item";
2653
- const target = join4(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
2829
+ const target = join5(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
2654
2830
  try {
2655
2831
  mkdirSync(trashDir, { recursive: true });
2656
2832
  renameSync(path, target);
@@ -2660,7 +2836,7 @@ function moveMacPathIntoTrash(path) {
2660
2836
  }
2661
2837
  }
2662
2838
  function movePathToTrash(path) {
2663
- lstatSync3(path);
2839
+ lstatSync4(path);
2664
2840
  if (process.platform === "darwin") {
2665
2841
  return moveMacPathIntoTrash(path);
2666
2842
  }
@@ -2692,8 +2868,8 @@ function restoreTrashPath(originalPath, trashPath) {
2692
2868
  if (!existsSync3(trashPath))
2693
2869
  return { ok: false, error: "trash item not found" };
2694
2870
  try {
2695
- const trashRoot = join4(homedir(), ".Trash");
2696
- const trashRelative = relative(trashRoot, trashPath);
2871
+ const trashRoot = join5(homedir(), ".Trash");
2872
+ const trashRelative = relative2(trashRoot, trashPath);
2697
2873
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
2698
2874
  return { ok: false, error: "invalid trash handle" };
2699
2875
  mkdirSync(dirname2(original), { recursive: true });
@@ -2799,9 +2975,7 @@ async function handleTrashPath(req) {
2799
2975
  trashPath: moved.trashPath
2800
2976
  }
2801
2977
  };
2802
- generation++;
2803
- clearMutableCaches();
2804
- sendSse("update");
2978
+ triggerUpdate();
2805
2979
  return json({ ok: true, generation, undo });
2806
2980
  }
2807
2981
  async function handleCreateDirectory(req) {
@@ -2844,7 +3018,7 @@ async function handleCreateDirectory(req) {
2844
3018
  const targetPath = dir ? `${dir}/${name}` : name;
2845
3019
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
2846
3020
  return text("invalid target", 400);
2847
- const target = join4(parent, name);
3021
+ const target = join5(parent, name);
2848
3022
  if (existsSync3(target))
2849
3023
  return text("already exists", 409);
2850
3024
  try {
@@ -2854,9 +3028,7 @@ async function handleCreateDirectory(req) {
2854
3028
  return text("already exists", 409);
2855
3029
  return text("create failed", 500);
2856
3030
  }
2857
- generation++;
2858
- clearMutableCaches();
2859
- sendSse("update");
3031
+ triggerUpdate();
2860
3032
  return json({ ok: true, path: targetPath, generation });
2861
3033
  }
2862
3034
  async function handleRestoreTrash(req) {
@@ -2888,9 +3060,7 @@ async function handleRestoreTrash(req) {
2888
3060
  const restored = restoreTrashPath(originalPath, trashPath || undefined);
2889
3061
  if (!restored.ok)
2890
3062
  return text(restored.error || "undo failed", 409);
2891
- generation++;
2892
- clearMutableCaches();
2893
- sendSse("update");
3063
+ triggerUpdate();
2894
3064
  return json({ ok: true, generation });
2895
3065
  }
2896
3066
  function sendSse(event, data = "tick") {
@@ -2954,9 +3124,7 @@ var server = await startServer({
2954
3124
  if (url.pathname === "/refresh" && req.method === "POST") {
2955
3125
  if (!sideEffectRequestAllowed(req))
2956
3126
  return text("forbidden", 403);
2957
- generation++;
2958
- clearMutableCaches();
2959
- sendSse("update");
3127
+ triggerUpdate();
2960
3128
  return json({ ok: true, generation });
2961
3129
  }
2962
3130
  if (url.pathname === "/events") {
@@ -3007,5 +3175,17 @@ startDevAssetReload({
3007
3175
  watch,
3008
3176
  sendReload: () => sendSse("reload")
3009
3177
  });
3178
+ startWorktreeUpdateWatch({
3179
+ root: cwd,
3180
+ omitDirNames: scopeOmitDirNames,
3181
+ excludeNames: scopeExcludeNames,
3182
+ watch,
3183
+ initialScanMode: "async",
3184
+ onUpdate: triggerUpdate,
3185
+ onError: (error) => {
3186
+ const message = error instanceof Error ? error.message : String(error);
3187
+ console.warn(`code-viewer worktree watch skipped: ${message}`);
3188
+ }
3189
+ });
3010
3190
  console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
3011
3191
  console.log(`git-diff-preview serving ${cwd}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -7002,7 +7002,9 @@ ${frontmatter.yaml}
7002
7002
  let SIDEBAR_VISIBLE_ROWS = [];
7003
7003
  let SIDEBAR_ROW_BY_PATH = new Map;
7004
7004
  let SIDEBAR_VIRTUAL_ACTIVE_PATH = "";
7005
- const SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
7005
+ let SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
7006
+ const SIDEBAR_LAZY_LOADED_DIRS = new Set;
7007
+ const SIDEBAR_LAZY_LOADING_DIRS = new Map;
7006
7008
  let REPO_SORT = {
7007
7009
  key: "name",
7008
7010
  direction: "asc"
@@ -8058,6 +8060,86 @@ ${frontmatter.yaml}
8058
8060
  SIDEBAR_TREE_ITEMS_CACHE.set(node, items);
8059
8061
  return items;
8060
8062
  }
8063
+ function sidebarTreeNodeHasChildren(node) {
8064
+ return Object.keys(node.dirs).length > 0 || node.files.length > 0;
8065
+ }
8066
+ function shouldLazyLoadSidebarDir(dir) {
8067
+ return isRepositorySidebarMode() && isVirtualSidebarActive() && !dir.children_omitted && !sidebarTreeNodeHasChildren(dir) && !SIDEBAR_LAZY_LOADED_DIRS.has(dir.path);
8068
+ }
8069
+ function upsertSidebarTreeEntry(entry, order) {
8070
+ if (!SIDEBAR_TREE_ROOT)
8071
+ return;
8072
+ const parts = entry.path.split("/").filter(Boolean);
8073
+ if (!parts.length)
8074
+ return;
8075
+ let node = SIDEBAR_TREE_ROOT;
8076
+ let acc = "";
8077
+ const dirPartCount = entry.type === "tree" ? parts.length : parts.length - 1;
8078
+ for (let i2 = 0;i2 < dirPartCount; i2++) {
8079
+ const part = parts[i2];
8080
+ acc = acc ? `${acc}/${part}` : part;
8081
+ if (!node.dirs[part]) {
8082
+ node.dirs[part] = {
8083
+ name: part,
8084
+ dirs: {},
8085
+ files: [],
8086
+ path: acc,
8087
+ minOrder: order
8088
+ };
8089
+ }
8090
+ node = node.dirs[part];
8091
+ node.minOrder = Math.min(node.minOrder, order);
8092
+ }
8093
+ if (entry.type === "tree") {
8094
+ node.explicit = true;
8095
+ if (entry.children_omitted === true) {
8096
+ node.children_omitted = true;
8097
+ node.children_omitted_reason = entry.children_omitted_reason;
8098
+ }
8099
+ return;
8100
+ }
8101
+ if (!node.files.some((file) => file.path === entry.path))
8102
+ node.files.push({ ...entry, order });
8103
+ }
8104
+ function mergeSidebarTreeEntries(entries) {
8105
+ entries.forEach((entry, index) => {
8106
+ upsertSidebarTreeEntry(entry, entry.order ?? index + 1);
8107
+ });
8108
+ SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
8109
+ if (SIDEBAR_TREE_ROOT)
8110
+ buildSidebarTreeRows(SIDEBAR_TREE_ROOT);
8111
+ }
8112
+ function ensureVirtualSidebarDirLoaded(dir) {
8113
+ if (!shouldLazyLoadSidebarDir(dir))
8114
+ return Promise.resolve();
8115
+ const existing = SIDEBAR_LAZY_LOADING_DIRS.get(dir.path);
8116
+ if (existing)
8117
+ return existing;
8118
+ const params = new URLSearchParams;
8119
+ params.set("ref", REPO_SIDEBAR_REF || "worktree");
8120
+ params.set("path", dir.path);
8121
+ appendScopeParams(params);
8122
+ const load2 = trackLoad(fetch(`/_tree?${params.toString()}`).then((response) => {
8123
+ if (!response.ok)
8124
+ throw new Error("failed to load repository tree");
8125
+ return response.json();
8126
+ })).then((meta) => {
8127
+ const entries = meta.entries.map((entry, index) => ({
8128
+ order: dir.minOrder + (index + 1) / 1e5,
8129
+ path: entry.path,
8130
+ display_path: entry.path,
8131
+ type: entry.type,
8132
+ children_omitted: entry.children_omitted,
8133
+ children_omitted_reason: entry.children_omitted_reason
8134
+ }));
8135
+ mergeSidebarTreeEntries(entries);
8136
+ SIDEBAR_LAZY_LOADED_DIRS.add(dir.path);
8137
+ }).finally(() => {
8138
+ SIDEBAR_LAZY_LOADING_DIRS.delete(dir.path);
8139
+ });
8140
+ SIDEBAR_LAZY_LOADING_DIRS.set(dir.path, load2);
8141
+ return load2;
8142
+ }
8061
8143
  function createTreeDirRow(dir, depth, onFileClick) {
8062
8144
  const li = document.createElement("li");
8063
8145
  li.className = "tree-dir";
@@ -8105,16 +8187,26 @@ ${frontmatter.yaml}
8105
8187
  const updateIcon = () => {
8106
8188
  setFolderIcon(dirIcon, li.classList.contains("collapsed"));
8107
8189
  };
8108
- const toggleDir = (e2) => {
8190
+ const toggleDir = async (e2) => {
8109
8191
  e2.stopPropagation();
8110
- li.classList.toggle("collapsed");
8111
- updateIcon();
8112
- if (li.classList.contains("collapsed"))
8113
- STATE.collapsedDirs.add(dir.path);
8114
- else
8115
- STATE.collapsedDirs.delete(dir.path);
8116
- localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
8117
- rerenderVirtualSidebar();
8192
+ if (li.dataset.toggling === "true")
8193
+ return;
8194
+ const expanding = li.classList.contains("collapsed");
8195
+ li.dataset.toggling = "true";
8196
+ try {
8197
+ if (expanding)
8198
+ await ensureVirtualSidebarDirLoaded(dir);
8199
+ li.classList.toggle("collapsed");
8200
+ updateIcon();
8201
+ if (li.classList.contains("collapsed"))
8202
+ STATE.collapsedDirs.add(dir.path);
8203
+ else
8204
+ STATE.collapsedDirs.delete(dir.path);
8205
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
8206
+ rerenderVirtualSidebar();
8207
+ } finally {
8208
+ delete li.dataset.toggling;
8209
+ }
8118
8210
  };
8119
8211
  li.classList.toggle("collapsed", STATE.collapsedDirs.has(dir.path));
8120
8212
  updateIcon();
@@ -8382,6 +8474,8 @@ ${frontmatter.yaml}
8382
8474
  SIDEBAR_TREE_ROWS = [];
8383
8475
  SIDEBAR_VISIBLE_ROWS = [];
8384
8476
  SIDEBAR_ROW_BY_PATH = new Map;
8477
+ SIDEBAR_LAZY_LOADED_DIRS.clear();
8478
+ SIDEBAR_LAZY_LOADING_DIRS.clear();
8385
8479
  STATE.files = files;
8386
8480
  SIDEBAR_FILES = files;
8387
8481
  SIDEBAR_ON_FILE_CLICK = onFileClick;
@@ -9851,6 +9945,14 @@ ${frontmatter.yaml}
9851
9945
  function activateRepoSidebarPath(currentPath) {
9852
9946
  markActive(currentPath, { reveal: true });
9853
9947
  applyFilter();
9948
+ const row = SIDEBAR_ROW_BY_PATH.get(currentPath);
9949
+ if (row?.kind === "dir" && row.dir && shouldLazyLoadSidebarDir(row.dir))
9950
+ ensureVirtualSidebarDirLoaded(row.dir).then(() => {
9951
+ if (SIDEBAR_VIRTUAL_ACTIVE_PATH === currentPath) {
9952
+ rerenderVirtualSidebar();
9953
+ scrollVirtualSidebarPathIntoView(currentPath);
9954
+ }
9955
+ });
9854
9956
  }
9855
9957
  function createPlaceholder(f2) {
9856
9958
  const card = document.createElement("div");