@youtyan/code-viewer 0.2.9 → 0.2.10

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/README.md CHANGED
@@ -115,7 +115,9 @@ rendered, and the page includes controls to copy the full file or reopen it in
115
115
  the full non-virtual view.
116
116
 
117
117
  The worktree is watched and changes are pushed to every open tab over SSE so
118
- files reload as you edit. When the OS file-watch limit is reached the viewer
118
+ files reload as you edit. The directory watcher is capped at 1024 directories
119
+ by default and can be tuned from Viewer Settings → **File change watcher**
120
+ (range slider + numeric input, 16–65536); when the cap is hit the viewer
119
121
  shows a banner so reloads are not silently missed.
120
122
 
121
123
  Large repositories load folder children on demand. The sidebar remembers which
@@ -129,13 +131,15 @@ views remain read-only. Open **Viewer Settings** in the header to toggle
129
131
  uploads off, edit the directories to skip while browsing/searching, and hide
130
132
  files or directory names completely.
131
133
 
132
- Scope settings control recursive repository browsing and search scope for the
133
- left tree, Ctrl+K file palette, and Ctrl+G grep palette. Everything you change
134
- in Viewer Settings is saved on the server under `.code-viewer/settings.json`
135
- (no separate project-level config file). `.DS_Store` and a small set of
134
+ Scope settings control directory exclusions shared by the sidebar, Ctrl+K file
135
+ palette, Ctrl+G grep palette, the Datastores browser, and the file change
136
+ watcher the same list applies to all five. Everything you change in Viewer
137
+ Settings is saved on the server under `.code-viewer/settings.json` (no
138
+ separate project-level config file). `.DS_Store` and a broad set of
136
139
  build/cache directories (`node_modules`, `dist`, `build`, `.next`, `.turbo`,
137
- `.venv`, …) are hidden by default. Pass `--scope-omit-dir <name>` (repeatable)
138
- to override the omit list on the command line for one session.
140
+ `.parcel-cache`, `.vite`, `.angular`, `.dart_tool`, `.venv`, …) are hidden by
141
+ default. Pass `--scope-omit-dir <name>` (repeatable) to override the omit
142
+ list on the command line for one session.
139
143
 
140
144
  The viewer keeps its per-project state under `.code-viewer/` at the repository
141
145
  root:
@@ -1514,6 +1514,7 @@ var init_git = __esm(() => {
1514
1514
  init_runtime();
1515
1515
  DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
1516
1516
  "node_modules",
1517
+ "bower_components",
1517
1518
  ".venv",
1518
1519
  "venv",
1519
1520
  ".next",
@@ -1521,6 +1522,11 @@ var init_git = __esm(() => {
1521
1522
  ".svelte-kit",
1522
1523
  ".astro",
1523
1524
  ".vercel",
1525
+ ".angular",
1526
+ ".docusaurus",
1527
+ ".expo",
1528
+ ".dart_tool",
1529
+ ".serverless",
1524
1530
  "dist",
1525
1531
  "build",
1526
1532
  "out",
@@ -1528,6 +1534,9 @@ var init_git = __esm(() => {
1528
1534
  ".gradle",
1529
1535
  ".pnpm-store",
1530
1536
  ".turbo",
1537
+ ".parcel-cache",
1538
+ ".vite",
1539
+ ".webpack",
1531
1540
  "__pycache__",
1532
1541
  ".pytest_cache",
1533
1542
  ".tox",
@@ -1537,6 +1546,7 @@ var init_git = __esm(() => {
1537
1546
  "vendor",
1538
1547
  ".cache",
1539
1548
  "coverage",
1549
+ ".nyc_output",
1540
1550
  "tmp",
1541
1551
  "log",
1542
1552
  "storage",
@@ -4410,7 +4420,7 @@ function startWorktreeUpdateWatch(options) {
4410
4420
  watchDirectory(options.root, true);
4411
4421
  return { started: watchers.size > 0, close: closeAll };
4412
4422
  }
4413
- var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 256;
4423
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
4414
4424
  var init_worktree_watcher = __esm(() => {
4415
4425
  init_search();
4416
4426
  });
@@ -11382,6 +11392,7 @@ Examples:
11382
11392
  if (scopeOmitDirCliOverride) {
11383
11393
  scopeOmitDirNames = scopeOmitDirCliOverride;
11384
11394
  }
11395
+ scopeWatchLimit = worktreeWatchDirectoryLimitFromEnv();
11385
11396
  }
11386
11397
  function warnIfLegacyConfigPresent() {
11387
11398
  try {
@@ -11391,6 +11402,9 @@ function warnIfLegacyConfigPresent() {
11391
11402
  } catch {}
11392
11403
  }
11393
11404
  function applyPersistedSettings(state) {
11405
+ const prevOmit = scopeOmitDirNames;
11406
+ const prevExclude = scopeExcludeNames;
11407
+ const prevWatchLimit = scopeWatchLimit;
11394
11408
  if (!scopeOmitDirCliOverride && Array.isArray(state.scopeOmitDirs) && state.scopeOmitDirs.length > 0) {
11395
11409
  scopeOmitDirNames = state.scopeOmitDirs;
11396
11410
  } else if (!scopeOmitDirCliOverride) {
@@ -11401,7 +11415,26 @@ function applyPersistedSettings(state) {
11401
11415
  } else {
11402
11416
  scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
11403
11417
  }
11418
+ if (state.scopeWatchLimit != null) {
11419
+ scopeWatchLimit = normalizeScopeWatchLimit(state.scopeWatchLimit);
11420
+ }
11404
11421
  uploadEnabled = state.uploadEnabled !== false;
11422
+ if (prevOmit !== scopeOmitDirNames || prevExclude !== scopeExcludeNames || prevWatchLimit !== scopeWatchLimit) {
11423
+ restartWorktreeWatch();
11424
+ }
11425
+ }
11426
+ function normalizeScopeWatchLimit(value) {
11427
+ if (typeof value !== "number" || !Number.isFinite(value)) {
11428
+ return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11429
+ }
11430
+ const floored = Math.floor(value);
11431
+ if (floored < MIN_WORKTREE_WATCH_DIRECTORY_LIMIT) {
11432
+ return MIN_WORKTREE_WATCH_DIRECTORY_LIMIT;
11433
+ }
11434
+ if (floored > MAX_WORKTREE_WATCH_DIRECTORY_LIMIT) {
11435
+ return MAX_WORKTREE_WATCH_DIRECTORY_LIMIT;
11436
+ }
11437
+ return floored;
11405
11438
  }
11406
11439
  function json2(data, init = {}) {
11407
11440
  return new Response(JSON.stringify(data), {
@@ -11883,7 +11916,11 @@ function handleSettings() {
11883
11916
  omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
11884
11917
  exclude_names_effective: scopeExcludeNames,
11885
11918
  exclude_names_built_in: DEFAULT_EXCLUDE_NAMES,
11886
- max_entries: WORKTREE_RECURSIVE_ENTRY_LIMIT
11919
+ max_entries: WORKTREE_RECURSIVE_ENTRY_LIMIT,
11920
+ watch_limit_effective: scopeWatchLimit,
11921
+ watch_limit_default: DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT,
11922
+ watch_limit_min: MIN_WORKTREE_WATCH_DIRECTORY_LIMIT,
11923
+ watch_limit_max: MAX_WORKTREE_WATCH_DIRECTORY_LIMIT
11887
11924
  }
11888
11925
  });
11889
11926
  }
@@ -11892,7 +11929,14 @@ function worktreeWatchDirectoryLimitFromEnv() {
11892
11929
  if (!raw)
11893
11930
  return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11894
11931
  const parsed = Number(raw);
11895
- return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11932
+ if (!Number.isFinite(parsed) || parsed <= 0)
11933
+ return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11934
+ const floored = Math.floor(parsed);
11935
+ if (floored < MIN_WORKTREE_WATCH_DIRECTORY_LIMIT)
11936
+ return MIN_WORKTREE_WATCH_DIRECTORY_LIMIT;
11937
+ if (floored > MAX_WORKTREE_WATCH_DIRECTORY_LIMIT)
11938
+ return MAX_WORKTREE_WATCH_DIRECTORY_LIMIT;
11939
+ return floored;
11896
11940
  }
11897
11941
  function handleFiles2(url) {
11898
11942
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
@@ -13033,7 +13077,43 @@ async function shutdown(exitCode = 0) {
13033
13077
  }
13034
13078
  process.exit(exitCode);
13035
13079
  }
13036
- var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
13080
+ function startScopedWorktreeWatch() {
13081
+ watchLimitReached = null;
13082
+ return startWorktreeUpdateWatch({
13083
+ root: cwd,
13084
+ omitDirNames: scopeOmitDirNames,
13085
+ excludeNames: scopeExcludeNames,
13086
+ watch,
13087
+ initialScanMode: "async",
13088
+ maxWatchedDirectories: scopeWatchLimit,
13089
+ onUpdate: triggerUpdate,
13090
+ onWatchLimit: (limit) => {
13091
+ watchLimitReached = limit;
13092
+ sendSse("watch-limit", String(limit));
13093
+ },
13094
+ onError: (error) => {
13095
+ const message = error instanceof Error ? error.message : String(error);
13096
+ console.warn(`code-viewer worktree watch skipped: ${message}`);
13097
+ }
13098
+ });
13099
+ }
13100
+ function restartWorktreeWatch() {
13101
+ try {
13102
+ if (shuttingDown)
13103
+ return;
13104
+ if (!worktreeWatch)
13105
+ return;
13106
+ } catch {
13107
+ return;
13108
+ }
13109
+ try {
13110
+ worktreeWatch.close();
13111
+ } catch (error) {
13112
+ console.warn(`code-viewer worktree watch restart close skipped: ${String(error)}`);
13113
+ }
13114
+ worktreeWatch = startScopedWorktreeWatch();
13115
+ }
13116
+ var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
13037
13117
  var init_preview = __esm(async () => {
13038
13118
  init_routes();
13039
13119
  init_annotations();
@@ -13096,6 +13176,7 @@ var init_preview = __esm(async () => {
13096
13176
  cliArgs = DEFAULT_ARGS;
13097
13177
  scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
13098
13178
  scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
13179
+ scopeWatchLimit = DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
13099
13180
  enc = new TextEncoder;
13100
13181
  sseClients = new Set;
13101
13182
  sseKeepalives = new Map;
@@ -13248,23 +13329,7 @@ data: ${watchLimitReached}
13248
13329
  watch,
13249
13330
  sendReload: () => sendSse("reload")
13250
13331
  });
13251
- worktreeWatch = startWorktreeUpdateWatch({
13252
- root: cwd,
13253
- omitDirNames: scopeOmitDirNames,
13254
- excludeNames: scopeExcludeNames,
13255
- watch,
13256
- initialScanMode: "async",
13257
- maxWatchedDirectories: worktreeWatchDirectoryLimitFromEnv(),
13258
- onUpdate: triggerUpdate,
13259
- onWatchLimit: (limit) => {
13260
- watchLimitReached = limit;
13261
- sendSse("watch-limit", String(limit));
13262
- },
13263
- onError: (error) => {
13264
- const message = error instanceof Error ? error.message : String(error);
13265
- console.warn(`code-viewer worktree watch skipped: ${message}`);
13266
- }
13267
- });
13332
+ worktreeWatch = startScopedWorktreeWatch();
13268
13333
  console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
13269
13334
  console.log(`git-diff-preview serving ${cwd}`);
13270
13335
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -25460,6 +25460,9 @@ code-viewer query clear --db app.db`
25460
25460
  let highlightLoadPromise = null;
25461
25461
  let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
25462
25462
  let SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = [];
25463
+ let SERVER_SCOPE_WATCH_LIMIT_DEFAULT = 1024;
25464
+ let SERVER_SCOPE_WATCH_LIMIT_MIN = 16;
25465
+ let SERVER_SCOPE_WATCH_LIMIT_MAX = 65536;
25463
25466
  const UNDO_STACK = [];
25464
25467
  let PENDING_G_SCOPE = null;
25465
25468
  let PENDING_G_UNTIL = 0;
@@ -25826,6 +25829,12 @@ code-viewer query clear --db app.db`
25826
25829
  }
25827
25830
  SERVER_SCOPE_OMIT_DIRS_DEFAULT = normalizeScopeOmitDirs(settings.scope.omit_dirs_effective);
25828
25831
  SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = normalizeScopeExcludeNames(settings.scope.exclude_names_effective);
25832
+ if (typeof settings.scope.watch_limit_default === "number")
25833
+ SERVER_SCOPE_WATCH_LIMIT_DEFAULT = settings.scope.watch_limit_default;
25834
+ if (typeof settings.scope.watch_limit_min === "number")
25835
+ SERVER_SCOPE_WATCH_LIMIT_MIN = settings.scope.watch_limit_min;
25836
+ if (typeof settings.scope.watch_limit_max === "number")
25837
+ SERVER_SCOPE_WATCH_LIMIT_MAX = settings.scope.watch_limit_max;
25829
25838
  return settings;
25830
25839
  } catch {
25831
25840
  return null;
@@ -26184,15 +26193,20 @@ code-viewer query clear --db app.db`
26184
26193
  displaySource: "Applies to all projects in this browser.",
26185
26194
  excludedDirectories: "Excluded directories",
26186
26195
  omitDirs: "Skip these directory names while browsing and searching",
26196
+ omitDirsHelp: "Reads no contents inside these directories. Applies to the sidebar (Files), Ctrl+K (file search), Ctrl+G (grep), Datastores, and the file change watcher.",
26187
26197
  excludeNames: "Hide these file or directory names completely",
26198
+ excludeNamesHelp: "Removes matching files or directories from the sidebar, search, and grep results entirely. Unlike Skip, the names themselves disappear from the UI.",
26188
26199
  reset: "Restore defaults",
26189
26200
  autosaveNote: "Changes save automatically.",
26190
- scopeSource: (project, source) => `Saved for project "${project}" in this browser. Source: ${source}. Used by tree, Ctrl+K, and Ctrl+G. Restore defaults removes the browser override.`,
26201
+ scopeSource: (project, source) => `Saved for project "${project}" in this browser. Source: ${source}. Used by the sidebar, Ctrl+K, Ctrl+G, Datastores, and the file change watcher. Restore defaults removes the browser override.`,
26191
26202
  browserOverride: "Browser override",
26192
26203
  serverDefault: "Server default",
26193
26204
  uploadsTitle: "Uploads",
26194
26205
  uploadEnabledLabel: "Allow file uploads into worktree folders",
26195
- uploadEnabledHelp: "Disable to make the worktree read-only for everyone using this server."
26206
+ uploadEnabledHelp: "Disable to make the worktree read-only for everyone using this server.",
26207
+ watchTitle: "File change watcher",
26208
+ watchLimit: "Maximum directories to watch",
26209
+ watchLimitHelp: (defaultLimit) => `Higher values reduce missed updates in deep trees at the cost of file handles. Combine with the Skip list above to keep heavy folders (node_modules, .git, dist...) out of the watch budget. Default: ${defaultLimit}.`
26196
26210
  },
26197
26211
  annotations: {
26198
26212
  title: "Code annotations",
@@ -26280,15 +26294,20 @@ code-viewer query clear --db app.db`
26280
26294
  displaySource: "このブラウザのすべてのプロジェクトに適用されます。",
26281
26295
  excludedDirectories: "除外ディレクトリ",
26282
26296
  omitDirs: "閲覧と検索でスキップするディレクトリ名",
26297
+ omitDirsHelp: "これらのディレクトリの中身は読み込みません。サイドバー(Files)・Ctrl+K(ファイル検索)・Ctrl+G(grep)・Datastores・File change watcher の5機能すべてに適用されます。",
26283
26298
  excludeNames: "完全に非表示にするファイル名またはディレクトリ名",
26299
+ excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。",
26284
26300
  reset: "デフォルトに戻す",
26285
26301
  autosaveNote: "変更は自動で保存されます。",
26286
- scopeSource: (project, source) => `このブラウザのプロジェクト "${project}" に保存されます。ソース: ${source}。ツリー、Ctrl+K、Ctrl+G で使われます。「デフォルトに戻す」でブラウザ側の上書きを削除します。`,
26302
+ scopeSource: (project, source) => `このブラウザのプロジェクト "${project}" に保存されます。ソース: ${source}。サイドバー、Ctrl+K、Ctrl+G、Datastores、File change watcher で使われます。「デフォルトに戻す」でブラウザ側の上書きを削除します。`,
26287
26303
  browserOverride: "ブラウザ側の上書き",
26288
26304
  serverDefault: "サーバ既定値",
26289
26305
  uploadsTitle: "アップロード",
26290
26306
  uploadEnabledLabel: "ワークツリーへのファイルアップロードを許可する",
26291
- uploadEnabledHelp: "オフにすると、このサーバを使う全員に対してワークツリーは読み取り専用になります。"
26307
+ uploadEnabledHelp: "オフにすると、このサーバを使う全員に対してワークツリーは読み取り専用になります。",
26308
+ watchTitle: "ファイル変更の監視",
26309
+ watchLimit: "監視するディレクトリ数の上限",
26310
+ watchLimitHelp: (defaultLimit) => `値を大きくすると深いツリーの変更を取りこぼしにくくなりますが、ファイルハンドル数を消費します。上の Skip リストと併用すると、重いフォルダ(node_modules, .git, dist など)を監視枠から外せます。既定値: ${defaultLimit}。`
26292
26311
  },
26293
26312
  annotations: {
26294
26313
  title: "コード注釈",
@@ -26406,14 +26425,20 @@ code-viewer query clear --db app.db`
26406
26425
  settingsSections[1].textContent = text2.settings.uploadsTitle;
26407
26426
  if (settingsSections[2])
26408
26427
  settingsSections[2].textContent = text2.settings.excludedDirectories;
26428
+ if (settingsSections[3])
26429
+ settingsSections[3].textContent = text2.settings.watchTitle;
26409
26430
  setElementText("#upload-enabled-label", text2.settings.uploadEnabledLabel);
26410
26431
  setElementText("#upload-help", text2.settings.uploadEnabledHelp);
26432
+ setElementText("#scope-omit-dirs-help", text2.settings.omitDirsHelp);
26433
+ setElementText("#scope-exclude-names-help", text2.settings.excludeNamesHelp);
26434
+ setElementText("#scope-watch-limit-help", text2.settings.watchLimitHelp(SERVER_SCOPE_WATCH_LIMIT_DEFAULT));
26411
26435
  const labelMap = {
26412
26436
  "viewer-language": text2.settings.language,
26413
26437
  "sidebar-font-size": text2.settings.fileListFontSize,
26414
26438
  "code-font-size": text2.settings.codeFontSize,
26415
26439
  "scope-omit-dirs": text2.settings.omitDirs,
26416
- "scope-exclude-names": text2.settings.excludeNames
26440
+ "scope-exclude-names": text2.settings.excludeNames,
26441
+ "scope-watch-limit": text2.settings.watchLimit
26417
26442
  };
26418
26443
  Object.entries(labelMap).forEach(([id, label]) => {
26419
26444
  const labelEl = document.querySelector(`label[for="${id}"]`);
@@ -26597,6 +26622,19 @@ code-viewer query clear --db app.db`
26597
26622
  `);
26598
26623
  excludeInput.value = effectiveScopeExcludeNames().join(`
26599
26624
  `);
26625
+ const watchLimitInput = document.querySelector("#scope-watch-limit");
26626
+ const watchLimitRange = document.querySelector("#scope-watch-limit-range");
26627
+ const watchLimitValue = effectiveScopeWatchLimit();
26628
+ if (watchLimitInput) {
26629
+ watchLimitInput.min = String(SERVER_SCOPE_WATCH_LIMIT_MIN);
26630
+ watchLimitInput.max = String(SERVER_SCOPE_WATCH_LIMIT_MAX);
26631
+ watchLimitInput.value = String(watchLimitValue);
26632
+ }
26633
+ if (watchLimitRange) {
26634
+ watchLimitRange.min = String(SERVER_SCOPE_WATCH_LIMIT_MIN);
26635
+ watchLimitRange.max = String(SERVER_SCOPE_WATCH_LIMIT_MAX);
26636
+ watchLimitRange.value = String(watchLimitValue);
26637
+ }
26600
26638
  const uploadToggle = document.querySelector("#upload-enabled");
26601
26639
  if (uploadToggle)
26602
26640
  uploadToggle.checked = APP_SETTINGS.uploadEnabled !== false;
@@ -26653,6 +26691,41 @@ code-viewer query clear --db app.db`
26653
26691
  refreshScopeSourceLabel();
26654
26692
  refreshRepositoryTreeAfterSettings();
26655
26693
  }
26694
+ function normalizeScopeWatchLimit(value) {
26695
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
26696
+ if (!Number.isFinite(parsed))
26697
+ return null;
26698
+ const floored = Math.floor(parsed);
26699
+ if (floored < SERVER_SCOPE_WATCH_LIMIT_MIN)
26700
+ return SERVER_SCOPE_WATCH_LIMIT_MIN;
26701
+ if (floored > SERVER_SCOPE_WATCH_LIMIT_MAX)
26702
+ return SERVER_SCOPE_WATCH_LIMIT_MAX;
26703
+ return floored;
26704
+ }
26705
+ function effectiveScopeWatchLimit() {
26706
+ const saved = normalizeScopeWatchLimit(APP_SETTINGS.scopeWatchLimit);
26707
+ return saved ?? SERVER_SCOPE_WATCH_LIMIT_DEFAULT;
26708
+ }
26709
+ function syncScopeWatchLimitInputs(value) {
26710
+ const numberInput = document.querySelector("#scope-watch-limit");
26711
+ const rangeInput = document.querySelector("#scope-watch-limit-range");
26712
+ if (numberInput && numberInput.value !== String(value))
26713
+ numberInput.value = String(value);
26714
+ if (rangeInput && rangeInput.value !== String(value))
26715
+ rangeInput.value = String(value);
26716
+ }
26717
+ function saveScopeWatchLimitField(value) {
26718
+ const next = normalizeScopeWatchLimit(value);
26719
+ const resolved = next ?? SERVER_SCOPE_WATCH_LIMIT_DEFAULT;
26720
+ mergeLocalSettings({ scopeWatchLimit: resolved });
26721
+ patchSettings({ scopeWatchLimit: resolved });
26722
+ syncScopeWatchLimitInputs(resolved);
26723
+ }
26724
+ function previewScopeWatchLimit(value) {
26725
+ const next = normalizeScopeWatchLimit(value);
26726
+ if (next != null)
26727
+ syncScopeWatchLimitInputs(next);
26728
+ }
26656
26729
  function resetScopeSettings() {
26657
26730
  setViewerLanguage("en", false);
26658
26731
  mergeLocalSettings({
@@ -26660,6 +26733,7 @@ code-viewer query clear --db app.db`
26660
26733
  codeFontSize: null,
26661
26734
  scopeOmitDirs: null,
26662
26735
  scopeExcludeNames: null,
26736
+ scopeWatchLimit: null,
26663
26737
  uploadEnabled: null
26664
26738
  });
26665
26739
  applySidebarFontSize("regular");
@@ -26670,6 +26744,7 @@ code-viewer query clear --db app.db`
26670
26744
  codeFontSize: null,
26671
26745
  scopeOmitDirs: null,
26672
26746
  scopeExcludeNames: null,
26747
+ scopeWatchLimit: null,
26673
26748
  uploadEnabled: null
26674
26749
  });
26675
26750
  const sidebarFontSize = document.querySelector("#sidebar-font-size");
@@ -27087,6 +27162,15 @@ code-viewer query clear --db app.db`
27087
27162
  $("#scope-exclude-names")?.addEventListener("change", (event) => {
27088
27163
  saveScopeExcludeNamesField(event.currentTarget.value);
27089
27164
  });
27165
+ $("#scope-watch-limit")?.addEventListener("change", (event) => {
27166
+ saveScopeWatchLimitField(event.currentTarget.value);
27167
+ });
27168
+ $("#scope-watch-limit-range")?.addEventListener("input", (event) => {
27169
+ previewScopeWatchLimit(event.currentTarget.value);
27170
+ });
27171
+ $("#scope-watch-limit-range")?.addEventListener("change", (event) => {
27172
+ saveScopeWatchLimitField(event.currentTarget.value);
27173
+ });
27090
27174
  $("#scope-settings-popover")?.addEventListener("keydown", (e2) => {
27091
27175
  if (isImeComposing(e2))
27092
27176
  return;
package/web/index.html CHANGED
@@ -177,10 +177,21 @@
177
177
  <strong class="scope-settings-section-title">Excluded directories</strong>
178
178
  <label for="scope-omit-dirs">Skip these directory names while browsing and searching</label>
179
179
  <textarea id="scope-omit-dirs" rows="6" spellcheck="false"></textarea>
180
+ <p id="scope-omit-dirs-help" class="scope-settings-help"></p>
180
181
  <label for="scope-exclude-names">Hide these file or directory names completely</label>
181
182
  <textarea id="scope-exclude-names" rows="4" spellcheck="false"></textarea>
183
+ <p id="scope-exclude-names-help" class="scope-settings-help"></p>
182
184
  <p id="scope-omit-source"></p>
183
185
  </div>
186
+ <div class="scope-settings-section">
187
+ <strong id="watch-section-title" class="scope-settings-section-title">File change watcher</strong>
188
+ <label for="scope-watch-limit">Maximum directories to watch</label>
189
+ <div class="scope-watch-limit-row">
190
+ <input id="scope-watch-limit-range" type="range" min="16" max="65536" step="16" aria-label="watch limit slider" />
191
+ <input id="scope-watch-limit" type="number" min="16" max="65536" step="1" />
192
+ </div>
193
+ <p id="scope-watch-limit-help" class="scope-settings-help"></p>
194
+ </div>
184
195
  <div class="scope-settings-footer">
185
196
  <p id="scope-settings-autosave-note" class="scope-settings-help"></p>
186
197
  <button id="scope-omit-reset" type="button">Restore defaults</button>
package/web/style.css CHANGED
@@ -1735,6 +1735,20 @@ body.gdp-history-resizing * {
1735
1735
  gap: 8px;
1736
1736
  margin-top: 10px;
1737
1737
  }
1738
+ .scope-watch-limit-row {
1739
+ display: flex;
1740
+ align-items: center;
1741
+ gap: 10px;
1742
+ }
1743
+ .scope-watch-limit-row input[type="range"] {
1744
+ flex: 1;
1745
+ min-width: 0;
1746
+ accent-color: var(--accent, #2b6cb0);
1747
+ }
1748
+ .scope-watch-limit-row input[type="number"] {
1749
+ width: 88px;
1750
+ flex: 0 0 auto;
1751
+ }
1738
1752
  .scope-settings-toggle {
1739
1753
  display: flex;
1740
1754
  align-items: center;