termdock 1.4.122 → 1.4.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{ModelPreview-CJe-PWxc.js → ModelPreview-Be2smem8.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-CZGYYTkL.js → abnfDiagram-VRR7QNED-BlflMbiO.js} +1 -1
- package/dist/client/assets/{arc-C9ovRRCB.js → arc-BLcH6XEC.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-CfijFgFE.js → architectureDiagram-ZJ3FMSHR-CnBbD-1o.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-_ewP5xzy.js → blockDiagram-677ZJIJ3-DPF-Ts3E.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-BdFNzfaB.js → c4Diagram-LMCZKHZV-bj_qgr87.js} +1 -1
- package/dist/client/assets/channel-CwqZRIbt.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-Bk-Zrlca.js → chunk-2Q5K7J3B-BLPLyR9K.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-CXmu1n-P.js → chunk-32BRIVSS-DWlXbXJt.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-D1ewEFAt.js → chunk-5VM5RSS4-CEM0N0ye.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-Nb-EMdx-.js → chunk-EX3LRPZG-Dty9y4gw.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-BMh6fTUm.js → chunk-JWPE2WC7-dlwhK_i1.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-DGTq8m1i.js → chunk-MOJQB5TN-CX08FiG7.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-DnIXl6Ku.js → chunk-RYQCIY6F-CoYfVCEL.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-d6x1hwlS.js → chunk-V7JOEXUC-C_nJAEZ6.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-Cst36NFV.js → chunk-VR4S4FIN-CxTZ4h0g.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-DDzcV3AA.js → chunk-XXDRQBXY-CRcElLUQ.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CsBWdSSp.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CsBWdSSp.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-DPcjHu-a.js → cose-bilkent-JH36ORCC-DopCQZMr.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-DLWGJOKu.js → cynefin-VYW2F7L2-C5lwo5rG.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-ChwiuhwW.js → cynefinDiagram-TSTJHNR4-BqE-HnNf.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-DMn68BCd.js → dagre-VKFMJZFB-Qnuvs3u5.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-BWzt083-.js → diagram-FQU43EPY-T6rXZ6sy.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-1Ifm4uV0.js → diagram-G47NLZAW-BlKH1yFL.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-Bo_SF-cE.js → diagram-NH7WQ7WH-BsAh_h83.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DLX-LCJB.js → diagram-OA4YK3LP-DFFBJ56F.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-Bf5qmb8B.js → diagram-WEI45ONY-BAt9TFgr.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-jmsp3qMg.js → ebnfDiagram-CCIWWBDH-CZqTgzyZ.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-BSJ9O_Uj.js → erDiagram-Q63AITRT-Des9G2E2.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-BhcIFwIa.js → flowDiagram-23GEKE2U-D-GCzapP.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-Dl4sz2ED.js → ganttDiagram-NO4QXBWP-BGH_iM7_.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-BpXwoKi_.js → gitGraphDiagram-IHSO6WYX-dheKFUpw.js} +1 -1
- package/dist/client/assets/index-BCdd_adt.css +1 -0
- package/dist/client/assets/index-DjYnI5kP.js +457 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-1n8vVZDv.js → infoDiagram-FWYZ7A6U-BZYW2clU.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-BOoubbH7.js → ishikawaDiagram-FXEZZL3T-Di9GvVpK.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-C4gMgc1B.js → journeyDiagram-5HDEW3XC-Bm_oN9-6.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-BHIXqAjV.js → kanban-definition-HUTT4EX6-tj1z1JQG.js} +1 -1
- package/dist/client/assets/{linear-CpIhx6Gc.js → linear-QXJ-Ae7F.js} +1 -1
- package/dist/client/assets/{mermaid.core-CiIhxnwD.js → mermaid.core-C2fDWTc4.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-YQsnBsmN.js → mindmap-definition-LN4V7U3C-BilCFNik.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-CwVOSvS2.js → pegDiagram-2B236MQR-Cedi8jLg.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-BwKxsCxJ.js → pieDiagram-ENE6RG2P-u8doKeCT.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-BQxwGIv5.js → quadrantDiagram-ABIIQ3AL-CTksDMFb.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-Cx9SFycV.js → railroadDiagram-RFXS5EU6-a3hmKcli.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-39fPSSwK.js → requirementDiagram-TGXJPOKE-0CbosJM9.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-DZCrgI-t.js → sankeyDiagram-HTMAVEWB-DM9MM942.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-gNACCCge.js → sequenceDiagram-DBY2YBRQ-DJYOI_VF.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-YBXDWHbB.js → sizeCapture-X5ZJPWSS-CayWxIRi.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-CTCeQOUM.js → stateDiagram-2N3HPSRC-BktX6ilb.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-DK505RoL.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-BjOcSMgp.js → swimlanes-5IMT3BWC-Duvbluwj.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CZTzB1q3.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-xMu52AIk.js → timeline-definition-FHXFAJF6-TdPH8RG8.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-CxJiaNeN.js → vennDiagram-L72KCM5P-MQSsBdGx.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-BDyiY3dI.js → wardleyDiagram-EHGQE667-BcsapLTw.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CA_fqKyA.js → xychartDiagram-FW5EYKEG-BSUPwtFh.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/cli.js +17 -2
- package/dist/server/routes/filesystem.js +239 -59
- package/dist/server/routes/terminal.js +47 -5
- package/dist/server/utils/fileWatchPolicy.js +136 -0
- package/dist/server/utils/settings.js +29 -0
- package/dist/server/utils/terminalColorEnvironment.js +46 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-WCBYqzX4.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-NZpzVwgO.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-NZpzVwgO.js +0 -1
- package/dist/client/assets/index-CoDf9qRl.js +0 -457
- package/dist/client/assets/index-UhZwBu7l.css +0 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-B_dCwMIY.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-DHr2qtGf.js +0 -8
|
@@ -18,6 +18,7 @@ import { deleteFilesystemFile } from '../utils/deleteFilesystemFile.js';
|
|
|
18
18
|
import { inspectKicadBoardPoint } from '../utils/kicadBoardInspection.js';
|
|
19
19
|
import { EdaPreviewCache, requestAcceptsEtag } from '../utils/edaPreviewCache.js';
|
|
20
20
|
import { convertHeicPreview, HEIC_PREVIEW_MIME_TYPES, isHeicPreviewPath } from '../utils/heicPreview.js';
|
|
21
|
+
import { getWatchErrorCode, enqueueLatestWatchEvent, inspectLinuxInotifyUsage, isWatchResourceExhaustion, minimizeRecursiveWatchRoots, WATCH_IGNORED_NAMES, WATCH_RESOURCE_BACKOFF_MS, } from '../utils/fileWatchPolicy.js';
|
|
21
22
|
const router = Router();
|
|
22
23
|
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
23
24
|
const MAX_IMAGE_PREVIEW_SIZE = 20 * 1024 * 1024; // 20MB
|
|
@@ -59,7 +60,81 @@ const FS_IO_LOG_NAME = 'fs-io.log';
|
|
|
59
60
|
const activeDiffSlots = new Map();
|
|
60
61
|
const activeIoSlots = new Map();
|
|
61
62
|
const sharedWatchRegistry = new Map();
|
|
62
|
-
const SHARED_WATCH_EVICT_DELAY_MS =
|
|
63
|
+
const SHARED_WATCH_EVICT_DELAY_MS = 5_000;
|
|
64
|
+
const WATCH_STAT_CONCURRENCY = 8;
|
|
65
|
+
let watchAdmissionBlockedUntil = 0;
|
|
66
|
+
function countActiveSharedWatches() {
|
|
67
|
+
let count = 0;
|
|
68
|
+
for (const entry of sharedWatchRegistry.values()) {
|
|
69
|
+
if (entry.subscription || entry.inFlight)
|
|
70
|
+
count += 1;
|
|
71
|
+
}
|
|
72
|
+
return count;
|
|
73
|
+
}
|
|
74
|
+
function broadcastSharedWatchEvent(entry, event) {
|
|
75
|
+
for (const client of entry.clients) {
|
|
76
|
+
if (!client.closed)
|
|
77
|
+
client.enqueue(event);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function drainSharedWatchEvents(entry) {
|
|
81
|
+
if (entry.failure || entry.clients.size === 0)
|
|
82
|
+
return;
|
|
83
|
+
while (entry.statWorkers < WATCH_STAT_CONCURRENCY && entry.pendingEvents.size > 0) {
|
|
84
|
+
const next = entry.pendingEvents.entries().next().value;
|
|
85
|
+
if (!next)
|
|
86
|
+
return;
|
|
87
|
+
const [changedPath, eventType] = next;
|
|
88
|
+
entry.pendingEvents.delete(changedPath);
|
|
89
|
+
if (eventType === 'delete') {
|
|
90
|
+
broadcastSharedWatchEvent(entry, { type: 'deleted', path: changedPath });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const generation = entry.eventGeneration;
|
|
94
|
+
entry.statWorkers += 1;
|
|
95
|
+
void fs.promises.lstat(changedPath)
|
|
96
|
+
.then((stat) => {
|
|
97
|
+
if (entry.failure || generation !== entry.eventGeneration)
|
|
98
|
+
return;
|
|
99
|
+
broadcastSharedWatchEvent(entry, {
|
|
100
|
+
type: eventType === 'create' ? 'created' : 'updated',
|
|
101
|
+
path: changedPath,
|
|
102
|
+
entry: toFileEntry(changedPath, stat),
|
|
103
|
+
});
|
|
104
|
+
})
|
|
105
|
+
.catch(() => {
|
|
106
|
+
if (entry.failure || generation !== entry.eventGeneration)
|
|
107
|
+
return;
|
|
108
|
+
broadcastSharedWatchEvent(entry, { type: 'deleted', path: changedPath });
|
|
109
|
+
})
|
|
110
|
+
.finally(() => {
|
|
111
|
+
entry.statWorkers -= 1;
|
|
112
|
+
drainSharedWatchEvents(entry);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function enqueueSharedNativeEvents(rootPath, entry, events) {
|
|
117
|
+
for (const event of events) {
|
|
118
|
+
const changedPath = path.resolve(event.path);
|
|
119
|
+
if (!isPathInside(rootPath, changedPath) || isIgnoredWatchPath(rootPath, changedPath))
|
|
120
|
+
continue;
|
|
121
|
+
if (entry.clients.size === 0) {
|
|
122
|
+
// The native subscription is kept briefly for cheap stream hand-off.
|
|
123
|
+
// Only request a rescan when something actually changed in that gap.
|
|
124
|
+
entry.needsRescanOnAttach = true;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (enqueueLatestWatchEvent(entry.pendingEvents, changedPath, event.type, WATCH_EVENT_STORM_LIMIT) === 'overflow') {
|
|
128
|
+
entry.pendingEvents.clear();
|
|
129
|
+
entry.eventGeneration += 1;
|
|
130
|
+
broadcastSharedWatchEvent(entry, { type: 'rescan-required', path: rootPath, reason: 'event-storm' });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// The latest event for one path wins. This turns save bursts and atomic
|
|
134
|
+
// editor renames into one stat operation instead of one operation/event.
|
|
135
|
+
}
|
|
136
|
+
drainSharedWatchEvents(entry);
|
|
137
|
+
}
|
|
63
138
|
function sharedWatchCallback(rootPath, entry) {
|
|
64
139
|
return (error, events) => {
|
|
65
140
|
if (entry.failure)
|
|
@@ -67,42 +142,35 @@ function sharedWatchCallback(rootPath, entry) {
|
|
|
67
142
|
if (error) {
|
|
68
143
|
entry.failure = error.message || 'watch-error';
|
|
69
144
|
const reason = entry.failure;
|
|
70
|
-
|
|
145
|
+
entry.pendingEvents.clear();
|
|
146
|
+
entry.eventGeneration += 1;
|
|
147
|
+
const resourceExhausted = isWatchResourceExhaustion(error);
|
|
148
|
+
if (resourceExhausted)
|
|
149
|
+
watchAdmissionBlockedUntil = Date.now() + WATCH_RESOURCE_BACKOFF_MS;
|
|
150
|
+
console.warn('[file-watch] Native subscription callback failed; degrading to manual refresh', {
|
|
151
|
+
rootPath,
|
|
152
|
+
code: getWatchErrorCode(error),
|
|
153
|
+
activeSubscriptions: countActiveSharedWatches(),
|
|
154
|
+
registrySize: sharedWatchRegistry.size,
|
|
155
|
+
resourceBackoffMs: resourceExhausted ? WATCH_RESOURCE_BACKOFF_MS : 0,
|
|
156
|
+
error: reason,
|
|
157
|
+
});
|
|
158
|
+
const failedSubscription = entry.subscription;
|
|
159
|
+
entry.subscription = null;
|
|
160
|
+
void failedSubscription?.unsubscribe().catch(() => undefined);
|
|
161
|
+
if (resourceExhausted) {
|
|
162
|
+
void inspectLinuxInotifyUsage().then((diagnostics) => {
|
|
163
|
+
if (diagnostics)
|
|
164
|
+
console.warn('[file-watch] Linux inotify usage after callback failure', diagnostics);
|
|
165
|
+
}).catch(() => undefined);
|
|
166
|
+
}
|
|
167
|
+
for (const client of [...entry.clients]) {
|
|
71
168
|
if (!client.closed)
|
|
72
|
-
client.
|
|
169
|
+
client.degrade(reason);
|
|
73
170
|
}
|
|
74
171
|
return;
|
|
75
172
|
}
|
|
76
|
-
|
|
77
|
-
const changedPath = path.resolve(event.path);
|
|
78
|
-
if (!isPathInside(rootPath, changedPath) || isIgnoredWatchPath(rootPath, changedPath))
|
|
79
|
-
continue;
|
|
80
|
-
if (event.type === 'delete') {
|
|
81
|
-
for (const client of entry.clients) {
|
|
82
|
-
if (!client.closed)
|
|
83
|
-
client.enqueue({ type: 'deleted', path: changedPath });
|
|
84
|
-
}
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
fs.promises.lstat(changedPath)
|
|
88
|
-
.then((stat) => {
|
|
89
|
-
const payload = {
|
|
90
|
-
type: event.type === 'create' ? 'created' : 'updated',
|
|
91
|
-
path: changedPath,
|
|
92
|
-
entry: toFileEntry(changedPath, stat),
|
|
93
|
-
};
|
|
94
|
-
for (const client of entry.clients) {
|
|
95
|
-
if (!client.closed)
|
|
96
|
-
client.enqueue(payload);
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
.catch(() => {
|
|
100
|
-
for (const client of entry.clients) {
|
|
101
|
-
if (!client.closed)
|
|
102
|
-
client.enqueue({ type: 'deleted', path: changedPath });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
173
|
+
enqueueSharedNativeEvents(rootPath, entry, events);
|
|
106
174
|
};
|
|
107
175
|
}
|
|
108
176
|
async function acquireSharedWatch(rootPath, client) {
|
|
@@ -115,6 +183,10 @@ async function acquireSharedWatch(rootPath, client) {
|
|
|
115
183
|
clients: new Set(),
|
|
116
184
|
inFlight: null,
|
|
117
185
|
evictTimer: null,
|
|
186
|
+
pendingEvents: new Map(),
|
|
187
|
+
statWorkers: 0,
|
|
188
|
+
eventGeneration: 0,
|
|
189
|
+
needsRescanOnAttach: false,
|
|
118
190
|
};
|
|
119
191
|
sharedWatchRegistry.set(rootPath, entry);
|
|
120
192
|
}
|
|
@@ -124,11 +196,32 @@ async function acquireSharedWatch(rootPath, client) {
|
|
|
124
196
|
}
|
|
125
197
|
entry.refs += 1;
|
|
126
198
|
entry.clients.add(client);
|
|
199
|
+
if (entry.needsRescanOnAttach) {
|
|
200
|
+
entry.needsRescanOnAttach = false;
|
|
201
|
+
client.enqueue({ type: 'rescan-required', path: rootPath, reason: 'reconnected' });
|
|
202
|
+
}
|
|
127
203
|
if (entry.failure)
|
|
128
204
|
return null;
|
|
129
205
|
if (entry.subscription)
|
|
130
206
|
return rootPath;
|
|
131
207
|
if (!entry.inFlight) {
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
const activeSubscriptions = countActiveSharedWatches();
|
|
210
|
+
if (now < watchAdmissionBlockedUntil) {
|
|
211
|
+
entry.failure = `native watcher admission paused for ${Math.ceil((watchAdmissionBlockedUntil - now) / 1000)}s after resource exhaustion`;
|
|
212
|
+
console.warn('[file-watch] Subscription skipped while resource backoff is active', {
|
|
213
|
+
rootPath,
|
|
214
|
+
activeSubscriptions,
|
|
215
|
+
registrySize: sharedWatchRegistry.size,
|
|
216
|
+
});
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const startedAt = Date.now();
|
|
220
|
+
console.info('[file-watch] Starting native subscription', {
|
|
221
|
+
rootPath,
|
|
222
|
+
activeSubscriptions: activeSubscriptions + 1,
|
|
223
|
+
registrySize: sharedWatchRegistry.size,
|
|
224
|
+
});
|
|
132
225
|
entry.inFlight = watcher.subscribe(rootPath, sharedWatchCallback(rootPath, entry), {
|
|
133
226
|
ignore: Array.from(WATCH_IGNORED_NAMES).map((name) => `**/${name}/**`),
|
|
134
227
|
})
|
|
@@ -141,12 +234,36 @@ async function acquireSharedWatch(rootPath, client) {
|
|
|
141
234
|
return null;
|
|
142
235
|
}
|
|
143
236
|
current.subscription = subscription;
|
|
237
|
+
console.info('[file-watch] Native subscription ready', {
|
|
238
|
+
rootPath,
|
|
239
|
+
scanDurationMs: Date.now() - startedAt,
|
|
240
|
+
activeSubscriptions: countActiveSharedWatches(),
|
|
241
|
+
registrySize: sharedWatchRegistry.size,
|
|
242
|
+
});
|
|
144
243
|
return subscription;
|
|
145
244
|
})
|
|
146
245
|
.catch((error) => {
|
|
147
246
|
const current = sharedWatchRegistry.get(rootPath);
|
|
148
247
|
if (current)
|
|
149
248
|
current.failure = error instanceof Error ? error.message : String(error);
|
|
249
|
+
const resourceExhausted = isWatchResourceExhaustion(error);
|
|
250
|
+
if (resourceExhausted)
|
|
251
|
+
watchAdmissionBlockedUntil = Date.now() + WATCH_RESOURCE_BACKOFF_MS;
|
|
252
|
+
console.warn('[file-watch] Native subscription failed; degrading to manual refresh', {
|
|
253
|
+
rootPath,
|
|
254
|
+
code: getWatchErrorCode(error),
|
|
255
|
+
scanDurationMs: Date.now() - startedAt,
|
|
256
|
+
activeSubscriptions: countActiveSharedWatches(),
|
|
257
|
+
registrySize: sharedWatchRegistry.size,
|
|
258
|
+
resourceBackoffMs: resourceExhausted ? WATCH_RESOURCE_BACKOFF_MS : 0,
|
|
259
|
+
error: error instanceof Error ? error.message : String(error),
|
|
260
|
+
});
|
|
261
|
+
if (resourceExhausted) {
|
|
262
|
+
void inspectLinuxInotifyUsage().then((diagnostics) => {
|
|
263
|
+
if (diagnostics)
|
|
264
|
+
console.warn('[file-watch] Linux inotify usage after resource exhaustion', diagnostics);
|
|
265
|
+
}).catch(() => undefined);
|
|
266
|
+
}
|
|
150
267
|
return null;
|
|
151
268
|
})
|
|
152
269
|
.finally(() => {
|
|
@@ -168,6 +285,15 @@ function releaseSharedWatch(rootPath, client) {
|
|
|
168
285
|
entry.refs -= 1;
|
|
169
286
|
if (entry.refs > 0)
|
|
170
287
|
return;
|
|
288
|
+
entry.needsRescanOnAttach = entry.needsRescanOnAttach
|
|
289
|
+
|| entry.pendingEvents.size > 0
|
|
290
|
+
|| entry.statWorkers > 0;
|
|
291
|
+
entry.pendingEvents.clear();
|
|
292
|
+
entry.eventGeneration += 1;
|
|
293
|
+
if (!entry.subscription && !entry.inFlight) {
|
|
294
|
+
sharedWatchRegistry.delete(rootPath);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
171
297
|
if (entry.evictTimer)
|
|
172
298
|
clearTimeout(entry.evictTimer);
|
|
173
299
|
entry.evictTimer = setTimeout(() => {
|
|
@@ -414,9 +540,6 @@ function compareDirents(a, b) {
|
|
|
414
540
|
return 1;
|
|
415
541
|
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
416
542
|
}
|
|
417
|
-
const WATCH_IGNORED_NAMES = new Set([
|
|
418
|
-
'.git', 'node_modules', 'dist', 'build', '.next', '.nuxt', '.turbo', 'coverage', 'target', '.gradle', '.idea', '.DS_Store',
|
|
419
|
-
]);
|
|
420
543
|
const NESTED_GIT_DISCOVERY_IGNORED_NAMES = new Set([
|
|
421
544
|
'.git', 'node_modules', 'dist', 'build', '.next', '.nuxt', '.turbo', 'coverage', 'target', '.gradle', '.idea', '.DS_Store',
|
|
422
545
|
'.cache', '.parcel-cache', '.yarn', '.pnpm-store', 'vendor',
|
|
@@ -1353,11 +1476,12 @@ function toFileEntry(entryPath, stat, isSymlink = false) {
|
|
|
1353
1476
|
path: entryPath,
|
|
1354
1477
|
type: stat.isDirectory() ? 'directory' : stat.isSymbolicLink() ? 'symlink' : 'file',
|
|
1355
1478
|
isSymlink,
|
|
1479
|
+
modified: stat.mtime.toISOString(),
|
|
1356
1480
|
};
|
|
1357
1481
|
}
|
|
1358
|
-
async function toDirectoryEntry(dir, dirent) {
|
|
1482
|
+
async function toDirectoryEntry(dir, dirent, includeModified = false) {
|
|
1359
1483
|
const entryPath = path.join(dir, dirent.name);
|
|
1360
|
-
if (!dirent.isSymbolicLink()) {
|
|
1484
|
+
if (!dirent.isSymbolicLink() && !includeModified) {
|
|
1361
1485
|
return {
|
|
1362
1486
|
name: dirent.name,
|
|
1363
1487
|
path: entryPath,
|
|
@@ -1369,19 +1493,39 @@ async function toDirectoryEntry(dir, dirent) {
|
|
|
1369
1493
|
return {
|
|
1370
1494
|
name: dirent.name,
|
|
1371
1495
|
path: entryPath,
|
|
1372
|
-
type: stat.isDirectory() ? 'directory' : 'symlink',
|
|
1373
|
-
isSymlink:
|
|
1496
|
+
type: stat.isDirectory() ? 'directory' : dirent.isSymbolicLink() ? 'symlink' : 'file',
|
|
1497
|
+
isSymlink: dirent.isSymbolicLink() || undefined,
|
|
1498
|
+
modified: includeModified ? stat.mtime.toISOString() : undefined,
|
|
1374
1499
|
};
|
|
1375
1500
|
}
|
|
1376
1501
|
catch {
|
|
1377
1502
|
return {
|
|
1378
1503
|
name: dirent.name,
|
|
1379
1504
|
path: entryPath,
|
|
1380
|
-
type: 'symlink',
|
|
1381
|
-
isSymlink:
|
|
1505
|
+
type: dirent.isDirectory() ? 'directory' : dirent.isSymbolicLink() ? 'symlink' : 'file',
|
|
1506
|
+
isSymlink: dirent.isSymbolicLink() || undefined,
|
|
1382
1507
|
};
|
|
1383
1508
|
}
|
|
1384
1509
|
}
|
|
1510
|
+
async function loadDirectoryEntriesWithModified(dir, dirents, signal) {
|
|
1511
|
+
const entries = [];
|
|
1512
|
+
const batchSize = 64;
|
|
1513
|
+
for (let offset = 0; offset < dirents.length; offset += batchSize) {
|
|
1514
|
+
throwIfAborted(signal, 'fs.list');
|
|
1515
|
+
entries.push(...await Promise.all(dirents.slice(offset, offset + batchSize).map((dirent) => toDirectoryEntry(dir, dirent, true))));
|
|
1516
|
+
}
|
|
1517
|
+
return entries;
|
|
1518
|
+
}
|
|
1519
|
+
function compareDirectoryEntriesByModified(a, b) {
|
|
1520
|
+
if (a.type === 'directory' && b.type !== 'directory')
|
|
1521
|
+
return -1;
|
|
1522
|
+
if (a.type !== 'directory' && b.type === 'directory')
|
|
1523
|
+
return 1;
|
|
1524
|
+
const modifiedDelta = Date.parse(b.modified ?? '') - Date.parse(a.modified ?? '');
|
|
1525
|
+
if (Number.isFinite(modifiedDelta) && modifiedDelta !== 0)
|
|
1526
|
+
return modifiedDelta;
|
|
1527
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
1528
|
+
}
|
|
1385
1529
|
function isIgnoredWatchPath(rootPath, candidatePath) {
|
|
1386
1530
|
const relative = path.relative(rootPath, candidatePath);
|
|
1387
1531
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
@@ -2378,13 +2522,18 @@ router.get('/list', async (req, res) => {
|
|
|
2378
2522
|
throw new Error('Path is not a directory');
|
|
2379
2523
|
}
|
|
2380
2524
|
const showHidden = req.query.showHidden === 'true';
|
|
2525
|
+
const sortMode = req.query.sort === 'modified' ? 'modified' : 'name';
|
|
2381
2526
|
const allEntries = await fs.promises.readdir(resolvedPath, { withFileTypes: true });
|
|
2382
2527
|
throwIfAborted(controller.signal, 'fs.list');
|
|
2383
2528
|
const visibleDirents = allEntries.filter(dirent => showHidden || !dirent.name.startsWith('.'));
|
|
2384
|
-
const
|
|
2385
|
-
.
|
|
2386
|
-
|
|
2387
|
-
|
|
2529
|
+
const entries = sortMode === 'modified'
|
|
2530
|
+
? (await loadDirectoryEntriesWithModified(resolvedPath, visibleDirents, controller.signal))
|
|
2531
|
+
.sort(compareDirectoryEntriesByModified)
|
|
2532
|
+
.slice(0, MAX_DIRECTORY_ENTRIES)
|
|
2533
|
+
: await Promise.all(visibleDirents
|
|
2534
|
+
.sort(compareDirents)
|
|
2535
|
+
.slice(0, MAX_DIRECTORY_ENTRIES)
|
|
2536
|
+
.map((dirent) => toDirectoryEntry(resolvedPath, dirent)));
|
|
2388
2537
|
throwIfAborted(controller.signal, 'fs.list');
|
|
2389
2538
|
return { resolvedPath, entries, total: visibleDirents.length };
|
|
2390
2539
|
})(), FS_ROUTE_TIMEOUT_MS, 'Directory listing took too long. The folder may be on a slow disk or network mount.', 'FS_LIST_TIMEOUT');
|
|
@@ -2410,20 +2559,21 @@ router.get('/watch', async (req, res) => {
|
|
|
2410
2559
|
const rootsParam = req.query.roots;
|
|
2411
2560
|
const rawRoots = (Array.isArray(rootsParam) ? rootsParam : typeof rootsParam === 'string' ? rootsParam.split('|') : [])
|
|
2412
2561
|
.filter((root) => typeof root === 'string');
|
|
2413
|
-
const
|
|
2562
|
+
const validatedRoots = [];
|
|
2414
2563
|
for (const rawRoot of rawRoots) {
|
|
2415
2564
|
if (!rawRoot)
|
|
2416
2565
|
continue;
|
|
2417
2566
|
try {
|
|
2418
2567
|
const resolved = await pathValidator.validatePathAsync(rawRoot);
|
|
2419
2568
|
const stat = await fs.promises.stat(resolved);
|
|
2420
|
-
if (stat.isDirectory() && !
|
|
2421
|
-
|
|
2569
|
+
if (stat.isDirectory() && !validatedRoots.includes(resolved))
|
|
2570
|
+
validatedRoots.push(resolved);
|
|
2422
2571
|
}
|
|
2423
2572
|
catch {
|
|
2424
2573
|
// Ignore invalid watch roots; the visible tree will still work via manual list requests.
|
|
2425
2574
|
}
|
|
2426
2575
|
}
|
|
2576
|
+
const roots = minimizeRecursiveWatchRoots(validatedRoots);
|
|
2427
2577
|
if (roots.length === 0) {
|
|
2428
2578
|
res.status(400).json({ error: 'No valid roots to watch' });
|
|
2429
2579
|
return;
|
|
@@ -2435,6 +2585,21 @@ router.get('/watch', async (req, res) => {
|
|
|
2435
2585
|
let pending = [];
|
|
2436
2586
|
let flushTimer = null;
|
|
2437
2587
|
const attachedRoots = new Map();
|
|
2588
|
+
// Register cleanup before the first native subscription starts. A recursive
|
|
2589
|
+
// initial scan can take seconds on a large repository; the browser may abort
|
|
2590
|
+
// during that await when the user switches files or roots. Registering this
|
|
2591
|
+
// only after attachRoot() leaked the pending subscription and its reference.
|
|
2592
|
+
const closeWatchStream = () => {
|
|
2593
|
+
if (closed)
|
|
2594
|
+
return;
|
|
2595
|
+
closed = true;
|
|
2596
|
+
if (flushTimer)
|
|
2597
|
+
clearTimeout(flushTimer);
|
|
2598
|
+
for (const [rootPath, client] of attachedRoots) {
|
|
2599
|
+
releaseSharedWatch(rootPath, client);
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
res.once('close', closeWatchStream);
|
|
2438
2603
|
const writeEvent = (type, payload) => {
|
|
2439
2604
|
if (closed || res.destroyed)
|
|
2440
2605
|
return;
|
|
@@ -2463,28 +2628,43 @@ router.get('/watch', async (req, res) => {
|
|
|
2463
2628
|
}
|
|
2464
2629
|
};
|
|
2465
2630
|
writeEvent('ready', { roots });
|
|
2631
|
+
console.info('[file-watch] Stream ready', {
|
|
2632
|
+
requestedRoots: rawRoots.length,
|
|
2633
|
+
validatedRoots: validatedRoots.length,
|
|
2634
|
+
subscribedRoots: roots.length,
|
|
2635
|
+
collapsedOverlappingRoots: validatedRoots.length - roots.length,
|
|
2636
|
+
activeSubscriptions: countActiveSharedWatches(),
|
|
2637
|
+
registrySize: sharedWatchRegistry.size,
|
|
2638
|
+
});
|
|
2466
2639
|
const attachRoot = async (rootPath) => {
|
|
2467
|
-
|
|
2640
|
+
if (closed)
|
|
2641
|
+
return;
|
|
2642
|
+
const client = {
|
|
2643
|
+
closed: false,
|
|
2644
|
+
enqueue,
|
|
2645
|
+
degrade: (reason) => {
|
|
2646
|
+
if (closed)
|
|
2647
|
+
return;
|
|
2648
|
+
enqueue({ type: 'rescan-required', path: rootPath, reason });
|
|
2649
|
+
flush();
|
|
2650
|
+
if (!res.writableEnded)
|
|
2651
|
+
res.end();
|
|
2652
|
+
},
|
|
2653
|
+
};
|
|
2468
2654
|
attachedRoots.set(rootPath, client);
|
|
2469
2655
|
const ok = await acquireSharedWatch(rootPath, client);
|
|
2470
2656
|
if (closed)
|
|
2471
2657
|
return;
|
|
2472
2658
|
if (!ok) {
|
|
2473
2659
|
const reason = sharedWatchRegistry.get(rootPath)?.failure ?? 'watch-unavailable';
|
|
2474
|
-
|
|
2660
|
+
client.degrade(reason);
|
|
2475
2661
|
}
|
|
2476
2662
|
};
|
|
2477
2663
|
for (const rootPath of roots) {
|
|
2664
|
+
if (closed)
|
|
2665
|
+
break;
|
|
2478
2666
|
await attachRoot(rootPath);
|
|
2479
2667
|
}
|
|
2480
|
-
req.on('close', () => {
|
|
2481
|
-
closed = true;
|
|
2482
|
-
if (flushTimer)
|
|
2483
|
-
clearTimeout(flushTimer);
|
|
2484
|
-
for (const [rootPath, client] of attachedRoots) {
|
|
2485
|
-
releaseSharedWatch(rootPath, client);
|
|
2486
|
-
}
|
|
2487
|
-
});
|
|
2488
2668
|
});
|
|
2489
2669
|
router.get('/cancel-slot', (req, res) => {
|
|
2490
2670
|
const requestSlotId = typeof req.query.requestSlotId === 'string' ? req.query.requestSlotId : undefined;
|
|
@@ -14,9 +14,10 @@ import { getPtyHostManager } from '../ptyhost/manager.js';
|
|
|
14
14
|
import { pathValidator } from '../utils/pathValidator.js';
|
|
15
15
|
import { TERMINAL, TMUX } from '../config.js';
|
|
16
16
|
import { localAccessManager } from '../utils/localAccess.js';
|
|
17
|
-
import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, getAutoRenamePromptPreferenceSetting, setAutoRenamePromptPreferenceSetting, getAutoRenamePromptPayloadCharsSetting, setAutoRenamePromptPayloadCharsSetting, getNewSessionAgentSlugSetting, setNewSessionAgentSlugSetting, getRunningSessionButtonEnabledSetting, setRunningSessionButtonEnabledSetting, } from '../utils/settings.js';
|
|
17
|
+
import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, getAutoRenamePromptPreferenceSetting, setAutoRenamePromptPreferenceSetting, getAutoRenamePromptPayloadCharsSetting, setAutoRenamePromptPayloadCharsSetting, getNewSessionAgentSlugSetting, setNewSessionAgentSlugSetting, getRunningSessionButtonEnabledSetting, setRunningSessionButtonEnabledSetting, getFileSortModesSetting, setFileSortModesSetting, setFileSortModeSetting, } from '../utils/settings.js';
|
|
18
18
|
import { loadContextDraft, saveContextDraft } from '../utils/contextDraft.js';
|
|
19
19
|
import { getOnboardingServerUrl } from '../onboardingServer.js';
|
|
20
|
+
import { inspectLinuxInotifyUsage, isWatchResourceExhaustion } from '../utils/fileWatchPolicy.js';
|
|
20
21
|
import { getFocusSequence, removeClientFocusState, scanFocusTrackingMode, setClientFocusState, } from '../utils/tmuxFocus.js';
|
|
21
22
|
import { extractProgramLabelFromArgs, normalizeProgramName, normalizeTmuxMetadataProgram, selectTmuxForegroundProgram, tmuxMetadataChanged, } from '../utils/tmuxProgramDetection.js';
|
|
22
23
|
import { agentBySlug, buildResumeCommand, detectAgentFromCommand, inferResumeSessionId, listAgents, } from '../agent/registry.js';
|
|
@@ -31,7 +32,7 @@ import { SessionSearchStore } from '../agent/sessionSearchStore.js';
|
|
|
31
32
|
import { listAllHookAgents, refreshStaleHooksAtLaunch, installHooksForSlug, uninstallHooksForSlug, } from '../agent/installers.js';
|
|
32
33
|
import { loadPlugins, savePlugin, removePlugin, readPluginIcon, validateManifest, } from '../agent/plugins.js';
|
|
33
34
|
import { TmuxLifecycleCoordinator } from '../utils/tmuxLifecycle.js';
|
|
34
|
-
import { buildInteractiveColorEnvironment, buildTmuxColorEnvironmentCommands, } from '../utils/terminalColorEnvironment.js';
|
|
35
|
+
import { buildInteractiveColorEnvironment, buildTmuxColorEnvironmentCommands, ensureTmuxColorCapabilities, resolveTmuxInnerTerm, } from '../utils/terminalColorEnvironment.js';
|
|
35
36
|
import { detectTmuxRecoveryIncident, normalizeTmuxRecoveryIncident, } from '../utils/tmuxRecovery.js';
|
|
36
37
|
import { checkPluginPackageUpdate, commitPreparedPlugin, preparePluginPackage, } from '../agent/pluginPackages.js';
|
|
37
38
|
import { clearPluginAgents, registerPluginAgents } from '../agent/registry.js';
|
|
@@ -457,6 +458,15 @@ function scheduleReloadGlobalSessionState() {
|
|
|
457
458
|
globalSessionStateReloadTimer.unref?.();
|
|
458
459
|
}
|
|
459
460
|
async function watchGlobalSessionStateFile() {
|
|
461
|
+
const reportFailure = (message, error) => {
|
|
462
|
+
console.warn(message, getErrorMessage(error));
|
|
463
|
+
if (!isWatchResourceExhaustion(error))
|
|
464
|
+
return;
|
|
465
|
+
void inspectLinuxInotifyUsage().then((diagnostics) => {
|
|
466
|
+
if (diagnostics)
|
|
467
|
+
console.warn('[session-persist] Linux inotify usage after watcher failure:', diagnostics);
|
|
468
|
+
}).catch(() => undefined);
|
|
469
|
+
};
|
|
460
470
|
try {
|
|
461
471
|
const dir = path.dirname(GLOBAL_SESSION_STATE_FILE);
|
|
462
472
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
@@ -468,11 +478,11 @@ async function watchGlobalSessionStateFile() {
|
|
|
468
478
|
scheduleReloadGlobalSessionState();
|
|
469
479
|
});
|
|
470
480
|
globalSessionStateWatcher.on('error', (error) => {
|
|
471
|
-
|
|
481
|
+
reportFailure('[session-persist] Global state watcher failed:', error);
|
|
472
482
|
});
|
|
473
483
|
}
|
|
474
484
|
catch (error) {
|
|
475
|
-
|
|
485
|
+
reportFailure('[session-persist] Failed to watch global state file:', error);
|
|
476
486
|
}
|
|
477
487
|
}
|
|
478
488
|
function schedulePersistGlobalState() {
|
|
@@ -1780,6 +1790,10 @@ async function ensureTmuxColorEnvironment(sessionName) {
|
|
|
1780
1790
|
await runTmux(args);
|
|
1781
1791
|
}
|
|
1782
1792
|
}
|
|
1793
|
+
async function ensureManagedTmuxColorCapabilities(sessionName) {
|
|
1794
|
+
const innerTerm = await resolveTmuxInnerTerm();
|
|
1795
|
+
await ensureTmuxColorCapabilities(runTmux, sessionName, innerTerm);
|
|
1796
|
+
}
|
|
1783
1797
|
async function listLiveTmuxInventorySessions() {
|
|
1784
1798
|
const format = [
|
|
1785
1799
|
'#{session_name}',
|
|
@@ -3487,7 +3501,12 @@ async function ensureTmuxSessionExists(sessionName, cwd) {
|
|
|
3487
3501
|
if (!serverWasMissing) {
|
|
3488
3502
|
await ensureTmuxColorEnvironment();
|
|
3489
3503
|
}
|
|
3490
|
-
const
|
|
3504
|
+
const innerTerm = await resolveTmuxInnerTerm();
|
|
3505
|
+
const args = [
|
|
3506
|
+
'new-session', '-d', '-s', sessionName,
|
|
3507
|
+
'-e', 'COLORTERM=truecolor',
|
|
3508
|
+
'-e', `TERM=${innerTerm}`,
|
|
3509
|
+
];
|
|
3491
3510
|
if (process.env.TERMDOCK_FORCE_COLOR === '1') {
|
|
3492
3511
|
args.push('-e', 'FORCE_COLOR=1');
|
|
3493
3512
|
}
|
|
@@ -3582,6 +3601,7 @@ async function enableTmuxExtendedKeys() {
|
|
|
3582
3601
|
}
|
|
3583
3602
|
async function ensureSharedTmuxServerReady() {
|
|
3584
3603
|
await ensureTmuxColorEnvironment();
|
|
3604
|
+
await ensureManagedTmuxColorCapabilities();
|
|
3585
3605
|
await applyTmuxScrollbackProfile();
|
|
3586
3606
|
await disableTmuxFocusEventsForSafety();
|
|
3587
3607
|
await enableTmuxExtendedKeys();
|
|
@@ -3606,6 +3626,12 @@ async function ensureManagedTmuxSessionReady(sessionName) {
|
|
|
3606
3626
|
catch (error) {
|
|
3607
3627
|
console.warn(`Failed to set tmux color environment for ${sessionName}: ${getErrorMessage(error)}`);
|
|
3608
3628
|
}
|
|
3629
|
+
try {
|
|
3630
|
+
await ensureManagedTmuxColorCapabilities(sessionName);
|
|
3631
|
+
}
|
|
3632
|
+
catch (error) {
|
|
3633
|
+
console.warn(`Failed to set tmux color capabilities for ${sessionName}: ${getErrorMessage(error)}`);
|
|
3634
|
+
}
|
|
3609
3635
|
try {
|
|
3610
3636
|
await disableTmuxStatus(sessionName);
|
|
3611
3637
|
}
|
|
@@ -5566,6 +5592,7 @@ async function getSettingsPayload() {
|
|
|
5566
5592
|
autoRenamePromptPayloadChars: getAutoRenamePromptPayloadCharsSetting(),
|
|
5567
5593
|
newSessionAgentSlug: getNewSessionAgentSlugSetting(),
|
|
5568
5594
|
runningSessionButtonEnabled: getRunningSessionButtonEnabledSetting(),
|
|
5595
|
+
fileSortModes: getFileSortModesSetting(),
|
|
5569
5596
|
localAccess: {
|
|
5570
5597
|
...localAccess,
|
|
5571
5598
|
interfaces,
|
|
@@ -5677,6 +5704,21 @@ router.put('/settings', async (req, res) => {
|
|
|
5677
5704
|
if (typeof body.runningSessionButtonEnabled === 'boolean') {
|
|
5678
5705
|
setRunningSessionButtonEnabledSetting(body.runningSessionButtonEnabled);
|
|
5679
5706
|
}
|
|
5707
|
+
if (body.fileSortModes && typeof body.fileSortModes === 'object' && !Array.isArray(body.fileSortModes)) {
|
|
5708
|
+
setFileSortModesSetting(body.fileSortModes);
|
|
5709
|
+
}
|
|
5710
|
+
if (body.fileSortMode && typeof body.fileSortMode === 'object') {
|
|
5711
|
+
const preference = body.fileSortMode;
|
|
5712
|
+
const validPath = typeof preference.path === 'string'
|
|
5713
|
+
&& preference.path.length > 0
|
|
5714
|
+
&& preference.path.length <= 4096
|
|
5715
|
+
&& (preference.path.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(preference.path));
|
|
5716
|
+
if (!validPath || (preference.mode !== 'name' && preference.mode !== 'modified')) {
|
|
5717
|
+
res.status(400).json({ error: 'Invalid file sort preference', code: 'FILE_SORT_MODE_INVALID' });
|
|
5718
|
+
return;
|
|
5719
|
+
}
|
|
5720
|
+
setFileSortModeSetting(preference.path, preference.mode);
|
|
5721
|
+
}
|
|
5680
5722
|
if (body.localAccess && typeof body.localAccess === 'object') {
|
|
5681
5723
|
const localAccessBody = body.localAccess;
|
|
5682
5724
|
if (localAccessBody.reset === true) {
|