clay-server 4.0.0-beta.23 → 4.0.0-beta.25
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/lib/project-connection.js +1 -0
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-request-access.js +66 -0
- package/lib/project.js +9 -0
- package/lib/public/app.js +1 -0
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/pane.css +45 -10
- package/lib/public/css/worker-proposal.css +2 -2
- package/lib/public/modules/app-messages.js +9 -5
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tools.js +10 -122
- package/lib/public/modules/worker-proposal.js +0 -8
- package/lib/sdk-bridge.js +3 -0
- package/lib/server.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
var path = require("path");
|
|
2
|
+
var execFileSync = require("child_process").execFileSync;
|
|
3
|
+
var wrapSpawnAsUser = require("./os-users").wrapSpawnAsUser;
|
|
4
|
+
|
|
5
|
+
function attachFileHistory(ctx, access) {
|
|
6
|
+
var cwd = ctx.cwd;
|
|
7
|
+
var sm = ctx.sm;
|
|
8
|
+
var sendTo = ctx.sendTo;
|
|
9
|
+
|
|
10
|
+
function runGit(ws, args, options) {
|
|
11
|
+
options = Object.assign({}, options, { stdio: ["ignore", "pipe", "pipe"] });
|
|
12
|
+
var identity = access.osIdentity(ws);
|
|
13
|
+
if (identity) options = Object.assign({}, options, { uid: identity.uid, gid: identity.gid });
|
|
14
|
+
var command = wrapSpawnAsUser("git", args, options);
|
|
15
|
+
return execFileSync(command.command, command.args, command.options);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function handleFileHistory(ws, msg) {
|
|
19
|
+
if (!/^fs_(file_history|git_diff|file_at)$/.test(msg.type)) return false;
|
|
20
|
+
if (typeof msg.path !== "string" || !msg.path || path.isAbsolute(msg.path) ||
|
|
21
|
+
path.relative(cwd, path.resolve(cwd, msg.path)).split(path.sep).indexOf("..") !== -1 ||
|
|
22
|
+
(msg.hash && !/^[a-fA-F0-9]{4,64}$/.test(msg.hash)) ||
|
|
23
|
+
(msg.hash2 && !/^[a-fA-F0-9]{4,64}$/.test(msg.hash2))) {
|
|
24
|
+
sendTo(ws, { type: msg.type + "_result", path: msg.path, error: "Invalid file history request" });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
// --- File edit history ---
|
|
28
|
+
if (msg.type === "fs_file_history") {
|
|
29
|
+
var histPath = msg.path;
|
|
30
|
+
if (!histPath) {
|
|
31
|
+
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: [] });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
var absHistPath = path.resolve(cwd, histPath);
|
|
35
|
+
var entries = [];
|
|
36
|
+
|
|
37
|
+
// Collect session edits
|
|
38
|
+
sm.sessions.forEach(function (session) {
|
|
39
|
+
if (!access.canReadSession(ws, session)) return;
|
|
40
|
+
var sessionLocalId = session.localId;
|
|
41
|
+
var sessionTitle = session.title || "Untitled";
|
|
42
|
+
var histLen = session.history.length || 1;
|
|
43
|
+
|
|
44
|
+
for (var hi = 0; hi < session.history.length; hi++) {
|
|
45
|
+
var entry = session.history[hi];
|
|
46
|
+
if (entry.type !== "tool_executing") continue;
|
|
47
|
+
if (entry.name !== "Edit" && entry.name !== "Write") continue;
|
|
48
|
+
if (!entry.input || !entry.input.file_path) continue;
|
|
49
|
+
if (entry.input.file_path !== absHistPath) continue;
|
|
50
|
+
|
|
51
|
+
// Find parent assistant UUID + message snippet by scanning backwards
|
|
52
|
+
var assistantUuid = null;
|
|
53
|
+
var uuidIndex = -1;
|
|
54
|
+
for (var hj = hi - 1; hj >= 0; hj--) {
|
|
55
|
+
if (session.history[hj].type === "message_uuid" && session.history[hj].messageType === "assistant") {
|
|
56
|
+
assistantUuid = session.history[hj].uuid;
|
|
57
|
+
uuidIndex = hj;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Find user prompt by scanning backwards from the assistant uuid
|
|
63
|
+
var messageSnippet = "";
|
|
64
|
+
var searchFrom = uuidIndex >= 0 ? uuidIndex : hi;
|
|
65
|
+
for (var hk = searchFrom - 1; hk >= 0; hk--) {
|
|
66
|
+
if (session.history[hk].type === "user_message" && session.history[hk].text) {
|
|
67
|
+
messageSnippet = session.history[hk].text.trim().substring(0, 100);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Collect Claude's explanation: scan backwards from tool_executing
|
|
73
|
+
// to find the nearest delta text block (skipping tool_start).
|
|
74
|
+
// If no delta found immediately before this tool, scan past
|
|
75
|
+
// intervening tool blocks to find the last delta text within
|
|
76
|
+
// the same assistant turn.
|
|
77
|
+
var assistantSnippet = "";
|
|
78
|
+
var deltaChunks = [];
|
|
79
|
+
for (var hd = hi - 1; hd >= 0; hd--) {
|
|
80
|
+
var hEntry = session.history[hd];
|
|
81
|
+
if (hEntry.type === "tool_start") continue;
|
|
82
|
+
if (hEntry.type === "delta" && hEntry.text) {
|
|
83
|
+
deltaChunks.unshift(hEntry.text);
|
|
84
|
+
} else {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (deltaChunks.length === 0) {
|
|
89
|
+
// No delta immediately before; scan past tool blocks
|
|
90
|
+
// to find the nearest preceding delta in the same turn
|
|
91
|
+
for (var hd2 = hi - 1; hd2 >= 0; hd2--) {
|
|
92
|
+
var hEntry2 = session.history[hd2];
|
|
93
|
+
if (hEntry2.type === "tool_start" || hEntry2.type === "tool_executing" || hEntry2.type === "tool_result") continue;
|
|
94
|
+
if (hEntry2.type === "delta" && hEntry2.text) {
|
|
95
|
+
// Found a delta before an earlier tool in the same turn.
|
|
96
|
+
// Collect this contiguous block of deltas.
|
|
97
|
+
for (var hd3 = hd2; hd3 >= 0; hd3--) {
|
|
98
|
+
var hEntry3 = session.history[hd3];
|
|
99
|
+
if (hEntry3.type === "tool_start") continue;
|
|
100
|
+
if (hEntry3.type === "delta" && hEntry3.text) {
|
|
101
|
+
deltaChunks.unshift(hEntry3.text);
|
|
102
|
+
} else {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
} else {
|
|
108
|
+
// Hit message_uuid, user_message, etc. Stop.
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
assistantSnippet = deltaChunks.join("").trim().substring(0, 150);
|
|
114
|
+
|
|
115
|
+
// Approximate timestamp: interpolate between session creation and last activity
|
|
116
|
+
var tStart = session.createdAt || 0;
|
|
117
|
+
var tEnd = session.lastActivity || tStart;
|
|
118
|
+
var ts = tStart + Math.floor((hi / histLen) * (tEnd - tStart));
|
|
119
|
+
|
|
120
|
+
var editRecord = {
|
|
121
|
+
source: "session",
|
|
122
|
+
timestamp: ts,
|
|
123
|
+
sessionLocalId: sessionLocalId,
|
|
124
|
+
sessionTitle: sessionTitle,
|
|
125
|
+
assistantUuid: assistantUuid,
|
|
126
|
+
toolId: entry.id,
|
|
127
|
+
messageSnippet: messageSnippet,
|
|
128
|
+
assistantSnippet: assistantSnippet,
|
|
129
|
+
toolName: entry.name,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (entry.name === "Edit") {
|
|
133
|
+
editRecord.old_string = entry.input.old_string || "";
|
|
134
|
+
editRecord.new_string = entry.input.new_string || "";
|
|
135
|
+
} else {
|
|
136
|
+
editRecord.isFullWrite = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
entries.push(editRecord);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Collect git commits
|
|
144
|
+
try {
|
|
145
|
+
var gitLog = runGit(ws,
|
|
146
|
+
["log", "--format=%H|%at|%an|%s", "--follow", "--", histPath],
|
|
147
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 }
|
|
148
|
+
);
|
|
149
|
+
var gitLines = gitLog.trim().split("\n");
|
|
150
|
+
for (var gi = 0; gi < gitLines.length; gi++) {
|
|
151
|
+
if (!gitLines[gi]) continue;
|
|
152
|
+
var parts = gitLines[gi].split("|");
|
|
153
|
+
if (parts.length < 4) continue;
|
|
154
|
+
entries.push({
|
|
155
|
+
source: "git",
|
|
156
|
+
hash: parts[0],
|
|
157
|
+
timestamp: parseInt(parts[1], 10) * 1000,
|
|
158
|
+
author: parts[2],
|
|
159
|
+
message: parts.slice(3).join("|"),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// Not a git repo or file not tracked, that is fine
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort by timestamp descending (newest first)
|
|
167
|
+
entries.sort(function (a, b) { return b.timestamp - a.timestamp; });
|
|
168
|
+
|
|
169
|
+
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: entries });
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Git diff for file history ---
|
|
174
|
+
if (msg.type === "fs_git_diff") {
|
|
175
|
+
var diffPath = msg.path;
|
|
176
|
+
var hash = msg.hash;
|
|
177
|
+
var hash2 = msg.hash2 || null;
|
|
178
|
+
if (!diffPath || !hash) {
|
|
179
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, path: diffPath, diff: "", error: "Missing params" });
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
var diff;
|
|
184
|
+
if (hash2) {
|
|
185
|
+
diff = runGit(ws, ["diff", hash, hash2, "--", diffPath],
|
|
186
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
187
|
+
} else {
|
|
188
|
+
diff = runGit(ws, ["show", hash, "--format=", "--", diffPath],
|
|
189
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
190
|
+
}
|
|
191
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: diff || "" });
|
|
192
|
+
} catch (e) {
|
|
193
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: "", error: e.message });
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- File content at a git commit ---
|
|
199
|
+
if (msg.type === "fs_file_at") {
|
|
200
|
+
var atPath = msg.path;
|
|
201
|
+
var atHash = msg.hash;
|
|
202
|
+
if (!atPath || !atHash) {
|
|
203
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: "Missing params" });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
// Convert to repo-relative path (git show requires hash:relative/path)
|
|
208
|
+
var atAbsPath = path.resolve(cwd, atPath);
|
|
209
|
+
var atRelPath = path.relative(cwd, atAbsPath);
|
|
210
|
+
var content = runGit(ws, ["show", atHash + ":" + atRelPath],
|
|
211
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
212
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: content });
|
|
213
|
+
} catch (e) {
|
|
214
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: e.message });
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return { handleFileHistory: handleFileHistory };
|
|
222
|
+
}
|
|
223
|
+
module.exports = { attachFileHistory: attachFileHistory };
|
|
@@ -5,7 +5,7 @@ var path = require("path");
|
|
|
5
5
|
* Attach file/directory watcher engine to a project context.
|
|
6
6
|
*
|
|
7
7
|
* ctx fields:
|
|
8
|
-
* cwd, send, sendTo, safePath, BINARY_EXTS, FS_MAX_SIZE, IGNORED_DIRS
|
|
8
|
+
* cwd, send, sendTo, safePath, BINARY_EXTS, FS_MAX_SIZE, IGNORED_DIRS, requestAccess, fsAsUser
|
|
9
9
|
*/
|
|
10
10
|
function attachFileWatch(ctx) {
|
|
11
11
|
var cwd = ctx.cwd;
|
|
@@ -15,6 +15,19 @@ function attachFileWatch(ctx) {
|
|
|
15
15
|
var BINARY_EXTS = ctx.BINARY_EXTS;
|
|
16
16
|
var FS_MAX_SIZE = ctx.FS_MAX_SIZE;
|
|
17
17
|
var IGNORED_DIRS = ctx.IGNORED_DIRS;
|
|
18
|
+
var access = ctx.requestAccess;
|
|
19
|
+
|
|
20
|
+
function identityFor(client) {
|
|
21
|
+
if (!access) return null;
|
|
22
|
+
if (!access.canUseFiles(client)) throw new Error("File browser access is not permitted");
|
|
23
|
+
return access.osIdentity(client);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function checkedPath(relPath) {
|
|
27
|
+
var resolved = safePath(cwd, relPath);
|
|
28
|
+
if (!resolved) throw new Error("Access denied");
|
|
29
|
+
return resolved;
|
|
30
|
+
}
|
|
18
31
|
|
|
19
32
|
// --- File watcher ---
|
|
20
33
|
// One open file per websocket client. A project-wide singleton watcher made
|
|
@@ -47,18 +60,21 @@ function attachFileWatch(ctx) {
|
|
|
47
60
|
}
|
|
48
61
|
}
|
|
49
62
|
|
|
50
|
-
function readFileSnapshot(
|
|
51
|
-
var
|
|
63
|
+
function readFileSnapshot(client, relPath) {
|
|
64
|
+
var identity = identityFor(client);
|
|
65
|
+
var absPath = checkedPath(relPath);
|
|
66
|
+
var stat = identity ? ctx.fsAsUser("stat", { file: absPath }, identity) : fs.statSync(absPath);
|
|
52
67
|
var ext = path.extname(absPath).toLowerCase();
|
|
53
68
|
if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return null;
|
|
69
|
+
if (identity) return ctx.fsAsUser("read", { file: absPath, readContent: true }, identity);
|
|
54
70
|
return { content: fs.readFileSync(absPath, "utf8"), size: stat.size };
|
|
55
71
|
}
|
|
56
72
|
|
|
57
|
-
function publishFileChanged(key, client, relPath
|
|
73
|
+
function publishFileChanged(key, client, relPath) {
|
|
58
74
|
var latest = fileWatchers.get(key);
|
|
59
75
|
if (!latest || latest.relPath !== relPath) return;
|
|
60
76
|
try {
|
|
61
|
-
var snapshot = readFileSnapshot(
|
|
77
|
+
var snapshot = readFileSnapshot(client, relPath);
|
|
62
78
|
if (!snapshot) return;
|
|
63
79
|
if (latest.hasSnapshot && latest.content === snapshot.content && latest.size === snapshot.size) return;
|
|
64
80
|
latest.hasSnapshot = true;
|
|
@@ -97,7 +113,9 @@ function attachFileWatch(ctx) {
|
|
|
97
113
|
var parentPath = path.dirname(absPath);
|
|
98
114
|
var baseName = path.basename(absPath);
|
|
99
115
|
var initialSnapshot = null;
|
|
100
|
-
try { initialSnapshot = readFileSnapshot(
|
|
116
|
+
try { initialSnapshot = readFileSnapshot(client, relPath); } catch (e) {
|
|
117
|
+
return Promise.resolve(false);
|
|
118
|
+
}
|
|
101
119
|
try {
|
|
102
120
|
var watcher = fs.watch(parentPath, function (eventType, filename) {
|
|
103
121
|
if (filename && String(filename) !== baseName) return;
|
|
@@ -105,7 +123,7 @@ function attachFileWatch(ctx) {
|
|
|
105
123
|
if (!active || active.relPath !== relPath) return;
|
|
106
124
|
clearTimeout(active.debounce);
|
|
107
125
|
active.debounce = setTimeout(function () {
|
|
108
|
-
publishFileChanged(key, client, relPath
|
|
126
|
+
publishFileChanged(key, client, relPath);
|
|
109
127
|
}, 200);
|
|
110
128
|
});
|
|
111
129
|
var resolveReady = null;
|
|
@@ -127,7 +145,7 @@ function attachFileWatch(ctx) {
|
|
|
127
145
|
// drop them under load. Periodic content reconciliation is the source of
|
|
128
146
|
// truth and also survives atomic replacements with identical metadata.
|
|
129
147
|
entry.pollTimer = setInterval(function () {
|
|
130
|
-
publishFileChanged(key, client, relPath
|
|
148
|
+
publishFileChanged(key, client, relPath);
|
|
131
149
|
}, 1000);
|
|
132
150
|
// fs.watch has no readiness event. Reconcile once on the next event-loop
|
|
133
151
|
// turn so a change between the initial read and native watcher activation
|
|
@@ -135,7 +153,7 @@ function attachFileWatch(ctx) {
|
|
|
135
153
|
entry.reconcile = setImmediate(function () {
|
|
136
154
|
var active = fileWatchers.get(key);
|
|
137
155
|
if (active) active.reconcile = null;
|
|
138
|
-
publishFileChanged(key, client, relPath
|
|
156
|
+
publishFileChanged(key, client, relPath);
|
|
139
157
|
if (fileWatchers.get(key) === entry) settleFileWatchReady(entry, true);
|
|
140
158
|
});
|
|
141
159
|
watcher.on("error", function () { closeFileWatch(key); });
|
|
@@ -155,55 +173,65 @@ function attachFileWatch(ctx) {
|
|
|
155
173
|
for (var i = 0; i < keys.length; i++) closeFileWatch(keys[i]);
|
|
156
174
|
}
|
|
157
175
|
|
|
158
|
-
//
|
|
159
|
-
var dirWatchers =
|
|
176
|
+
// Directory subscriptions are private to the requesting socket.
|
|
177
|
+
var dirWatchers = new Map();
|
|
160
178
|
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
var absPath =
|
|
164
|
-
|
|
179
|
+
function readDirectory(client, relPath) {
|
|
180
|
+
var identity = identityFor(client);
|
|
181
|
+
var absPath = checkedPath(relPath);
|
|
182
|
+
var items = identity ? ctx.fsAsUser("list", { dir: absPath }, identity) :
|
|
183
|
+
fs.readdirSync(absPath, { withFileTypes: true }).map(function (item) {
|
|
184
|
+
return { name: item.name, isDir: item.isDirectory() };
|
|
185
|
+
});
|
|
186
|
+
return items.filter(function (item) {
|
|
187
|
+
return !item.isDir || !IGNORED_DIRS.has(item.name);
|
|
188
|
+
}).map(function (item) {
|
|
189
|
+
return { name: item.name, type: item.isDir ? "dir" : "file",
|
|
190
|
+
path: path.relative(cwd, path.join(absPath, item.name)).split(path.sep).join("/") };
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function startDirWatch(client, relPath) {
|
|
195
|
+
var subscriptions = dirWatchers.get(client);
|
|
196
|
+
if (!subscriptions) { subscriptions = new Map(); dirWatchers.set(client, subscriptions); }
|
|
197
|
+
if (subscriptions.has(relPath)) return;
|
|
165
198
|
try {
|
|
166
|
-
|
|
167
|
-
var
|
|
168
|
-
|
|
169
|
-
debounce
|
|
170
|
-
|
|
199
|
+
readDirectory(client, relPath);
|
|
200
|
+
var entry = { watcher: null, debounce: null };
|
|
201
|
+
entry.watcher = fs.watch(checkedPath(relPath), function () {
|
|
202
|
+
clearTimeout(entry.debounce);
|
|
203
|
+
entry.debounce = setTimeout(function () {
|
|
171
204
|
try {
|
|
172
|
-
var
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (items[i].isDirectory() && IGNORED_DIRS.has(items[i].name)) continue;
|
|
176
|
-
entries.push({
|
|
177
|
-
name: items[i].name,
|
|
178
|
-
type: items[i].isDirectory() ? "dir" : "file",
|
|
179
|
-
path: path.relative(cwd, path.join(absPath, items[i].name)).split(path.sep).join("/"),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
send({ type: "fs_dir_changed", path: relPath, entries: entries });
|
|
183
|
-
} catch (e) {
|
|
184
|
-
stopDirWatch(relPath);
|
|
185
|
-
}
|
|
205
|
+
var entries = readDirectory(client, relPath);
|
|
206
|
+
sendTo(client, { type: "fs_dir_changed", path: relPath, entries: entries });
|
|
207
|
+
} catch (e) { stopDirWatch(client, relPath); }
|
|
186
208
|
}, 300);
|
|
187
209
|
});
|
|
188
|
-
watcher.on("error", function () { stopDirWatch(relPath); });
|
|
189
|
-
|
|
190
|
-
} catch (e) {
|
|
210
|
+
entry.watcher.on("error", function () { stopDirWatch(client, relPath); });
|
|
211
|
+
subscriptions.set(relPath, entry);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (!subscriptions.size) dirWatchers.delete(client);
|
|
214
|
+
}
|
|
191
215
|
}
|
|
192
216
|
|
|
193
|
-
function stopDirWatch(relPath) {
|
|
194
|
-
var
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
217
|
+
function stopDirWatch(client, relPath) {
|
|
218
|
+
var subscriptions = dirWatchers.get(client);
|
|
219
|
+
var entry = subscriptions && subscriptions.get(relPath);
|
|
220
|
+
if (!entry) return;
|
|
221
|
+
clearTimeout(entry.debounce);
|
|
222
|
+
try { entry.watcher.close(); } catch (e) {}
|
|
223
|
+
subscriptions.delete(relPath);
|
|
224
|
+
if (!subscriptions.size) dirWatchers.delete(client);
|
|
200
225
|
}
|
|
201
226
|
|
|
202
|
-
function stopAllDirWatches() {
|
|
203
|
-
var
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
function stopAllDirWatches(client) {
|
|
228
|
+
var clients = arguments.length ? [client] : Array.from(dirWatchers.keys());
|
|
229
|
+
clients.forEach(function (key) {
|
|
230
|
+
var subscriptions = dirWatchers.get(key);
|
|
231
|
+
if (subscriptions) Array.from(subscriptions.keys()).forEach(function (relPath) {
|
|
232
|
+
stopDirWatch(key, relPath);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
207
235
|
}
|
|
208
236
|
|
|
209
237
|
return {
|