@youtyan/code-viewer 0.2.9 → 0.2.11

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
@@ -27,8 +27,9 @@ Requires Node.js 20 or newer when installed from npm. Development uses
27
27
  viewer.
28
28
  - Browse SQLite, PostgreSQL, MySQL, Redis, Elasticsearch, and S3-compatible
29
29
  object storage (MinIO, LocalStack) with a built-in datastore viewer.
30
- - Read the built-in Help page for repository browsing, diffs, annotations,
31
- the datastore viewer, agent skills, and shortcuts.
30
+ - Read the built-in Help page for getting started, the `.code-viewer/`
31
+ project files, AI annotations, datastores, the agent skill, and
32
+ keybindings.
32
33
  - Open repository folders (and parent folders of files) in the OS file
33
34
  manager, create folders, and trash/restore files from localhost-only
34
35
  actions.
@@ -115,7 +116,9 @@ rendered, and the page includes controls to copy the full file or reopen it in
115
116
  the full non-virtual view.
116
117
 
117
118
  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
119
+ files reload as you edit. The directory watcher is capped at 1024 directories
120
+ by default and can be tuned from Viewer Settings → **File change watcher**
121
+ (range slider + numeric input, 16–65536); when the cap is hit the viewer
119
122
  shows a banner so reloads are not silently missed.
120
123
 
121
124
  Large repositories load folder children on demand. The sidebar remembers which
@@ -129,13 +132,15 @@ views remain read-only. Open **Viewer Settings** in the header to toggle
129
132
  uploads off, edit the directories to skip while browsing/searching, and hide
130
133
  files or directory names completely.
131
134
 
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
135
+ Scope settings control directory exclusions shared by the sidebar, Ctrl+K file
136
+ palette, Ctrl+G grep palette, the Datastores browser, and the file change
137
+ watcher the same list applies to all five. Everything you change in Viewer
138
+ Settings is saved on the server under `.code-viewer/settings.json` (no
139
+ separate project-level config file). `.DS_Store` and a broad set of
136
140
  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.
141
+ `.parcel-cache`, `.vite`, `.angular`, `.dart_tool`, `.venv`, …) are hidden by
142
+ default. Pass `--scope-omit-dir <name>` (repeatable) to override the omit
143
+ list on the command line for one session.
139
144
 
140
145
  The viewer keeps its per-project state under `.code-viewer/` at the repository
141
146
  root:
@@ -235,9 +240,10 @@ Open Datastores in the global navigation to access:
235
240
 
236
241
  ### CLI
237
242
 
238
- AI agents can read data and query history from the command line. The current
239
- CLI ships `exec`, `list`, and `clear`; search, snapshot, and diff operations
240
- are performed from the browser UI.
243
+ AI agents can run read-only queries, search content across tables, and
244
+ capture snapshots / diffs from the command line. The same operations are
245
+ mirrored under the Datastores tab in the browser UI, and every CLI result is
246
+ written to the per-repository history visible in the browser.
241
247
 
242
248
  ```sh
243
249
  code-viewer query exec --db data.db --sql "SELECT * FROM users LIMIT 10" \
@@ -248,6 +254,22 @@ code-viewer query exec --db app.db --sql "SELECT count(*) FROM orders" \
248
254
 
249
255
  code-viewer query list --db app.db --json
250
256
  code-viewer query clear --db app.db
257
+
258
+ code-viewer query search --db app.db --term "john@example.com" \
259
+ --tables users,orders --include-non-text --max-hits 20
260
+
261
+ code-viewer query snapshot create --db app.db --tables users,orders \
262
+ --note "Before user registration test"
263
+ code-viewer query snapshot list --db app.db --json
264
+ code-viewer query snapshot note --id snap-abc123 --note "Updated context"
265
+ code-viewer query snapshot delete --id snap-abc123
266
+
267
+ code-viewer query diff create --before snap-abc123 --after snap-def456 \
268
+ --note "User registration test"
269
+ code-viewer query diff tables --id diff-xyz789
270
+ code-viewer query diff rows --id diff-xyz789 --table users --type inserted
271
+ code-viewer query diff list --db app.db --json
272
+ code-viewer query diff delete --id diff-xyz789
251
273
  ```
252
274
 
253
275
  `code-viewer query --help` shows all command syntax. `code-viewer query
@@ -303,9 +325,9 @@ with `--include-non-text` and `--run-search`. For AI agents,
303
325
 
304
326
  Annotations are grouped into sessions and persisted in
305
327
  `.code-viewer/annotations.json` at the repository root, so the walkthrough
306
- survives reloads and server restarts. The `.code-viewer` directory is
307
- tool-internal: it never shows up in the file tree, searches, or diffs. Add it
308
- to `.gitignore` if you do not want to share annotations through git.
328
+ survives reloads and server restarts. See **Uploads and Scope Settings**
329
+ above for how `.code-viewer/` is treated by the viewer and how to opt out of
330
+ sharing it through git.
309
331
 
310
332
  In the browser, the annotation icon in the header opens the side panel. The
311
333
  current explanation is rendered at the top of the panel while the code stays
@@ -337,9 +359,11 @@ write concise Markdown explanations, and how to install the bundled agent skill.
337
359
 
338
360
  ### Agent Skill
339
361
 
340
- The package bundles [Agent Skills](https://agentskills.io) (the SKILL.md
341
- open standard) that teach AI coding agents when and how to use
342
- `annotate`, `query`, and `snapshot`. Install them into the current project:
362
+ The package bundles three [Agent Skills](https://agentskills.io)
363
+ (the SKILL.md open standard) `code-viewer-annotate`, `code-viewer-query`,
364
+ and `code-viewer-snapshot` that teach AI coding agents when and how to use
365
+ `annotate`, read-only `query`, and snapshot / diff workflows. A single
366
+ `skill install` copies all three into the selected agent directories:
343
367
 
344
368
  ```sh
345
369
  npx -y @youtyan/code-viewer skill install # Claude Code (.claude/skills/)
@@ -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",
@@ -2765,7 +2775,7 @@ __export(exports_skill_cli, {
2765
2775
  SKILL_HELP: () => SKILL_HELP,
2766
2776
  AGENT_SKILL_DIRS: () => AGENT_SKILL_DIRS
2767
2777
  });
2768
- import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
2778
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "node:fs";
2769
2779
  import { homedir as homedir2 } from "node:os";
2770
2780
  import { join as join5, resolve } from "node:path";
2771
2781
  function parseAgentList(value) {
@@ -2821,25 +2831,34 @@ function parseSkillArgs(argv) {
2821
2831
  }
2822
2832
  return { ok: true, args: { kind: "install", agents, global, cwd } };
2823
2833
  }
2834
+ function discoverBundledSkills(skillsRoot) {
2835
+ if (!existsSync4(skillsRoot))
2836
+ return [];
2837
+ return readdirSync2(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync4(join5(skillsRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
2838
+ }
2824
2839
  function installSkill(args, deps) {
2825
- if (!existsSync4(join5(deps.sourceDir, "SKILL.md"))) {
2840
+ const skills = discoverBundledSkills(deps.skillsRoot);
2841
+ if (skills.length === 0) {
2826
2842
  return {
2827
2843
  ok: false,
2828
- error: `bundled skill not found at ${deps.sourceDir}`
2844
+ error: `no bundled skills found under ${deps.skillsRoot}`
2829
2845
  };
2830
2846
  }
2831
2847
  const base = args.global ? deps.homeDir : resolve(args.cwd ?? deps.projectDir);
2832
2848
  const results = [];
2833
2849
  for (const agent of args.agents) {
2834
- const target = join5(base, AGENT_SKILL_DIRS[agent], "skills", SKILL_NAME);
2835
- const action = existsSync4(target) ? "updated" : "installed";
2836
- try {
2837
- mkdirSync2(target, { recursive: true });
2838
- cpSync(deps.sourceDir, target, { recursive: true });
2839
- } catch (error) {
2840
- return { ok: false, error: String(error) };
2850
+ for (const skill of skills) {
2851
+ const sourceDir = join5(deps.skillsRoot, skill);
2852
+ const target = join5(base, AGENT_SKILL_DIRS[agent], "skills", skill);
2853
+ const action = existsSync4(target) ? "updated" : "installed";
2854
+ try {
2855
+ mkdirSync2(target, { recursive: true });
2856
+ cpSync(sourceDir, target, { recursive: true });
2857
+ } catch (error) {
2858
+ return { ok: false, error: String(error) };
2859
+ }
2860
+ results.push({ agent, skill, action, target });
2841
2861
  }
2842
- results.push({ agent, action, target });
2843
2862
  }
2844
2863
  return { ok: true, results };
2845
2864
  }
@@ -2855,7 +2874,7 @@ function runSkillCli(argv) {
2855
2874
  return;
2856
2875
  }
2857
2876
  const result = installSkill(parsed.args, {
2858
- sourceDir: join5(ROOT, "skills", SKILL_NAME),
2877
+ skillsRoot: join5(ROOT, "skills"),
2859
2878
  homeDir: homedir2(),
2860
2879
  projectDir: process.cwd()
2861
2880
  });
@@ -2864,13 +2883,13 @@ function runSkillCli(argv) {
2864
2883
  process.exit(1);
2865
2884
  }
2866
2885
  for (const entry of result.results) {
2867
- console.log(`${entry.action} (${entry.agent}): ${entry.target}`);
2886
+ console.log(`${entry.action} (${entry.agent}/${entry.skill}): ${entry.target}`);
2868
2887
  }
2869
2888
  if (result.results.some((entry) => entry.action === "installed")) {
2870
- console.log("Re-run the same command anytime to update the skill.");
2889
+ console.log("Re-run the same command anytime to update the skills.");
2871
2890
  }
2872
2891
  }
2873
- var SKILL_NAME = "code-viewer-annotate", AGENT_SKILL_DIRS, AGENT_NAMES, SKILL_HELP;
2892
+ var AGENT_SKILL_DIRS, AGENT_NAMES, SKILL_HELP;
2874
2893
  var init_skill_cli = __esm(() => {
2875
2894
  init_root();
2876
2895
  AGENT_SKILL_DIRS = {
@@ -2881,15 +2900,16 @@ var init_skill_cli = __esm(() => {
2881
2900
  agents: ".agents"
2882
2901
  };
2883
2902
  AGENT_NAMES = Object.keys(AGENT_SKILL_DIRS);
2884
- SKILL_HELP = `code-viewer skill — manage the bundled agent skill
2903
+ SKILL_HELP = `code-viewer skill — manage the bundled agent skills
2885
2904
 
2886
2905
  Usage:
2887
2906
  code-viewer skill install [--agent <list>] [--global] [--cwd <dir>]
2888
2907
 
2889
- Installs the ${SKILL_NAME} skill (SKILL.md for AI coding agents) into the
2890
- skills directory of each selected agent in the current project, or into the
2891
- home directory equivalents with --global. Running install again overwrites
2892
- the files, so the same command also updates an existing installation.
2908
+ Installs every bundled skill (each SKILL.md directory under the package's
2909
+ skills/) into the skills directory of each selected agent in the current
2910
+ project, or into the home directory equivalents with --global. Running
2911
+ install again overwrites the files, so the same command also updates an
2912
+ existing installation.
2893
2913
 
2894
2914
  Options:
2895
2915
  --agent <list> comma separated agents: ${AGENT_NAMES.join(", ")}, or all
@@ -4410,7 +4430,7 @@ function startWorktreeUpdateWatch(options) {
4410
4430
  watchDirectory(options.root, true);
4411
4431
  return { started: watchers.size > 0, close: closeAll };
4412
4432
  }
4413
- var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 256;
4433
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
4414
4434
  var init_worktree_watcher = __esm(() => {
4415
4435
  init_search();
4416
4436
  });
@@ -11382,6 +11402,7 @@ Examples:
11382
11402
  if (scopeOmitDirCliOverride) {
11383
11403
  scopeOmitDirNames = scopeOmitDirCliOverride;
11384
11404
  }
11405
+ scopeWatchLimit = worktreeWatchDirectoryLimitFromEnv();
11385
11406
  }
11386
11407
  function warnIfLegacyConfigPresent() {
11387
11408
  try {
@@ -11391,6 +11412,9 @@ function warnIfLegacyConfigPresent() {
11391
11412
  } catch {}
11392
11413
  }
11393
11414
  function applyPersistedSettings(state) {
11415
+ const prevOmit = scopeOmitDirNames;
11416
+ const prevExclude = scopeExcludeNames;
11417
+ const prevWatchLimit = scopeWatchLimit;
11394
11418
  if (!scopeOmitDirCliOverride && Array.isArray(state.scopeOmitDirs) && state.scopeOmitDirs.length > 0) {
11395
11419
  scopeOmitDirNames = state.scopeOmitDirs;
11396
11420
  } else if (!scopeOmitDirCliOverride) {
@@ -11401,7 +11425,26 @@ function applyPersistedSettings(state) {
11401
11425
  } else {
11402
11426
  scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
11403
11427
  }
11428
+ if (state.scopeWatchLimit != null) {
11429
+ scopeWatchLimit = normalizeScopeWatchLimit(state.scopeWatchLimit);
11430
+ }
11404
11431
  uploadEnabled = state.uploadEnabled !== false;
11432
+ if (prevOmit !== scopeOmitDirNames || prevExclude !== scopeExcludeNames || prevWatchLimit !== scopeWatchLimit) {
11433
+ restartWorktreeWatch();
11434
+ }
11435
+ }
11436
+ function normalizeScopeWatchLimit(value) {
11437
+ if (typeof value !== "number" || !Number.isFinite(value)) {
11438
+ return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11439
+ }
11440
+ const floored = Math.floor(value);
11441
+ if (floored < MIN_WORKTREE_WATCH_DIRECTORY_LIMIT) {
11442
+ return MIN_WORKTREE_WATCH_DIRECTORY_LIMIT;
11443
+ }
11444
+ if (floored > MAX_WORKTREE_WATCH_DIRECTORY_LIMIT) {
11445
+ return MAX_WORKTREE_WATCH_DIRECTORY_LIMIT;
11446
+ }
11447
+ return floored;
11405
11448
  }
11406
11449
  function json2(data, init = {}) {
11407
11450
  return new Response(JSON.stringify(data), {
@@ -11883,7 +11926,11 @@ function handleSettings() {
11883
11926
  omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
11884
11927
  exclude_names_effective: scopeExcludeNames,
11885
11928
  exclude_names_built_in: DEFAULT_EXCLUDE_NAMES,
11886
- max_entries: WORKTREE_RECURSIVE_ENTRY_LIMIT
11929
+ max_entries: WORKTREE_RECURSIVE_ENTRY_LIMIT,
11930
+ watch_limit_effective: scopeWatchLimit,
11931
+ watch_limit_default: DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT,
11932
+ watch_limit_min: MIN_WORKTREE_WATCH_DIRECTORY_LIMIT,
11933
+ watch_limit_max: MAX_WORKTREE_WATCH_DIRECTORY_LIMIT
11887
11934
  }
11888
11935
  });
11889
11936
  }
@@ -11892,7 +11939,14 @@ function worktreeWatchDirectoryLimitFromEnv() {
11892
11939
  if (!raw)
11893
11940
  return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11894
11941
  const parsed = Number(raw);
11895
- return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11942
+ if (!Number.isFinite(parsed) || parsed <= 0)
11943
+ return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11944
+ const floored = Math.floor(parsed);
11945
+ if (floored < MIN_WORKTREE_WATCH_DIRECTORY_LIMIT)
11946
+ return MIN_WORKTREE_WATCH_DIRECTORY_LIMIT;
11947
+ if (floored > MAX_WORKTREE_WATCH_DIRECTORY_LIMIT)
11948
+ return MAX_WORKTREE_WATCH_DIRECTORY_LIMIT;
11949
+ return floored;
11896
11950
  }
11897
11951
  function handleFiles2(url) {
11898
11952
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
@@ -13033,7 +13087,43 @@ async function shutdown(exitCode = 0) {
13033
13087
  }
13034
13088
  process.exit(exitCode);
13035
13089
  }
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;
13090
+ function startScopedWorktreeWatch() {
13091
+ watchLimitReached = null;
13092
+ return startWorktreeUpdateWatch({
13093
+ root: cwd,
13094
+ omitDirNames: scopeOmitDirNames,
13095
+ excludeNames: scopeExcludeNames,
13096
+ watch,
13097
+ initialScanMode: "async",
13098
+ maxWatchedDirectories: scopeWatchLimit,
13099
+ onUpdate: triggerUpdate,
13100
+ onWatchLimit: (limit) => {
13101
+ watchLimitReached = limit;
13102
+ sendSse("watch-limit", String(limit));
13103
+ },
13104
+ onError: (error) => {
13105
+ const message = error instanceof Error ? error.message : String(error);
13106
+ console.warn(`code-viewer worktree watch skipped: ${message}`);
13107
+ }
13108
+ });
13109
+ }
13110
+ function restartWorktreeWatch() {
13111
+ try {
13112
+ if (shuttingDown)
13113
+ return;
13114
+ if (!worktreeWatch)
13115
+ return;
13116
+ } catch {
13117
+ return;
13118
+ }
13119
+ try {
13120
+ worktreeWatch.close();
13121
+ } catch (error) {
13122
+ console.warn(`code-viewer worktree watch restart close skipped: ${String(error)}`);
13123
+ }
13124
+ worktreeWatch = startScopedWorktreeWatch();
13125
+ }
13126
+ 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
13127
  var init_preview = __esm(async () => {
13038
13128
  init_routes();
13039
13129
  init_annotations();
@@ -13096,6 +13186,7 @@ var init_preview = __esm(async () => {
13096
13186
  cliArgs = DEFAULT_ARGS;
13097
13187
  scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
13098
13188
  scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
13189
+ scopeWatchLimit = DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
13099
13190
  enc = new TextEncoder;
13100
13191
  sseClients = new Set;
13101
13192
  sseKeepalives = new Map;
@@ -13248,23 +13339,7 @@ data: ${watchLimitReached}
13248
13339
  watch,
13249
13340
  sendReload: () => sendSse("reload")
13250
13341
  });
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
- });
13342
+ worktreeWatch = startScopedWorktreeWatch();
13268
13343
  console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
13269
13344
  console.log(`git-diff-preview serving ${cwd}`);
13270
13345
  });
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.11",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -18383,6 +18383,7 @@ ${frontmatter.yaml}
18383
18383
  var HELP_LANGUAGES = ["en", "ja"];
18384
18384
  var HELP_SECTIONS = [
18385
18385
  "overview",
18386
+ "storage",
18386
18387
  "annotations",
18387
18388
  "database",
18388
18389
  "skills",
@@ -18461,6 +18462,73 @@ ${frontmatter.yaml}
18461
18462
  }
18462
18463
  ]
18463
18464
  },
18465
+ storage: {
18466
+ nav: "Project Files",
18467
+ title: ".code-viewer Directory",
18468
+ intro: "code-viewer keeps every per-repository state file under .code-viewer/ at the root of the opened repository. The directory is created on demand, can be deleted at any time to reset state, and should be gitignored in most projects.",
18469
+ groups: [
18470
+ {
18471
+ title: "What lives there",
18472
+ blocks: [
18473
+ {
18474
+ kind: "table",
18475
+ rows: [
18476
+ [
18477
+ "settings.json",
18478
+ "Viewer Settings — diff layout, theme, language, sidebar/history widths, font sizes, syntax highlight, ignore-whitespace, hide-tests, scope overrides (omitted dirs / excluded names), upload toggle, annotation panel open/follow/mute/rate, and the last viewed diff range."
18479
+ ],
18480
+ [
18481
+ "view-state.json",
18482
+ "Sidebar tree state — collapsed directories, lazy-expanded directories (folders opened on demand in large repos), and the viewed-file list used to dim already-read entries."
18483
+ ],
18484
+ [
18485
+ "tabs.json",
18486
+ "Multi-DB tab layout — open datastore tabs, the active tab, per-tab selected table / view / SQL draft, per-tab Elasticsearch index and Redis DB index, and per-tab sidebar and history panel widths."
18487
+ ],
18488
+ [
18489
+ "db-ui.json",
18490
+ "Datastore UI preferences — column widths per (DB, table, column) and toggle states such as Rails FK inference and the S3 tooltip."
18491
+ ],
18492
+ [
18493
+ "annotations.json",
18494
+ "AI annotation walkthroughs — sessions with their ordered steps (file, line range, title, body), used by the annotation panel and synced live to open tabs over SSE."
18495
+ ],
18496
+ [
18497
+ "query-history.json",
18498
+ "SQL query history — recent statements with column list, preview rows, row count, elapsed time, executor (browser or CLI), and execution timestamp."
18499
+ ],
18500
+ [
18501
+ "db-snapshots.sqlite (+ -shm / -wal)",
18502
+ "SQLite store used by the Datastore Snapshot tab to keep point-in-time captures of tables / indices / key spaces for diffing. The -shm / -wal sidecar files are SQLite WAL artifacts; do not edit them directly."
18503
+ ]
18504
+ ]
18505
+ }
18506
+ ]
18507
+ },
18508
+ {
18509
+ title: "Source of truth and editing",
18510
+ blocks: [
18511
+ {
18512
+ kind: "paragraph",
18513
+ text: "Everything in .code-viewer/ is owned by code-viewer. JSON files are rewritten safely with validation on every change, so unknown keys are dropped and invalid values are reset to defaults. Hand-edit at your own risk — a corrupt file is renamed with a .corrupt suffix and replaced with an empty default."
18514
+ },
18515
+ {
18516
+ kind: "paragraph",
18517
+ text: "Deleting the whole directory is the supported way to reset all per-repository state. Removing a single file resets only that subsystem (for example, deleting tabs.json closes all DB tabs on the next load)."
18518
+ }
18519
+ ]
18520
+ },
18521
+ {
18522
+ title: "Sharing across machines",
18523
+ blocks: [
18524
+ {
18525
+ kind: "paragraph",
18526
+ text: "These files contain local UI state and should normally stay out of version control — add .code-viewer/ to .gitignore. To share AI walkthroughs, commit annotations.json explicitly (or copy it between checkouts) and leave the other files ignored."
18527
+ }
18528
+ ]
18529
+ }
18530
+ ]
18531
+ },
18464
18532
  annotations: {
18465
18533
  nav: "AI Annotations",
18466
18534
  title: "AI Code Annotations",
@@ -18683,7 +18751,7 @@ code-viewer annotate add-db --db app.db --tab query \\
18683
18751
  blocks: [
18684
18752
  {
18685
18753
  kind: "paragraph",
18686
- text: "AI agents can execute read-only queries and manage per-DB query history from the CLI. Results are saved into the same history visible in the browser. Search, snapshot, and diff operations live in the browser UI (Search tab / Snapshot tab) rather than the CLI."
18754
+ text: "AI agents can execute read-only queries, search across tables, and capture snapshots / diffs from the CLI. Results are written to the same per-repository history visible in the browser, and the browser UI exposes the same operations through the Search tab and the Snapshot tab."
18687
18755
  },
18688
18756
  {
18689
18757
  kind: "command",
@@ -18700,6 +18768,37 @@ code-viewer annotate add-db --db app.db --tab query \\
18700
18768
  title: "List or clear query history",
18701
18769
  command: `code-viewer query list --db app.db --json
18702
18770
  code-viewer query clear --db app.db`
18771
+ },
18772
+ {
18773
+ kind: "command",
18774
+ title: "Search across all tables",
18775
+ command: 'code-viewer query search --db app.db --term "john@example.com" \\\n --tables users,orders --include-non-text --max-hits 20'
18776
+ },
18777
+ {
18778
+ kind: "command",
18779
+ title: "Capture a snapshot",
18780
+ command: 'code-viewer query snapshot create --db app.db --tables users,orders \\\n --note "Before user registration test"'
18781
+ },
18782
+ {
18783
+ kind: "command",
18784
+ title: "List, annotate, or delete snapshots",
18785
+ command: `code-viewer query snapshot list --db app.db --json
18786
+ code-viewer query snapshot note --id snap-abc123 --note "Updated context"
18787
+ code-viewer query snapshot delete --id snap-abc123`
18788
+ },
18789
+ {
18790
+ kind: "command",
18791
+ title: "Diff two snapshots",
18792
+ command: `code-viewer query diff create --before snap-abc123 --after snap-def456 \\
18793
+ --note "User registration test"
18794
+ code-viewer query diff list --db app.db --json
18795
+ code-viewer query diff delete --id diff-xyz789`
18796
+ },
18797
+ {
18798
+ kind: "command",
18799
+ title: "Inspect a diff",
18800
+ command: `code-viewer query diff tables --id diff-xyz789
18801
+ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --limit 50`
18703
18802
  },
18704
18803
  {
18705
18804
  kind: "command",
@@ -18713,10 +18812,10 @@ code-viewer query clear --db app.db`
18713
18812
  skills: {
18714
18813
  nav: "Agent Skill",
18715
18814
  title: "Agent Skill Setup",
18716
- intro: "The package includes a code-viewer annotate skill so AI agents know when and how to create browser walkthroughs.",
18815
+ intro: "The package bundles three skills — code-viewer-annotate, code-viewer-query, and code-viewer-snapshot — so AI agents know when and how to create browser walkthroughs, run read-only queries, and capture snapshot / diff sessions. A single skill install command copies all three into the selected agent directories.",
18717
18816
  groups: [
18718
18817
  {
18719
- title: "Install the skill",
18818
+ title: "Install the skills",
18720
18819
  blocks: [
18721
18820
  {
18722
18821
  kind: "command",
@@ -18738,11 +18837,17 @@ code-viewer query clear --db app.db`
18738
18837
  kind: "command",
18739
18838
  title: "Install globally for a user (not per project)",
18740
18839
  command: "npx -y @youtyan/code-viewer skill install --agent all --global"
18840
+ },
18841
+ {
18842
+ kind: "command",
18843
+ title: "Install into a different project directory",
18844
+ command: `npx -y @youtyan/code-viewer skill install --agent all --cwd /path/to/other/repo
18845
+ # --cwd selects the target project; ignored when --global is also given.`
18741
18846
  }
18742
18847
  ]
18743
18848
  },
18744
18849
  {
18745
- title: "What the skill teaches",
18850
+ title: "What the skills teach",
18746
18851
  blocks: [
18747
18852
  {
18748
18853
  kind: "table",
@@ -18916,6 +19021,73 @@ code-viewer query clear --db app.db`
18916
19021
  }
18917
19022
  ]
18918
19023
  },
19024
+ storage: {
19025
+ nav: "プロジェクトファイル",
19026
+ title: ".code-viewer ディレクトリ",
19027
+ intro: "code-viewer はリポジトリ単位の状態をすべて、開いたリポジトリ直下の .code-viewer/ ディレクトリに保存します。必要になったときに自動で作られ、削除すれば全状態をリセットでき、通常は .gitignore に登録しておきます。",
19028
+ groups: [
19029
+ {
19030
+ title: "中に置かれるファイル",
19031
+ blocks: [
19032
+ {
19033
+ kind: "table",
19034
+ rows: [
19035
+ [
19036
+ "settings.json",
19037
+ "Viewer Settings — diff レイアウト、テーマ、言語、サイドバー/履歴幅、フォントサイズ、シンタックスハイライト、whitespace 無視、テスト非表示、scope 上書き(除外ディレクトリ / 除外名)、アップロード許可、注釈パネルの開閉/follow/ミュート/再生速度、最後に表示した diff 範囲を保存します。"
19038
+ ],
19039
+ [
19040
+ "view-state.json",
19041
+ "サイドバーツリーの状態 — 折りたたみ済みディレクトリ、遅延展開済みディレクトリ(大規模リポジトリで必要に応じて開かれたフォルダ)、既読扱いするための表示済みファイル一覧。"
19042
+ ],
19043
+ [
19044
+ "tabs.json",
19045
+ "マルチ DB タブのレイアウト — 開いているデータストアタブ、アクティブタブ、タブごとの選択テーブル / ビュー / SQL ドラフト、Elasticsearch のインデックスや Redis の DB index、タブ単位のサイドバー幅・履歴パネル高。"
19046
+ ],
19047
+ [
19048
+ "db-ui.json",
19049
+ "データストア UI の設定 — (DB, テーブル, カラム) ごとの列幅、Rails FK 推測や S3 ツールチップなどのトグル状態。"
19050
+ ],
19051
+ [
19052
+ "annotations.json",
19053
+ "AI 注釈のウォークスルー — セッションと順序付きステップ(ファイル、行範囲、タイトル、本文)。注釈パネルが読み、SSE で開いているタブへもライブ同期されます。"
19054
+ ],
19055
+ [
19056
+ "query-history.json",
19057
+ "SQL クエリ履歴 — 直近のクエリ、カラム一覧、プレビュー行、行数、実行時間、実行元(browser / CLI)、実行時刻。"
19058
+ ],
19059
+ [
19060
+ "db-snapshots.sqlite (+ -shm / -wal)",
19061
+ "Snapshot タブが使う SQLite ストア。テーブル / インデックス / キー空間の時点スナップショットを保持し、差分表示に使います。-shm / -wal は SQLite の WAL 用付随ファイル。手動編集しないでください。"
19062
+ ]
19063
+ ]
19064
+ }
19065
+ ]
19066
+ },
19067
+ {
19068
+ title: "ファイルの所有権と編集",
19069
+ blocks: [
19070
+ {
19071
+ kind: "paragraph",
19072
+ text: ".code-viewer/ の中身はすべて code-viewer が管理します。JSON ファイルは書き込みごとにバリデーションを通して安全に書き換えられるため、未知のキーは破棄され、不正な値は既定値に戻されます。手で編集すると壊れる可能性があり、壊れたファイルは .corrupt サフィックスに改名されて空の既定値で置き換えられます。"
19073
+ },
19074
+ {
19075
+ kind: "paragraph",
19076
+ text: "ディレクトリごと削除するのが、このリポジトリの全状態をリセットする推奨手順です。個別ファイルだけを消した場合はその系統だけがリセットされます(例: tabs.json を消すと次回起動時に DB タブがすべて閉じた状態になります)。"
19077
+ }
19078
+ ]
19079
+ },
19080
+ {
19081
+ title: "他マシンとの共有",
19082
+ blocks: [
19083
+ {
19084
+ kind: "paragraph",
19085
+ text: "これらはローカル UI 状態なので、原則としてバージョン管理には入れません — .code-viewer/ を .gitignore に追加しておきます。AI のウォークスルーを共有したい場合は annotations.json だけを明示的に commit(または別チェックアウトへコピー)し、それ以外は無視したままにします。"
19086
+ }
19087
+ ]
19088
+ }
19089
+ ]
19090
+ },
18919
19091
  annotations: {
18920
19092
  nav: "AI注釈",
18921
19093
  title: "AI コード注釈",
@@ -19138,7 +19310,7 @@ code-viewer annotate add-db --db app.db --tab query \\
19138
19310
  blocks: [
19139
19311
  {
19140
19312
  kind: "paragraph",
19141
- text: "AI エージェントは CLI から read-only クエリの実行と per-DB クエリ履歴の管理ができます。結果はブラウザでも見える同じ履歴に保存されます。Search / Snapshot / Diff は CLI ではなくブラウザ UI Search タブ / Snapshot タブから操作します。"
19313
+ text: "AI エージェントは CLI から read-only クエリの実行、テーブル横断検索、スナップショット / 差分作成までできます。結果はブラウザで見えるのと同じリポジトリ単位の履歴に保存され、ブラウザ UI 側でも Search タブ / Snapshot タブから同じ操作が行えます。"
19142
19314
  },
19143
19315
  {
19144
19316
  kind: "command",
@@ -19152,9 +19324,40 @@ code-viewer annotate add-db --db app.db --tab query \\
19152
19324
  },
19153
19325
  {
19154
19326
  kind: "command",
19155
- title: "履歴の一覧/削除",
19327
+ title: "履歴の一覧 / 削除",
19156
19328
  command: `code-viewer query list --db app.db --json
19157
19329
  code-viewer query clear --db app.db`
19330
+ },
19331
+ {
19332
+ kind: "command",
19333
+ title: "テーブル横断の全文検索",
19334
+ command: 'code-viewer query search --db app.db --term "john@example.com" \\\n --tables users,orders --include-non-text --max-hits 20'
19335
+ },
19336
+ {
19337
+ kind: "command",
19338
+ title: "スナップショットを作成",
19339
+ command: 'code-viewer query snapshot create --db app.db --tables users,orders \\\n --note "ユーザー登録テスト前"'
19340
+ },
19341
+ {
19342
+ kind: "command",
19343
+ title: "スナップショットの一覧 / メモ更新 / 削除",
19344
+ command: `code-viewer query snapshot list --db app.db --json
19345
+ code-viewer query snapshot note --id snap-abc123 --note "メモを更新"
19346
+ code-viewer query snapshot delete --id snap-abc123`
19347
+ },
19348
+ {
19349
+ kind: "command",
19350
+ title: "2 つのスナップショットを diff",
19351
+ command: `code-viewer query diff create --before snap-abc123 --after snap-def456 \\
19352
+ --note "ユーザー登録テストの差分"
19353
+ code-viewer query diff list --db app.db --json
19354
+ code-viewer query diff delete --id diff-xyz789`
19355
+ },
19356
+ {
19357
+ kind: "command",
19358
+ title: "diff の中身を確認",
19359
+ command: `code-viewer query diff tables --id diff-xyz789
19360
+ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --limit 50`
19158
19361
  },
19159
19362
  {
19160
19363
  kind: "command",
@@ -19168,7 +19371,7 @@ code-viewer query clear --db app.db`
19168
19371
  skills: {
19169
19372
  nav: "スキル登録",
19170
19373
  title: "Agent Skill の登録",
19171
- intro: "このパッケージには、AI エージェントが code-viewer annotate 機能を正しく使うためのスキルが同梱されています。",
19374
+ intro: "このパッケージには 3 つのスキル (code-viewer-annotate / code-viewer-query / code-viewer-snapshot) が同梱されており、AI エージェントに annotate でのウォークスルー、read-only な query、スナップショット / 差分の使い分けを教えます。skill install を 1 回叩くと、選んだエージェントすべてに 3 スキルがコピーされます。",
19172
19375
  groups: [
19173
19376
  {
19174
19377
  title: "スキルをインストールする",
@@ -19193,6 +19396,12 @@ code-viewer query clear --db app.db`
19193
19396
  kind: "command",
19194
19397
  title: "ユーザー全体(プロジェクト外)へ登録",
19195
19398
  command: "npx -y @youtyan/code-viewer skill install --agent all --global"
19399
+ },
19400
+ {
19401
+ kind: "command",
19402
+ title: "別プロジェクトのディレクトリへ登録",
19403
+ command: `npx -y @youtyan/code-viewer skill install --agent all --cwd /path/to/other/repo
19404
+ # --cwd でインストール先プロジェクトを指定。--global を併用したときは無視されます。`
19196
19405
  }
19197
19406
  ]
19198
19407
  },
@@ -20313,7 +20522,7 @@ code-viewer query clear --db app.db`
20313
20522
  } else {
20314
20523
  lnHtml = '<td class="d2h-code-linenumber d2h-cntx">' + '<div class="line-num1">' + (oldStart + i2) + "</div>" + '<div class="line-num2">' + (newStart + i2) + "</div>" + "</td>";
20315
20524
  }
20316
- tr.innerHTML = lnHtml + '<td class="d2h-cntx">' + '<div class="' + (isSplit ? "d2h-code-side-line" : "d2h-code-line") + '">' + '<span class="d2h-code-line-prefix">&nbsp;</span>' + '<span class="d2h-code-line-ctn">' + escapeHtmlText(lines[i2]) + "</span>" + "</div>" + "</td>";
20525
+ tr.innerHTML = lnHtml + '<td class="d2h-cntx">' + '<div class="' + (isSplit ? "d2h-code-side-line" : "d2h-code-line") + '">' + '<span class="d2h-code-line-prefix">&nbsp;</span> ' + '<span class="d2h-code-line-ctn">' + escapeHtmlText(lines[i2]) + "</span>" + "</div>" + "</td>";
20317
20526
  frag.appendChild(tr);
20318
20527
  }
20319
20528
  tbody.insertBefore(frag, anchor);
@@ -21359,7 +21568,7 @@ code-viewer query clear --db app.db`
21359
21568
  li.dataset.path = f2.path;
21360
21569
  li.dataset.type = "blob";
21361
21570
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
21362
- li.classList.toggle("hidden-by-tests", STATE.hideTests && isTestPath(f2.path || ""));
21571
+ li.classList.toggle("hidden-by-tests", STATE.hideTests && !isRepositorySidebarMode() && isTestPath(f2.path || ""));
21363
21572
  li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
21364
21573
  const spacer = document.createElement("span");
21365
21574
  spacer.className = "chev-spacer";
@@ -21458,7 +21667,7 @@ code-viewer query clear --db app.db`
21458
21667
  }
21459
21668
  subtreeVisible = subtreeVisible || visible;
21460
21669
  } else {
21461
- const testHidden = STATE.hideTests && isTestPath(item.file.path || "");
21670
+ const testHidden = STATE.hideTests && !isRepositorySidebarMode() && isTestPath(item.file.path || "");
21462
21671
  const visible = !testHidden && matches(item.file.path || "");
21463
21672
  if (visible) {
21464
21673
  rows.push({
@@ -25460,6 +25669,9 @@ code-viewer query clear --db app.db`
25460
25669
  let highlightLoadPromise = null;
25461
25670
  let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
25462
25671
  let SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = [];
25672
+ let SERVER_SCOPE_WATCH_LIMIT_DEFAULT = 1024;
25673
+ let SERVER_SCOPE_WATCH_LIMIT_MIN = 16;
25674
+ let SERVER_SCOPE_WATCH_LIMIT_MAX = 65536;
25463
25675
  const UNDO_STACK = [];
25464
25676
  let PENDING_G_SCOPE = null;
25465
25677
  let PENDING_G_UNTIL = 0;
@@ -25826,6 +26038,12 @@ code-viewer query clear --db app.db`
25826
26038
  }
25827
26039
  SERVER_SCOPE_OMIT_DIRS_DEFAULT = normalizeScopeOmitDirs(settings.scope.omit_dirs_effective);
25828
26040
  SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = normalizeScopeExcludeNames(settings.scope.exclude_names_effective);
26041
+ if (typeof settings.scope.watch_limit_default === "number")
26042
+ SERVER_SCOPE_WATCH_LIMIT_DEFAULT = settings.scope.watch_limit_default;
26043
+ if (typeof settings.scope.watch_limit_min === "number")
26044
+ SERVER_SCOPE_WATCH_LIMIT_MIN = settings.scope.watch_limit_min;
26045
+ if (typeof settings.scope.watch_limit_max === "number")
26046
+ SERVER_SCOPE_WATCH_LIMIT_MAX = settings.scope.watch_limit_max;
25829
26047
  return settings;
25830
26048
  } catch {
25831
26049
  return null;
@@ -26184,15 +26402,20 @@ code-viewer query clear --db app.db`
26184
26402
  displaySource: "Applies to all projects in this browser.",
26185
26403
  excludedDirectories: "Excluded directories",
26186
26404
  omitDirs: "Skip these directory names while browsing and searching",
26405
+ 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
26406
  excludeNames: "Hide these file or directory names completely",
26407
+ excludeNamesHelp: "Removes matching files or directories from the sidebar, search, and grep results entirely. Unlike Skip, the names themselves disappear from the UI.",
26188
26408
  reset: "Restore defaults",
26189
26409
  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.`,
26410
+ 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
26411
  browserOverride: "Browser override",
26192
26412
  serverDefault: "Server default",
26193
26413
  uploadsTitle: "Uploads",
26194
26414
  uploadEnabledLabel: "Allow file uploads into worktree folders",
26195
- uploadEnabledHelp: "Disable to make the worktree read-only for everyone using this server."
26415
+ uploadEnabledHelp: "Disable to make the worktree read-only for everyone using this server.",
26416
+ watchTitle: "File change watcher",
26417
+ watchLimit: "Maximum directories to watch",
26418
+ 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
26419
  },
26197
26420
  annotations: {
26198
26421
  title: "Code annotations",
@@ -26280,15 +26503,20 @@ code-viewer query clear --db app.db`
26280
26503
  displaySource: "このブラウザのすべてのプロジェクトに適用されます。",
26281
26504
  excludedDirectories: "除外ディレクトリ",
26282
26505
  omitDirs: "閲覧と検索でスキップするディレクトリ名",
26506
+ omitDirsHelp: "これらのディレクトリの中身は読み込みません。サイドバー(Files)・Ctrl+K(ファイル検索)・Ctrl+G(grep)・Datastores・File change watcher の5機能すべてに適用されます。",
26283
26507
  excludeNames: "完全に非表示にするファイル名またはディレクトリ名",
26508
+ excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。",
26284
26509
  reset: "デフォルトに戻す",
26285
26510
  autosaveNote: "変更は自動で保存されます。",
26286
- scopeSource: (project, source) => `このブラウザのプロジェクト "${project}" に保存されます。ソース: ${source}。ツリー、Ctrl+K、Ctrl+G で使われます。「デフォルトに戻す」でブラウザ側の上書きを削除します。`,
26511
+ scopeSource: (project, source) => `このブラウザのプロジェクト "${project}" に保存されます。ソース: ${source}。サイドバー、Ctrl+K、Ctrl+G、Datastores、File change watcher で使われます。「デフォルトに戻す」でブラウザ側の上書きを削除します。`,
26287
26512
  browserOverride: "ブラウザ側の上書き",
26288
26513
  serverDefault: "サーバ既定値",
26289
26514
  uploadsTitle: "アップロード",
26290
26515
  uploadEnabledLabel: "ワークツリーへのファイルアップロードを許可する",
26291
- uploadEnabledHelp: "オフにすると、このサーバを使う全員に対してワークツリーは読み取り専用になります。"
26516
+ uploadEnabledHelp: "オフにすると、このサーバを使う全員に対してワークツリーは読み取り専用になります。",
26517
+ watchTitle: "ファイル変更の監視",
26518
+ watchLimit: "監視するディレクトリ数の上限",
26519
+ watchLimitHelp: (defaultLimit) => `値を大きくすると深いツリーの変更を取りこぼしにくくなりますが、ファイルハンドル数を消費します。上の Skip リストと併用すると、重いフォルダ(node_modules, .git, dist など)を監視枠から外せます。既定値: ${defaultLimit}。`
26292
26520
  },
26293
26521
  annotations: {
26294
26522
  title: "コード注釈",
@@ -26406,14 +26634,20 @@ code-viewer query clear --db app.db`
26406
26634
  settingsSections[1].textContent = text2.settings.uploadsTitle;
26407
26635
  if (settingsSections[2])
26408
26636
  settingsSections[2].textContent = text2.settings.excludedDirectories;
26637
+ if (settingsSections[3])
26638
+ settingsSections[3].textContent = text2.settings.watchTitle;
26409
26639
  setElementText("#upload-enabled-label", text2.settings.uploadEnabledLabel);
26410
26640
  setElementText("#upload-help", text2.settings.uploadEnabledHelp);
26641
+ setElementText("#scope-omit-dirs-help", text2.settings.omitDirsHelp);
26642
+ setElementText("#scope-exclude-names-help", text2.settings.excludeNamesHelp);
26643
+ setElementText("#scope-watch-limit-help", text2.settings.watchLimitHelp(SERVER_SCOPE_WATCH_LIMIT_DEFAULT));
26411
26644
  const labelMap = {
26412
26645
  "viewer-language": text2.settings.language,
26413
26646
  "sidebar-font-size": text2.settings.fileListFontSize,
26414
26647
  "code-font-size": text2.settings.codeFontSize,
26415
26648
  "scope-omit-dirs": text2.settings.omitDirs,
26416
- "scope-exclude-names": text2.settings.excludeNames
26649
+ "scope-exclude-names": text2.settings.excludeNames,
26650
+ "scope-watch-limit": text2.settings.watchLimit
26417
26651
  };
26418
26652
  Object.entries(labelMap).forEach(([id, label]) => {
26419
26653
  const labelEl = document.querySelector(`label[for="${id}"]`);
@@ -26597,6 +26831,19 @@ code-viewer query clear --db app.db`
26597
26831
  `);
26598
26832
  excludeInput.value = effectiveScopeExcludeNames().join(`
26599
26833
  `);
26834
+ const watchLimitInput = document.querySelector("#scope-watch-limit");
26835
+ const watchLimitRange = document.querySelector("#scope-watch-limit-range");
26836
+ const watchLimitValue = effectiveScopeWatchLimit();
26837
+ if (watchLimitInput) {
26838
+ watchLimitInput.min = String(SERVER_SCOPE_WATCH_LIMIT_MIN);
26839
+ watchLimitInput.max = String(SERVER_SCOPE_WATCH_LIMIT_MAX);
26840
+ watchLimitInput.value = String(watchLimitValue);
26841
+ }
26842
+ if (watchLimitRange) {
26843
+ watchLimitRange.min = String(SERVER_SCOPE_WATCH_LIMIT_MIN);
26844
+ watchLimitRange.max = String(SERVER_SCOPE_WATCH_LIMIT_MAX);
26845
+ watchLimitRange.value = String(watchLimitValue);
26846
+ }
26600
26847
  const uploadToggle = document.querySelector("#upload-enabled");
26601
26848
  if (uploadToggle)
26602
26849
  uploadToggle.checked = APP_SETTINGS.uploadEnabled !== false;
@@ -26653,6 +26900,41 @@ code-viewer query clear --db app.db`
26653
26900
  refreshScopeSourceLabel();
26654
26901
  refreshRepositoryTreeAfterSettings();
26655
26902
  }
26903
+ function normalizeScopeWatchLimit(value) {
26904
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
26905
+ if (!Number.isFinite(parsed))
26906
+ return null;
26907
+ const floored = Math.floor(parsed);
26908
+ if (floored < SERVER_SCOPE_WATCH_LIMIT_MIN)
26909
+ return SERVER_SCOPE_WATCH_LIMIT_MIN;
26910
+ if (floored > SERVER_SCOPE_WATCH_LIMIT_MAX)
26911
+ return SERVER_SCOPE_WATCH_LIMIT_MAX;
26912
+ return floored;
26913
+ }
26914
+ function effectiveScopeWatchLimit() {
26915
+ const saved = normalizeScopeWatchLimit(APP_SETTINGS.scopeWatchLimit);
26916
+ return saved ?? SERVER_SCOPE_WATCH_LIMIT_DEFAULT;
26917
+ }
26918
+ function syncScopeWatchLimitInputs(value) {
26919
+ const numberInput = document.querySelector("#scope-watch-limit");
26920
+ const rangeInput = document.querySelector("#scope-watch-limit-range");
26921
+ if (numberInput && numberInput.value !== String(value))
26922
+ numberInput.value = String(value);
26923
+ if (rangeInput && rangeInput.value !== String(value))
26924
+ rangeInput.value = String(value);
26925
+ }
26926
+ function saveScopeWatchLimitField(value) {
26927
+ const next = normalizeScopeWatchLimit(value);
26928
+ const resolved = next ?? SERVER_SCOPE_WATCH_LIMIT_DEFAULT;
26929
+ mergeLocalSettings({ scopeWatchLimit: resolved });
26930
+ patchSettings({ scopeWatchLimit: resolved });
26931
+ syncScopeWatchLimitInputs(resolved);
26932
+ }
26933
+ function previewScopeWatchLimit(value) {
26934
+ const next = normalizeScopeWatchLimit(value);
26935
+ if (next != null)
26936
+ syncScopeWatchLimitInputs(next);
26937
+ }
26656
26938
  function resetScopeSettings() {
26657
26939
  setViewerLanguage("en", false);
26658
26940
  mergeLocalSettings({
@@ -26660,6 +26942,7 @@ code-viewer query clear --db app.db`
26660
26942
  codeFontSize: null,
26661
26943
  scopeOmitDirs: null,
26662
26944
  scopeExcludeNames: null,
26945
+ scopeWatchLimit: null,
26663
26946
  uploadEnabled: null
26664
26947
  });
26665
26948
  applySidebarFontSize("regular");
@@ -26670,6 +26953,7 @@ code-viewer query clear --db app.db`
26670
26953
  codeFontSize: null,
26671
26954
  scopeOmitDirs: null,
26672
26955
  scopeExcludeNames: null,
26956
+ scopeWatchLimit: null,
26673
26957
  uploadEnabled: null
26674
26958
  });
26675
26959
  const sidebarFontSize = document.querySelector("#sidebar-font-size");
@@ -26848,6 +27132,7 @@ code-viewer query clear --db app.db`
26848
27132
  if (STATE.route.screen !== "database") {
26849
27133
  setQueryHistoryPanelOpen(false);
26850
27134
  }
27135
+ applyHideTests();
26851
27136
  }
26852
27137
  function syncHeaderMenu() {
26853
27138
  document.querySelectorAll(".app-menu-item, .global-help-link").forEach((link2) => {
@@ -27087,6 +27372,15 @@ code-viewer query clear --db app.db`
27087
27372
  $("#scope-exclude-names")?.addEventListener("change", (event) => {
27088
27373
  saveScopeExcludeNamesField(event.currentTarget.value);
27089
27374
  });
27375
+ $("#scope-watch-limit")?.addEventListener("change", (event) => {
27376
+ saveScopeWatchLimitField(event.currentTarget.value);
27377
+ });
27378
+ $("#scope-watch-limit-range")?.addEventListener("input", (event) => {
27379
+ previewScopeWatchLimit(event.currentTarget.value);
27380
+ });
27381
+ $("#scope-watch-limit-range")?.addEventListener("change", (event) => {
27382
+ saveScopeWatchLimitField(event.currentTarget.value);
27383
+ });
27090
27384
  $("#scope-settings-popover")?.addEventListener("keydown", (e2) => {
27091
27385
  if (isImeComposing(e2))
27092
27386
  return;
@@ -27740,13 +28034,14 @@ code-viewer query clear --db app.db`
27740
28034
  const btn = $("#hide-tests");
27741
28035
  if (btn)
27742
28036
  btn.classList.toggle("active", STATE.hideTests);
28037
+ const effective = STATE.hideTests && !isRepositorySidebarMode();
27743
28038
  document.querySelectorAll(".gdp-file-shell").forEach((card) => {
27744
28039
  const isTest = TEST_RE.test(card.dataset.path || "");
27745
- card.classList.toggle("hidden-by-tests", STATE.hideTests && isTest);
28040
+ card.classList.toggle("hidden-by-tests", effective && isTest);
27746
28041
  });
27747
28042
  document.querySelectorAll("#filelist li[data-path]").forEach((li) => {
27748
28043
  const isTest = TEST_RE.test(li.dataset.path || "");
27749
- li.classList.toggle("hidden-by-tests", STATE.hideTests && isTest);
28044
+ li.classList.toggle("hidden-by-tests", effective && isTest);
27750
28045
  });
27751
28046
  if (isVirtualSidebarActive())
27752
28047
  rerenderVirtualSidebar();
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;