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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
|
-
var
|
|
3
|
+
var attachRequestAccess = require("./project-request-access").attachRequestAccess;
|
|
4
|
+
var attachFileHistory = require("./project-file-history").attachFileHistory;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Attach filesystem-related message handlers to a project context.
|
|
@@ -20,17 +21,12 @@ var execFileSync = require("child_process").execFileSync;
|
|
|
20
21
|
function attachFilesystem(ctx) {
|
|
21
22
|
var cwd = ctx.cwd;
|
|
22
23
|
var slug = ctx.slug;
|
|
23
|
-
var osUsers = ctx.osUsers;
|
|
24
|
-
var sm = ctx.sm;
|
|
25
|
-
var send = ctx.send;
|
|
26
24
|
var sendTo = ctx.sendTo;
|
|
27
25
|
var safePath = ctx.safePath;
|
|
28
26
|
var safeAbsPath = ctx.safeAbsPath;
|
|
29
|
-
var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
|
|
30
27
|
var startFileWatch = ctx.startFileWatch;
|
|
31
28
|
var stopFileWatch = ctx.stopFileWatch;
|
|
32
29
|
var startDirWatch = ctx.startDirWatch;
|
|
33
|
-
var usersModule = ctx.usersModule;
|
|
34
30
|
var fsAsUser = ctx.fsAsUser;
|
|
35
31
|
var validateEnvString = ctx.validateEnvString;
|
|
36
32
|
var onEnvironmentChanged = ctx.onEnvironmentChanged || function () {};
|
|
@@ -40,15 +36,19 @@ function attachFilesystem(ctx) {
|
|
|
40
36
|
var IMAGE_EXTS = ctx.IMAGE_EXTS;
|
|
41
37
|
var FS_MAX_SIZE = ctx.FS_MAX_SIZE;
|
|
42
38
|
|
|
39
|
+
var access = ctx.requestAccess || attachRequestAccess(ctx);
|
|
40
|
+
var fileHistory = attachFileHistory(ctx, access);
|
|
41
|
+
|
|
43
42
|
function handleFilesystemMessage(ws, msg) {
|
|
44
|
-
//
|
|
45
|
-
if (msg.type
|
|
46
|
-
if (ws
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
43
|
+
// Unsubscription must remain available after permission revocation.
|
|
44
|
+
if (/^fs_/.test(msg.type) && msg.type !== "fs_unwatch") {
|
|
45
|
+
if (!access.canUseFiles(ws)) {
|
|
46
|
+
sendTo(ws, { type: msg.type + "_result", error: "File browser access is not permitted" });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
try { access.osIdentity(ws); } catch (e) {
|
|
50
|
+
sendTo(ws, { type: msg.type + "_result", error: e.message });
|
|
51
|
+
return true;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -56,7 +56,7 @@ function attachFilesystem(ctx) {
|
|
|
56
56
|
if (msg.type === "fs_list") {
|
|
57
57
|
var fsDir = safePath(cwd, msg.path || ".");
|
|
58
58
|
// In OS user mode, fall back to absolute path resolution (ACL enforces access)
|
|
59
|
-
if (!fsDir &&
|
|
59
|
+
if (!fsDir && access.osIdentity(ws)) {
|
|
60
60
|
fsDir = safeAbsPath(msg.path);
|
|
61
61
|
}
|
|
62
62
|
if (!fsDir) {
|
|
@@ -64,7 +64,7 @@ function attachFilesystem(ctx) {
|
|
|
64
64
|
return true;
|
|
65
65
|
}
|
|
66
66
|
try {
|
|
67
|
-
var fsListUserInfo =
|
|
67
|
+
var fsListUserInfo = access.osIdentity(ws);
|
|
68
68
|
var entries = [];
|
|
69
69
|
if (fsListUserInfo) {
|
|
70
70
|
// Run as target OS user to respect Linux file permissions
|
|
@@ -92,7 +92,7 @@ function attachFilesystem(ctx) {
|
|
|
92
92
|
}
|
|
93
93
|
sendTo(ws, { type: "fs_list_result", path: msg.path || ".", entries: entries });
|
|
94
94
|
// Auto-watch the directory for changes
|
|
95
|
-
startDirWatch(msg.path || ".");
|
|
95
|
+
startDirWatch(ws, msg.path || ".");
|
|
96
96
|
} catch (e) {
|
|
97
97
|
sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: e.message });
|
|
98
98
|
}
|
|
@@ -109,7 +109,7 @@ function attachFilesystem(ctx) {
|
|
|
109
109
|
try {
|
|
110
110
|
var searchResults = [];
|
|
111
111
|
var MAX_RESULTS = 50;
|
|
112
|
-
var searchUserInfo =
|
|
112
|
+
var searchUserInfo = access.osIdentity(ws);
|
|
113
113
|
|
|
114
114
|
function walkDir(dir, relPrefix) {
|
|
115
115
|
if (searchResults.length >= MAX_RESULTS) return;
|
|
@@ -148,7 +148,7 @@ function attachFilesystem(ctx) {
|
|
|
148
148
|
// --- fs_read ---
|
|
149
149
|
if (msg.type === "fs_read") {
|
|
150
150
|
var fsFile = safePath(cwd, msg.path);
|
|
151
|
-
if (!fsFile &&
|
|
151
|
+
if (!fsFile && access.osIdentity(ws)) {
|
|
152
152
|
fsFile = safeAbsPath(msg.path);
|
|
153
153
|
}
|
|
154
154
|
if (!fsFile) {
|
|
@@ -156,7 +156,7 @@ function attachFilesystem(ctx) {
|
|
|
156
156
|
return true;
|
|
157
157
|
}
|
|
158
158
|
try {
|
|
159
|
-
var fsReadUserInfo =
|
|
159
|
+
var fsReadUserInfo = access.osIdentity(ws);
|
|
160
160
|
var ext = path.extname(fsFile).toLowerCase();
|
|
161
161
|
if (fsReadUserInfo) {
|
|
162
162
|
// Run stat and read as target OS user
|
|
@@ -197,7 +197,7 @@ function attachFilesystem(ctx) {
|
|
|
197
197
|
// --- fs_write ---
|
|
198
198
|
if (msg.type === "fs_write") {
|
|
199
199
|
var fsWriteFile = safePath(cwd, msg.path);
|
|
200
|
-
if (!fsWriteFile &&
|
|
200
|
+
if (!fsWriteFile && access.osIdentity(ws)) {
|
|
201
201
|
fsWriteFile = safeAbsPath(msg.path);
|
|
202
202
|
}
|
|
203
203
|
if (!fsWriteFile) {
|
|
@@ -205,7 +205,7 @@ function attachFilesystem(ctx) {
|
|
|
205
205
|
return true;
|
|
206
206
|
}
|
|
207
207
|
try {
|
|
208
|
-
var fsWriteUserInfo =
|
|
208
|
+
var fsWriteUserInfo = access.osIdentity(ws);
|
|
209
209
|
if (fsWriteUserInfo) {
|
|
210
210
|
fsAsUser("write", { file: fsWriteFile, content: msg.content || "" }, fsWriteUserInfo);
|
|
211
211
|
} else {
|
|
@@ -223,12 +223,12 @@ function attachFilesystem(ctx) {
|
|
|
223
223
|
msg.type === "read_global_claude_md" || msg.type === "write_global_claude_md" ||
|
|
224
224
|
msg.type === "get_shared_env" || msg.type === "set_shared_env" ||
|
|
225
225
|
msg.type === "transfer_project_owner") {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
226
|
+
var globalSetting = /^(read_global_claude_md|write_global_claude_md|get_shared_env|set_shared_env)$/.test(msg.type);
|
|
227
|
+
var targetSlug = msg.slug || slug;
|
|
228
|
+
if (!access.hasPermission(ws, "projectSettings") ||
|
|
229
|
+
(globalSetting ? !access.isAdmin(ws) : !access.canAccessProject(ws, targetSlug))) {
|
|
230
|
+
sendTo(ws, { type: "error", text: "Project settings access is not permitted" });
|
|
231
|
+
return true;
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -236,7 +236,7 @@ function attachFilesystem(ctx) {
|
|
|
236
236
|
if (msg.type === "get_project_env") {
|
|
237
237
|
var envrc = "";
|
|
238
238
|
if (typeof opts.onGetProjectEnv === "function") {
|
|
239
|
-
var envResult = opts.onGetProjectEnv(
|
|
239
|
+
var envResult = opts.onGetProjectEnv(targetSlug);
|
|
240
240
|
envrc = envResult.envrc || "";
|
|
241
241
|
}
|
|
242
242
|
sendTo(ws, { type: "project_env_result", slug: msg.slug, envrc: envrc });
|
|
@@ -250,7 +250,7 @@ function attachFilesystem(ctx) {
|
|
|
250
250
|
sendTo(ws, { type: "set_project_env_result", ok: false, slug: msg.slug, error: envError });
|
|
251
251
|
return true;
|
|
252
252
|
}
|
|
253
|
-
var setResult = opts.onSetProjectEnv(
|
|
253
|
+
var setResult = opts.onSetProjectEnv(targetSlug, msg.envrc || "");
|
|
254
254
|
if (setResult.ok) onEnvironmentChanged();
|
|
255
255
|
sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: msg.slug, error: setResult.error, timing: setResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
|
|
256
256
|
} else {
|
|
@@ -324,196 +324,7 @@ function attachFilesystem(ctx) {
|
|
|
324
324
|
return true;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
if (msg.type === "fs_file_history") {
|
|
329
|
-
var histPath = msg.path;
|
|
330
|
-
if (!histPath) {
|
|
331
|
-
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: [] });
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
var absHistPath = path.resolve(cwd, histPath);
|
|
335
|
-
var entries = [];
|
|
336
|
-
|
|
337
|
-
// Collect session edits
|
|
338
|
-
sm.sessions.forEach(function (session) {
|
|
339
|
-
var sessionLocalId = session.localId;
|
|
340
|
-
var sessionTitle = session.title || "Untitled";
|
|
341
|
-
var histLen = session.history.length || 1;
|
|
342
|
-
|
|
343
|
-
for (var hi = 0; hi < session.history.length; hi++) {
|
|
344
|
-
var entry = session.history[hi];
|
|
345
|
-
if (entry.type !== "tool_executing") continue;
|
|
346
|
-
if (entry.name !== "Edit" && entry.name !== "Write") continue;
|
|
347
|
-
if (!entry.input || !entry.input.file_path) continue;
|
|
348
|
-
if (entry.input.file_path !== absHistPath) continue;
|
|
349
|
-
|
|
350
|
-
// Find parent assistant UUID + message snippet by scanning backwards
|
|
351
|
-
var assistantUuid = null;
|
|
352
|
-
var uuidIndex = -1;
|
|
353
|
-
for (var hj = hi - 1; hj >= 0; hj--) {
|
|
354
|
-
if (session.history[hj].type === "message_uuid" && session.history[hj].messageType === "assistant") {
|
|
355
|
-
assistantUuid = session.history[hj].uuid;
|
|
356
|
-
uuidIndex = hj;
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Find user prompt by scanning backwards from the assistant uuid
|
|
362
|
-
var messageSnippet = "";
|
|
363
|
-
var searchFrom = uuidIndex >= 0 ? uuidIndex : hi;
|
|
364
|
-
for (var hk = searchFrom - 1; hk >= 0; hk--) {
|
|
365
|
-
if (session.history[hk].type === "user_message" && session.history[hk].text) {
|
|
366
|
-
messageSnippet = session.history[hk].text.trim().substring(0, 100);
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Collect Claude's explanation: scan backwards from tool_executing
|
|
372
|
-
// to find the nearest delta text block (skipping tool_start).
|
|
373
|
-
// If no delta found immediately before this tool, scan past
|
|
374
|
-
// intervening tool blocks to find the last delta text within
|
|
375
|
-
// the same assistant turn.
|
|
376
|
-
var assistantSnippet = "";
|
|
377
|
-
var deltaChunks = [];
|
|
378
|
-
for (var hd = hi - 1; hd >= 0; hd--) {
|
|
379
|
-
var hEntry = session.history[hd];
|
|
380
|
-
if (hEntry.type === "tool_start") continue;
|
|
381
|
-
if (hEntry.type === "delta" && hEntry.text) {
|
|
382
|
-
deltaChunks.unshift(hEntry.text);
|
|
383
|
-
} else {
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (deltaChunks.length === 0) {
|
|
388
|
-
// No delta immediately before; scan past tool blocks
|
|
389
|
-
// to find the nearest preceding delta in the same turn
|
|
390
|
-
for (var hd2 = hi - 1; hd2 >= 0; hd2--) {
|
|
391
|
-
var hEntry2 = session.history[hd2];
|
|
392
|
-
if (hEntry2.type === "tool_start" || hEntry2.type === "tool_executing" || hEntry2.type === "tool_result") continue;
|
|
393
|
-
if (hEntry2.type === "delta" && hEntry2.text) {
|
|
394
|
-
// Found a delta before an earlier tool in the same turn.
|
|
395
|
-
// Collect this contiguous block of deltas.
|
|
396
|
-
for (var hd3 = hd2; hd3 >= 0; hd3--) {
|
|
397
|
-
var hEntry3 = session.history[hd3];
|
|
398
|
-
if (hEntry3.type === "tool_start") continue;
|
|
399
|
-
if (hEntry3.type === "delta" && hEntry3.text) {
|
|
400
|
-
deltaChunks.unshift(hEntry3.text);
|
|
401
|
-
} else {
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
break;
|
|
406
|
-
} else {
|
|
407
|
-
// Hit message_uuid, user_message, etc. Stop.
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
assistantSnippet = deltaChunks.join("").trim().substring(0, 150);
|
|
413
|
-
|
|
414
|
-
// Approximate timestamp: interpolate between session creation and last activity
|
|
415
|
-
var tStart = session.createdAt || 0;
|
|
416
|
-
var tEnd = session.lastActivity || tStart;
|
|
417
|
-
var ts = tStart + Math.floor((hi / histLen) * (tEnd - tStart));
|
|
418
|
-
|
|
419
|
-
var editRecord = {
|
|
420
|
-
source: "session",
|
|
421
|
-
timestamp: ts,
|
|
422
|
-
sessionLocalId: sessionLocalId,
|
|
423
|
-
sessionTitle: sessionTitle,
|
|
424
|
-
assistantUuid: assistantUuid,
|
|
425
|
-
toolId: entry.id,
|
|
426
|
-
messageSnippet: messageSnippet,
|
|
427
|
-
assistantSnippet: assistantSnippet,
|
|
428
|
-
toolName: entry.name,
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
if (entry.name === "Edit") {
|
|
432
|
-
editRecord.old_string = entry.input.old_string || "";
|
|
433
|
-
editRecord.new_string = entry.input.new_string || "";
|
|
434
|
-
} else {
|
|
435
|
-
editRecord.isFullWrite = true;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
entries.push(editRecord);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// Collect git commits
|
|
443
|
-
try {
|
|
444
|
-
var gitLog = execFileSync(
|
|
445
|
-
"git", ["log", "--format=%H|%at|%an|%s", "--follow", "--", histPath],
|
|
446
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 }
|
|
447
|
-
);
|
|
448
|
-
var gitLines = gitLog.trim().split("\n");
|
|
449
|
-
for (var gi = 0; gi < gitLines.length; gi++) {
|
|
450
|
-
if (!gitLines[gi]) continue;
|
|
451
|
-
var parts = gitLines[gi].split("|");
|
|
452
|
-
if (parts.length < 4) continue;
|
|
453
|
-
entries.push({
|
|
454
|
-
source: "git",
|
|
455
|
-
hash: parts[0],
|
|
456
|
-
timestamp: parseInt(parts[1], 10) * 1000,
|
|
457
|
-
author: parts[2],
|
|
458
|
-
message: parts.slice(3).join("|"),
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
} catch (e) {
|
|
462
|
-
// Not a git repo or file not tracked, that is fine
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Sort by timestamp descending (newest first)
|
|
466
|
-
entries.sort(function (a, b) { return b.timestamp - a.timestamp; });
|
|
467
|
-
|
|
468
|
-
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: entries });
|
|
469
|
-
return true;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// --- Git diff for file history ---
|
|
473
|
-
if (msg.type === "fs_git_diff") {
|
|
474
|
-
var diffPath = msg.path;
|
|
475
|
-
var hash = msg.hash;
|
|
476
|
-
var hash2 = msg.hash2 || null;
|
|
477
|
-
if (!diffPath || !hash) {
|
|
478
|
-
sendTo(ws, { type: "fs_git_diff_result", hash: hash, path: diffPath, diff: "", error: "Missing params" });
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
try {
|
|
482
|
-
var diff;
|
|
483
|
-
if (hash2) {
|
|
484
|
-
diff = execFileSync("git", ["diff", hash, hash2, "--", diffPath],
|
|
485
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
486
|
-
} else {
|
|
487
|
-
diff = execFileSync("git", ["show", hash, "--format=", "--", diffPath],
|
|
488
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
489
|
-
}
|
|
490
|
-
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: diff || "" });
|
|
491
|
-
} catch (e) {
|
|
492
|
-
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: "", error: e.message });
|
|
493
|
-
}
|
|
494
|
-
return true;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// --- File content at a git commit ---
|
|
498
|
-
if (msg.type === "fs_file_at") {
|
|
499
|
-
var atPath = msg.path;
|
|
500
|
-
var atHash = msg.hash;
|
|
501
|
-
if (!atPath || !atHash) {
|
|
502
|
-
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: "Missing params" });
|
|
503
|
-
return true;
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
// Convert to repo-relative path (git show requires hash:relative/path)
|
|
507
|
-
var atAbsPath = path.resolve(cwd, atPath);
|
|
508
|
-
var atRelPath = path.relative(cwd, atAbsPath);
|
|
509
|
-
var content = execFileSync("git", ["show", atHash + ":" + atRelPath],
|
|
510
|
-
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
511
|
-
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: content });
|
|
512
|
-
} catch (e) {
|
|
513
|
-
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: e.message });
|
|
514
|
-
}
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
327
|
+
if (fileHistory.handleFileHistory(ws, msg)) return true;
|
|
517
328
|
|
|
518
329
|
return false;
|
|
519
330
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Shared authorization for project requests and long-lived file subscriptions.
|
|
2
|
+
function attachRequestAccess(ctx) {
|
|
3
|
+
var users = ctx.usersModule;
|
|
4
|
+
var opts = ctx.opts || {};
|
|
5
|
+
|
|
6
|
+
function isMultiUser() {
|
|
7
|
+
return !!(users && users.isMultiUser && users.isMultiUser());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function userFor(ws) {
|
|
11
|
+
var user = ws && ws._clayUser;
|
|
12
|
+
if (user && users.findUserById) return users.findUserById(user.id);
|
|
13
|
+
return user || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function canAccessProject(ws, slug) {
|
|
17
|
+
if (!isMultiUser()) return true;
|
|
18
|
+
var user = userFor(ws);
|
|
19
|
+
if (!user || typeof slug !== "string" || !slug) return false;
|
|
20
|
+
try {
|
|
21
|
+
return typeof opts.canAccessProjectSlug === "function" && opts.canAccessProjectSlug(user.id, slug) === true;
|
|
22
|
+
} catch (e) { return false; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function permitMessage(ws, msg) {
|
|
26
|
+
if (canAccessProject(ws, ctx.slug) && (!msg.targetSlug || canAccessProject(ws, msg.targetSlug))) return true;
|
|
27
|
+
ctx.sendTo(ws, { type: "error", text: "Project access is not permitted" });
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasPermission(ws, permission) {
|
|
32
|
+
var user = userFor(ws);
|
|
33
|
+
if (!user) return !isMultiUser();
|
|
34
|
+
return users.getEffectivePermissions(user, ctx.osUsers)[permission] === true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function canUseFiles(ws) {
|
|
38
|
+
return canAccessProject(ws, ctx.slug) && hasPermission(ws, "fileBrowser");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isAdmin(ws) {
|
|
42
|
+
if (!isMultiUser() && !(ws && ws._clayUser)) return true;
|
|
43
|
+
var user = userFor(ws);
|
|
44
|
+
return !!(user && user.role === "admin");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function canReadSession(ws, session) {
|
|
48
|
+
if (!isMultiUser()) return true;
|
|
49
|
+
var user = userFor(ws);
|
|
50
|
+
return !!(user && canAccessProject(ws, ctx.slug) && users.canAccessSession(user.id, session, { visibility: "public" }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function osIdentity(ws) {
|
|
54
|
+
if (!ctx.osUsers) return null;
|
|
55
|
+
var user = userFor(ws);
|
|
56
|
+
if (!user || !user.linuxUser) throw new Error("OS user identity is unavailable");
|
|
57
|
+
var info = ctx.getOsUserInfoForWs({ _clayUser: user });
|
|
58
|
+
if (!info) throw new Error("OS user identity is unavailable");
|
|
59
|
+
return info;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { permitMessage: permitMessage, canAccessProject: canAccessProject, hasPermission: hasPermission,
|
|
63
|
+
canUseFiles: canUseFiles, isAdmin: isAdmin, canReadSession: canReadSession, osIdentity: osIdentity };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { attachRequestAccess: attachRequestAccess };
|
package/lib/project.js
CHANGED
|
@@ -26,6 +26,7 @@ var { attachMemory } = require("./project-memory");
|
|
|
26
26
|
var { attachMateInteraction } = require("./project-mate-interaction");
|
|
27
27
|
var { attachUserMention } = require("./project-user-mention");
|
|
28
28
|
var { attachLoop } = require("./project-loop");
|
|
29
|
+
var { attachRequestAccess } = require("./project-request-access");
|
|
29
30
|
var { attachFileWatch } = require("./project-file-watch");
|
|
30
31
|
var { attachHTTP } = require("./project-http");
|
|
31
32
|
var { attachImage } = require("./project-image");
|
|
@@ -513,8 +514,14 @@ function createProjectContext(opts) {
|
|
|
513
514
|
getProjectOwnerId: function () { return projectOwnerId; },
|
|
514
515
|
});
|
|
515
516
|
|
|
517
|
+
var _requestAccess = attachRequestAccess({
|
|
518
|
+
slug: slug, opts: opts, usersModule: usersModule, osUsers: osUsers,
|
|
519
|
+
sendTo: sendTo, getOsUserInfoForWs: getOsUserInfoForWs,
|
|
520
|
+
});
|
|
521
|
+
|
|
516
522
|
// --- File/directory watcher engine (delegated to project-file-watch.js) ---
|
|
517
523
|
var _fileWatch = attachFileWatch({
|
|
524
|
+
requestAccess: _requestAccess, fsAsUser: fsAsUser,
|
|
518
525
|
cwd: cwd,
|
|
519
526
|
send: send,
|
|
520
527
|
sendTo: sendTo,
|
|
@@ -1294,6 +1301,7 @@ function createProjectContext(opts) {
|
|
|
1294
1301
|
}
|
|
1295
1302
|
|
|
1296
1303
|
function handleMessage(ws, msg) {
|
|
1304
|
+
if (!_requestAccess.permitMessage(ws, msg)) return;
|
|
1297
1305
|
// --- Keep-alive (delegated to project-connection.js) ---
|
|
1298
1306
|
// Answered before any routing so a ping can never be forwarded elsewhere.
|
|
1299
1307
|
// _connection is assigned later in this factory than the other modules, so
|
|
@@ -1770,6 +1778,7 @@ function createProjectContext(opts) {
|
|
|
1770
1778
|
|
|
1771
1779
|
// --- Filesystem handler (delegated to project-filesystem.js) ---
|
|
1772
1780
|
var _filesystem = attachFilesystem({
|
|
1781
|
+
requestAccess: _requestAccess,
|
|
1773
1782
|
cwd: cwd,
|
|
1774
1783
|
slug: slug,
|
|
1775
1784
|
osUsers: osUsers,
|
package/lib/public/app.js
CHANGED
|
@@ -377,6 +377,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
377
377
|
currentMsgEl: null,
|
|
378
378
|
currentFullText: "",
|
|
379
379
|
matePreThinkingEl: null,
|
|
380
|
+
thinkingState: null,
|
|
380
381
|
// panels
|
|
381
382
|
fileViewerFullscreen: false,
|
|
382
383
|
markdownSlidesActive: false,
|
|
@@ -693,6 +693,8 @@ pre.mermaid-error {
|
|
|
693
693
|
|
|
694
694
|
.thinking-header {
|
|
695
695
|
display: inline-flex;
|
|
696
|
+
max-width: 100%;
|
|
697
|
+
box-sizing: border-box;
|
|
696
698
|
align-items: center;
|
|
697
699
|
gap: 6px;
|
|
698
700
|
cursor: pointer;
|
|
@@ -726,9 +728,44 @@ pre.mermaid-error {
|
|
|
726
728
|
.thinking-label {
|
|
727
729
|
font-size: 13px;
|
|
728
730
|
color: var(--text-muted);
|
|
731
|
+
min-width: 0;
|
|
732
|
+
overflow: hidden;
|
|
733
|
+
text-overflow: ellipsis;
|
|
734
|
+
white-space: nowrap;
|
|
729
735
|
}
|
|
730
736
|
|
|
737
|
+
.thinking-live .thinking-header {
|
|
738
|
+
padding: 6px 0;
|
|
739
|
+
background: transparent;
|
|
740
|
+
border-radius: 0;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.thinking-live .thinking-chevron { display: none; }
|
|
744
|
+
|
|
745
|
+
.thinking-live .thinking-label {
|
|
746
|
+
background: linear-gradient(100deg, var(--text-muted) 40%, var(--text-dimmer) 50%, var(--text-muted) 60%);
|
|
747
|
+
background-size: 300% 100%;
|
|
748
|
+
background-clip: text;
|
|
749
|
+
-webkit-background-clip: text;
|
|
750
|
+
color: transparent;
|
|
751
|
+
animation: thinking-summary-shimmer 2.8s ease-in-out infinite;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
@keyframes thinking-summary-shimmer {
|
|
755
|
+
from { background-position: 100% 0; }
|
|
756
|
+
to { background-position: 0% 0; }
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
@media (prefers-reduced-motion: reduce) {
|
|
760
|
+
.thinking-live .thinking-label { animation: none; background: none; color: var(--text-muted); }
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.thinking-header[aria-disabled="true"] { cursor: default; }
|
|
764
|
+
.thinking-header:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
765
|
+
.thinking-item .thinking-spinner { display: none; }
|
|
766
|
+
|
|
731
767
|
.thinking-duration {
|
|
768
|
+
flex-shrink: 0;
|
|
732
769
|
color: var(--text-dimmer);
|
|
733
770
|
font-size: 12px;
|
|
734
771
|
}
|
|
@@ -753,7 +790,7 @@ pre.mermaid-error {
|
|
|
753
790
|
font-size: 13px;
|
|
754
791
|
line-height: 1.55;
|
|
755
792
|
color: var(--text-muted);
|
|
756
|
-
white-space:
|
|
793
|
+
white-space: normal;
|
|
757
794
|
word-break: break-word;
|
|
758
795
|
max-height: 300px;
|
|
759
796
|
overflow-y: auto;
|
|
@@ -762,6 +799,8 @@ pre.mermaid-error {
|
|
|
762
799
|
}
|
|
763
800
|
|
|
764
801
|
.thinking-item.expanded .thinking-content { display: block; }
|
|
802
|
+
.thinking-content > :first-child { margin-top: 0; }
|
|
803
|
+
.thinking-content > :last-child { margin-bottom: 0; }
|
|
765
804
|
|
|
766
805
|
/* ==========================================================================
|
|
767
806
|
Tool Items
|
package/lib/public/css/pane.css
CHANGED
|
@@ -104,6 +104,49 @@ body.pane-mode #input-area {
|
|
|
104
104
|
flex-shrink: 0;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
.split-pane-header.has-worker-runtime {
|
|
108
|
+
flex-wrap: nowrap;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.split-pane-header.has-worker-runtime .split-pane-context {
|
|
112
|
+
order: 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.split-pane-header.has-worker-runtime .split-pane-close {
|
|
116
|
+
order: 2;
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.split-worker-runtime {
|
|
121
|
+
flex: 0 1 auto;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 8px;
|
|
126
|
+
font-size: 11px;
|
|
127
|
+
line-height: 18px;
|
|
128
|
+
white-space: nowrap;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.split-worker-runtime-model {
|
|
132
|
+
min-width: 0;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
text-overflow: ellipsis;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
color: var(--text);
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.split-worker-runtime-thinking {
|
|
141
|
+
min-width: 0;
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
text-overflow: ellipsis;
|
|
144
|
+
padding: 0 6px;
|
|
145
|
+
border-radius: 4px;
|
|
146
|
+
background: var(--bg-alt);
|
|
147
|
+
color: var(--text-secondary);
|
|
148
|
+
}
|
|
149
|
+
|
|
107
150
|
.split-pane-title {
|
|
108
151
|
flex: 0 1 auto;
|
|
109
152
|
max-width: 46%;
|
|
@@ -192,8 +235,7 @@ body.pane-mode #input-area {
|
|
|
192
235
|
@keyframes pair-divider-flow-left { 0%, 100% { transform: translateX(3px); opacity: 0.65; } 50% { transform: translateX(-3px); opacity: 1; } }
|
|
193
236
|
@keyframes pair-status-pulse { 50% { opacity: 0.55; } }
|
|
194
237
|
|
|
195
|
-
/* Worker
|
|
196
|
-
delegated turn. Informational only -- the composer stays enabled. */
|
|
238
|
+
/* Worker activity notice shown while this pane processes a delegated task. */
|
|
197
239
|
.pane-delegation-notice {
|
|
198
240
|
display: none;
|
|
199
241
|
align-items: center;
|
|
@@ -202,7 +244,7 @@ body.pane-mode #input-area {
|
|
|
202
244
|
max-width: var(--content-width);
|
|
203
245
|
margin: 0 auto;
|
|
204
246
|
padding: 4px 10px;
|
|
205
|
-
border-radius: 8px
|
|
247
|
+
border-radius: 8px;
|
|
206
248
|
background: color-mix(in srgb, var(--accent) 8%, var(--bg));
|
|
207
249
|
color: var(--text-secondary);
|
|
208
250
|
font-size: 11px;
|
|
@@ -210,13 +252,6 @@ body.pane-mode #input-area {
|
|
|
210
252
|
transition: box-shadow 0.2s;
|
|
211
253
|
}
|
|
212
254
|
.pane-delegation-notice.visible { display: flex; }
|
|
213
|
-
.pane-delegation-notice.visible:has(+ #input-wrapper #input-row:focus-within) {
|
|
214
|
-
box-shadow: 0 0 0 1px var(--border);
|
|
215
|
-
clip-path: inset(-1px -1px 0 -1px);
|
|
216
|
-
}
|
|
217
|
-
.pane-delegation-notice.visible + #input-wrapper #input-row {
|
|
218
|
-
border-radius: 0 0 8px 8px;
|
|
219
|
-
}
|
|
220
255
|
.pane-delegation-notice::before {
|
|
221
256
|
content: "";
|
|
222
257
|
width: 6px;
|
|
@@ -141,9 +141,9 @@
|
|
|
141
141
|
.worker-proposal-action:not(:disabled):hover { filter: brightness(1.12); }
|
|
142
142
|
.worker-proposal-action:disabled, .worker-proposal-select:disabled, .worker-proposal-effort-btn:disabled { cursor: default; opacity: 0.58; }
|
|
143
143
|
|
|
144
|
-
.worker-proposal-error
|
|
144
|
+
.worker-proposal-error { margin-top: 10px; padding: 8px 10px; border-radius: 8px; font-size: 10px; line-height: 1.45; }
|
|
145
145
|
.worker-proposal-error { color: var(--error); background: var(--error-8); }
|
|
146
|
-
.worker-proposal-
|
|
146
|
+
.worker-proposal-error.hidden { display: none; }
|
|
147
147
|
.worker-proposal-card[data-status="starting"] .worker-proposal-status,
|
|
148
148
|
.worker-proposal-card[data-status="running"] .worker-proposal-status { animation: worker-proposal-pulse 1.6s ease-in-out infinite; }
|
|
149
149
|
.worker-proposal-card[data-status="completed"] { border-color: color-mix(in srgb, var(--success) 35%, var(--border)); }
|