@youtyan/code-viewer 0.2.8 → 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.
@@ -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",
@@ -3726,509 +3736,283 @@ var init_search = __esm(() => {
3726
3736
  DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
3727
3737
  });
3728
3738
 
3729
- // web-src/server/worktree-watcher.ts
3730
- import {
3731
- lstatSync as lstatSync3,
3732
- readdirSync as nodeReaddirSync,
3733
- watch as nodeWatch
3734
- } from "node:fs";
3735
- import { join as join7, relative } from "node:path";
3736
- function normalizeRelativePath(path) {
3737
- return path.replace(/\\/g, "/").replace(/^\/+/, "");
3739
+ // web-src/server/state-store.ts
3740
+ import { join as join7 } from "node:path";
3741
+ function codeViewerPath(root, fileName) {
3742
+ return join7(root, CODE_VIEWER_DIR2, fileName);
3738
3743
  }
3739
- function isInsideRoot(root, path) {
3740
- const rel = relative(root, path).replace(/\\/g, "/");
3741
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
3744
+ function isRecord(value) {
3745
+ return !!value && typeof value === "object" && !Array.isArray(value);
3742
3746
  }
3743
- function startWorktreeUpdateWatch(options) {
3744
- const watch = options.watch || nodeWatch;
3745
- const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
3746
- const isDirectory = options.isDirectory || ((path) => {
3747
- try {
3748
- return lstatSync3(path).isDirectory();
3749
- } catch {
3750
- return false;
3747
+ function optionalString(value, maxLen) {
3748
+ if (typeof value !== "string")
3749
+ return;
3750
+ if (value.length === 0 || value.length > maxLen)
3751
+ return;
3752
+ if (value.includes("\x00"))
3753
+ return;
3754
+ return value;
3755
+ }
3756
+ function optionalBoolean(value) {
3757
+ return typeof value === "boolean" ? value : undefined;
3758
+ }
3759
+ function optionalNumber(value, min, max) {
3760
+ if (typeof value !== "number" || !Number.isFinite(value))
3761
+ return;
3762
+ return Math.max(min, Math.min(max, Math.round(value)));
3763
+ }
3764
+ function optionalFloat(value, min, max) {
3765
+ if (typeof value !== "number" || !Number.isFinite(value))
3766
+ return;
3767
+ return Math.max(min, Math.min(max, value));
3768
+ }
3769
+ function optionalFontSize(value) {
3770
+ return value === "compact" || value === "regular" || value === "large" || value === "xlarge" ? value : undefined;
3771
+ }
3772
+ function normalizeStringList(value, options) {
3773
+ if (!Array.isArray(value))
3774
+ return;
3775
+ const out = [];
3776
+ const seen = new Set;
3777
+ const items = options.keepLast ? [...value].reverse() : value;
3778
+ for (const item of items) {
3779
+ if (out.length >= options.maxItems)
3780
+ break;
3781
+ if (typeof item !== "string")
3782
+ continue;
3783
+ const name = item.trim();
3784
+ if (!name || name.length > options.maxLen || name.includes("\x00"))
3785
+ continue;
3786
+ if (options.pathSafe && (name.includes("/") || name.includes("\\") || name === "." || name === ".." || name === ".git")) {
3787
+ continue;
3751
3788
  }
3789
+ if (seen.has(name))
3790
+ continue;
3791
+ seen.add(name);
3792
+ out.push(name);
3793
+ }
3794
+ if (options.keepLast)
3795
+ out.reverse();
3796
+ return options.sort === false ? out : out.sort((a, b) => a.localeCompare(b));
3797
+ }
3798
+ function emptySettings() {
3799
+ return { version: 1 };
3800
+ }
3801
+ function emptyViewState() {
3802
+ return {
3803
+ version: 1,
3804
+ collapsedDirs: [],
3805
+ lazyExpandedDirs: [],
3806
+ viewedFiles: []
3807
+ };
3808
+ }
3809
+ function emptyDbUiState() {
3810
+ return { version: 1, columnWidths: {} };
3811
+ }
3812
+ function sanitizeSettings(raw) {
3813
+ if (!isRecord(raw))
3814
+ return emptySettings();
3815
+ const out = { version: 1 };
3816
+ if (raw.layout === "side-by-side" || raw.layout === "line-by-line")
3817
+ out.layout = raw.layout;
3818
+ if (raw.theme === "light" || raw.theme === "dark")
3819
+ out.theme = raw.theme;
3820
+ if (raw.language === "en" || raw.language === "ja")
3821
+ out.language = raw.language;
3822
+ if (raw.sidebarView === "tree" || raw.sidebarView === "flat")
3823
+ out.sidebarView = raw.sidebarView;
3824
+ const sidebarWidth = optionalNumber(raw.sidebarWidth, 180, 900);
3825
+ if (sidebarWidth !== undefined)
3826
+ out.sidebarWidth = sidebarWidth;
3827
+ const historyWidth = optionalNumber(raw.historyWidth, 220, 640);
3828
+ if (historyWidth !== undefined)
3829
+ out.historyWidth = historyWidth;
3830
+ const sidebarHidden = optionalBoolean(raw.sidebarHidden);
3831
+ if (sidebarHidden !== undefined)
3832
+ out.sidebarHidden = sidebarHidden;
3833
+ const sidebarFontSize = optionalFontSize(raw.sidebarFontSize);
3834
+ if (sidebarFontSize)
3835
+ out.sidebarFontSize = sidebarFontSize;
3836
+ const codeFontSize = optionalFontSize(raw.codeFontSize);
3837
+ if (codeFontSize)
3838
+ out.codeFontSize = codeFontSize;
3839
+ const syntaxHighlight = optionalBoolean(raw.syntaxHighlight);
3840
+ if (syntaxHighlight !== undefined)
3841
+ out.syntaxHighlight = syntaxHighlight;
3842
+ const autoUpdate = optionalBoolean(raw.autoUpdate);
3843
+ if (autoUpdate !== undefined)
3844
+ out.autoUpdate = autoUpdate;
3845
+ const queryHistoryPanelWidth = optionalNumber(raw.queryHistoryPanelWidth, 280, 800);
3846
+ if (queryHistoryPanelWidth !== undefined)
3847
+ out.queryHistoryPanelWidth = queryHistoryPanelWidth;
3848
+ const annotationPanelOpen = optionalBoolean(raw.annotationPanelOpen);
3849
+ if (annotationPanelOpen !== undefined)
3850
+ out.annotationPanelOpen = annotationPanelOpen;
3851
+ const annotationFollow = optionalBoolean(raw.annotationFollow);
3852
+ if (annotationFollow !== undefined)
3853
+ out.annotationFollow = annotationFollow;
3854
+ const annotationMuted = optionalBoolean(raw.annotationMuted);
3855
+ if (annotationMuted !== undefined)
3856
+ out.annotationMuted = annotationMuted;
3857
+ const annotationRate = optionalFloat(raw.annotationRate, 0.5, 2);
3858
+ if (annotationRate !== undefined)
3859
+ out.annotationRate = annotationRate;
3860
+ const ignoreWhitespace = optionalBoolean(raw.ignoreWhitespace);
3861
+ if (ignoreWhitespace !== undefined)
3862
+ out.ignoreWhitespace = ignoreWhitespace;
3863
+ const hideTests = optionalBoolean(raw.hideTests);
3864
+ if (hideTests !== undefined)
3865
+ out.hideTests = hideTests;
3866
+ const scopeOmitDirs = normalizeStringList(raw.scopeOmitDirs, {
3867
+ maxItems: 100,
3868
+ maxLen: 64,
3869
+ pathSafe: true
3752
3870
  });
3753
- const directorySignature = options.directorySignature || ((path) => {
3754
- try {
3755
- const stats = lstatSync3(path);
3756
- if (!stats.isDirectory())
3757
- return null;
3758
- return `${stats.dev}:${stats.ino}`;
3759
- } catch {
3760
- return null;
3761
- }
3871
+ if (scopeOmitDirs)
3872
+ out.scopeOmitDirs = scopeOmitDirs;
3873
+ const scopeExcludeNames = normalizeStringList(raw.scopeExcludeNames, {
3874
+ maxItems: 200,
3875
+ maxLen: 128,
3876
+ pathSafe: true
3762
3877
  });
3763
- const setTimer = options.setTimeoutFn || setTimeout;
3764
- const clearTimer = options.clearTimeoutFn || clearTimeout;
3765
- const debounceMs = options.debounceMs ?? 250;
3766
- const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
3767
- const watchers = new Map;
3768
- const signatures = new Map;
3769
- const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
3770
- const initialScanQueue = [];
3771
- let initialScanTimer = null;
3772
- const pendingPathInspections = new Map;
3773
- let pathInspectionTimer = null;
3774
- let timer = null;
3775
- const pendingChangedPaths = new Set;
3776
- let watchLimitReported = false;
3777
- const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
3778
- const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
3779
- const ignoredDirectory = (dir) => {
3780
- const rel = directoryRelativePath(dir);
3781
- return Boolean(rel && ignored(rel));
3782
- };
3783
- const scheduleUpdate = (changedPath) => {
3784
- if (changedPath)
3785
- pendingChangedPaths.add(changedPath);
3786
- if (timer)
3787
- clearTimer(timer);
3788
- timer = setTimer(() => {
3789
- timer = null;
3790
- const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
3791
- pendingChangedPaths.clear();
3792
- options.onUpdate(paths);
3793
- }, debounceMs);
3794
- };
3795
- const reportWatchLimit = () => {
3796
- if (watchLimitReported)
3797
- return;
3798
- watchLimitReported = true;
3799
- options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
3800
- };
3801
- const closeSubtree = (dir) => {
3802
- for (const [watchedDir, watcher] of [...watchers]) {
3803
- if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
3804
- continue;
3805
- try {
3806
- watcher.close?.();
3807
- } catch {}
3808
- watchers.delete(watchedDir);
3809
- signatures.delete(watchedDir);
3810
- }
3878
+ if (scopeExcludeNames)
3879
+ out.scopeExcludeNames = scopeExcludeNames;
3880
+ const uploadEnabled = optionalBoolean(raw.uploadEnabled);
3881
+ if (uploadEnabled !== undefined)
3882
+ out.uploadEnabled = uploadEnabled;
3883
+ if (isRecord(raw.range)) {
3884
+ const from = optionalString(raw.range.from, MAX_REF_LEN);
3885
+ const to = optionalString(raw.range.to, MAX_REF_LEN);
3886
+ if (from && to)
3887
+ out.range = { from, to };
3888
+ }
3889
+ return out;
3890
+ }
3891
+ function mergeSettings(current, patch) {
3892
+ if (!isRecord(patch))
3893
+ return current;
3894
+ const raw = { ...current };
3895
+ for (const [key, value] of Object.entries(patch)) {
3896
+ if (key === "version")
3897
+ continue;
3898
+ if (value === null)
3899
+ delete raw[key];
3900
+ else
3901
+ raw[key] = value;
3902
+ }
3903
+ return sanitizeSettings({ ...raw, version: 1 });
3904
+ }
3905
+ function sanitizeViewState(raw) {
3906
+ if (!isRecord(raw))
3907
+ return emptyViewState();
3908
+ return {
3909
+ version: 1,
3910
+ collapsedDirs: normalizeStringList(raw.collapsedDirs, {
3911
+ maxItems: MAX_VIEW_ITEMS,
3912
+ maxLen: MAX_KEY_LEN,
3913
+ keepLast: true,
3914
+ sort: false
3915
+ }) ?? [],
3916
+ lazyExpandedDirs: normalizeStringList(raw.lazyExpandedDirs, {
3917
+ maxItems: MAX_VIEW_ITEMS,
3918
+ maxLen: MAX_KEY_LEN,
3919
+ keepLast: true,
3920
+ sort: false
3921
+ }) ?? [],
3922
+ viewedFiles: normalizeStringList(raw.viewedFiles, {
3923
+ maxItems: MAX_VIEW_ITEMS,
3924
+ maxLen: MAX_KEY_LEN,
3925
+ keepLast: true,
3926
+ sort: false
3927
+ }) ?? []
3811
3928
  };
3812
- const closeAll = () => {
3813
- if (initialScanTimer) {
3814
- clearTimer(initialScanTimer);
3815
- initialScanTimer = null;
3816
- }
3817
- if (pathInspectionTimer) {
3818
- clearTimer(pathInspectionTimer);
3819
- pathInspectionTimer = null;
3820
- }
3821
- initialScanQueue.length = 0;
3822
- pendingPathInspections.clear();
3823
- for (const watcher of [...watchers.values()]) {
3824
- try {
3825
- watcher.close?.();
3826
- } catch {}
3827
- }
3828
- watchers.clear();
3829
- signatures.clear();
3830
- };
3831
- const readChildDirectories = (dir) => {
3832
- let entries;
3833
- try {
3834
- entries = readDirs(dir);
3835
- } catch (error) {
3836
- options.onError?.(error);
3837
- return [];
3838
- }
3839
- const children = [];
3840
- for (const entry of entries) {
3841
- if (!entry.isDirectory())
3842
- continue;
3843
- const child = join7(dir, entry.name);
3844
- if (ignoredDirectory(child))
3845
- continue;
3846
- children.push(child);
3847
- }
3848
- return children;
3849
- };
3850
- const processInitialScanQueue = () => {
3851
- initialScanTimer = null;
3852
- if (watchers.size >= maxWatchedDirectories) {
3853
- reportWatchLimit();
3854
- initialScanQueue.length = 0;
3855
- return;
3856
- }
3857
- const next = initialScanQueue.shift();
3858
- if (next)
3859
- watchDirectory(next, true);
3860
- if (watchers.size >= maxWatchedDirectories) {
3861
- reportWatchLimit();
3862
- initialScanQueue.length = 0;
3863
- }
3864
- if (initialScanQueue.length)
3865
- initialScanTimer = setTimer(processInitialScanQueue, 50);
3866
- };
3867
- const queueInitialChildren = (dir) => {
3868
- const remaining = maxWatchedDirectories - watchers.size;
3869
- if (remaining <= 0) {
3870
- reportWatchLimit();
3871
- return;
3872
- }
3873
- const children = readChildDirectories(dir);
3874
- if (children.length > remaining)
3875
- reportWatchLimit();
3876
- initialScanQueue.push(...children.slice(0, remaining));
3877
- if (!initialScanTimer)
3878
- initialScanTimer = setTimer(processInitialScanQueue, 5000);
3879
- };
3880
- const processChangedPath = (changed, fullChangedPath) => {
3881
- const known = watchers.has(fullChangedPath);
3882
- if (isDirectory(fullChangedPath)) {
3883
- if (known) {
3884
- const signature = directorySignature(fullChangedPath);
3885
- if (signature && signature !== signatures.get(fullChangedPath)) {
3886
- closeSubtree(fullChangedPath);
3887
- watchDirectory(fullChangedPath, initialScanAsync);
3888
- }
3889
- scheduleUpdate(changed);
3890
- return;
3891
- }
3892
- watchDirectory(fullChangedPath, initialScanAsync);
3893
- } else if (known) {
3894
- closeSubtree(fullChangedPath);
3895
- }
3896
- scheduleUpdate(changed);
3897
- };
3898
- const processPathInspections = () => {
3899
- pathInspectionTimer = null;
3900
- const entries = [...pendingPathInspections];
3901
- pendingPathInspections.clear();
3902
- for (const [changed, fullChangedPath] of entries) {
3903
- processChangedPath(changed, fullChangedPath);
3904
- }
3905
- };
3906
- const queuePathInspection = (changed, fullChangedPath) => {
3907
- pendingPathInspections.set(changed, fullChangedPath);
3908
- if (!pathInspectionTimer)
3909
- pathInspectionTimer = setTimer(processPathInspections, 25);
3910
- };
3911
- const watchDirectory = (dir, initialScan = false) => {
3912
- if (watchers.has(dir))
3913
- return;
3914
- if (watchers.size >= maxWatchedDirectories) {
3915
- reportWatchLimit();
3916
- return;
3917
- }
3918
- const rel = directoryRelativePath(dir);
3919
- if (rel && ignored(rel))
3920
- return;
3921
- try {
3922
- const watcher = watch(dir, { persistent: false }, (_event, filename) => {
3923
- if (!filename) {
3924
- scheduleUpdate();
3925
- return;
3926
- }
3927
- const changed = normalizeRelativePath(join7(rel, filename.toString()));
3928
- if (ignored(changed))
3929
- return;
3930
- const fullChangedPath = join7(options.root, changed);
3931
- if (!isInsideRoot(options.root, fullChangedPath))
3932
- return;
3933
- if (initialScanAsync) {
3934
- queuePathInspection(changed, fullChangedPath);
3935
- return;
3936
- }
3937
- processChangedPath(changed, fullChangedPath);
3938
- }) || {};
3939
- watchers.set(dir, watcher);
3940
- const signature = directorySignature(dir);
3941
- if (signature)
3942
- signatures.set(dir, signature);
3943
- watcher.on?.("error", () => {
3944
- if (watchers.get(dir) === watcher) {
3945
- watchers.delete(dir);
3946
- signatures.delete(dir);
3947
- }
3948
- });
3949
- watcher.on?.("close", () => {
3950
- if (watchers.get(dir) === watcher) {
3951
- watchers.delete(dir);
3952
- signatures.delete(dir);
3953
- }
3954
- });
3955
- } catch (error) {
3956
- options.onError?.(error);
3957
- return;
3958
- }
3959
- if (initialScanAsync && initialScan) {
3960
- queueInitialChildren(dir);
3961
- return;
3962
- }
3963
- if (watchers.size >= maxWatchedDirectories) {
3964
- reportWatchLimit();
3965
- return;
3966
- }
3967
- for (const child of readChildDirectories(dir))
3968
- watchDirectory(child);
3969
- };
3970
- watchDirectory(options.root, true);
3971
- return { started: watchers.size > 0, close: closeAll };
3972
- }
3973
- var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 256;
3974
- var init_worktree_watcher = __esm(() => {
3975
- init_search();
3976
- });
3977
-
3978
- // web-src/core/control-chars.ts
3979
- function hasControlCharacter(value) {
3980
- for (const ch of value) {
3981
- const code = ch.charCodeAt(0);
3982
- if (code < 32 || code === 127)
3983
- return true;
3984
- }
3985
- return false;
3986
- }
3987
-
3988
- // web-src/core/id.ts
3989
- function bytesToHex(bytes) {
3990
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
3991
3929
  }
3992
- function makeId(prefix) {
3993
- const cryptoApi = globalThis.crypto;
3994
- if (typeof cryptoApi?.randomUUID === "function") {
3995
- return `${prefix}-${cryptoApi.randomUUID().replace(/-/g, "").slice(0, 16)}`;
3930
+ function mergeViewState(current, patch) {
3931
+ if (!isRecord(patch))
3932
+ return current;
3933
+ const base = sanitizeViewState({ ...current, version: 1 });
3934
+ const collapsedDirs = new Set(base.collapsedDirs);
3935
+ const lazyExpandedDirs = new Set(base.lazyExpandedDirs);
3936
+ const viewedFiles = new Set(base.viewedFiles);
3937
+ const addedCollapsedDirs = normalizeStringList(patch.addedCollapsedDirs, {
3938
+ maxItems: MAX_VIEW_ITEMS,
3939
+ maxLen: MAX_KEY_LEN,
3940
+ keepLast: true,
3941
+ sort: false
3942
+ });
3943
+ for (const path of addedCollapsedDirs || []) {
3944
+ collapsedDirs.add(path);
3945
+ lazyExpandedDirs.delete(path);
3996
3946
  }
3997
- if (typeof cryptoApi?.getRandomValues === "function") {
3998
- const bytes = new Uint8Array(8);
3999
- cryptoApi.getRandomValues(bytes);
4000
- return `${prefix}-${bytesToHex(bytes)}`;
3947
+ const removedCollapsedDirs = normalizeStringList(patch.removedCollapsedDirs, {
3948
+ maxItems: MAX_VIEW_ITEMS,
3949
+ maxLen: MAX_KEY_LEN,
3950
+ sort: false
3951
+ });
3952
+ for (const path of removedCollapsedDirs || [])
3953
+ collapsedDirs.delete(path);
3954
+ const addedLazyExpandedDirs = normalizeStringList(patch.addedLazyExpandedDirs, {
3955
+ maxItems: MAX_VIEW_ITEMS,
3956
+ maxLen: MAX_KEY_LEN,
3957
+ keepLast: true,
3958
+ sort: false
3959
+ });
3960
+ for (const path of addedLazyExpandedDirs || []) {
3961
+ if (!collapsedDirs.has(path))
3962
+ lazyExpandedDirs.add(path);
4001
3963
  }
4002
- return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
4003
- }
4004
-
4005
- // web-src/server/state-store.ts
4006
- import { join as join8 } from "node:path";
4007
- function codeViewerPath(root, fileName) {
4008
- return join8(root, CODE_VIEWER_DIR2, fileName);
4009
- }
4010
- function isRecord(value) {
4011
- return !!value && typeof value === "object" && !Array.isArray(value);
3964
+ const removedLazyExpandedDirs = normalizeStringList(patch.removedLazyExpandedDirs, {
3965
+ maxItems: MAX_VIEW_ITEMS,
3966
+ maxLen: MAX_KEY_LEN,
3967
+ sort: false
3968
+ });
3969
+ for (const path of removedLazyExpandedDirs || [])
3970
+ lazyExpandedDirs.delete(path);
3971
+ const addedViewedFiles = normalizeStringList(patch.addedViewedFiles, {
3972
+ maxItems: MAX_VIEW_ITEMS,
3973
+ maxLen: MAX_KEY_LEN,
3974
+ keepLast: true,
3975
+ sort: false
3976
+ });
3977
+ for (const path of addedViewedFiles || [])
3978
+ viewedFiles.add(path);
3979
+ const removedViewedFiles = normalizeStringList(patch.removedViewedFiles, {
3980
+ maxItems: MAX_VIEW_ITEMS,
3981
+ maxLen: MAX_KEY_LEN,
3982
+ sort: false
3983
+ });
3984
+ for (const path of removedViewedFiles || [])
3985
+ viewedFiles.delete(path);
3986
+ return sanitizeViewState({
3987
+ version: 1,
3988
+ collapsedDirs: [...collapsedDirs],
3989
+ lazyExpandedDirs: [...lazyExpandedDirs],
3990
+ viewedFiles: [...viewedFiles]
3991
+ });
4012
3992
  }
4013
- function optionalString(value, maxLen) {
4014
- if (typeof value !== "string")
4015
- return;
4016
- if (value.length === 0 || value.length > maxLen)
4017
- return;
4018
- if (value.includes("\x00"))
4019
- return;
3993
+ function safeObjectKey(value) {
3994
+ if (!value || value.length > MAX_KEY_LEN || value.includes("\x00"))
3995
+ return null;
4020
3996
  return value;
4021
3997
  }
4022
- function optionalBoolean(value) {
4023
- return typeof value === "boolean" ? value : undefined;
4024
- }
4025
- function optionalNumber(value, min, max) {
4026
- if (typeof value !== "number" || !Number.isFinite(value))
3998
+ function sanitizeDbUiPrefs(raw) {
3999
+ if (!isRecord(raw))
4027
4000
  return;
4028
- return Math.max(min, Math.min(max, Math.round(value)));
4029
- }
4030
- function optionalFloat(value, min, max) {
4031
- if (typeof value !== "number" || !Number.isFinite(value))
4032
- return;
4033
- return Math.max(min, Math.min(max, value));
4034
- }
4035
- function optionalFontSize(value) {
4036
- return value === "compact" || value === "regular" || value === "large" || value === "xlarge" ? value : undefined;
4037
- }
4038
- function normalizeStringList(value, options) {
4039
- if (!Array.isArray(value))
4040
- return;
4041
- const out = [];
4042
- const seen = new Set;
4043
- const items = options.keepLast ? [...value].reverse() : value;
4044
- for (const item of items) {
4045
- if (out.length >= options.maxItems)
4046
- break;
4047
- if (typeof item !== "string")
4048
- continue;
4049
- const name = item.trim();
4050
- if (!name || name.length > options.maxLen || name.includes("\x00"))
4051
- continue;
4052
- if (options.pathSafe && (name.includes("/") || name.includes("\\") || name === "." || name === ".." || name === ".git")) {
4053
- continue;
4054
- }
4055
- if (seen.has(name))
4056
- continue;
4057
- seen.add(name);
4058
- out.push(name);
4059
- }
4060
- if (options.keepLast)
4061
- out.reverse();
4062
- return options.sort === false ? out : out.sort((a, b) => a.localeCompare(b));
4063
- }
4064
- function emptySettings() {
4065
- return { version: 1 };
4066
- }
4067
- function emptyViewState() {
4068
- return { version: 1, collapsedDirs: [], viewedFiles: [] };
4069
- }
4070
- function emptyDbUiState() {
4071
- return { version: 1, columnWidths: {} };
4072
- }
4073
- function sanitizeSettings(raw) {
4074
- if (!isRecord(raw))
4075
- return emptySettings();
4076
- const out = { version: 1 };
4077
- if (raw.layout === "side-by-side" || raw.layout === "line-by-line")
4078
- out.layout = raw.layout;
4079
- if (raw.theme === "light" || raw.theme === "dark")
4080
- out.theme = raw.theme;
4081
- if (raw.language === "en" || raw.language === "ja")
4082
- out.language = raw.language;
4083
- if (raw.sidebarView === "tree" || raw.sidebarView === "flat")
4084
- out.sidebarView = raw.sidebarView;
4085
- const sidebarWidth = optionalNumber(raw.sidebarWidth, 180, 900);
4086
- if (sidebarWidth !== undefined)
4087
- out.sidebarWidth = sidebarWidth;
4088
- const historyWidth = optionalNumber(raw.historyWidth, 220, 640);
4089
- if (historyWidth !== undefined)
4090
- out.historyWidth = historyWidth;
4091
- const sidebarHidden = optionalBoolean(raw.sidebarHidden);
4092
- if (sidebarHidden !== undefined)
4093
- out.sidebarHidden = sidebarHidden;
4094
- const sidebarFontSize = optionalFontSize(raw.sidebarFontSize);
4095
- if (sidebarFontSize)
4096
- out.sidebarFontSize = sidebarFontSize;
4097
- const codeFontSize = optionalFontSize(raw.codeFontSize);
4098
- if (codeFontSize)
4099
- out.codeFontSize = codeFontSize;
4100
- const syntaxHighlight = optionalBoolean(raw.syntaxHighlight);
4101
- if (syntaxHighlight !== undefined)
4102
- out.syntaxHighlight = syntaxHighlight;
4103
- const autoUpdate = optionalBoolean(raw.autoUpdate);
4104
- if (autoUpdate !== undefined)
4105
- out.autoUpdate = autoUpdate;
4106
- const queryHistoryPanelWidth = optionalNumber(raw.queryHistoryPanelWidth, 280, 800);
4107
- if (queryHistoryPanelWidth !== undefined)
4108
- out.queryHistoryPanelWidth = queryHistoryPanelWidth;
4109
- const annotationPanelOpen = optionalBoolean(raw.annotationPanelOpen);
4110
- if (annotationPanelOpen !== undefined)
4111
- out.annotationPanelOpen = annotationPanelOpen;
4112
- const annotationFollow = optionalBoolean(raw.annotationFollow);
4113
- if (annotationFollow !== undefined)
4114
- out.annotationFollow = annotationFollow;
4115
- const annotationMuted = optionalBoolean(raw.annotationMuted);
4116
- if (annotationMuted !== undefined)
4117
- out.annotationMuted = annotationMuted;
4118
- const annotationRate = optionalFloat(raw.annotationRate, 0.5, 2);
4119
- if (annotationRate !== undefined)
4120
- out.annotationRate = annotationRate;
4121
- const ignoreWhitespace = optionalBoolean(raw.ignoreWhitespace);
4122
- if (ignoreWhitespace !== undefined)
4123
- out.ignoreWhitespace = ignoreWhitespace;
4124
- const hideTests = optionalBoolean(raw.hideTests);
4125
- if (hideTests !== undefined)
4126
- out.hideTests = hideTests;
4127
- const scopeOmitDirs = normalizeStringList(raw.scopeOmitDirs, {
4128
- maxItems: 100,
4129
- maxLen: 64,
4130
- pathSafe: true
4131
- });
4132
- if (scopeOmitDirs)
4133
- out.scopeOmitDirs = scopeOmitDirs;
4134
- const scopeExcludeNames = normalizeStringList(raw.scopeExcludeNames, {
4135
- maxItems: 200,
4136
- maxLen: 128,
4137
- pathSafe: true
4138
- });
4139
- if (scopeExcludeNames)
4140
- out.scopeExcludeNames = scopeExcludeNames;
4141
- if (isRecord(raw.range)) {
4142
- const from = optionalString(raw.range.from, MAX_REF_LEN);
4143
- const to = optionalString(raw.range.to, MAX_REF_LEN);
4144
- if (from && to)
4145
- out.range = { from, to };
4146
- }
4147
- return out;
4148
- }
4149
- function mergeSettings(current, patch) {
4150
- if (!isRecord(patch))
4151
- return current;
4152
- const raw = { ...current };
4153
- for (const [key, value] of Object.entries(patch)) {
4154
- if (key === "version")
4155
- continue;
4156
- if (value === null)
4157
- delete raw[key];
4158
- else
4159
- raw[key] = value;
4160
- }
4161
- return sanitizeSettings({ ...raw, version: 1 });
4162
- }
4163
- function sanitizeViewState(raw) {
4164
- if (!isRecord(raw))
4165
- return emptyViewState();
4166
- return {
4167
- version: 1,
4168
- collapsedDirs: normalizeStringList(raw.collapsedDirs, {
4169
- maxItems: MAX_VIEW_ITEMS,
4170
- maxLen: MAX_KEY_LEN,
4171
- keepLast: true,
4172
- sort: false
4173
- }) ?? [],
4174
- viewedFiles: normalizeStringList(raw.viewedFiles, {
4175
- maxItems: MAX_VIEW_ITEMS,
4176
- maxLen: MAX_KEY_LEN,
4177
- keepLast: true,
4178
- sort: false
4179
- }) ?? []
4180
- };
4181
- }
4182
- function mergeViewState(current, patch) {
4183
- if (!isRecord(patch))
4184
- return current;
4185
- const base = sanitizeViewState({ ...current, version: 1 });
4186
- const collapsedDirs = new Set(base.collapsedDirs);
4187
- const viewedFiles = new Set(base.viewedFiles);
4188
- const addedCollapsedDirs = normalizeStringList(patch.addedCollapsedDirs, {
4189
- maxItems: MAX_VIEW_ITEMS,
4190
- maxLen: MAX_KEY_LEN,
4191
- keepLast: true,
4192
- sort: false
4193
- });
4194
- for (const path of addedCollapsedDirs || [])
4195
- collapsedDirs.add(path);
4196
- const removedCollapsedDirs = normalizeStringList(patch.removedCollapsedDirs, {
4197
- maxItems: MAX_VIEW_ITEMS,
4198
- maxLen: MAX_KEY_LEN,
4199
- sort: false
4200
- });
4201
- for (const path of removedCollapsedDirs || [])
4202
- collapsedDirs.delete(path);
4203
- const addedViewedFiles = normalizeStringList(patch.addedViewedFiles, {
4204
- maxItems: MAX_VIEW_ITEMS,
4205
- maxLen: MAX_KEY_LEN,
4206
- keepLast: true,
4207
- sort: false
4208
- });
4209
- for (const path of addedViewedFiles || [])
4210
- viewedFiles.add(path);
4211
- const removedViewedFiles = normalizeStringList(patch.removedViewedFiles, {
4212
- maxItems: MAX_VIEW_ITEMS,
4213
- maxLen: MAX_KEY_LEN,
4214
- sort: false
4215
- });
4216
- for (const path of removedViewedFiles || [])
4217
- viewedFiles.delete(path);
4218
- return sanitizeViewState({
4219
- version: 1,
4220
- collapsedDirs: [...collapsedDirs],
4221
- viewedFiles: [...viewedFiles]
4222
- });
4223
- }
4224
- function safeObjectKey(value) {
4225
- if (!value || value.length > MAX_KEY_LEN || value.includes("\x00"))
4226
- return null;
4227
- return value;
4001
+ const out = {};
4002
+ for (const key of DB_UI_BOOL_PREF_KEYS) {
4003
+ const v = raw[key];
4004
+ if (v === true || v === false)
4005
+ out[key] = v;
4006
+ }
4007
+ return Object.keys(out).length > 0 ? out : undefined;
4228
4008
  }
4229
4009
  function sanitizeDbUiState(raw) {
4230
- if (!isRecord(raw) || !isRecord(raw.columnWidths))
4010
+ if (!isRecord(raw))
4231
4011
  return emptyDbUiState();
4012
+ const prefs = sanitizeDbUiPrefs(raw.prefs);
4013
+ if (!isRecord(raw.columnWidths)) {
4014
+ return prefs ? { ...emptyDbUiState(), prefs } : emptyDbUiState();
4015
+ }
4232
4016
  const columnWidths = {};
4233
4017
  let dbCount = 0;
4234
4018
  for (const [dbIdRaw, tablesRaw] of Object.entries(raw.columnWidths)) {
@@ -4262,105 +4046,411 @@ function sanitizeDbUiState(raw) {
4262
4046
  tables[table] = columns;
4263
4047
  tableCount++;
4264
4048
  }
4265
- if (Object.keys(tables).length === 0)
4266
- continue;
4267
- columnWidths[dbId] = tables;
4268
- dbCount++;
4269
- }
4270
- return { version: 1, columnWidths };
4271
- }
4272
- function mergeDbUiState(current, patch) {
4273
- if (!isRecord(patch))
4274
- return current;
4275
- if (!isRecord(patch.columnWidths)) {
4276
- return sanitizeDbUiState({ ...current, ...patch, version: 1 });
4277
- }
4278
- const columnWidths = {
4279
- ...current.columnWidths
4280
- };
4281
- for (const [dbId, tablesRaw] of Object.entries(patch.columnWidths)) {
4282
- if (tablesRaw === null) {
4283
- delete columnWidths[dbId];
4284
- continue;
4049
+ if (Object.keys(tables).length === 0)
4050
+ continue;
4051
+ columnWidths[dbId] = tables;
4052
+ dbCount++;
4053
+ }
4054
+ const out = { version: 1, columnWidths };
4055
+ if (prefs)
4056
+ out.prefs = prefs;
4057
+ return out;
4058
+ }
4059
+ function mergeDbUiPrefs(current, patch) {
4060
+ if (!isRecord(patch))
4061
+ return current;
4062
+ const next = { ...current ?? {} };
4063
+ for (const key of DB_UI_BOOL_PREF_KEYS) {
4064
+ const v = patch[key];
4065
+ if (v === null)
4066
+ delete next[key];
4067
+ else if (v === true || v === false)
4068
+ next[key] = v;
4069
+ }
4070
+ return Object.keys(next).length > 0 ? next : undefined;
4071
+ }
4072
+ function mergeDbUiState(current, patch) {
4073
+ if (!isRecord(patch))
4074
+ return current;
4075
+ const mergedPrefs = "prefs" in patch ? mergeDbUiPrefs(current.prefs, patch.prefs) : current.prefs;
4076
+ if (!isRecord(patch.columnWidths)) {
4077
+ const merged = { ...current, version: 1 };
4078
+ if (mergedPrefs)
4079
+ merged.prefs = mergedPrefs;
4080
+ else
4081
+ delete merged.prefs;
4082
+ return sanitizeDbUiState(merged);
4083
+ }
4084
+ const columnWidths = {
4085
+ ...current.columnWidths
4086
+ };
4087
+ for (const [dbId, tablesRaw] of Object.entries(patch.columnWidths)) {
4088
+ if (tablesRaw === null) {
4089
+ delete columnWidths[dbId];
4090
+ continue;
4091
+ }
4092
+ if (!isRecord(tablesRaw))
4093
+ continue;
4094
+ const tables = { ...columnWidths[dbId] || {} };
4095
+ for (const [table, columnsRaw] of Object.entries(tablesRaw)) {
4096
+ if (columnsRaw === null) {
4097
+ delete tables[table];
4098
+ continue;
4099
+ }
4100
+ if (!isRecord(columnsRaw))
4101
+ continue;
4102
+ const columns = { ...tables[table] || {} };
4103
+ for (const [column, widthRaw] of Object.entries(columnsRaw)) {
4104
+ if (widthRaw === null)
4105
+ delete columns[column];
4106
+ else
4107
+ columns[column] = widthRaw;
4108
+ }
4109
+ tables[table] = columns;
4110
+ }
4111
+ columnWidths[dbId] = tables;
4112
+ }
4113
+ return sanitizeDbUiState({
4114
+ ...current,
4115
+ ...patch,
4116
+ columnWidths,
4117
+ prefs: mergedPrefs,
4118
+ version: 1
4119
+ });
4120
+ }
4121
+ async function loadAppSettingsState(root) {
4122
+ return settingsStore.load(root);
4123
+ }
4124
+ async function patchAppSettingsState(root, patch) {
4125
+ return settingsStore.update(root, (state) => {
4126
+ const next = mergeSettings(state, patch);
4127
+ return { state: next, result: next };
4128
+ });
4129
+ }
4130
+ async function loadViewState(root) {
4131
+ return viewStateStore.load(root);
4132
+ }
4133
+ async function patchViewState(root, patch) {
4134
+ return viewStateStore.update(root, (state) => {
4135
+ const next = mergeViewState(state, patch);
4136
+ return { state: next, result: next };
4137
+ });
4138
+ }
4139
+ async function loadDbUiState(root) {
4140
+ return dbUiStore.load(root);
4141
+ }
4142
+ async function patchDbUiState(root, patch) {
4143
+ return dbUiStore.update(root, (state) => {
4144
+ const next = mergeDbUiState(state, patch);
4145
+ return { state: next, result: next };
4146
+ });
4147
+ }
4148
+ var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
4149
+ var init_state_store = __esm(() => {
4150
+ init_json_store();
4151
+ DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
4152
+ settingsStore = createJsonFileStore({
4153
+ filePath: (root) => codeViewerPath(root, SETTINGS_FILE_NAME),
4154
+ empty: emptySettings,
4155
+ sanitize: sanitizeSettings,
4156
+ maxBytes: MAX_SETTINGS_BYTES,
4157
+ backupSuffix: "corrupt",
4158
+ sizeErrorMessage: "settings state too large"
4159
+ });
4160
+ viewStateStore = createJsonFileStore({
4161
+ filePath: (root) => codeViewerPath(root, VIEW_STATE_FILE_NAME),
4162
+ empty: emptyViewState,
4163
+ sanitize: sanitizeViewState,
4164
+ maxBytes: MAX_VIEW_STATE_BYTES,
4165
+ backupSuffix: "corrupt",
4166
+ sizeErrorMessage: "view state too large"
4167
+ });
4168
+ dbUiStore = createJsonFileStore({
4169
+ filePath: (root) => codeViewerPath(root, DB_UI_FILE_NAME),
4170
+ empty: emptyDbUiState,
4171
+ sanitize: sanitizeDbUiState,
4172
+ maxBytes: MAX_DB_UI_BYTES,
4173
+ backupSuffix: "corrupt",
4174
+ sizeErrorMessage: "db UI state too large"
4175
+ });
4176
+ });
4177
+
4178
+ // web-src/server/worktree-watcher.ts
4179
+ import {
4180
+ lstatSync as lstatSync3,
4181
+ readdirSync as nodeReaddirSync,
4182
+ watch as nodeWatch
4183
+ } from "node:fs";
4184
+ import { join as join8, relative } from "node:path";
4185
+ function normalizeRelativePath(path) {
4186
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
4187
+ }
4188
+ function isInsideRoot(root, path) {
4189
+ const rel = relative(root, path).replace(/\\/g, "/");
4190
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
4191
+ }
4192
+ function startWorktreeUpdateWatch(options) {
4193
+ const watch = options.watch || nodeWatch;
4194
+ const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
4195
+ const isDirectory = options.isDirectory || ((path) => {
4196
+ try {
4197
+ return lstatSync3(path).isDirectory();
4198
+ } catch {
4199
+ return false;
4200
+ }
4201
+ });
4202
+ const directorySignature = options.directorySignature || ((path) => {
4203
+ try {
4204
+ const stats = lstatSync3(path);
4205
+ if (!stats.isDirectory())
4206
+ return null;
4207
+ return `${stats.dev}:${stats.ino}`;
4208
+ } catch {
4209
+ return null;
4210
+ }
4211
+ });
4212
+ const setTimer = options.setTimeoutFn || setTimeout;
4213
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
4214
+ const debounceMs = options.debounceMs ?? 250;
4215
+ const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
4216
+ const watchers = new Map;
4217
+ const signatures = new Map;
4218
+ const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
4219
+ const initialScanQueue = [];
4220
+ let initialScanTimer = null;
4221
+ const pendingPathInspections = new Map;
4222
+ let pathInspectionTimer = null;
4223
+ let timer = null;
4224
+ const pendingChangedPaths = new Set;
4225
+ let watchLimitReported = false;
4226
+ const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
4227
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
4228
+ const ignoredDirectory = (dir) => {
4229
+ const rel = directoryRelativePath(dir);
4230
+ return Boolean(rel && ignored(rel));
4231
+ };
4232
+ const scheduleUpdate = (changedPath) => {
4233
+ if (changedPath)
4234
+ pendingChangedPaths.add(changedPath);
4235
+ if (timer)
4236
+ clearTimer(timer);
4237
+ timer = setTimer(() => {
4238
+ timer = null;
4239
+ const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
4240
+ pendingChangedPaths.clear();
4241
+ options.onUpdate(paths);
4242
+ }, debounceMs);
4243
+ };
4244
+ const reportWatchLimit = () => {
4245
+ if (watchLimitReported)
4246
+ return;
4247
+ watchLimitReported = true;
4248
+ options.onWatchLimit?.(maxWatchedDirectories);
4249
+ options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
4250
+ };
4251
+ const closeSubtree = (dir) => {
4252
+ for (const [watchedDir, watcher] of [...watchers]) {
4253
+ if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
4254
+ continue;
4255
+ try {
4256
+ watcher.close?.();
4257
+ } catch {}
4258
+ watchers.delete(watchedDir);
4259
+ signatures.delete(watchedDir);
4260
+ }
4261
+ };
4262
+ const closeAll = () => {
4263
+ if (initialScanTimer) {
4264
+ clearTimer(initialScanTimer);
4265
+ initialScanTimer = null;
4266
+ }
4267
+ if (pathInspectionTimer) {
4268
+ clearTimer(pathInspectionTimer);
4269
+ pathInspectionTimer = null;
4270
+ }
4271
+ initialScanQueue.length = 0;
4272
+ pendingPathInspections.clear();
4273
+ for (const watcher of [...watchers.values()]) {
4274
+ try {
4275
+ watcher.close?.();
4276
+ } catch {}
4277
+ }
4278
+ watchers.clear();
4279
+ signatures.clear();
4280
+ };
4281
+ const readChildDirectories = (dir) => {
4282
+ let entries;
4283
+ try {
4284
+ entries = readDirs(dir);
4285
+ } catch (error) {
4286
+ options.onError?.(error);
4287
+ return [];
4288
+ }
4289
+ const children = [];
4290
+ for (const entry of entries) {
4291
+ if (!entry.isDirectory())
4292
+ continue;
4293
+ const child = join8(dir, entry.name);
4294
+ if (ignoredDirectory(child))
4295
+ continue;
4296
+ children.push(child);
4297
+ }
4298
+ return children;
4299
+ };
4300
+ const processInitialScanQueue = () => {
4301
+ initialScanTimer = null;
4302
+ if (watchers.size >= maxWatchedDirectories) {
4303
+ reportWatchLimit();
4304
+ initialScanQueue.length = 0;
4305
+ return;
4306
+ }
4307
+ const next = initialScanQueue.shift();
4308
+ if (next)
4309
+ watchDirectory(next, true);
4310
+ if (watchers.size >= maxWatchedDirectories) {
4311
+ reportWatchLimit();
4312
+ initialScanQueue.length = 0;
4313
+ }
4314
+ if (initialScanQueue.length)
4315
+ initialScanTimer = setTimer(processInitialScanQueue, 50);
4316
+ };
4317
+ const queueInitialChildren = (dir) => {
4318
+ const remaining = maxWatchedDirectories - watchers.size;
4319
+ if (remaining <= 0) {
4320
+ reportWatchLimit();
4321
+ return;
4322
+ }
4323
+ const children = readChildDirectories(dir);
4324
+ if (children.length > remaining)
4325
+ reportWatchLimit();
4326
+ initialScanQueue.push(...children.slice(0, remaining));
4327
+ if (!initialScanTimer)
4328
+ initialScanTimer = setTimer(processInitialScanQueue, 5000);
4329
+ };
4330
+ const processChangedPath = (changed, fullChangedPath) => {
4331
+ const known = watchers.has(fullChangedPath);
4332
+ if (isDirectory(fullChangedPath)) {
4333
+ if (known) {
4334
+ const signature = directorySignature(fullChangedPath);
4335
+ if (signature && signature !== signatures.get(fullChangedPath)) {
4336
+ closeSubtree(fullChangedPath);
4337
+ watchDirectory(fullChangedPath, initialScanAsync);
4338
+ }
4339
+ scheduleUpdate(changed);
4340
+ return;
4341
+ }
4342
+ watchDirectory(fullChangedPath, initialScanAsync);
4343
+ } else if (known) {
4344
+ closeSubtree(fullChangedPath);
4345
+ }
4346
+ scheduleUpdate(changed);
4347
+ };
4348
+ const processPathInspections = () => {
4349
+ pathInspectionTimer = null;
4350
+ const entries = [...pendingPathInspections];
4351
+ pendingPathInspections.clear();
4352
+ for (const [changed, fullChangedPath] of entries) {
4353
+ processChangedPath(changed, fullChangedPath);
4354
+ }
4355
+ };
4356
+ const queuePathInspection = (changed, fullChangedPath) => {
4357
+ pendingPathInspections.set(changed, fullChangedPath);
4358
+ if (!pathInspectionTimer)
4359
+ pathInspectionTimer = setTimer(processPathInspections, 25);
4360
+ };
4361
+ const watchDirectory = (dir, initialScan = false) => {
4362
+ if (watchers.has(dir))
4363
+ return;
4364
+ if (watchers.size >= maxWatchedDirectories) {
4365
+ reportWatchLimit();
4366
+ return;
4367
+ }
4368
+ const rel = directoryRelativePath(dir);
4369
+ if (rel && ignored(rel))
4370
+ return;
4371
+ try {
4372
+ const watcher = watch(dir, { persistent: false }, (_event, filename) => {
4373
+ if (!filename) {
4374
+ scheduleUpdate();
4375
+ return;
4376
+ }
4377
+ const changed = normalizeRelativePath(join8(rel, filename.toString()));
4378
+ if (ignored(changed))
4379
+ return;
4380
+ const fullChangedPath = join8(options.root, changed);
4381
+ if (!isInsideRoot(options.root, fullChangedPath))
4382
+ return;
4383
+ if (initialScanAsync) {
4384
+ queuePathInspection(changed, fullChangedPath);
4385
+ return;
4386
+ }
4387
+ processChangedPath(changed, fullChangedPath);
4388
+ }) || {};
4389
+ watchers.set(dir, watcher);
4390
+ const signature = directorySignature(dir);
4391
+ if (signature)
4392
+ signatures.set(dir, signature);
4393
+ watcher.on?.("error", () => {
4394
+ if (watchers.get(dir) === watcher) {
4395
+ watchers.delete(dir);
4396
+ signatures.delete(dir);
4397
+ }
4398
+ });
4399
+ watcher.on?.("close", () => {
4400
+ if (watchers.get(dir) === watcher) {
4401
+ watchers.delete(dir);
4402
+ signatures.delete(dir);
4403
+ }
4404
+ });
4405
+ } catch (error) {
4406
+ options.onError?.(error);
4407
+ return;
4408
+ }
4409
+ if (initialScanAsync && initialScan) {
4410
+ queueInitialChildren(dir);
4411
+ return;
4285
4412
  }
4286
- if (!isRecord(tablesRaw))
4287
- continue;
4288
- const tables = { ...columnWidths[dbId] || {} };
4289
- for (const [table, columnsRaw] of Object.entries(tablesRaw)) {
4290
- if (columnsRaw === null) {
4291
- delete tables[table];
4292
- continue;
4293
- }
4294
- if (!isRecord(columnsRaw))
4295
- continue;
4296
- const columns = { ...tables[table] || {} };
4297
- for (const [column, widthRaw] of Object.entries(columnsRaw)) {
4298
- if (widthRaw === null)
4299
- delete columns[column];
4300
- else
4301
- columns[column] = widthRaw;
4302
- }
4303
- tables[table] = columns;
4413
+ if (watchers.size >= maxWatchedDirectories) {
4414
+ reportWatchLimit();
4415
+ return;
4304
4416
  }
4305
- columnWidths[dbId] = tables;
4306
- }
4307
- return sanitizeDbUiState({ ...current, ...patch, columnWidths, version: 1 });
4308
- }
4309
- async function loadAppSettingsState(root) {
4310
- return settingsStore.load(root);
4311
- }
4312
- async function patchAppSettingsState(root, patch) {
4313
- return settingsStore.update(root, (state) => {
4314
- const next = mergeSettings(state, patch);
4315
- return { state: next, result: next };
4316
- });
4317
- }
4318
- async function loadViewState(root) {
4319
- return viewStateStore.load(root);
4417
+ for (const child of readChildDirectories(dir))
4418
+ watchDirectory(child);
4419
+ };
4420
+ watchDirectory(options.root, true);
4421
+ return { started: watchers.size > 0, close: closeAll };
4320
4422
  }
4321
- async function patchViewState(root, patch) {
4322
- return viewStateStore.update(root, (state) => {
4323
- const next = mergeViewState(state, patch);
4324
- return { state: next, result: next };
4325
- });
4423
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
4424
+ var init_worktree_watcher = __esm(() => {
4425
+ init_search();
4426
+ });
4427
+
4428
+ // web-src/core/control-chars.ts
4429
+ function hasControlCharacter(value) {
4430
+ for (const ch of value) {
4431
+ const code = ch.charCodeAt(0);
4432
+ if (code < 32 || code === 127)
4433
+ return true;
4434
+ }
4435
+ return false;
4326
4436
  }
4327
- async function loadDbUiState(root) {
4328
- return dbUiStore.load(root);
4437
+
4438
+ // web-src/core/id.ts
4439
+ function bytesToHex(bytes) {
4440
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
4329
4441
  }
4330
- async function patchDbUiState(root, patch) {
4331
- return dbUiStore.update(root, (state) => {
4332
- const next = mergeDbUiState(state, patch);
4333
- return { state: next, result: next };
4334
- });
4442
+ function makeId(prefix) {
4443
+ const cryptoApi = globalThis.crypto;
4444
+ if (typeof cryptoApi?.randomUUID === "function") {
4445
+ return `${prefix}-${cryptoApi.randomUUID().replace(/-/g, "").slice(0, 16)}`;
4446
+ }
4447
+ if (typeof cryptoApi?.getRandomValues === "function") {
4448
+ const bytes = new Uint8Array(8);
4449
+ cryptoApi.getRandomValues(bytes);
4450
+ return `${prefix}-${bytesToHex(bytes)}`;
4451
+ }
4452
+ return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
4335
4453
  }
4336
- var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, settingsStore, viewStateStore, dbUiStore;
4337
- var init_state_store = __esm(() => {
4338
- init_json_store();
4339
- settingsStore = createJsonFileStore({
4340
- filePath: (root) => codeViewerPath(root, SETTINGS_FILE_NAME),
4341
- empty: emptySettings,
4342
- sanitize: sanitizeSettings,
4343
- maxBytes: MAX_SETTINGS_BYTES,
4344
- backupSuffix: "corrupt",
4345
- sizeErrorMessage: "settings state too large"
4346
- });
4347
- viewStateStore = createJsonFileStore({
4348
- filePath: (root) => codeViewerPath(root, VIEW_STATE_FILE_NAME),
4349
- empty: emptyViewState,
4350
- sanitize: sanitizeViewState,
4351
- maxBytes: MAX_VIEW_STATE_BYTES,
4352
- backupSuffix: "corrupt",
4353
- sizeErrorMessage: "view state too large"
4354
- });
4355
- dbUiStore = createJsonFileStore({
4356
- filePath: (root) => codeViewerPath(root, DB_UI_FILE_NAME),
4357
- empty: emptyDbUiState,
4358
- sanitize: sanitizeDbUiState,
4359
- maxBytes: MAX_DB_UI_BYTES,
4360
- backupSuffix: "corrupt",
4361
- sizeErrorMessage: "db UI state too large"
4362
- });
4363
- });
4364
4454
 
4365
4455
  // web-src/server/database/adapters/abort.ts
4366
4456
  function abortError(message = "operation aborted") {
@@ -4528,25 +4618,23 @@ function sanitizeIdentifier(name, kind = "sqlite") {
4528
4618
  return `\`${name.replace(/`/g, "``")}\``;
4529
4619
  return `"${name.replace(/"/g, '""')}"`;
4530
4620
  }
4531
- function escapeSqlString(value) {
4532
- return `'${value.replace(/'/g, "''")}'`;
4621
+ function escapeSqlString(value, kind) {
4622
+ const escaped = kind === "mysql" ? value.replace(/\\/g, "\\\\").replace(/'/g, "''") : value.replace(/'/g, "''");
4623
+ return `'${escaped}'`;
4533
4624
  }
4534
- function buildFilterWhere(grouped, kind) {
4625
+ function buildFilterWhere(grouped, kind, exact) {
4535
4626
  const whereParts = [];
4536
4627
  const params = [];
4537
4628
  const useParams = kind === "sqlite";
4629
+ const castOf = (column) => kind === "mysql" ? `CAST(${sanitizeIdentifier(column, kind)} AS CHAR)` : `CAST(${sanitizeIdentifier(column, kind)} AS TEXT)`;
4538
4630
  for (const [value, cols] of grouped) {
4539
- const likeVal = useParams ? "?" : escapeSqlString(`%${value}%`);
4631
+ const likeVal = useParams ? "?" : escapeSqlString(`%${value}%`, kind);
4540
4632
  if (cols.length === 1) {
4541
- const cast = kind === "mysql" ? `CAST(${sanitizeIdentifier(cols[0], kind)} AS CHAR)` : `CAST(${sanitizeIdentifier(cols[0], kind)} AS TEXT)`;
4542
- whereParts.push(`${cast} LIKE ${likeVal}`);
4633
+ whereParts.push(`${castOf(cols[0])} LIKE ${likeVal}`);
4543
4634
  if (useParams)
4544
4635
  params.push(`%${value}%`);
4545
4636
  } else {
4546
- const orParts = cols.map((column) => {
4547
- const cast = kind === "mysql" ? `CAST(${sanitizeIdentifier(column, kind)} AS CHAR)` : `CAST(${sanitizeIdentifier(column, kind)} AS TEXT)`;
4548
- return `${cast} LIKE ${likeVal}`;
4549
- });
4637
+ const orParts = cols.map((column) => `${castOf(column)} LIKE ${likeVal}`);
4550
4638
  whereParts.push(`(${orParts.join(" OR ")})`);
4551
4639
  if (useParams) {
4552
4640
  for (let i = 0;i < cols.length; i++)
@@ -4554,6 +4642,12 @@ function buildFilterWhere(grouped, kind) {
4554
4642
  }
4555
4643
  }
4556
4644
  }
4645
+ for (const cond of exact ?? []) {
4646
+ const rhs = useParams ? "?" : escapeSqlString(cond.value, kind);
4647
+ whereParts.push(`${castOf(cond.column)} = ${rhs}`);
4648
+ if (useParams)
4649
+ params.push(cond.value);
4650
+ }
4557
4651
  return { where: whereParts.join(" AND "), params, useParams };
4558
4652
  }
4559
4653
  function filterGroupedColumns(grouped, columnNames) {
@@ -4566,6 +4660,12 @@ function filterGroupedColumns(grouped, columnNames) {
4566
4660
  }
4567
4661
  return filtered;
4568
4662
  }
4663
+ function filterExactColumns(exact, columnNames) {
4664
+ if (!exact || exact.length === 0)
4665
+ return [];
4666
+ const validColumns = new Set(columnNames);
4667
+ return exact.filter((cond) => validColumns.has(cond.column));
4668
+ }
4569
4669
  function filterOrderByColumns(orderBy, columnNames) {
4570
4670
  if (!orderBy)
4571
4671
  return;
@@ -4869,6 +4969,8 @@ function buildExecArgs(config, sql) {
4869
4969
  "-A",
4870
4970
  "-F",
4871
4971
  "\t",
4972
+ "-R",
4973
+ PG_RECORD_SEPARATOR,
4872
4974
  "-v",
4873
4975
  "ON_ERROR_STOP=1",
4874
4976
  "-c",
@@ -5044,11 +5146,14 @@ function splitTsvLine(line, decodeFields) {
5044
5146
  const fields = line.split("\t");
5045
5147
  return decodeFields ? fields.map(decodeMysqlBatchField) : fields;
5046
5148
  }
5047
- function parseTsvOutput(stdout, hasHeader) {
5048
- const text = stripFinalLineBreak(stdout);
5149
+ function stripFinalRecordSeparator(text, recordSeparator) {
5150
+ return text.endsWith(recordSeparator) ? text.slice(0, -recordSeparator.length) : text;
5151
+ }
5152
+ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
5153
+ const text = recordSeparator ? stripFinalRecordSeparator(stdout, recordSeparator) : stripFinalLineBreak(stdout);
5049
5154
  if (text.length === 0)
5050
5155
  return { columns: [], rows: [] };
5051
- const lines = text.split(/\r?\n/);
5156
+ const lines = recordSeparator ? text.split(recordSeparator) : text.split(/\r?\n/);
5052
5157
  if (lines.length === 0)
5053
5158
  return { columns: [], rows: [] };
5054
5159
  if (hasHeader) {
@@ -5125,7 +5230,7 @@ function createDockerAdapter(config) {
5125
5230
  if (result.code !== 0) {
5126
5231
  throw new Error(result.stderr.trim() || "query failed");
5127
5232
  }
5128
- return parseTsvOutput(result.stdout, config.kind === "mysql");
5233
+ return parseTsvOutput(result.stdout, config.kind === "mysql", config.kind === "postgresql" ? PG_RECORD_SEPARATOR : undefined);
5129
5234
  }
5130
5235
  function toDbValue(val) {
5131
5236
  if (val === "NULL" || val === "\\N")
@@ -5218,25 +5323,18 @@ function createDockerAdapter(config) {
5218
5323
  },
5219
5324
  async getIndexesAsync(signal) {
5220
5325
  let sql;
5326
+ const INDEX_COL_SEP = "\x1F";
5221
5327
  if (config.kind === "postgresql") {
5222
- sql = `SELECT indexname, tablename FROM pg_indexes WHERE schemaname = ${postgresSchemaLiteral()} AND indexname NOT LIKE 'pg_%' ORDER BY indexname`;
5328
+ sql = `SELECT i.relname, t.relname, CASE WHEN ix.indisunique THEN '1' ELSE '0' END, COALESCE(string_agg(a.attname, E'\\x1f' ORDER BY k.ord), '') FROM pg_index ix JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_class t ON t.oid = ix.indrelid JOIN pg_namespace n ON n.oid = t.relnamespace LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON true LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum WHERE n.nspname = ${postgresSchemaLiteral()} AND i.relname NOT LIKE 'pg_%' GROUP BY i.relname, t.relname, ix.indisunique ORDER BY t.relname, i.relname`;
5223
5329
  } else {
5224
- sql = `SELECT DISTINCT index_name, table_name, non_unique FROM information_schema.statistics WHERE table_schema = DATABASE() ORDER BY index_name`;
5330
+ sql = `SELECT index_name, table_name, IF(MAX(non_unique) = 0, '1', '0'), GROUP_CONCAT(column_name ORDER BY seq_in_index SEPARATOR '\x1F') FROM information_schema.statistics WHERE table_schema = DATABASE() GROUP BY index_name, table_name ORDER BY table_name, index_name`;
5225
5331
  }
5226
5332
  const result = await execAsync(sql, signal);
5227
- if (config.kind === "postgresql") {
5228
- return result.rows.map((row) => ({
5229
- name: row[0],
5230
- table: row[1],
5231
- columns: [],
5232
- unique: false
5233
- }));
5234
- }
5235
5333
  return result.rows.map((row) => ({
5236
5334
  name: row[0],
5237
5335
  table: row[1],
5238
- columns: [],
5239
- unique: row[2] === "0"
5336
+ unique: row[2] === "1",
5337
+ columns: row[3] ? row[3].split(INDEX_COL_SEP).filter((s) => s.length > 0) : []
5240
5338
  }));
5241
5339
  },
5242
5340
  async getForeignKeysAsync(signal) {
@@ -5426,7 +5524,7 @@ function createDockerAdapter(config) {
5426
5524
  }
5427
5525
  const columnNames = columns.map((column) => column.name);
5428
5526
  const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames), config.kind);
5429
- const where = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), config.kind).where;
5527
+ const where = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), config.kind, filterExactColumns(options.exact, columnNames)).where;
5430
5528
  const whereClause = where ? ` WHERE ${where}` : "";
5431
5529
  const countSql = `SELECT COUNT(*) AS cnt FROM ${id}${whereClause}`;
5432
5530
  const countResultPromise = execAsync(countSql, signal);
@@ -5606,7 +5704,7 @@ async function listDockerDatabasesAsync(serviceName, kind, env, cwd, signal) {
5606
5704
  return fallback;
5607
5705
  return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
5608
5706
  }
5609
- const parsed = parseTsvOutput(result.stdout, kind === "mysql");
5707
+ const parsed = parseTsvOutput(result.stdout, kind === "mysql", kind === "postgresql" ? PG_RECORD_SEPARATOR : undefined);
5610
5708
  const dbs = parsed.rows.map((r) => r[0]).filter(Boolean);
5611
5709
  const value = dbs.length > 0 ? dbs : fallbackDockerDatabases(defaultDb);
5612
5710
  return setDockerDatabasesCache(cacheKey, value, value.length > 0 ? DOCKER_DATABASES_POSITIVE_TTL_MS : DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
@@ -5647,7 +5745,7 @@ async function listDockerSchemasAsync(serviceName, kind, env, cwd, overrideDatab
5647
5745
  if (result.code !== 0) {
5648
5746
  return setDockerSchemasCache(cacheKey, ["public"], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
5649
5747
  }
5650
- const parsed = parseTsvOutput(result.stdout, false);
5748
+ const parsed = parseTsvOutput(result.stdout, false, PG_RECORD_SEPARATOR);
5651
5749
  const schemas = parsed.rows.map((r) => r[0]).filter(Boolean);
5652
5750
  const value = schemas.length > 0 ? schemas : ["public"];
5653
5751
  return setDockerSchemasCache(cacheKey, value, DOCKER_DATABASES_POSITIVE_TTL_MS, now);
@@ -5671,7 +5769,7 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
5671
5769
  ...kind === "postgresql" && schema ? { schema } : {}
5672
5770
  });
5673
5771
  }
5674
- var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, dockerSchemasCache, spawnSyncImpl2, MYSQL_SPATIAL_TYPES;
5772
+ var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, dockerSchemasCache, spawnSyncImpl2, PG_RECORD_SEPARATOR = "\x1E", MYSQL_SPATIAL_TYPES;
5675
5773
  var init_docker = __esm(() => {
5676
5774
  init_sql_snapshot();
5677
5775
  init_docker_utils();
@@ -5870,7 +5968,7 @@ function createSqliteAdapter(db) {
5870
5968
  async getFilteredTablePageWithMeta(table, options) {
5871
5969
  const columns = queryColumns(db, table);
5872
5970
  const columnNames = columns.map((column) => column.name);
5873
- const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite");
5971
+ const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite", filterExactColumns(options.exact, columnNames));
5874
5972
  const order = buildOrderClause2(filterOrderByColumns(options.orderBy, columnNames));
5875
5973
  const tableId = sanitizeIdentifier(table);
5876
5974
  const whereClause = filter.where ? ` WHERE ${filter.where}` : "";
@@ -5881,7 +5979,7 @@ function createSqliteAdapter(db) {
5881
5979
  columns,
5882
5980
  rows: result.rows,
5883
5981
  rowCount: result.rowCount,
5884
- totalRows: countRow?.cnt ?? 0
5982
+ totalRows: Number(countRow?.cnt ?? 0)
5885
5983
  };
5886
5984
  },
5887
5985
  executeReadonlyQuery(sql, params, maxRows = 1000) {
@@ -8570,9 +8668,16 @@ function parseBuckets(xml) {
8570
8668
  };
8571
8669
  }).filter((bucket) => bucket.name).sort((a, b) => a.name.localeCompare(b.name));
8572
8670
  }
8671
+ function decodeS3UrlEncoded(value) {
8672
+ try {
8673
+ return decodeURIComponent(value);
8674
+ } catch {
8675
+ return value;
8676
+ }
8677
+ }
8573
8678
  function parseObjects(xml) {
8574
8679
  const objects = xmlBlocks(xml, "Contents").map((block) => {
8575
- const key = xmlText(block, "Key") || "";
8680
+ const key = decodeS3UrlEncoded(xmlText(block, "Key") || "");
8576
8681
  const updatedAt = toIsoDate(xmlText(block, "LastModified"));
8577
8682
  return {
8578
8683
  key,
@@ -8582,8 +8687,10 @@ function parseObjects(xml) {
8582
8687
  ...xmlText(block, "StorageClass") ? { storageClass: xmlText(block, "StorageClass") } : {}
8583
8688
  };
8584
8689
  }).filter((object) => object.key);
8690
+ const commonPrefixes = xmlBlocks(xml, "CommonPrefixes").map((block) => decodeS3UrlEncoded(xmlText(block, "Prefix") || "")).filter(Boolean);
8585
8691
  return {
8586
8692
  objects,
8693
+ commonPrefixes,
8587
8694
  nextToken: xmlText(xml, "NextContinuationToken"),
8588
8695
  truncated: xmlText(xml, "IsTruncated") === "true"
8589
8696
  };
@@ -8665,8 +8772,10 @@ function createS3Adapter(config) {
8665
8772
  bucket: opts.bucket,
8666
8773
  query: {
8667
8774
  "list-type": "2",
8775
+ "encoding-type": "url",
8668
8776
  "max-keys": String(Math.min(1000, Math.max(1, opts.maxKeys ?? 200))),
8669
8777
  ...opts.prefix ? { prefix: opts.prefix } : {},
8778
+ ...opts.delimiter ? { delimiter: opts.delimiter } : {},
8670
8779
  ...opts.continuationToken ? { "continuation-token": opts.continuationToken } : {}
8671
8780
  },
8672
8781
  signal: opts.signal
@@ -9024,6 +9133,42 @@ ${s3ObjectName(object.key)}`.toLowerCase();
9024
9133
  return s3ErrorResponse(err, "list s3 objects");
9025
9134
  }
9026
9135
  }
9136
+ async function handleFolder(cwd, req, url, omitDirNames) {
9137
+ const r = await resolveS3(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
9138
+ if (r instanceof Response)
9139
+ return r;
9140
+ const bucket = validateBucket(url.searchParams.get("bucket"));
9141
+ if (bucket instanceof Response)
9142
+ return bucket;
9143
+ const prefix = validateOptionalText(url.searchParams.get("prefix"), "prefix", 2048);
9144
+ if (prefix instanceof Response)
9145
+ return prefix;
9146
+ const token = validateOptionalText(url.searchParams.get("token"), "token", 4096);
9147
+ if (token instanceof Response)
9148
+ return token;
9149
+ try {
9150
+ const page = await r.explorer.listObjects({
9151
+ bucket,
9152
+ prefix,
9153
+ delimiter: "/",
9154
+ continuationToken: token || undefined,
9155
+ maxKeys: MAX_OBJECT_LIMIT,
9156
+ signal: req.signal
9157
+ });
9158
+ const objects = page.objects.filter((object) => object.key !== prefix);
9159
+ const body = {
9160
+ dbId: r.dbId,
9161
+ bucket,
9162
+ prefix,
9163
+ folders: page.commonPrefixes ?? [],
9164
+ objects,
9165
+ ...page.nextToken ? { nextToken: page.nextToken } : {}
9166
+ };
9167
+ return json(body);
9168
+ } catch (err) {
9169
+ return s3ErrorResponse(err, "list s3 folder");
9170
+ }
9171
+ }
9027
9172
  async function handleHead(cwd, req, url, omitDirNames) {
9028
9173
  const r = await resolveS3(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
9029
9174
  if (r instanceof Response)
@@ -9109,6 +9254,10 @@ async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
9109
9254
  methods: ["GET"],
9110
9255
  handler: () => handleObjects(cwd, req, url, omitDirNames)
9111
9256
  },
9257
+ "/_db/s3/folder": {
9258
+ methods: ["GET"],
9259
+ handler: () => handleFolder(cwd, req, url, omitDirNames)
9260
+ },
9112
9261
  "/_db/s3/head": {
9113
9262
  methods: ["GET"],
9114
9263
  handler: () => handleHead(cwd, req, url, omitDirNames)
@@ -9803,6 +9952,9 @@ function sanitize(input) {
9803
9952
  const sidebarWidth = sanitizeCssSize(tab.sidebarWidth);
9804
9953
  if (sidebarWidth !== undefined)
9805
9954
  out.sidebarWidth = sidebarWidth;
9955
+ const relatedPanelHeight = sanitizeCssSize(tab.relatedPanelHeight);
9956
+ if (relatedPanelHeight !== undefined)
9957
+ out.relatedPanelHeight = relatedPanelHeight;
9806
9958
  const redis = sanitizeRedis(tab.redis);
9807
9959
  if (redis !== undefined)
9808
9960
  out.redis = redis;
@@ -10043,19 +10195,25 @@ async function handleSchema(cwd, url, omitDirNames, signal) {
10043
10195
  linkedAbort.cleanup();
10044
10196
  }
10045
10197
  }
10046
- function parseFilters(url) {
10047
- const raw = url.searchParams.get("filters");
10198
+ function parseColumnValuePairs(url, param) {
10199
+ const raw = url.searchParams.get(param);
10048
10200
  if (!raw)
10049
10201
  return [];
10050
10202
  try {
10051
10203
  const parsed = JSON.parse(raw);
10052
10204
  if (!Array.isArray(parsed))
10053
10205
  return [];
10054
- return parsed.filter((f) => !!f && typeof f === "object" && typeof f.column === "string" && typeof f.value === "string");
10206
+ return parsed.filter((f) => !!f && typeof f === "object" && typeof f.column === "string" && typeof f.value === "string").filter((f) => f.column.length <= MAX_FILTER_COLUMN_LEN && f.value.length <= MAX_FILTER_VALUE_LEN).slice(0, MAX_COLUMN_VALUE_PAIRS);
10055
10207
  } catch {
10056
10208
  return [];
10057
10209
  }
10058
10210
  }
10211
+ function parseFilters(url) {
10212
+ return parseColumnValuePairs(url, "filters");
10213
+ }
10214
+ function parseExactConditions(url) {
10215
+ return parseColumnValuePairs(url, "eq");
10216
+ }
10059
10217
  function groupFiltersByValue(filters) {
10060
10218
  const grouped = new Map;
10061
10219
  for (const filter of filters) {
@@ -10086,14 +10244,16 @@ async function handleTable(cwd, url, omitDirNames, signal) {
10086
10244
  ];
10087
10245
  }
10088
10246
  const filters = parseFilters(url);
10247
+ const exact = parseExactConditions(url);
10089
10248
  try {
10090
10249
  const adapter = await getAdapter(r, cwd, signal);
10091
- if (filters.length > 0) {
10250
+ if (filters.length > 0 || exact.length > 0) {
10092
10251
  const meta2 = await adapter.getFilteredTablePageWithMeta(table, {
10093
10252
  offset,
10094
10253
  limit,
10095
10254
  orderBy,
10096
- grouped: groupFiltersByValue(filters)
10255
+ grouped: groupFiltersByValue(filters),
10256
+ ...exact.length > 0 ? { exact } : {}
10097
10257
  }, signal);
10098
10258
  const colNames2 = new Set(meta2.columns.map((c) => c.name));
10099
10259
  if (sortCol && !colNames2.has(sortCol)) {
@@ -10367,6 +10527,7 @@ async function handleExport(cwd, url, omitDirNames, signal) {
10367
10527
  ];
10368
10528
  }
10369
10529
  const filters = parseFilters(url);
10530
+ const exact = parseExactConditions(url);
10370
10531
  try {
10371
10532
  const adapter = await getAdapter(r, cwd, signal);
10372
10533
  const db = asAsync(adapter);
@@ -10377,12 +10538,13 @@ async function handleExport(cwd, url, omitDirNames, signal) {
10377
10538
  return textError(`invalid sort column: ${sortCol}`, 400);
10378
10539
  }
10379
10540
  let rawRows;
10380
- if (filters.length > 0) {
10541
+ if (filters.length > 0 || exact.length > 0) {
10381
10542
  const meta = await adapter.getFilteredTablePageWithMeta(table, {
10382
10543
  offset: 0,
10383
10544
  limit: EXPORT_MAX_ROWS,
10384
10545
  orderBy,
10385
- grouped: groupFiltersByValue(filters)
10546
+ grouped: groupFiltersByValue(filters),
10547
+ ...exact.length > 0 ? { exact } : {}
10386
10548
  }, signal);
10387
10549
  rawRows = meta.rows;
10388
10550
  } else {
@@ -11019,7 +11181,7 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
11019
11181
  }
11020
11182
  }, sideEffectAllowed, wrapResponse, (err) => handleError("database", "handle database request", err));
11021
11183
  }
11022
- var initialized = false, dockerAdapterCache, MAX_SCHEMA_NAME_LEN2 = 1024, EXPORT_MAX_ROWS = 1e5, MAX_TABS_BODY_BYTES = 1e6, MAX_DB_UI_BODY_BYTES = 1e6, MAX_SNAPSHOT_TABLES = 512, MAX_SNAPSHOT_TABLE_NAME_LEN = 1024, searchJobs, snapshotJobs, DOCKER_CLOSE_REGISTRY, SNAPSHOT_DOCKER_SOURCE_REGISTRY;
11184
+ var initialized = false, dockerAdapterCache, MAX_SCHEMA_NAME_LEN2 = 1024, MAX_COLUMN_VALUE_PAIRS = 64, MAX_FILTER_COLUMN_LEN = 128, MAX_FILTER_VALUE_LEN = 4096, EXPORT_MAX_ROWS = 1e5, MAX_TABS_BODY_BYTES = 1e6, MAX_DB_UI_BODY_BYTES = 1e6, MAX_SNAPSHOT_TABLES = 512, MAX_SNAPSHOT_TABLE_NAME_LEN = 1024, searchJobs, snapshotJobs, DOCKER_CLOSE_REGISTRY, SNAPSHOT_DOCKER_SOURCE_REGISTRY;
11023
11185
  var init_handle = __esm(() => {
11024
11186
  init_state_store();
11025
11187
  init_docker();
@@ -11086,12 +11248,20 @@ async function parseJsonBody(req) {
11086
11248
  async function handleSettingsGet(cwd) {
11087
11249
  return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
11088
11250
  }
11089
- async function handleSettingsPatch(cwd, req) {
11251
+ async function handleSettingsPatch(cwd, req, onChange) {
11090
11252
  const body = await parseJsonBody(req);
11091
11253
  if (body instanceof Response)
11092
11254
  return body;
11093
11255
  try {
11094
- return json(await patchAppSettingsState(cwd, body));
11256
+ const next = await patchAppSettingsState(cwd, body);
11257
+ if (onChange) {
11258
+ try {
11259
+ onChange(next);
11260
+ } catch (notifyErr) {
11261
+ console.warn("[code-viewer] settings change notify failed:", notifyErr);
11262
+ }
11263
+ }
11264
+ return json(next);
11095
11265
  } catch (err) {
11096
11266
  const message = err instanceof Error ? err.message : String(err);
11097
11267
  if (message === "settings state too large")
@@ -11117,12 +11287,12 @@ async function handleViewPatch(cwd, req) {
11117
11287
  return textError("failed to save view state", 500);
11118
11288
  }
11119
11289
  }
11120
- async function handleStateRoute(req, url, cwd, sideEffectAllowed) {
11290
+ async function handleStateRoute(req, url, cwd, sideEffectAllowed, options = {}) {
11121
11291
  return dispatchRoutes(req, url, {
11122
11292
  "/_state/settings": {
11123
11293
  methods: ["GET", "PATCH"],
11124
11294
  sideEffect: (method) => method !== "GET",
11125
- handler: () => req.method === "GET" ? handleSettingsGet(cwd) : handleSettingsPatch(cwd, req)
11295
+ handler: () => req.method === "GET" ? handleSettingsGet(cwd) : handleSettingsPatch(cwd, req, options.onSettingsChange)
11126
11296
  },
11127
11297
  "/_state/view": {
11128
11298
  methods: ["GET", "PATCH"],
@@ -11218,16 +11388,53 @@ Examples:
11218
11388
  }
11219
11389
  if (rest.length)
11220
11390
  cliArgs = rest;
11221
- const configScopeOmitDirs = loadProjectConfigScopeOmitDirs();
11222
- const configScopeExcludeNames = loadProjectConfigScopeExcludeNames();
11223
- uploadDisabledByConfig = loadProjectConfigUploadDisabled();
11391
+ warnIfLegacyConfigPresent();
11224
11392
  if (scopeOmitDirCliOverride) {
11225
11393
  scopeOmitDirNames = scopeOmitDirCliOverride;
11226
- } else if (configScopeOmitDirs) {
11227
- scopeOmitDirNames = configScopeOmitDirs;
11228
11394
  }
11229
- if (configScopeExcludeNames)
11230
- scopeExcludeNames = configScopeExcludeNames;
11395
+ scopeWatchLimit = worktreeWatchDirectoryLimitFromEnv();
11396
+ }
11397
+ function warnIfLegacyConfigPresent() {
11398
+ try {
11399
+ if (existsSync6(join13(cwd, ".code-viewer.json"))) {
11400
+ console.warn("[code-viewer] .code-viewer.json is no longer used; configure scope and upload from Viewer Settings instead. The file can be safely removed.");
11401
+ }
11402
+ } catch {}
11403
+ }
11404
+ function applyPersistedSettings(state) {
11405
+ const prevOmit = scopeOmitDirNames;
11406
+ const prevExclude = scopeExcludeNames;
11407
+ const prevWatchLimit = scopeWatchLimit;
11408
+ if (!scopeOmitDirCliOverride && Array.isArray(state.scopeOmitDirs) && state.scopeOmitDirs.length > 0) {
11409
+ scopeOmitDirNames = state.scopeOmitDirs;
11410
+ } else if (!scopeOmitDirCliOverride) {
11411
+ scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
11412
+ }
11413
+ if (Array.isArray(state.scopeExcludeNames) && state.scopeExcludeNames.length > 0) {
11414
+ scopeExcludeNames = state.scopeExcludeNames;
11415
+ } else {
11416
+ scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
11417
+ }
11418
+ if (state.scopeWatchLimit != null) {
11419
+ scopeWatchLimit = normalizeScopeWatchLimit(state.scopeWatchLimit);
11420
+ }
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;
11231
11438
  }
11232
11439
  function json2(data, init = {}) {
11233
11440
  return new Response(JSON.stringify(data), {
@@ -11529,45 +11736,6 @@ function parseScopeExcludeNamesQuery(value) {
11529
11736
  }
11530
11737
  return normalizeScopeExcludeNames(names);
11531
11738
  }
11532
- function loadProjectConfig() {
11533
- const full = join13(cwd, ".code-viewer.json");
11534
- if (!existsSync6(full))
11535
- return null;
11536
- let realCwd;
11537
- let realConfig;
11538
- try {
11539
- realCwd = realpathSync4(cwd);
11540
- realConfig = realpathSync4(full);
11541
- } catch {
11542
- return null;
11543
- }
11544
- if (dirname3(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
11545
- return null;
11546
- try {
11547
- const parsed = JSON.parse(readFileSync4(realConfig, "utf8"));
11548
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
11549
- return null;
11550
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
11551
- } catch {
11552
- return null;
11553
- }
11554
- }
11555
- function loadProjectConfigUploadDisabled() {
11556
- const config = loadProjectConfig();
11557
- return config?.upload?.enabled === false;
11558
- }
11559
- function loadProjectConfigScopeOmitDirs() {
11560
- const config = loadProjectConfig();
11561
- if (!config?.scope || !Array.isArray(config.scope.omitDirs))
11562
- return null;
11563
- return normalizeScopeOmitDirNames(config.scope.omitDirs);
11564
- }
11565
- function loadProjectConfigScopeExcludeNames() {
11566
- const config = loadProjectConfig();
11567
- if (!config?.scope || !Array.isArray(config.scope.excludeNames))
11568
- return null;
11569
- return normalizeScopeExcludeNames(config.scope.excludeNames);
11570
- }
11571
11739
  function scopeOmitDirNamesFromQuery(url) {
11572
11740
  if (!url.searchParams.has("omit_dirs"))
11573
11741
  return scopeOmitDirNames;
@@ -11735,7 +11903,7 @@ function handleTree(url) {
11735
11903
  branch: currentBranch(cwd) || undefined,
11736
11904
  entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
11737
11905
  readme: readReadme(target, path),
11738
- upload_enabled: !uploadDisabledByConfig && (target === "worktree" || target === "")
11906
+ upload_enabled: uploadEnabled && (target === "worktree" || target === "")
11739
11907
  });
11740
11908
  }
11741
11909
  function handleSettings() {
@@ -11748,7 +11916,11 @@ function handleSettings() {
11748
11916
  omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
11749
11917
  exclude_names_effective: scopeExcludeNames,
11750
11918
  exclude_names_built_in: DEFAULT_EXCLUDE_NAMES,
11751
- 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
11752
11924
  }
11753
11925
  });
11754
11926
  }
@@ -11757,7 +11929,14 @@ function worktreeWatchDirectoryLimitFromEnv() {
11757
11929
  if (!raw)
11758
11930
  return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11759
11931
  const parsed = Number(raw);
11760
- 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;
11761
11940
  }
11762
11941
  function handleFiles2(url) {
11763
11942
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
@@ -12297,8 +12476,8 @@ function uploadOpenFlags() {
12297
12476
  return constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0);
12298
12477
  }
12299
12478
  async function handleUploadFiles(req) {
12300
- if (uploadDisabledByConfig)
12301
- return text("upload disabled by project config", 403);
12479
+ if (!uploadEnabled)
12480
+ return text("upload disabled by viewer settings", 403);
12302
12481
  if (req.method !== "POST")
12303
12482
  return text("method not allowed", 405);
12304
12483
  if (!sideEffectRequestAllowed(req))
@@ -12851,9 +13030,6 @@ async function handleAnnotations(req) {
12851
13030
  }
12852
13031
  return text("invalid action", 400);
12853
13032
  }
12854
- function isCodeViewerInternalPath(path) {
12855
- return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
12856
- }
12857
13033
  function sendSse(event, data = "tick") {
12858
13034
  const payload = enc.encode(`event: ${event}
12859
13035
  data: ${data}
@@ -12901,7 +13077,43 @@ async function shutdown(exitCode = 0) {
12901
13077
  }
12902
13078
  process.exit(exitCode);
12903
13079
  }
12904
- 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, uploadDisabledByConfig = false, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, 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;
12905
13117
  var init_preview = __esm(async () => {
12906
13118
  init_routes();
12907
13119
  init_annotations();
@@ -12913,6 +13125,7 @@ var init_preview = __esm(async () => {
12913
13125
  init_runtime();
12914
13126
  init_search();
12915
13127
  init_server_registry();
13128
+ init_state_store();
12916
13129
  init_worktree_watcher();
12917
13130
  WEB_ROOT = join13(ROOT, "web");
12918
13131
  VERSION = JSON.parse(readFileSync4(join13(ROOT, "package.json"), "utf8")).version;
@@ -12963,6 +13176,7 @@ var init_preview = __esm(async () => {
12963
13176
  cliArgs = DEFAULT_ARGS;
12964
13177
  scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
12965
13178
  scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
13179
+ scopeWatchLimit = DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
12966
13180
  enc = new TextEncoder;
12967
13181
  sseClients = new Set;
12968
13182
  sseKeepalives = new Map;
@@ -12972,7 +13186,9 @@ var init_preview = __esm(async () => {
12972
13186
  lineIndexCache = new Map;
12973
13187
  blobLineIndexCache = new Map;
12974
13188
  blobBytesCache = new Map;
13189
+ isCodeViewerInternalPath = isToolInternalPath;
12975
13190
  parseCli();
13191
+ applyPersistedSettings(await loadAppSettingsState(cwd));
12976
13192
  server = await startServer({
12977
13193
  hostname: "127.0.0.1",
12978
13194
  port: listenPort,
@@ -13021,7 +13237,7 @@ var init_preview = __esm(async () => {
13021
13237
  }
13022
13238
  if (url.pathname.startsWith("/_state/")) {
13023
13239
  const { handleStateRoute: handleStateRoute2 } = await Promise.resolve().then(() => (init_state_route(), exports_state_route));
13024
- const stateResponse = await handleStateRoute2(req, url, cwd, sideEffectRequestAllowed);
13240
+ const stateResponse = await handleStateRoute2(req, url, cwd, sideEffectRequestAllowed, { onSettingsChange: applyPersistedSettings });
13025
13241
  if (stateResponse)
13026
13242
  return stateResponse;
13027
13243
  }
@@ -13046,6 +13262,12 @@ var init_preview = __esm(async () => {
13046
13262
  data: ok
13047
13263
 
13048
13264
  `));
13265
+ if (watchLimitReached !== null) {
13266
+ controller.enqueue(enc.encode(`event: watch-limit
13267
+ data: ${watchLimitReached}
13268
+
13269
+ `));
13270
+ }
13049
13271
  keepalive = setInterval(() => {
13050
13272
  try {
13051
13273
  controller.enqueue(enc.encode(`: ping
@@ -13107,19 +13329,7 @@ data: ok
13107
13329
  watch,
13108
13330
  sendReload: () => sendSse("reload")
13109
13331
  });
13110
- worktreeWatch = startWorktreeUpdateWatch({
13111
- root: cwd,
13112
- omitDirNames: scopeOmitDirNames,
13113
- excludeNames: scopeExcludeNames,
13114
- watch,
13115
- initialScanMode: "async",
13116
- maxWatchedDirectories: worktreeWatchDirectoryLimitFromEnv(),
13117
- onUpdate: triggerUpdate,
13118
- onError: (error) => {
13119
- const message = error instanceof Error ? error.message : String(error);
13120
- console.warn(`code-viewer worktree watch skipped: ${message}`);
13121
- }
13122
- });
13332
+ worktreeWatch = startScopedWorktreeWatch();
13123
13333
  console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
13124
13334
  console.log(`git-diff-preview serving ${cwd}`);
13125
13335
  });