clay-server 4.0.1-beta.1 → 4.1.0-beta.2

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.
Files changed (46) hide show
  1. package/lib/daemon-project-access.js +118 -0
  2. package/lib/daemon-projects.js +19 -5
  3. package/lib/daemon.js +19 -18
  4. package/lib/project-logs-access-scope.js +94 -0
  5. package/lib/project-logs-query.js +3 -0
  6. package/lib/project-logs-service.js +86 -84
  7. package/lib/project-logs-store.js +5 -3
  8. package/lib/project-pair-lifecycle.js +140 -102
  9. package/lib/project-pair-message.js +50 -0
  10. package/lib/project-pair-replacement-state.js +72 -0
  11. package/lib/project-pair-task-control.js +264 -0
  12. package/lib/project-pair-usage.js +76 -0
  13. package/lib/project-session-pair.js +115 -116
  14. package/lib/project-sessions.js +25 -1
  15. package/lib/project-worker-proposal.js +69 -58
  16. package/lib/project.js +21 -4
  17. package/lib/public/css/icon-strip.css +40 -0
  18. package/lib/public/modules/app-messages.js +9 -0
  19. package/lib/public/modules/project-access.js +141 -0
  20. package/lib/public/modules/project-grouping.js +22 -0
  21. package/lib/public/modules/sidebar-projects.js +14 -171
  22. package/lib/public/modules/worker-proposal.js +4 -0
  23. package/lib/sdk-bridge.js +20 -0
  24. package/lib/server-admin.js +3 -3
  25. package/lib/server-global-ws.js +10 -0
  26. package/lib/server-mates.js +4 -3
  27. package/lib/server-palette.js +17 -14
  28. package/lib/server.js +184 -102
  29. package/lib/session-pair-factory.js +3 -0
  30. package/lib/session-pair-history.js +40 -0
  31. package/lib/session-pair-mcp-server.js +19 -2
  32. package/lib/session-pair-operation.js +11 -0
  33. package/lib/session-pair-prompts.js +24 -2
  34. package/lib/session-pair-turn-control.js +8 -3
  35. package/lib/session-provenance.js +1 -0
  36. package/lib/session-spawn-mcp-server.js +2 -1
  37. package/lib/sessions.js +2 -0
  38. package/lib/worker-proposal-control.js +69 -0
  39. package/lib/worker-proposal-decision.js +7 -0
  40. package/lib/worker-runtime-catalog.js +52 -0
  41. package/lib/workspace-query-access.js +3 -2
  42. package/lib/workspace-query-service.js +1 -1
  43. package/lib/worktree.js +8 -4
  44. package/lib/ws-schema.js +1 -0
  45. package/lib/yoke/adapters/codex.js +25 -7
  46. package/package.json +1 -1
@@ -14,6 +14,8 @@ import { switchProject, openAddProjectModal, getCachedProjects } from './app-pro
14
14
  import { showHomeHub } from './app-home-hub.js';
15
15
  import { getPendingTuiAttention } from './app-notifications.js';
16
16
  import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
17
+ import { groupProjects } from './project-grouping.js';
18
+ import { showProjectAccessPopover } from './project-access.js';
17
19
 
18
20
  // --- Project state ---
19
21
  var cachedProjectList = [];
@@ -22,9 +24,6 @@ var cachedCurrentSlug = null;
22
24
  // --- Project context menu ---
23
25
  var projectCtxMenu = null;
24
26
 
25
- // --- Project Access Popover ---
26
- var projectAccessPopover = null;
27
-
28
27
  // --- Emoji picker ---
29
28
  var emojiPickerEl = null;
30
29
 
@@ -338,149 +337,6 @@ function getProjectAbbrev(name) {
338
337
 
339
338
  export { getProjectAbbrev };
340
339
 
341
- // --- Project Access Popover ---
342
-
343
- function closeAccessOnOutside(e) {
344
- if (projectAccessPopover && !projectAccessPopover.contains(e.target)) closeProjectAccessPopover();
345
- }
346
- function closeAccessOnEscape(e) {
347
- if (e.key === "Escape") closeProjectAccessPopover();
348
- }
349
-
350
- function closeProjectAccessPopover() {
351
- if (projectAccessPopover) {
352
- projectAccessPopover.remove();
353
- projectAccessPopover = null;
354
- document.removeEventListener("click", closeAccessOnOutside);
355
- document.removeEventListener("keydown", closeAccessOnEscape);
356
- }
357
- }
358
-
359
- function showProjectAccessPopover(anchorEl, slug) {
360
- closeProjectAccessPopover();
361
-
362
- var popover = document.createElement("div");
363
- popover.className = "project-access-popover";
364
- popover.innerHTML = '<div class="project-access-loading">Loading...</div>';
365
- popover.addEventListener("click", function (e) { e.stopPropagation(); });
366
- document.body.appendChild(popover);
367
- projectAccessPopover = popover;
368
-
369
- requestAnimationFrame(function () {
370
- var rect = anchorEl.getBoundingClientRect();
371
- popover.style.position = "fixed";
372
- popover.style.left = (rect.right + 8) + "px";
373
- popover.style.top = rect.top + "px";
374
- popover.style.zIndex = "9999";
375
- var popRect = popover.getBoundingClientRect();
376
- if (popRect.right > window.innerWidth - 8) {
377
- popover.style.left = (rect.left - popRect.width - 8) + "px";
378
- }
379
- if (popRect.bottom > window.innerHeight - 8) {
380
- popover.style.top = (window.innerHeight - popRect.height - 8) + "px";
381
- }
382
- });
383
-
384
- setTimeout(function () {
385
- document.addEventListener("click", closeAccessOnOutside);
386
- document.addEventListener("keydown", closeAccessOnEscape);
387
- }, 0);
388
-
389
- Promise.all([
390
- fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/access").then(function (r) { return r.json(); }),
391
- fetch("/api/admin/users").then(function (r) { return r.json(); }),
392
- ]).then(function (results) {
393
- var access = results[0];
394
- var usersData = results[1];
395
- if (access.error || usersData.error) {
396
- popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
397
- return;
398
- }
399
- renderAccessPopover(popover, slug, access, usersData.users || []);
400
- }).catch(function () {
401
- popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
402
- });
403
- }
404
-
405
- function renderAccessPopover(popover, slug, access, allUsers) {
406
- var visibility = access.visibility || "public";
407
- var allowedUsers = access.allowedUsers || [];
408
- var ownerId = access.ownerId;
409
-
410
- var selectableUsers = allUsers.filter(function (u) { return u.id !== ownerId; });
411
-
412
- var html = '';
413
- html += '<div class="project-access-header">';
414
- html += '<span class="project-access-title">Project Access</span>';
415
- html += '<button class="project-access-close">&times;</button>';
416
- html += '</div>';
417
-
418
- html += '<div class="project-access-section">';
419
- html += '<label class="project-access-label">Visibility</label>';
420
- html += '<div class="project-access-vis-row">';
421
- html += '<button class="project-access-vis-btn' + (visibility === "private" ? ' active' : '') + '" data-vis="private">';
422
- html += iconHtml("lock") + ' Private';
423
- html += '</button>';
424
- html += '<button class="project-access-vis-btn' + (visibility === "public" ? ' active' : '') + '" data-vis="public">';
425
- html += iconHtml("globe") + ' Public';
426
- html += '</button>';
427
- html += '</div>';
428
- html += '</div>';
429
-
430
- html += '<div class="project-access-section project-access-users-section"' + (visibility !== "private" ? ' style="display:none"' : '') + '>';
431
- html += '<label class="project-access-label">Allowed Users</label>';
432
- html += '<div class="project-access-user-list">';
433
- for (var i = 0; i < selectableUsers.length; i++) {
434
- var u = selectableUsers[i];
435
- var checked = allowedUsers.indexOf(u.id) !== -1 ? " checked" : "";
436
- html += '<label class="project-access-user-item">';
437
- html += '<input type="checkbox" data-uid="' + u.id + '"' + checked + '>';
438
- html += '<span>' + escapeHtml(u.displayName || u.username || u.id) + '</span>';
439
- html += '</label>';
440
- }
441
- if (selectableUsers.length === 0) {
442
- html += '<div class="project-access-empty">No other users</div>';
443
- }
444
- html += '</div>';
445
- html += '</div>';
446
-
447
- popover.innerHTML = html;
448
- refreshIcons();
449
-
450
- popover.querySelector(".project-access-close").addEventListener("click", function () {
451
- closeProjectAccessPopover();
452
- });
453
-
454
- popover.querySelectorAll(".project-access-vis-btn").forEach(function (btn) {
455
- btn.addEventListener("click", function () {
456
- var newVis = btn.dataset.vis;
457
- popover.querySelectorAll(".project-access-vis-btn").forEach(function (b) { b.classList.remove("active"); });
458
- btn.classList.add("active");
459
- var usersSection = popover.querySelector(".project-access-users-section");
460
- if (usersSection) usersSection.style.display = newVis === "private" ? "" : "none";
461
- fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/visibility", {
462
- method: "PUT",
463
- headers: { "Content-Type": "application/json" },
464
- body: JSON.stringify({ visibility: newVis }),
465
- });
466
- });
467
- });
468
-
469
- popover.querySelectorAll('.project-access-user-item input[type="checkbox"]').forEach(function (cb) {
470
- cb.addEventListener("change", function () {
471
- var selected = [];
472
- popover.querySelectorAll('.project-access-user-item input[type="checkbox"]:checked').forEach(function (c) {
473
- selected.push(c.dataset.uid);
474
- });
475
- fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/users", {
476
- method: "PUT",
477
- headers: { "Content-Type": "application/json" },
478
- body: JSON.stringify({ allowedUsers: selected }),
479
- });
480
- });
481
- });
482
- }
483
-
484
340
  // --- Project context menu ---
485
341
 
486
342
  export function closeProjectCtxMenu() {
@@ -603,7 +459,7 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
603
459
  menu.appendChild(shareItem);
604
460
 
605
461
  // --- Manage Access ---
606
- if (store.get('isMultiUserMode') && slug.indexOf("--") === -1) {
462
+ if (store.get('isMultiUserMode')) {
607
463
  var isProjectOwner = store.get('myUserId') && store.get('currentProjectOwnerId') && store.get('myUserId') === store.get('currentProjectOwnerId');
608
464
  var isAdmin = store.get('permissions') && store.get('permissions').projectSettings !== false;
609
465
  if (isProjectOwner || isAdmin) {
@@ -624,15 +480,17 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
624
480
  menu.appendChild(sep2);
625
481
 
626
482
  // --- Add Worktree ---
627
- var wtItem = document.createElement("button");
628
- wtItem.className = "project-ctx-item";
629
- wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
630
- wtItem.addEventListener("click", function (e) {
631
- e.stopPropagation();
632
- closeProjectCtxMenu();
633
- showWorktreeModal(slug, name || slug);
634
- });
635
- menu.appendChild(wtItem);
483
+ if (slug.indexOf("--") === -1) {
484
+ var wtItem = document.createElement("button");
485
+ wtItem.className = "project-ctx-item";
486
+ wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
487
+ wtItem.addEventListener("click", function (e) {
488
+ e.stopPropagation();
489
+ closeProjectCtxMenu();
490
+ showWorktreeModal(slug, name || slug);
491
+ });
492
+ menu.appendChild(wtItem);
493
+ }
636
494
 
637
495
  if (!store.get('permissions') || store.get('permissions').deleteProject !== false) {
638
496
  var sep3 = document.createElement("div");
@@ -959,21 +817,6 @@ function setWtCollapsed(slug, collapsed) {
959
817
  wtCollapsed[slug] = collapsed;
960
818
  }
961
819
 
962
- function groupProjects(projects) {
963
- var parents = [];
964
- var wtByParent = {};
965
- for (var i = 0; i < projects.length; i++) {
966
- var p = projects[i];
967
- if (p.isWorktree && p.parentSlug) {
968
- if (!wtByParent[p.parentSlug]) wtByParent[p.parentSlug] = [];
969
- wtByParent[p.parentSlug].push(p);
970
- } else {
971
- parents.push(p);
972
- }
973
- }
974
- return { parents: parents, wtByParent: wtByParent };
975
- }
976
-
977
820
  // --- Icon item creation ---
978
821
 
979
822
  function createIconItem(p, currentSlug) {
@@ -98,6 +98,8 @@ function statusLabel(status) {
98
98
  if (status === "completed") return "Completed";
99
99
  if (status === "interrupted") return "Interrupted";
100
100
  if (status === "declined") return "Continuing here";
101
+ if (status === "cancelled") return "Cancelled";
102
+ if (status === "superseded") return "Superseded";
101
103
  if (status === "error") return "Needs attention";
102
104
  return "Awaiting your choice";
103
105
  }
@@ -106,6 +108,8 @@ function decisionLabel(status, autoAccepted) {
106
108
  if (autoAccepted) return "Driver recommendation auto-accepted under Full auto";
107
109
  if (status === "pending") return "Runtime choice remains with you";
108
110
  if (status === "declined") return "Runtime proposal declined by user";
111
+ if (status === "cancelled") return "Runtime proposal cancelled by Driver";
112
+ if (status === "superseded") return "Replaced by a newer runtime proposal";
109
113
  return "Runtime configuration accepted by user";
110
114
  }
111
115
 
package/lib/sdk-bridge.js CHANGED
@@ -627,8 +627,18 @@ function createSDKBridge(opts) {
627
627
  var PAIR_MCP_PREFIX = "mcp__clay-sessions__";
628
628
  var pairMcpTools = {
629
629
  propose_worker: true,
630
+ inspect_worker_proposal: true,
631
+ cancel_worker_proposal: true,
632
+ worker_runtime_catalog: true,
630
633
  send_to_partner: true,
631
634
  read_partner: true,
635
+ message_partner: true,
636
+ queue_partner_followup: true,
637
+ inspect_partner_followups: true,
638
+ cancel_partner_followup: true,
639
+ replace_partner_task: true,
640
+ resume_partner_task: true,
641
+ report_partner_outcome: true,
632
642
  partner_status: true,
633
643
  replace_partner: true,
634
644
  interrupt_partner: true,
@@ -638,6 +648,16 @@ function createSDKBridge(opts) {
638
648
  };
639
649
  var pairDynamicTools = {
640
650
  propose_worker: true,
651
+ inspect_worker_proposal: true,
652
+ cancel_worker_proposal: true,
653
+ worker_runtime_catalog: true,
654
+ message_partner: true,
655
+ queue_partner_followup: true,
656
+ inspect_partner_followups: true,
657
+ cancel_partner_followup: true,
658
+ replace_partner_task: true,
659
+ resume_partner_task: true,
660
+ report_partner_outcome: true,
641
661
  partner_status: true,
642
662
  replace_partner: true,
643
663
  interrupt_partner: true,
@@ -632,9 +632,9 @@ function attachAdmin(ctx) {
632
632
  return true;
633
633
  }
634
634
  var projSlug = fullUrl.split("/")[4];
635
- if (projSlug.indexOf("--") !== -1) {
636
- res.writeHead(400, { "Content-Type": "application/json" });
637
- res.end('{"error":"Worktree projects inherit parent settings"}');
635
+ if (!projects.get(projSlug)) {
636
+ res.writeHead(404, { "Content-Type": "application/json" });
637
+ res.end('{"error":"Project not found"}');
638
638
  return true;
639
639
  }
640
640
  var body = "";
@@ -40,6 +40,8 @@ function attachGlobalWs(opts) {
40
40
  var onCloneProject = opts.onCloneProject;
41
41
  var onHomePreferenceMessage = opts.onHomePreferenceMessage;
42
42
  var onAppMessage = opts.onAppMessage;
43
+ var onConnection = opts.onConnection;
44
+ var clients = new Set();
43
45
 
44
46
  function handleMessage(ws, msg) {
45
47
  if (!msg || typeof msg !== "object") return;
@@ -184,16 +186,24 @@ function attachGlobalWs(opts) {
184
186
 
185
187
  function handleConnection(ws, wsUser) {
186
188
  ws._clayUser = wsUser || null;
189
+ clients.add(ws);
190
+ ws.on("close", function () { clients.delete(ws); });
187
191
  ws.on("message", function (data) {
188
192
  var msg;
189
193
  try { msg = JSON.parse(data); } catch (e) { return; }
190
194
  handleMessage(ws, msg);
191
195
  });
196
+ if (typeof onConnection === "function") onConnection(ws);
197
+ }
198
+
199
+ function forEachClient(visitor) {
200
+ clients.forEach(function (ws) { visitor(ws); });
192
201
  }
193
202
 
194
203
  return {
195
204
  handleConnection: handleConnection,
196
205
  handleMessage: handleMessage,
206
+ forEachClient: forEachClient,
197
207
  };
198
208
  }
199
209
 
@@ -20,11 +20,12 @@ function attachMates(ctx) {
20
20
  var projList = [];
21
21
  projects.forEach(function (pCtx, pSlug) {
22
22
  var st = pCtx.getStatus();
23
- if (st.isMate || st.isWorktree) return;
23
+ if (st.isMate) return;
24
24
  // Filter by user access in multi-user mode
25
- if (userId && users.isMultiUser() && onGetProjectAccess) {
25
+ if (users.isMultiUser() && onGetProjectAccess) {
26
+ if (!userId) return;
26
27
  var access = onGetProjectAccess(pSlug);
27
- if (access && !access.error && !users.canAccessProject(userId, access)) return;
28
+ if (!access || access.error || !users.canAccessProject(userId, access)) return;
28
29
  }
29
30
  projList.push(st);
30
31
  });
@@ -5,6 +5,17 @@ function attachPalette(ctx) {
5
5
  var projects = ctx.projects;
6
6
  var getMultiUserFromReq = ctx.getMultiUserFromReq;
7
7
  var onGetProjectAccess = ctx.onGetProjectAccess;
8
+ var canUserAccessSlug = ctx.canUserAccessSlug;
9
+
10
+ function accessFor(user, slug, status) {
11
+ if (!user) return { visibility: "public" };
12
+ if (typeof canUserAccessSlug !== "function" || !canUserAccessSlug(user.id, slug)) return null;
13
+ if (status && status.isMate) {
14
+ return { visibility: "private", ownerId: status.projectOwnerId || null, allowedUsers: [] };
15
+ }
16
+ var access = onGetProjectAccess ? onGetProjectAccess(slug) : null;
17
+ return access && !access.error ? access : null;
18
+ }
8
19
 
9
20
  function handleRequest(req, res, fullUrl) {
10
21
  if (req.method !== "GET" || fullUrl !== "/api/palette/search") return false;
@@ -26,17 +37,13 @@ function attachPalette(ctx) {
26
37
  // Recent mode: return all sessions sorted by lastActivity
27
38
  projects.forEach(function (pCtx, pSlug) {
28
39
  var status = pCtx.getStatus();
29
- if (status.isWorktree) return;
30
- if (paletteUser && onGetProjectAccess) {
31
- var pAccess = onGetProjectAccess(pSlug);
32
- if (pAccess && !pAccess.error && !users.canAccessProject(paletteUser.id, pAccess)) return;
33
- }
40
+ var pAccess = accessFor(paletteUser, pSlug, status);
41
+ if (paletteUser && !pAccess) return;
34
42
  pCtx.sm.sessions.forEach(function (session) {
35
43
  if (session.hidden) return;
36
44
  if (paletteUser) {
37
45
  if (users.isMultiUser()) {
38
- var sAccess = onGetProjectAccess ? onGetProjectAccess(pSlug) : null;
39
- if (!users.canAccessSession(paletteUser.id, session, sAccess)) return;
46
+ if (!users.canAccessSession(paletteUser.id, session, pAccess)) return;
40
47
  }
41
48
  } else {
42
49
  if (session.ownerId) return;
@@ -65,18 +72,14 @@ function attachPalette(ctx) {
65
72
  var projectSessions = [];
66
73
  projects.forEach(function (pCtx, pSlug) {
67
74
  var status = pCtx.getStatus();
68
- if (status.isWorktree) return;
69
- if (paletteUser && onGetProjectAccess) {
70
- var pAccess = onGetProjectAccess(pSlug);
71
- if (pAccess && !pAccess.error && !users.canAccessProject(paletteUser.id, pAccess)) return;
72
- }
75
+ var pAccess = accessFor(paletteUser, pSlug, status);
76
+ if (paletteUser && !pAccess) return;
73
77
  var accessibleSessions = [];
74
78
  pCtx.sm.sessions.forEach(function (session) {
75
79
  if (session.hidden) return;
76
80
  if (paletteUser) {
77
81
  if (users.isMultiUser()) {
78
- var sAccess = onGetProjectAccess ? onGetProjectAccess(pSlug) : null;
79
- if (!users.canAccessSession(paletteUser.id, session, sAccess)) return;
82
+ if (!users.canAccessSession(paletteUser.id, session, pAccess)) return;
80
83
  }
81
84
  } else {
82
85
  if (session.ownerId) return;