@youtyan/code-viewer 0.2.7 → 0.2.9

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.
@@ -3726,286 +3726,10 @@ var init_search = __esm(() => {
3726
3726
  DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
3727
3727
  });
3728
3728
 
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(/^\/+/, "");
3738
- }
3739
- function isInsideRoot(root, path) {
3740
- const rel = relative(root, path).replace(/\\/g, "/");
3741
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
3742
- }
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;
3751
- }
3752
- });
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
- }
3762
- });
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
- }
3811
- };
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
- }
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)}`;
3996
- }
3997
- if (typeof cryptoApi?.getRandomValues === "function") {
3998
- const bytes = new Uint8Array(8);
3999
- cryptoApi.getRandomValues(bytes);
4000
- return `${prefix}-${bytesToHex(bytes)}`;
4001
- }
4002
- return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
4003
- }
4004
-
4005
3729
  // web-src/server/state-store.ts
4006
- import { join as join8 } from "node:path";
3730
+ import { join as join7 } from "node:path";
4007
3731
  function codeViewerPath(root, fileName) {
4008
- return join8(root, CODE_VIEWER_DIR2, fileName);
3732
+ return join7(root, CODE_VIEWER_DIR2, fileName);
4009
3733
  }
4010
3734
  function isRecord(value) {
4011
3735
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -4065,7 +3789,12 @@ function emptySettings() {
4065
3789
  return { version: 1 };
4066
3790
  }
4067
3791
  function emptyViewState() {
4068
- return { version: 1, collapsedDirs: [], viewedFiles: [] };
3792
+ return {
3793
+ version: 1,
3794
+ collapsedDirs: [],
3795
+ lazyExpandedDirs: [],
3796
+ viewedFiles: []
3797
+ };
4069
3798
  }
4070
3799
  function emptyDbUiState() {
4071
3800
  return { version: 1, columnWidths: {} };
@@ -4138,6 +3867,9 @@ function sanitizeSettings(raw) {
4138
3867
  });
4139
3868
  if (scopeExcludeNames)
4140
3869
  out.scopeExcludeNames = scopeExcludeNames;
3870
+ const uploadEnabled = optionalBoolean(raw.uploadEnabled);
3871
+ if (uploadEnabled !== undefined)
3872
+ out.uploadEnabled = uploadEnabled;
4141
3873
  if (isRecord(raw.range)) {
4142
3874
  const from = optionalString(raw.range.from, MAX_REF_LEN);
4143
3875
  const to = optionalString(raw.range.to, MAX_REF_LEN);
@@ -4171,6 +3903,12 @@ function sanitizeViewState(raw) {
4171
3903
  keepLast: true,
4172
3904
  sort: false
4173
3905
  }) ?? [],
3906
+ lazyExpandedDirs: normalizeStringList(raw.lazyExpandedDirs, {
3907
+ maxItems: MAX_VIEW_ITEMS,
3908
+ maxLen: MAX_KEY_LEN,
3909
+ keepLast: true,
3910
+ sort: false
3911
+ }) ?? [],
4174
3912
  viewedFiles: normalizeStringList(raw.viewedFiles, {
4175
3913
  maxItems: MAX_VIEW_ITEMS,
4176
3914
  maxLen: MAX_KEY_LEN,
@@ -4184,6 +3922,7 @@ function mergeViewState(current, patch) {
4184
3922
  return current;
4185
3923
  const base = sanitizeViewState({ ...current, version: 1 });
4186
3924
  const collapsedDirs = new Set(base.collapsedDirs);
3925
+ const lazyExpandedDirs = new Set(base.lazyExpandedDirs);
4187
3926
  const viewedFiles = new Set(base.viewedFiles);
4188
3927
  const addedCollapsedDirs = normalizeStringList(patch.addedCollapsedDirs, {
4189
3928
  maxItems: MAX_VIEW_ITEMS,
@@ -4191,8 +3930,10 @@ function mergeViewState(current, patch) {
4191
3930
  keepLast: true,
4192
3931
  sort: false
4193
3932
  });
4194
- for (const path of addedCollapsedDirs || [])
3933
+ for (const path of addedCollapsedDirs || []) {
4195
3934
  collapsedDirs.add(path);
3935
+ lazyExpandedDirs.delete(path);
3936
+ }
4196
3937
  const removedCollapsedDirs = normalizeStringList(patch.removedCollapsedDirs, {
4197
3938
  maxItems: MAX_VIEW_ITEMS,
4198
3939
  maxLen: MAX_KEY_LEN,
@@ -4200,167 +3941,506 @@ function mergeViewState(current, patch) {
4200
3941
  });
4201
3942
  for (const path of removedCollapsedDirs || [])
4202
3943
  collapsedDirs.delete(path);
3944
+ const addedLazyExpandedDirs = normalizeStringList(patch.addedLazyExpandedDirs, {
3945
+ maxItems: MAX_VIEW_ITEMS,
3946
+ maxLen: MAX_KEY_LEN,
3947
+ keepLast: true,
3948
+ sort: false
3949
+ });
3950
+ for (const path of addedLazyExpandedDirs || []) {
3951
+ if (!collapsedDirs.has(path))
3952
+ lazyExpandedDirs.add(path);
3953
+ }
3954
+ const removedLazyExpandedDirs = normalizeStringList(patch.removedLazyExpandedDirs, {
3955
+ maxItems: MAX_VIEW_ITEMS,
3956
+ maxLen: MAX_KEY_LEN,
3957
+ sort: false
3958
+ });
3959
+ for (const path of removedLazyExpandedDirs || [])
3960
+ lazyExpandedDirs.delete(path);
4203
3961
  const addedViewedFiles = normalizeStringList(patch.addedViewedFiles, {
4204
3962
  maxItems: MAX_VIEW_ITEMS,
4205
3963
  maxLen: MAX_KEY_LEN,
4206
3964
  keepLast: true,
4207
3965
  sort: false
4208
3966
  });
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
3967
+ for (const path of addedViewedFiles || [])
3968
+ viewedFiles.add(path);
3969
+ const removedViewedFiles = normalizeStringList(patch.removedViewedFiles, {
3970
+ maxItems: MAX_VIEW_ITEMS,
3971
+ maxLen: MAX_KEY_LEN,
3972
+ sort: false
3973
+ });
3974
+ for (const path of removedViewedFiles || [])
3975
+ viewedFiles.delete(path);
3976
+ return sanitizeViewState({
3977
+ version: 1,
3978
+ collapsedDirs: [...collapsedDirs],
3979
+ lazyExpandedDirs: [...lazyExpandedDirs],
3980
+ viewedFiles: [...viewedFiles]
3981
+ });
3982
+ }
3983
+ function safeObjectKey(value) {
3984
+ if (!value || value.length > MAX_KEY_LEN || value.includes("\x00"))
3985
+ return null;
3986
+ return value;
3987
+ }
3988
+ function sanitizeDbUiPrefs(raw) {
3989
+ if (!isRecord(raw))
3990
+ return;
3991
+ const out = {};
3992
+ for (const key of DB_UI_BOOL_PREF_KEYS) {
3993
+ const v = raw[key];
3994
+ if (v === true || v === false)
3995
+ out[key] = v;
3996
+ }
3997
+ return Object.keys(out).length > 0 ? out : undefined;
3998
+ }
3999
+ function sanitizeDbUiState(raw) {
4000
+ if (!isRecord(raw))
4001
+ return emptyDbUiState();
4002
+ const prefs = sanitizeDbUiPrefs(raw.prefs);
4003
+ if (!isRecord(raw.columnWidths)) {
4004
+ return prefs ? { ...emptyDbUiState(), prefs } : emptyDbUiState();
4005
+ }
4006
+ const columnWidths = {};
4007
+ let dbCount = 0;
4008
+ for (const [dbIdRaw, tablesRaw] of Object.entries(raw.columnWidths)) {
4009
+ if (dbCount >= MAX_DB_UI_DBS)
4010
+ break;
4011
+ const dbId = safeObjectKey(dbIdRaw);
4012
+ if (!dbId || !isRecord(tablesRaw))
4013
+ continue;
4014
+ const tables = {};
4015
+ let tableCount = 0;
4016
+ for (const [tableRaw, columnsRaw] of Object.entries(tablesRaw)) {
4017
+ if (tableCount >= MAX_DB_UI_TABLES)
4018
+ break;
4019
+ const table = safeObjectKey(tableRaw);
4020
+ if (!table || !isRecord(columnsRaw))
4021
+ continue;
4022
+ const columns = {};
4023
+ let columnCount = 0;
4024
+ for (const [columnRaw, widthRaw] of Object.entries(columnsRaw)) {
4025
+ if (columnCount >= MAX_DB_UI_COLUMNS)
4026
+ break;
4027
+ const column = safeObjectKey(columnRaw);
4028
+ const width = optionalNumber(widthRaw, 60, 1200);
4029
+ if (!column || width === undefined)
4030
+ continue;
4031
+ columns[column] = width;
4032
+ columnCount++;
4033
+ }
4034
+ if (Object.keys(columns).length === 0)
4035
+ continue;
4036
+ tables[table] = columns;
4037
+ tableCount++;
4038
+ }
4039
+ if (Object.keys(tables).length === 0)
4040
+ continue;
4041
+ columnWidths[dbId] = tables;
4042
+ dbCount++;
4043
+ }
4044
+ const out = { version: 1, columnWidths };
4045
+ if (prefs)
4046
+ out.prefs = prefs;
4047
+ return out;
4048
+ }
4049
+ function mergeDbUiPrefs(current, patch) {
4050
+ if (!isRecord(patch))
4051
+ return current;
4052
+ const next = { ...current ?? {} };
4053
+ for (const key of DB_UI_BOOL_PREF_KEYS) {
4054
+ const v = patch[key];
4055
+ if (v === null)
4056
+ delete next[key];
4057
+ else if (v === true || v === false)
4058
+ next[key] = v;
4059
+ }
4060
+ return Object.keys(next).length > 0 ? next : undefined;
4061
+ }
4062
+ function mergeDbUiState(current, patch) {
4063
+ if (!isRecord(patch))
4064
+ return current;
4065
+ const mergedPrefs = "prefs" in patch ? mergeDbUiPrefs(current.prefs, patch.prefs) : current.prefs;
4066
+ if (!isRecord(patch.columnWidths)) {
4067
+ const merged = { ...current, version: 1 };
4068
+ if (mergedPrefs)
4069
+ merged.prefs = mergedPrefs;
4070
+ else
4071
+ delete merged.prefs;
4072
+ return sanitizeDbUiState(merged);
4073
+ }
4074
+ const columnWidths = {
4075
+ ...current.columnWidths
4076
+ };
4077
+ for (const [dbId, tablesRaw] of Object.entries(patch.columnWidths)) {
4078
+ if (tablesRaw === null) {
4079
+ delete columnWidths[dbId];
4080
+ continue;
4081
+ }
4082
+ if (!isRecord(tablesRaw))
4083
+ continue;
4084
+ const tables = { ...columnWidths[dbId] || {} };
4085
+ for (const [table, columnsRaw] of Object.entries(tablesRaw)) {
4086
+ if (columnsRaw === null) {
4087
+ delete tables[table];
4088
+ continue;
4089
+ }
4090
+ if (!isRecord(columnsRaw))
4091
+ continue;
4092
+ const columns = { ...tables[table] || {} };
4093
+ for (const [column, widthRaw] of Object.entries(columnsRaw)) {
4094
+ if (widthRaw === null)
4095
+ delete columns[column];
4096
+ else
4097
+ columns[column] = widthRaw;
4098
+ }
4099
+ tables[table] = columns;
4100
+ }
4101
+ columnWidths[dbId] = tables;
4102
+ }
4103
+ return sanitizeDbUiState({
4104
+ ...current,
4105
+ ...patch,
4106
+ columnWidths,
4107
+ prefs: mergedPrefs,
4108
+ version: 1
4109
+ });
4110
+ }
4111
+ async function loadAppSettingsState(root) {
4112
+ return settingsStore.load(root);
4113
+ }
4114
+ async function patchAppSettingsState(root, patch) {
4115
+ return settingsStore.update(root, (state) => {
4116
+ const next = mergeSettings(state, patch);
4117
+ return { state: next, result: next };
4118
+ });
4119
+ }
4120
+ async function loadViewState(root) {
4121
+ return viewStateStore.load(root);
4122
+ }
4123
+ async function patchViewState(root, patch) {
4124
+ return viewStateStore.update(root, (state) => {
4125
+ const next = mergeViewState(state, patch);
4126
+ return { state: next, result: next };
4127
+ });
4128
+ }
4129
+ async function loadDbUiState(root) {
4130
+ return dbUiStore.load(root);
4131
+ }
4132
+ async function patchDbUiState(root, patch) {
4133
+ return dbUiStore.update(root, (state) => {
4134
+ const next = mergeDbUiState(state, patch);
4135
+ return { state: next, result: next };
4136
+ });
4137
+ }
4138
+ 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;
4139
+ var init_state_store = __esm(() => {
4140
+ init_json_store();
4141
+ DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
4142
+ settingsStore = createJsonFileStore({
4143
+ filePath: (root) => codeViewerPath(root, SETTINGS_FILE_NAME),
4144
+ empty: emptySettings,
4145
+ sanitize: sanitizeSettings,
4146
+ maxBytes: MAX_SETTINGS_BYTES,
4147
+ backupSuffix: "corrupt",
4148
+ sizeErrorMessage: "settings state too large"
4215
4149
  });
4216
- for (const path of removedViewedFiles || [])
4217
- viewedFiles.delete(path);
4218
- return sanitizeViewState({
4219
- version: 1,
4220
- collapsedDirs: [...collapsedDirs],
4221
- viewedFiles: [...viewedFiles]
4150
+ viewStateStore = createJsonFileStore({
4151
+ filePath: (root) => codeViewerPath(root, VIEW_STATE_FILE_NAME),
4152
+ empty: emptyViewState,
4153
+ sanitize: sanitizeViewState,
4154
+ maxBytes: MAX_VIEW_STATE_BYTES,
4155
+ backupSuffix: "corrupt",
4156
+ sizeErrorMessage: "view state too large"
4157
+ });
4158
+ dbUiStore = createJsonFileStore({
4159
+ filePath: (root) => codeViewerPath(root, DB_UI_FILE_NAME),
4160
+ empty: emptyDbUiState,
4161
+ sanitize: sanitizeDbUiState,
4162
+ maxBytes: MAX_DB_UI_BYTES,
4163
+ backupSuffix: "corrupt",
4164
+ sizeErrorMessage: "db UI state too large"
4222
4165
  });
4166
+ });
4167
+
4168
+ // web-src/server/worktree-watcher.ts
4169
+ import {
4170
+ lstatSync as lstatSync3,
4171
+ readdirSync as nodeReaddirSync,
4172
+ watch as nodeWatch
4173
+ } from "node:fs";
4174
+ import { join as join8, relative } from "node:path";
4175
+ function normalizeRelativePath(path) {
4176
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
4223
4177
  }
4224
- function safeObjectKey(value) {
4225
- if (!value || value.length > MAX_KEY_LEN || value.includes("\x00"))
4226
- return null;
4227
- return value;
4178
+ function isInsideRoot(root, path) {
4179
+ const rel = relative(root, path).replace(/\\/g, "/");
4180
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
4228
4181
  }
4229
- function sanitizeDbUiState(raw) {
4230
- if (!isRecord(raw) || !isRecord(raw.columnWidths))
4231
- return emptyDbUiState();
4232
- const columnWidths = {};
4233
- let dbCount = 0;
4234
- for (const [dbIdRaw, tablesRaw] of Object.entries(raw.columnWidths)) {
4235
- if (dbCount >= MAX_DB_UI_DBS)
4236
- break;
4237
- const dbId = safeObjectKey(dbIdRaw);
4238
- if (!dbId || !isRecord(tablesRaw))
4239
- continue;
4240
- const tables = {};
4241
- let tableCount = 0;
4242
- for (const [tableRaw, columnsRaw] of Object.entries(tablesRaw)) {
4243
- if (tableCount >= MAX_DB_UI_TABLES)
4244
- break;
4245
- const table = safeObjectKey(tableRaw);
4246
- if (!table || !isRecord(columnsRaw))
4182
+ function startWorktreeUpdateWatch(options) {
4183
+ const watch = options.watch || nodeWatch;
4184
+ const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
4185
+ const isDirectory = options.isDirectory || ((path) => {
4186
+ try {
4187
+ return lstatSync3(path).isDirectory();
4188
+ } catch {
4189
+ return false;
4190
+ }
4191
+ });
4192
+ const directorySignature = options.directorySignature || ((path) => {
4193
+ try {
4194
+ const stats = lstatSync3(path);
4195
+ if (!stats.isDirectory())
4196
+ return null;
4197
+ return `${stats.dev}:${stats.ino}`;
4198
+ } catch {
4199
+ return null;
4200
+ }
4201
+ });
4202
+ const setTimer = options.setTimeoutFn || setTimeout;
4203
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
4204
+ const debounceMs = options.debounceMs ?? 250;
4205
+ const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
4206
+ const watchers = new Map;
4207
+ const signatures = new Map;
4208
+ const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
4209
+ const initialScanQueue = [];
4210
+ let initialScanTimer = null;
4211
+ const pendingPathInspections = new Map;
4212
+ let pathInspectionTimer = null;
4213
+ let timer = null;
4214
+ const pendingChangedPaths = new Set;
4215
+ let watchLimitReported = false;
4216
+ const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
4217
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
4218
+ const ignoredDirectory = (dir) => {
4219
+ const rel = directoryRelativePath(dir);
4220
+ return Boolean(rel && ignored(rel));
4221
+ };
4222
+ const scheduleUpdate = (changedPath) => {
4223
+ if (changedPath)
4224
+ pendingChangedPaths.add(changedPath);
4225
+ if (timer)
4226
+ clearTimer(timer);
4227
+ timer = setTimer(() => {
4228
+ timer = null;
4229
+ const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
4230
+ pendingChangedPaths.clear();
4231
+ options.onUpdate(paths);
4232
+ }, debounceMs);
4233
+ };
4234
+ const reportWatchLimit = () => {
4235
+ if (watchLimitReported)
4236
+ return;
4237
+ watchLimitReported = true;
4238
+ options.onWatchLimit?.(maxWatchedDirectories);
4239
+ options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
4240
+ };
4241
+ const closeSubtree = (dir) => {
4242
+ for (const [watchedDir, watcher] of [...watchers]) {
4243
+ if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
4247
4244
  continue;
4248
- const columns = {};
4249
- let columnCount = 0;
4250
- for (const [columnRaw, widthRaw] of Object.entries(columnsRaw)) {
4251
- if (columnCount >= MAX_DB_UI_COLUMNS)
4252
- break;
4253
- const column = safeObjectKey(columnRaw);
4254
- const width = optionalNumber(widthRaw, 60, 1200);
4255
- if (!column || width === undefined)
4256
- continue;
4257
- columns[column] = width;
4258
- columnCount++;
4259
- }
4260
- if (Object.keys(columns).length === 0)
4245
+ try {
4246
+ watcher.close?.();
4247
+ } catch {}
4248
+ watchers.delete(watchedDir);
4249
+ signatures.delete(watchedDir);
4250
+ }
4251
+ };
4252
+ const closeAll = () => {
4253
+ if (initialScanTimer) {
4254
+ clearTimer(initialScanTimer);
4255
+ initialScanTimer = null;
4256
+ }
4257
+ if (pathInspectionTimer) {
4258
+ clearTimer(pathInspectionTimer);
4259
+ pathInspectionTimer = null;
4260
+ }
4261
+ initialScanQueue.length = 0;
4262
+ pendingPathInspections.clear();
4263
+ for (const watcher of [...watchers.values()]) {
4264
+ try {
4265
+ watcher.close?.();
4266
+ } catch {}
4267
+ }
4268
+ watchers.clear();
4269
+ signatures.clear();
4270
+ };
4271
+ const readChildDirectories = (dir) => {
4272
+ let entries;
4273
+ try {
4274
+ entries = readDirs(dir);
4275
+ } catch (error) {
4276
+ options.onError?.(error);
4277
+ return [];
4278
+ }
4279
+ const children = [];
4280
+ for (const entry of entries) {
4281
+ if (!entry.isDirectory())
4261
4282
  continue;
4262
- tables[table] = columns;
4263
- tableCount++;
4283
+ const child = join8(dir, entry.name);
4284
+ if (ignoredDirectory(child))
4285
+ continue;
4286
+ children.push(child);
4287
+ }
4288
+ return children;
4289
+ };
4290
+ const processInitialScanQueue = () => {
4291
+ initialScanTimer = null;
4292
+ if (watchers.size >= maxWatchedDirectories) {
4293
+ reportWatchLimit();
4294
+ initialScanQueue.length = 0;
4295
+ return;
4296
+ }
4297
+ const next = initialScanQueue.shift();
4298
+ if (next)
4299
+ watchDirectory(next, true);
4300
+ if (watchers.size >= maxWatchedDirectories) {
4301
+ reportWatchLimit();
4302
+ initialScanQueue.length = 0;
4303
+ }
4304
+ if (initialScanQueue.length)
4305
+ initialScanTimer = setTimer(processInitialScanQueue, 50);
4306
+ };
4307
+ const queueInitialChildren = (dir) => {
4308
+ const remaining = maxWatchedDirectories - watchers.size;
4309
+ if (remaining <= 0) {
4310
+ reportWatchLimit();
4311
+ return;
4312
+ }
4313
+ const children = readChildDirectories(dir);
4314
+ if (children.length > remaining)
4315
+ reportWatchLimit();
4316
+ initialScanQueue.push(...children.slice(0, remaining));
4317
+ if (!initialScanTimer)
4318
+ initialScanTimer = setTimer(processInitialScanQueue, 5000);
4319
+ };
4320
+ const processChangedPath = (changed, fullChangedPath) => {
4321
+ const known = watchers.has(fullChangedPath);
4322
+ if (isDirectory(fullChangedPath)) {
4323
+ if (known) {
4324
+ const signature = directorySignature(fullChangedPath);
4325
+ if (signature && signature !== signatures.get(fullChangedPath)) {
4326
+ closeSubtree(fullChangedPath);
4327
+ watchDirectory(fullChangedPath, initialScanAsync);
4328
+ }
4329
+ scheduleUpdate(changed);
4330
+ return;
4331
+ }
4332
+ watchDirectory(fullChangedPath, initialScanAsync);
4333
+ } else if (known) {
4334
+ closeSubtree(fullChangedPath);
4335
+ }
4336
+ scheduleUpdate(changed);
4337
+ };
4338
+ const processPathInspections = () => {
4339
+ pathInspectionTimer = null;
4340
+ const entries = [...pendingPathInspections];
4341
+ pendingPathInspections.clear();
4342
+ for (const [changed, fullChangedPath] of entries) {
4343
+ processChangedPath(changed, fullChangedPath);
4264
4344
  }
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
4345
  };
4281
- for (const [dbId, tablesRaw] of Object.entries(patch.columnWidths)) {
4282
- if (tablesRaw === null) {
4283
- delete columnWidths[dbId];
4284
- continue;
4346
+ const queuePathInspection = (changed, fullChangedPath) => {
4347
+ pendingPathInspections.set(changed, fullChangedPath);
4348
+ if (!pathInspectionTimer)
4349
+ pathInspectionTimer = setTimer(processPathInspections, 25);
4350
+ };
4351
+ const watchDirectory = (dir, initialScan = false) => {
4352
+ if (watchers.has(dir))
4353
+ return;
4354
+ if (watchers.size >= maxWatchedDirectories) {
4355
+ reportWatchLimit();
4356
+ return;
4285
4357
  }
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;
4358
+ const rel = directoryRelativePath(dir);
4359
+ if (rel && ignored(rel))
4360
+ return;
4361
+ try {
4362
+ const watcher = watch(dir, { persistent: false }, (_event, filename) => {
4363
+ if (!filename) {
4364
+ scheduleUpdate();
4365
+ return;
4366
+ }
4367
+ const changed = normalizeRelativePath(join8(rel, filename.toString()));
4368
+ if (ignored(changed))
4369
+ return;
4370
+ const fullChangedPath = join8(options.root, changed);
4371
+ if (!isInsideRoot(options.root, fullChangedPath))
4372
+ return;
4373
+ if (initialScanAsync) {
4374
+ queuePathInspection(changed, fullChangedPath);
4375
+ return;
4376
+ }
4377
+ processChangedPath(changed, fullChangedPath);
4378
+ }) || {};
4379
+ watchers.set(dir, watcher);
4380
+ const signature = directorySignature(dir);
4381
+ if (signature)
4382
+ signatures.set(dir, signature);
4383
+ watcher.on?.("error", () => {
4384
+ if (watchers.get(dir) === watcher) {
4385
+ watchers.delete(dir);
4386
+ signatures.delete(dir);
4387
+ }
4388
+ });
4389
+ watcher.on?.("close", () => {
4390
+ if (watchers.get(dir) === watcher) {
4391
+ watchers.delete(dir);
4392
+ signatures.delete(dir);
4393
+ }
4394
+ });
4395
+ } catch (error) {
4396
+ options.onError?.(error);
4397
+ return;
4304
4398
  }
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);
4399
+ if (initialScanAsync && initialScan) {
4400
+ queueInitialChildren(dir);
4401
+ return;
4402
+ }
4403
+ if (watchers.size >= maxWatchedDirectories) {
4404
+ reportWatchLimit();
4405
+ return;
4406
+ }
4407
+ for (const child of readChildDirectories(dir))
4408
+ watchDirectory(child);
4409
+ };
4410
+ watchDirectory(options.root, true);
4411
+ return { started: watchers.size > 0, close: closeAll };
4320
4412
  }
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
- });
4413
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 256;
4414
+ var init_worktree_watcher = __esm(() => {
4415
+ init_search();
4416
+ });
4417
+
4418
+ // web-src/core/control-chars.ts
4419
+ function hasControlCharacter(value) {
4420
+ for (const ch of value) {
4421
+ const code = ch.charCodeAt(0);
4422
+ if (code < 32 || code === 127)
4423
+ return true;
4424
+ }
4425
+ return false;
4326
4426
  }
4327
- async function loadDbUiState(root) {
4328
- return dbUiStore.load(root);
4427
+
4428
+ // web-src/core/id.ts
4429
+ function bytesToHex(bytes) {
4430
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
4329
4431
  }
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
- });
4432
+ function makeId(prefix) {
4433
+ const cryptoApi = globalThis.crypto;
4434
+ if (typeof cryptoApi?.randomUUID === "function") {
4435
+ return `${prefix}-${cryptoApi.randomUUID().replace(/-/g, "").slice(0, 16)}`;
4436
+ }
4437
+ if (typeof cryptoApi?.getRandomValues === "function") {
4438
+ const bytes = new Uint8Array(8);
4439
+ cryptoApi.getRandomValues(bytes);
4440
+ return `${prefix}-${bytesToHex(bytes)}`;
4441
+ }
4442
+ return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
4335
4443
  }
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
4444
 
4365
4445
  // web-src/server/database/adapters/abort.ts
4366
4446
  function abortError(message = "operation aborted") {
@@ -4528,25 +4608,23 @@ function sanitizeIdentifier(name, kind = "sqlite") {
4528
4608
  return `\`${name.replace(/`/g, "``")}\``;
4529
4609
  return `"${name.replace(/"/g, '""')}"`;
4530
4610
  }
4531
- function escapeSqlString(value) {
4532
- return `'${value.replace(/'/g, "''")}'`;
4611
+ function escapeSqlString(value, kind) {
4612
+ const escaped = kind === "mysql" ? value.replace(/\\/g, "\\\\").replace(/'/g, "''") : value.replace(/'/g, "''");
4613
+ return `'${escaped}'`;
4533
4614
  }
4534
- function buildFilterWhere(grouped, kind) {
4615
+ function buildFilterWhere(grouped, kind, exact) {
4535
4616
  const whereParts = [];
4536
4617
  const params = [];
4537
4618
  const useParams = kind === "sqlite";
4619
+ const castOf = (column) => kind === "mysql" ? `CAST(${sanitizeIdentifier(column, kind)} AS CHAR)` : `CAST(${sanitizeIdentifier(column, kind)} AS TEXT)`;
4538
4620
  for (const [value, cols] of grouped) {
4539
- const likeVal = useParams ? "?" : escapeSqlString(`%${value}%`);
4621
+ const likeVal = useParams ? "?" : escapeSqlString(`%${value}%`, kind);
4540
4622
  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}`);
4623
+ whereParts.push(`${castOf(cols[0])} LIKE ${likeVal}`);
4543
4624
  if (useParams)
4544
4625
  params.push(`%${value}%`);
4545
4626
  } 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
- });
4627
+ const orParts = cols.map((column) => `${castOf(column)} LIKE ${likeVal}`);
4550
4628
  whereParts.push(`(${orParts.join(" OR ")})`);
4551
4629
  if (useParams) {
4552
4630
  for (let i = 0;i < cols.length; i++)
@@ -4554,6 +4632,12 @@ function buildFilterWhere(grouped, kind) {
4554
4632
  }
4555
4633
  }
4556
4634
  }
4635
+ for (const cond of exact ?? []) {
4636
+ const rhs = useParams ? "?" : escapeSqlString(cond.value, kind);
4637
+ whereParts.push(`${castOf(cond.column)} = ${rhs}`);
4638
+ if (useParams)
4639
+ params.push(cond.value);
4640
+ }
4557
4641
  return { where: whereParts.join(" AND "), params, useParams };
4558
4642
  }
4559
4643
  function filterGroupedColumns(grouped, columnNames) {
@@ -4566,6 +4650,12 @@ function filterGroupedColumns(grouped, columnNames) {
4566
4650
  }
4567
4651
  return filtered;
4568
4652
  }
4653
+ function filterExactColumns(exact, columnNames) {
4654
+ if (!exact || exact.length === 0)
4655
+ return [];
4656
+ const validColumns = new Set(columnNames);
4657
+ return exact.filter((cond) => validColumns.has(cond.column));
4658
+ }
4569
4659
  function filterOrderByColumns(orderBy, columnNames) {
4570
4660
  if (!orderBy)
4571
4661
  return;
@@ -4869,6 +4959,8 @@ function buildExecArgs(config, sql) {
4869
4959
  "-A",
4870
4960
  "-F",
4871
4961
  "\t",
4962
+ "-R",
4963
+ PG_RECORD_SEPARATOR,
4872
4964
  "-v",
4873
4965
  "ON_ERROR_STOP=1",
4874
4966
  "-c",
@@ -5044,11 +5136,14 @@ function splitTsvLine(line, decodeFields) {
5044
5136
  const fields = line.split("\t");
5045
5137
  return decodeFields ? fields.map(decodeMysqlBatchField) : fields;
5046
5138
  }
5047
- function parseTsvOutput(stdout, hasHeader) {
5048
- const text = stripFinalLineBreak(stdout);
5139
+ function stripFinalRecordSeparator(text, recordSeparator) {
5140
+ return text.endsWith(recordSeparator) ? text.slice(0, -recordSeparator.length) : text;
5141
+ }
5142
+ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
5143
+ const text = recordSeparator ? stripFinalRecordSeparator(stdout, recordSeparator) : stripFinalLineBreak(stdout);
5049
5144
  if (text.length === 0)
5050
5145
  return { columns: [], rows: [] };
5051
- const lines = text.split(/\r?\n/);
5146
+ const lines = recordSeparator ? text.split(recordSeparator) : text.split(/\r?\n/);
5052
5147
  if (lines.length === 0)
5053
5148
  return { columns: [], rows: [] };
5054
5149
  if (hasHeader) {
@@ -5115,13 +5210,17 @@ function createTableMetaCache(now = () => Date.now()) {
5115
5210
  }
5116
5211
  };
5117
5212
  }
5213
+ function observeBackgroundRejection(promise) {
5214
+ promise.catch(() => {});
5215
+ return promise;
5216
+ }
5118
5217
  function createDockerAdapter(config) {
5119
5218
  async function execAsync(sql, signal) {
5120
5219
  const result = await execInContainerAsync(config, sql, 1e4, signal);
5121
5220
  if (result.code !== 0) {
5122
5221
  throw new Error(result.stderr.trim() || "query failed");
5123
5222
  }
5124
- return parseTsvOutput(result.stdout, config.kind === "mysql");
5223
+ return parseTsvOutput(result.stdout, config.kind === "mysql", config.kind === "postgresql" ? PG_RECORD_SEPARATOR : undefined);
5125
5224
  }
5126
5225
  function toDbValue(val) {
5127
5226
  if (val === "NULL" || val === "\\N")
@@ -5214,25 +5313,18 @@ function createDockerAdapter(config) {
5214
5313
  },
5215
5314
  async getIndexesAsync(signal) {
5216
5315
  let sql;
5316
+ const INDEX_COL_SEP = "\x1F";
5217
5317
  if (config.kind === "postgresql") {
5218
- sql = `SELECT indexname, tablename FROM pg_indexes WHERE schemaname = ${postgresSchemaLiteral()} AND indexname NOT LIKE 'pg_%' ORDER BY indexname`;
5318
+ 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`;
5219
5319
  } else {
5220
- sql = `SELECT DISTINCT index_name, table_name, non_unique FROM information_schema.statistics WHERE table_schema = DATABASE() ORDER BY index_name`;
5320
+ 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`;
5221
5321
  }
5222
5322
  const result = await execAsync(sql, signal);
5223
- if (config.kind === "postgresql") {
5224
- return result.rows.map((row) => ({
5225
- name: row[0],
5226
- table: row[1],
5227
- columns: [],
5228
- unique: false
5229
- }));
5230
- }
5231
5323
  return result.rows.map((row) => ({
5232
5324
  name: row[0],
5233
5325
  table: row[1],
5234
- columns: [],
5235
- unique: row[2] === "0"
5326
+ unique: row[2] === "1",
5327
+ columns: row[3] ? row[3].split(INDEX_COL_SEP).filter((s) => s.length > 0) : []
5236
5328
  }));
5237
5329
  },
5238
5330
  async getForeignKeysAsync(signal) {
@@ -5386,7 +5478,7 @@ function createDockerAdapter(config) {
5386
5478
  const id = tableIdentifier(table);
5387
5479
  const countSql = `SELECT COUNT(*) AS cnt FROM ${id}`;
5388
5480
  const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table, signal));
5389
- const totalRowsPromise = tableMetaCache.getRowCount(table, async () => rowCountFromResult(await execAsync(countSql, signal)));
5481
+ const totalRowsPromise = observeBackgroundRejection(tableMetaCache.getRowCount(table, async () => rowCountFromResult(await execAsync(countSql, signal))));
5390
5482
  let columns;
5391
5483
  try {
5392
5484
  columns = await columnsPromise;
@@ -5422,7 +5514,7 @@ function createDockerAdapter(config) {
5422
5514
  }
5423
5515
  const columnNames = columns.map((column) => column.name);
5424
5516
  const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames), config.kind);
5425
- const where = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), config.kind).where;
5517
+ const where = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), config.kind, filterExactColumns(options.exact, columnNames)).where;
5426
5518
  const whereClause = where ? ` WHERE ${where}` : "";
5427
5519
  const countSql = `SELECT COUNT(*) AS cnt FROM ${id}${whereClause}`;
5428
5520
  const countResultPromise = execAsync(countSql, signal);
@@ -5602,7 +5694,7 @@ async function listDockerDatabasesAsync(serviceName, kind, env, cwd, signal) {
5602
5694
  return fallback;
5603
5695
  return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
5604
5696
  }
5605
- const parsed = parseTsvOutput(result.stdout, kind === "mysql");
5697
+ const parsed = parseTsvOutput(result.stdout, kind === "mysql", kind === "postgresql" ? PG_RECORD_SEPARATOR : undefined);
5606
5698
  const dbs = parsed.rows.map((r) => r[0]).filter(Boolean);
5607
5699
  const value = dbs.length > 0 ? dbs : fallbackDockerDatabases(defaultDb);
5608
5700
  return setDockerDatabasesCache(cacheKey, value, value.length > 0 ? DOCKER_DATABASES_POSITIVE_TTL_MS : DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
@@ -5643,7 +5735,7 @@ async function listDockerSchemasAsync(serviceName, kind, env, cwd, overrideDatab
5643
5735
  if (result.code !== 0) {
5644
5736
  return setDockerSchemasCache(cacheKey, ["public"], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
5645
5737
  }
5646
- const parsed = parseTsvOutput(result.stdout, false);
5738
+ const parsed = parseTsvOutput(result.stdout, false, PG_RECORD_SEPARATOR);
5647
5739
  const schemas = parsed.rows.map((r) => r[0]).filter(Boolean);
5648
5740
  const value = schemas.length > 0 ? schemas : ["public"];
5649
5741
  return setDockerSchemasCache(cacheKey, value, DOCKER_DATABASES_POSITIVE_TTL_MS, now);
@@ -5667,7 +5759,7 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
5667
5759
  ...kind === "postgresql" && schema ? { schema } : {}
5668
5760
  });
5669
5761
  }
5670
- 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;
5762
+ 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;
5671
5763
  var init_docker = __esm(() => {
5672
5764
  init_sql_snapshot();
5673
5765
  init_docker_utils();
@@ -5866,7 +5958,7 @@ function createSqliteAdapter(db) {
5866
5958
  async getFilteredTablePageWithMeta(table, options) {
5867
5959
  const columns = queryColumns(db, table);
5868
5960
  const columnNames = columns.map((column) => column.name);
5869
- const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite");
5961
+ const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite", filterExactColumns(options.exact, columnNames));
5870
5962
  const order = buildOrderClause2(filterOrderByColumns(options.orderBy, columnNames));
5871
5963
  const tableId = sanitizeIdentifier(table);
5872
5964
  const whereClause = filter.where ? ` WHERE ${filter.where}` : "";
@@ -5877,7 +5969,7 @@ function createSqliteAdapter(db) {
5877
5969
  columns,
5878
5970
  rows: result.rows,
5879
5971
  rowCount: result.rowCount,
5880
- totalRows: countRow?.cnt ?? 0
5972
+ totalRows: Number(countRow?.cnt ?? 0)
5881
5973
  };
5882
5974
  },
5883
5975
  executeReadonlyQuery(sql, params, maxRows = 1000) {
@@ -8566,9 +8658,16 @@ function parseBuckets(xml) {
8566
8658
  };
8567
8659
  }).filter((bucket) => bucket.name).sort((a, b) => a.name.localeCompare(b.name));
8568
8660
  }
8661
+ function decodeS3UrlEncoded(value) {
8662
+ try {
8663
+ return decodeURIComponent(value);
8664
+ } catch {
8665
+ return value;
8666
+ }
8667
+ }
8569
8668
  function parseObjects(xml) {
8570
8669
  const objects = xmlBlocks(xml, "Contents").map((block) => {
8571
- const key = xmlText(block, "Key") || "";
8670
+ const key = decodeS3UrlEncoded(xmlText(block, "Key") || "");
8572
8671
  const updatedAt = toIsoDate(xmlText(block, "LastModified"));
8573
8672
  return {
8574
8673
  key,
@@ -8578,8 +8677,10 @@ function parseObjects(xml) {
8578
8677
  ...xmlText(block, "StorageClass") ? { storageClass: xmlText(block, "StorageClass") } : {}
8579
8678
  };
8580
8679
  }).filter((object) => object.key);
8680
+ const commonPrefixes = xmlBlocks(xml, "CommonPrefixes").map((block) => decodeS3UrlEncoded(xmlText(block, "Prefix") || "")).filter(Boolean);
8581
8681
  return {
8582
8682
  objects,
8683
+ commonPrefixes,
8583
8684
  nextToken: xmlText(xml, "NextContinuationToken"),
8584
8685
  truncated: xmlText(xml, "IsTruncated") === "true"
8585
8686
  };
@@ -8661,8 +8762,10 @@ function createS3Adapter(config) {
8661
8762
  bucket: opts.bucket,
8662
8763
  query: {
8663
8764
  "list-type": "2",
8765
+ "encoding-type": "url",
8664
8766
  "max-keys": String(Math.min(1000, Math.max(1, opts.maxKeys ?? 200))),
8665
8767
  ...opts.prefix ? { prefix: opts.prefix } : {},
8768
+ ...opts.delimiter ? { delimiter: opts.delimiter } : {},
8666
8769
  ...opts.continuationToken ? { "continuation-token": opts.continuationToken } : {}
8667
8770
  },
8668
8771
  signal: opts.signal
@@ -9020,6 +9123,42 @@ ${s3ObjectName(object.key)}`.toLowerCase();
9020
9123
  return s3ErrorResponse(err, "list s3 objects");
9021
9124
  }
9022
9125
  }
9126
+ async function handleFolder(cwd, req, url, omitDirNames) {
9127
+ const r = await resolveS3(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
9128
+ if (r instanceof Response)
9129
+ return r;
9130
+ const bucket = validateBucket(url.searchParams.get("bucket"));
9131
+ if (bucket instanceof Response)
9132
+ return bucket;
9133
+ const prefix = validateOptionalText(url.searchParams.get("prefix"), "prefix", 2048);
9134
+ if (prefix instanceof Response)
9135
+ return prefix;
9136
+ const token = validateOptionalText(url.searchParams.get("token"), "token", 4096);
9137
+ if (token instanceof Response)
9138
+ return token;
9139
+ try {
9140
+ const page = await r.explorer.listObjects({
9141
+ bucket,
9142
+ prefix,
9143
+ delimiter: "/",
9144
+ continuationToken: token || undefined,
9145
+ maxKeys: MAX_OBJECT_LIMIT,
9146
+ signal: req.signal
9147
+ });
9148
+ const objects = page.objects.filter((object) => object.key !== prefix);
9149
+ const body = {
9150
+ dbId: r.dbId,
9151
+ bucket,
9152
+ prefix,
9153
+ folders: page.commonPrefixes ?? [],
9154
+ objects,
9155
+ ...page.nextToken ? { nextToken: page.nextToken } : {}
9156
+ };
9157
+ return json(body);
9158
+ } catch (err) {
9159
+ return s3ErrorResponse(err, "list s3 folder");
9160
+ }
9161
+ }
9023
9162
  async function handleHead(cwd, req, url, omitDirNames) {
9024
9163
  const r = await resolveS3(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
9025
9164
  if (r instanceof Response)
@@ -9105,6 +9244,10 @@ async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
9105
9244
  methods: ["GET"],
9106
9245
  handler: () => handleObjects(cwd, req, url, omitDirNames)
9107
9246
  },
9247
+ "/_db/s3/folder": {
9248
+ methods: ["GET"],
9249
+ handler: () => handleFolder(cwd, req, url, omitDirNames)
9250
+ },
9108
9251
  "/_db/s3/head": {
9109
9252
  methods: ["GET"],
9110
9253
  handler: () => handleHead(cwd, req, url, omitDirNames)
@@ -9799,6 +9942,9 @@ function sanitize(input) {
9799
9942
  const sidebarWidth = sanitizeCssSize(tab.sidebarWidth);
9800
9943
  if (sidebarWidth !== undefined)
9801
9944
  out.sidebarWidth = sidebarWidth;
9945
+ const relatedPanelHeight = sanitizeCssSize(tab.relatedPanelHeight);
9946
+ if (relatedPanelHeight !== undefined)
9947
+ out.relatedPanelHeight = relatedPanelHeight;
9802
9948
  const redis = sanitizeRedis(tab.redis);
9803
9949
  if (redis !== undefined)
9804
9950
  out.redis = redis;
@@ -10039,19 +10185,25 @@ async function handleSchema(cwd, url, omitDirNames, signal) {
10039
10185
  linkedAbort.cleanup();
10040
10186
  }
10041
10187
  }
10042
- function parseFilters(url) {
10043
- const raw = url.searchParams.get("filters");
10188
+ function parseColumnValuePairs(url, param) {
10189
+ const raw = url.searchParams.get(param);
10044
10190
  if (!raw)
10045
10191
  return [];
10046
10192
  try {
10047
10193
  const parsed = JSON.parse(raw);
10048
10194
  if (!Array.isArray(parsed))
10049
10195
  return [];
10050
- return parsed.filter((f) => !!f && typeof f === "object" && typeof f.column === "string" && typeof f.value === "string");
10196
+ 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);
10051
10197
  } catch {
10052
10198
  return [];
10053
10199
  }
10054
10200
  }
10201
+ function parseFilters(url) {
10202
+ return parseColumnValuePairs(url, "filters");
10203
+ }
10204
+ function parseExactConditions(url) {
10205
+ return parseColumnValuePairs(url, "eq");
10206
+ }
10055
10207
  function groupFiltersByValue(filters) {
10056
10208
  const grouped = new Map;
10057
10209
  for (const filter of filters) {
@@ -10082,14 +10234,16 @@ async function handleTable(cwd, url, omitDirNames, signal) {
10082
10234
  ];
10083
10235
  }
10084
10236
  const filters = parseFilters(url);
10237
+ const exact = parseExactConditions(url);
10085
10238
  try {
10086
10239
  const adapter = await getAdapter(r, cwd, signal);
10087
- if (filters.length > 0) {
10240
+ if (filters.length > 0 || exact.length > 0) {
10088
10241
  const meta2 = await adapter.getFilteredTablePageWithMeta(table, {
10089
10242
  offset,
10090
10243
  limit,
10091
10244
  orderBy,
10092
- grouped: groupFiltersByValue(filters)
10245
+ grouped: groupFiltersByValue(filters),
10246
+ ...exact.length > 0 ? { exact } : {}
10093
10247
  }, signal);
10094
10248
  const colNames2 = new Set(meta2.columns.map((c) => c.name));
10095
10249
  if (sortCol && !colNames2.has(sortCol)) {
@@ -10363,6 +10517,7 @@ async function handleExport(cwd, url, omitDirNames, signal) {
10363
10517
  ];
10364
10518
  }
10365
10519
  const filters = parseFilters(url);
10520
+ const exact = parseExactConditions(url);
10366
10521
  try {
10367
10522
  const adapter = await getAdapter(r, cwd, signal);
10368
10523
  const db = asAsync(adapter);
@@ -10373,12 +10528,13 @@ async function handleExport(cwd, url, omitDirNames, signal) {
10373
10528
  return textError(`invalid sort column: ${sortCol}`, 400);
10374
10529
  }
10375
10530
  let rawRows;
10376
- if (filters.length > 0) {
10531
+ if (filters.length > 0 || exact.length > 0) {
10377
10532
  const meta = await adapter.getFilteredTablePageWithMeta(table, {
10378
10533
  offset: 0,
10379
10534
  limit: EXPORT_MAX_ROWS,
10380
10535
  orderBy,
10381
- grouped: groupFiltersByValue(filters)
10536
+ grouped: groupFiltersByValue(filters),
10537
+ ...exact.length > 0 ? { exact } : {}
10382
10538
  }, signal);
10383
10539
  rawRows = meta.rows;
10384
10540
  } else {
@@ -11015,7 +11171,7 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
11015
11171
  }
11016
11172
  }, sideEffectAllowed, wrapResponse, (err) => handleError("database", "handle database request", err));
11017
11173
  }
11018
- 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;
11174
+ 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;
11019
11175
  var init_handle = __esm(() => {
11020
11176
  init_state_store();
11021
11177
  init_docker();
@@ -11082,12 +11238,20 @@ async function parseJsonBody(req) {
11082
11238
  async function handleSettingsGet(cwd) {
11083
11239
  return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
11084
11240
  }
11085
- async function handleSettingsPatch(cwd, req) {
11241
+ async function handleSettingsPatch(cwd, req, onChange) {
11086
11242
  const body = await parseJsonBody(req);
11087
11243
  if (body instanceof Response)
11088
11244
  return body;
11089
11245
  try {
11090
- return json(await patchAppSettingsState(cwd, body));
11246
+ const next = await patchAppSettingsState(cwd, body);
11247
+ if (onChange) {
11248
+ try {
11249
+ onChange(next);
11250
+ } catch (notifyErr) {
11251
+ console.warn("[code-viewer] settings change notify failed:", notifyErr);
11252
+ }
11253
+ }
11254
+ return json(next);
11091
11255
  } catch (err) {
11092
11256
  const message = err instanceof Error ? err.message : String(err);
11093
11257
  if (message === "settings state too large")
@@ -11113,12 +11277,12 @@ async function handleViewPatch(cwd, req) {
11113
11277
  return textError("failed to save view state", 500);
11114
11278
  }
11115
11279
  }
11116
- async function handleStateRoute(req, url, cwd, sideEffectAllowed) {
11280
+ async function handleStateRoute(req, url, cwd, sideEffectAllowed, options = {}) {
11117
11281
  return dispatchRoutes(req, url, {
11118
11282
  "/_state/settings": {
11119
11283
  methods: ["GET", "PATCH"],
11120
11284
  sideEffect: (method) => method !== "GET",
11121
- handler: () => req.method === "GET" ? handleSettingsGet(cwd) : handleSettingsPatch(cwd, req)
11285
+ handler: () => req.method === "GET" ? handleSettingsGet(cwd) : handleSettingsPatch(cwd, req, options.onSettingsChange)
11122
11286
  },
11123
11287
  "/_state/view": {
11124
11288
  methods: ["GET", "PATCH"],
@@ -11214,16 +11378,30 @@ Examples:
11214
11378
  }
11215
11379
  if (rest.length)
11216
11380
  cliArgs = rest;
11217
- const configScopeOmitDirs = loadProjectConfigScopeOmitDirs();
11218
- const configScopeExcludeNames = loadProjectConfigScopeExcludeNames();
11219
- uploadDisabledByConfig = loadProjectConfigUploadDisabled();
11381
+ warnIfLegacyConfigPresent();
11220
11382
  if (scopeOmitDirCliOverride) {
11221
11383
  scopeOmitDirNames = scopeOmitDirCliOverride;
11222
- } else if (configScopeOmitDirs) {
11223
- scopeOmitDirNames = configScopeOmitDirs;
11224
11384
  }
11225
- if (configScopeExcludeNames)
11226
- scopeExcludeNames = configScopeExcludeNames;
11385
+ }
11386
+ function warnIfLegacyConfigPresent() {
11387
+ try {
11388
+ if (existsSync6(join13(cwd, ".code-viewer.json"))) {
11389
+ 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.");
11390
+ }
11391
+ } catch {}
11392
+ }
11393
+ function applyPersistedSettings(state) {
11394
+ if (!scopeOmitDirCliOverride && Array.isArray(state.scopeOmitDirs) && state.scopeOmitDirs.length > 0) {
11395
+ scopeOmitDirNames = state.scopeOmitDirs;
11396
+ } else if (!scopeOmitDirCliOverride) {
11397
+ scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
11398
+ }
11399
+ if (Array.isArray(state.scopeExcludeNames) && state.scopeExcludeNames.length > 0) {
11400
+ scopeExcludeNames = state.scopeExcludeNames;
11401
+ } else {
11402
+ scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
11403
+ }
11404
+ uploadEnabled = state.uploadEnabled !== false;
11227
11405
  }
11228
11406
  function json2(data, init = {}) {
11229
11407
  return new Response(JSON.stringify(data), {
@@ -11525,45 +11703,6 @@ function parseScopeExcludeNamesQuery(value) {
11525
11703
  }
11526
11704
  return normalizeScopeExcludeNames(names);
11527
11705
  }
11528
- function loadProjectConfig() {
11529
- const full = join13(cwd, ".code-viewer.json");
11530
- if (!existsSync6(full))
11531
- return null;
11532
- let realCwd;
11533
- let realConfig;
11534
- try {
11535
- realCwd = realpathSync4(cwd);
11536
- realConfig = realpathSync4(full);
11537
- } catch {
11538
- return null;
11539
- }
11540
- if (dirname3(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
11541
- return null;
11542
- try {
11543
- const parsed = JSON.parse(readFileSync4(realConfig, "utf8"));
11544
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
11545
- return null;
11546
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
11547
- } catch {
11548
- return null;
11549
- }
11550
- }
11551
- function loadProjectConfigUploadDisabled() {
11552
- const config = loadProjectConfig();
11553
- return config?.upload?.enabled === false;
11554
- }
11555
- function loadProjectConfigScopeOmitDirs() {
11556
- const config = loadProjectConfig();
11557
- if (!config?.scope || !Array.isArray(config.scope.omitDirs))
11558
- return null;
11559
- return normalizeScopeOmitDirNames(config.scope.omitDirs);
11560
- }
11561
- function loadProjectConfigScopeExcludeNames() {
11562
- const config = loadProjectConfig();
11563
- if (!config?.scope || !Array.isArray(config.scope.excludeNames))
11564
- return null;
11565
- return normalizeScopeExcludeNames(config.scope.excludeNames);
11566
- }
11567
11706
  function scopeOmitDirNamesFromQuery(url) {
11568
11707
  if (!url.searchParams.has("omit_dirs"))
11569
11708
  return scopeOmitDirNames;
@@ -11731,7 +11870,7 @@ function handleTree(url) {
11731
11870
  branch: currentBranch(cwd) || undefined,
11732
11871
  entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
11733
11872
  readme: readReadme(target, path),
11734
- upload_enabled: !uploadDisabledByConfig && (target === "worktree" || target === "")
11873
+ upload_enabled: uploadEnabled && (target === "worktree" || target === "")
11735
11874
  });
11736
11875
  }
11737
11876
  function handleSettings() {
@@ -12293,8 +12432,8 @@ function uploadOpenFlags() {
12293
12432
  return constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0);
12294
12433
  }
12295
12434
  async function handleUploadFiles(req) {
12296
- if (uploadDisabledByConfig)
12297
- return text("upload disabled by project config", 403);
12435
+ if (!uploadEnabled)
12436
+ return text("upload disabled by viewer settings", 403);
12298
12437
  if (req.method !== "POST")
12299
12438
  return text("method not allowed", 405);
12300
12439
  if (!sideEffectRequestAllowed(req))
@@ -12847,9 +12986,6 @@ async function handleAnnotations(req) {
12847
12986
  }
12848
12987
  return text("invalid action", 400);
12849
12988
  }
12850
- function isCodeViewerInternalPath(path) {
12851
- return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
12852
- }
12853
12989
  function sendSse(event, data = "tick") {
12854
12990
  const payload = enc.encode(`event: ${event}
12855
12991
  data: ${data}
@@ -12897,7 +13033,7 @@ async function shutdown(exitCode = 0) {
12897
13033
  }
12898
13034
  process.exit(exitCode);
12899
13035
  }
12900
- 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;
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;
12901
13037
  var init_preview = __esm(async () => {
12902
13038
  init_routes();
12903
13039
  init_annotations();
@@ -12909,6 +13045,7 @@ var init_preview = __esm(async () => {
12909
13045
  init_runtime();
12910
13046
  init_search();
12911
13047
  init_server_registry();
13048
+ init_state_store();
12912
13049
  init_worktree_watcher();
12913
13050
  WEB_ROOT = join13(ROOT, "web");
12914
13051
  VERSION = JSON.parse(readFileSync4(join13(ROOT, "package.json"), "utf8")).version;
@@ -12968,7 +13105,9 @@ var init_preview = __esm(async () => {
12968
13105
  lineIndexCache = new Map;
12969
13106
  blobLineIndexCache = new Map;
12970
13107
  blobBytesCache = new Map;
13108
+ isCodeViewerInternalPath = isToolInternalPath;
12971
13109
  parseCli();
13110
+ applyPersistedSettings(await loadAppSettingsState(cwd));
12972
13111
  server = await startServer({
12973
13112
  hostname: "127.0.0.1",
12974
13113
  port: listenPort,
@@ -13017,7 +13156,7 @@ var init_preview = __esm(async () => {
13017
13156
  }
13018
13157
  if (url.pathname.startsWith("/_state/")) {
13019
13158
  const { handleStateRoute: handleStateRoute2 } = await Promise.resolve().then(() => (init_state_route(), exports_state_route));
13020
- const stateResponse = await handleStateRoute2(req, url, cwd, sideEffectRequestAllowed);
13159
+ const stateResponse = await handleStateRoute2(req, url, cwd, sideEffectRequestAllowed, { onSettingsChange: applyPersistedSettings });
13021
13160
  if (stateResponse)
13022
13161
  return stateResponse;
13023
13162
  }
@@ -13042,6 +13181,12 @@ var init_preview = __esm(async () => {
13042
13181
  data: ok
13043
13182
 
13044
13183
  `));
13184
+ if (watchLimitReached !== null) {
13185
+ controller.enqueue(enc.encode(`event: watch-limit
13186
+ data: ${watchLimitReached}
13187
+
13188
+ `));
13189
+ }
13045
13190
  keepalive = setInterval(() => {
13046
13191
  try {
13047
13192
  controller.enqueue(enc.encode(`: ping
@@ -13111,6 +13256,10 @@ data: ok
13111
13256
  initialScanMode: "async",
13112
13257
  maxWatchedDirectories: worktreeWatchDirectoryLimitFromEnv(),
13113
13258
  onUpdate: triggerUpdate,
13259
+ onWatchLimit: (limit) => {
13260
+ watchLimitReached = limit;
13261
+ sendSse("watch-limit", String(limit));
13262
+ },
13114
13263
  onError: (error) => {
13115
13264
  const message = error instanceof Error ? error.message : String(error);
13116
13265
  console.warn(`code-viewer worktree watch skipped: ${message}`);