clay-server 4.0.0 → 4.1.0-beta.1

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 (52) 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 +71 -61
  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 +73 -0
  13. package/lib/project-session-pair.js +115 -116
  14. package/lib/project-sessions.js +40 -6
  15. package/lib/project-worker-proposal.js +54 -57
  16. package/lib/project.js +27 -4
  17. package/lib/public/app.js +1 -0
  18. package/lib/public/css/command-palette.css +26 -0
  19. package/lib/public/css/icon-strip.css +40 -0
  20. package/lib/public/index.html +2 -2
  21. package/lib/public/modules/app-message-router.js +1 -1
  22. package/lib/public/modules/app-messages.js +9 -0
  23. package/lib/public/modules/project-access.js +141 -0
  24. package/lib/public/modules/project-grouping.js +22 -0
  25. package/lib/public/modules/search-clay-chat.js +47 -6
  26. package/lib/public/modules/search-clay-controls.js +86 -0
  27. package/lib/public/modules/sidebar-projects.js +14 -171
  28. package/lib/public/modules/worker-proposal.js +4 -0
  29. package/lib/public/style.css +1 -1
  30. package/lib/sdk-bridge.js +21 -1
  31. package/lib/server-admin.js +3 -3
  32. package/lib/server-global-ws.js +10 -0
  33. package/lib/server-home-chat-events.js +3 -0
  34. package/lib/server-home-chat.js +35 -0
  35. package/lib/server-mates.js +4 -3
  36. package/lib/server-palette.js +17 -14
  37. package/lib/server.js +184 -102
  38. package/lib/session-pair-factory.js +3 -0
  39. package/lib/session-pair-history.js +40 -0
  40. package/lib/session-pair-mcp-server.js +12 -1
  41. package/lib/session-pair-operation.js +11 -0
  42. package/lib/session-pair-prompts.js +24 -2
  43. package/lib/session-pair-turn-control.js +8 -3
  44. package/lib/worker-proposal-control.js +69 -0
  45. package/lib/worker-proposal-decision.js +7 -0
  46. package/lib/worker-runtime-catalog.js +52 -0
  47. package/lib/workspace-query-access.js +3 -2
  48. package/lib/workspace-query-service.js +1 -1
  49. package/lib/worktree.js +8 -4
  50. package/lib/ws-schema.js +7 -0
  51. package/lib/yoke/adapters/codex.js +6 -2
  52. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ import { escapeHtml } from './utils.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+
4
+ var projectAccessPopover = null;
5
+
6
+ function closeAccessOnOutside(e) {
7
+ if (projectAccessPopover && !projectAccessPopover.contains(e.target)) closeProjectAccessPopover();
8
+ }
9
+
10
+ function closeAccessOnEscape(e) {
11
+ if (e.key === "Escape") closeProjectAccessPopover();
12
+ }
13
+
14
+ export function closeProjectAccessPopover() {
15
+ if (!projectAccessPopover) return;
16
+ projectAccessPopover.remove();
17
+ projectAccessPopover = null;
18
+ document.removeEventListener("click", closeAccessOnOutside);
19
+ document.removeEventListener("keydown", closeAccessOnEscape);
20
+ }
21
+
22
+ function positionPopover(popover, anchorEl) {
23
+ var rect = anchorEl.getBoundingClientRect();
24
+ popover.style.position = "fixed";
25
+ popover.style.left = (rect.right + 8) + "px";
26
+ popover.style.top = rect.top + "px";
27
+ popover.style.zIndex = "9999";
28
+ var popRect = popover.getBoundingClientRect();
29
+ if (popRect.right > window.innerWidth - 8) popover.style.left = (rect.left - popRect.width - 8) + "px";
30
+ if (popRect.bottom > window.innerHeight - 8) popover.style.top = (window.innerHeight - popRect.height - 8) + "px";
31
+ }
32
+
33
+ function selectedExactUsers(popover) {
34
+ var selected = [];
35
+ popover.querySelectorAll('.project-access-user-item input[type="checkbox"]:checked').forEach(function (checkbox) {
36
+ if (!checkbox.disabled || checkbox.dataset.explicit === "true") selected.push(checkbox.dataset.uid);
37
+ });
38
+ return selected;
39
+ }
40
+
41
+ function renderAccessPopover(popover, slug, access, allUsers) {
42
+ var visibility = access.visibility || "public";
43
+ var isWorktree = access.isWorktree === true;
44
+ var allowedUsers = isWorktree ? (access.worktreeAllowedUsers || []) : (access.allowedUsers || []);
45
+ var inheritedUsers = isWorktree ? (access.parentAllowedUsers || []) : [];
46
+ var ownerId = access.ownerId;
47
+ var selectableUsers = allUsers.filter(function (user) { return user.id !== ownerId; });
48
+ var html = '';
49
+ html += '<div class="project-access-header">';
50
+ html += '<span class="project-access-title">' + (isWorktree ? 'Worktree Access' : 'Project Access') + '</span>';
51
+ html += '<button class="project-access-close" aria-label="Close access settings">&times;</button>';
52
+ html += '</div>';
53
+
54
+ if (isWorktree) {
55
+ var scopeTitle = visibility === "public" ? "Public parent project" : "This worktree only";
56
+ var scopeText = visibility === "public"
57
+ ? "Every authenticated user can access this worktree. Exact grants are retained if the parent becomes private."
58
+ : "Entire-project access is inherited automatically.";
59
+ html += '<div class="project-access-scope">';
60
+ html += '<span class="project-access-scope-icon">' + iconHtml(visibility === "public" ? "globe" : "git-branch") + '</span>';
61
+ html += '<span><strong>' + scopeTitle + '</strong><small>' + scopeText + '</small></span>';
62
+ html += '</div>';
63
+ } else {
64
+ html += '<div class="project-access-section">';
65
+ html += '<label class="project-access-label">Visibility</label>';
66
+ html += '<div class="project-access-vis-row">';
67
+ html += '<button class="project-access-vis-btn' + (visibility === "private" ? ' active' : '') + '" data-vis="private">' + iconHtml("lock") + ' Private</button>';
68
+ html += '<button class="project-access-vis-btn' + (visibility === "public" ? ' active' : '') + '" data-vis="public">' + iconHtml("globe") + ' Public</button>';
69
+ html += '</div></div>';
70
+ }
71
+
72
+ html += '<div class="project-access-section project-access-users-section"' + (!isWorktree && visibility !== "private" ? ' style="display:none"' : '') + '>';
73
+ html += '<label class="project-access-label">' + (isWorktree ? 'People with exact access' : 'Allowed Users') + '</label>';
74
+ html += '<div class="project-access-user-list">';
75
+ for (var i = 0; i < selectableUsers.length; i++) {
76
+ var user = selectableUsers[i];
77
+ var explicit = allowedUsers.indexOf(user.id) !== -1;
78
+ var inherited = inheritedUsers.indexOf(user.id) !== -1;
79
+ html += '<label class="project-access-user-item' + (inherited ? ' is-inherited' : '') + '">';
80
+ html += '<input type="checkbox" data-uid="' + user.id + '" data-explicit="' + (explicit ? 'true' : 'false') + '"' + (explicit || inherited ? ' checked' : '') + (inherited ? ' disabled' : '') + '>';
81
+ html += '<span>' + escapeHtml(user.displayName || user.username || user.id) + '</span>';
82
+ if (inherited) html += '<small>Entire project</small>';
83
+ html += '</label>';
84
+ }
85
+ if (selectableUsers.length === 0) html += '<div class="project-access-empty">No other users</div>';
86
+ html += '</div></div>';
87
+ popover.innerHTML = html;
88
+ refreshIcons();
89
+
90
+ popover.querySelector(".project-access-close").addEventListener("click", closeProjectAccessPopover);
91
+ popover.querySelectorAll(".project-access-vis-btn").forEach(function (button) {
92
+ button.addEventListener("click", function () {
93
+ var visibilityValue = button.dataset.vis;
94
+ popover.querySelectorAll(".project-access-vis-btn").forEach(function (item) { item.classList.remove("active"); });
95
+ button.classList.add("active");
96
+ var usersSection = popover.querySelector(".project-access-users-section");
97
+ if (usersSection) usersSection.style.display = visibilityValue === "private" ? "" : "none";
98
+ fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/visibility", {
99
+ method: "PUT",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify({ visibility: visibilityValue }),
102
+ });
103
+ });
104
+ });
105
+ popover.querySelectorAll('.project-access-user-item input[type="checkbox"]').forEach(function (checkbox) {
106
+ checkbox.addEventListener("change", function () {
107
+ fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/users", {
108
+ method: "PUT",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({ allowedUsers: selectedExactUsers(popover) }),
111
+ });
112
+ });
113
+ });
114
+ }
115
+
116
+ export function showProjectAccessPopover(anchorEl, slug) {
117
+ closeProjectAccessPopover();
118
+ var popover = document.createElement("div");
119
+ popover.className = "project-access-popover";
120
+ popover.innerHTML = '<div class="project-access-loading">Loading...</div>';
121
+ popover.addEventListener("click", function (e) { e.stopPropagation(); });
122
+ document.body.appendChild(popover);
123
+ projectAccessPopover = popover;
124
+ requestAnimationFrame(function () { positionPopover(popover, anchorEl); });
125
+ setTimeout(function () {
126
+ document.addEventListener("click", closeAccessOnOutside);
127
+ document.addEventListener("keydown", closeAccessOnEscape);
128
+ }, 0);
129
+ Promise.all([
130
+ fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/access").then(function (response) { return response.json(); }),
131
+ fetch("/api/admin/users").then(function (response) { return response.json(); }),
132
+ ]).then(function (results) {
133
+ if (results[0].error || results[1].error) {
134
+ popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
135
+ return;
136
+ }
137
+ renderAccessPopover(popover, slug, results[0], results[1].users || []);
138
+ }).catch(function () {
139
+ popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
140
+ });
141
+ }
@@ -0,0 +1,22 @@
1
+ // Groups visible parent projects with their visible worktrees. A worktree whose
2
+ // parent is not in the authorized projection remains a standalone item.
3
+
4
+ export function groupProjects(projects) {
5
+ var source = Array.isArray(projects) ? projects : [];
6
+ var parentSlugs = {};
7
+ var parents = [];
8
+ var wtByParent = {};
9
+ for (var i = 0; i < source.length; i++) {
10
+ if (!source[i].isWorktree) parentSlugs[source[i].slug] = true;
11
+ }
12
+ for (var j = 0; j < source.length; j++) {
13
+ var project = source[j];
14
+ if (project.isWorktree && project.parentSlug && parentSlugs[project.parentSlug]) {
15
+ if (!wtByParent[project.parentSlug]) wtByParent[project.parentSlug] = [];
16
+ wtByParent[project.parentSlug].push(project);
17
+ } else {
18
+ parents.push(project);
19
+ }
20
+ }
21
+ return { parents: parents, wtByParent: wtByParent };
22
+ }
@@ -6,8 +6,9 @@ import { refreshIcons } from './icons.js';
6
6
  import { store } from './store.js';
7
7
  import { mateAvatarUrl } from './avatar.js';
8
8
  import { createAssistantBubble, createUserBubble, finalizeAssistantBubble, renderAssistantBubbleText } from './chat-bubble-renderer.js';
9
+ import { isExactSearchControlMessage, replaceSearchPermissions, applySearchPermissionMessage, createSearchPermissionCard } from './search-clay-controls.js';
9
10
 
10
- var state = null;
11
+ var state = store.get('searchClayChatState') || null;
11
12
  var hostEl = null;
12
13
  var backHandler = null;
13
14
  var closeHandler = null;
@@ -213,6 +214,22 @@ function submit(input, sendButton) {
213
214
  render();
214
215
  }
215
216
 
217
+ function stopSearchClayChat() {
218
+ if (!state || !state.processing || state.stopping || !state.mateId || !state.sessionId) return false;
219
+ if (!send({ type: "home_mate_stop", mateId: state.mateId, sessionId: state.sessionId, requestId: state.requestId })) return false;
220
+ state.stopping = true;
221
+ render();
222
+ return true;
223
+ }
224
+
225
+ function respondToPermission(permission, decision) {
226
+ if (!state || permission.status !== "pending") return false;
227
+ if (!send({ type: "home_mate_permission_response", mateId: state.mateId, sessionId: state.sessionId, requestId: state.requestId, permissionRequestId: permission.permissionRequestId, decision: decision })) return false;
228
+ permission.status = "answering";
229
+ render();
230
+ return true;
231
+ }
232
+
216
233
  function expand() {
217
234
  if (!state || !state.mateId || !state.sessionId) return;
218
235
  var mateId = state.mateId;
@@ -260,9 +277,9 @@ function render() {
260
277
  freshSendButton.className = "search-clay-send";
261
278
  freshSendButton.setAttribute("aria-label", "Send to Clay");
262
279
  freshSendButton.innerHTML = '<i data-lucide="arrow-up" aria-hidden="true"></i>';
263
- freshInput.addEventListener("input", function () { state.draft = freshInput.value; freshSendButton.disabled = !state.sessionId || !freshInput.value.trim(); });
280
+ freshInput.addEventListener("input", function () { state.draft = freshInput.value; freshSendButton.disabled = state.processing ? (!state.sessionId || state.stopping) : (!state.sessionId || !freshInput.value.trim()); });
264
281
  freshInput.addEventListener("keydown", function (event) { if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { event.preventDefault(); submit(freshInput, freshSendButton); } });
265
- freshSendButton.addEventListener("click", function () { submit(freshInput, freshSendButton); });
282
+ freshSendButton.addEventListener("click", function () { if (state.processing) stopSearchClayChat(); else submit(freshInput, freshSendButton); });
266
283
  composer.appendChild(freshInput);
267
284
  composer.appendChild(freshSendButton);
268
285
  shell.appendChild(composer);
@@ -271,6 +288,7 @@ function render() {
271
288
  transcript.innerHTML = "";
272
289
  for (var i = 0; i < state.messages.length; i++) transcript.appendChild(renderMessage(state.messages[i], false));
273
290
  if (state.activities.length || state.stream) transcript.appendChild(renderMessage({ role: "assistant", text: state.stream, activities: state.activities, activitiesExpanded: state.activityExpanded, toggleActivities: function (expanded) { state.activityExpanded = expanded; } }, true));
291
+ for (var p = 0; p < state.permissions.length; p++) transcript.appendChild(createSearchPermissionCard(state.permissions[p], respondToPermission));
274
292
  if (state.processing && !state.stream && !state.activities.length) {
275
293
  var progress = progressCopy();
276
294
  var pending = document.createElement("div");
@@ -297,7 +315,9 @@ function render() {
297
315
  var sendButton = shell.querySelector(".search-clay-send");
298
316
  input.placeholder = state.sessionId ? "Message Clay…" : "Starting Clay…";
299
317
  input.disabled = !state.sessionId;
300
- sendButton.disabled = !state.sessionId || !input.value.trim();
318
+ sendButton.disabled = state.processing ? (!state.sessionId || state.stopping) : (!state.sessionId || !input.value.trim());
319
+ sendButton.setAttribute("aria-label", state.processing ? (state.stopping ? "Stopping Clay" : "Stop Clay") : "Send to Clay");
320
+ sendButton.innerHTML = state.processing ? '<i data-lucide="square" aria-hidden="true"></i>' : '<i data-lucide="arrow-up" aria-hidden="true"></i>';
301
321
  var expandButton = shell.querySelector('.search-clay-icon-button[aria-label="Open this conversation in Home"]');
302
322
  if (expandButton) expandButton.disabled = !state.sessionId;
303
323
  refreshIcons();
@@ -311,7 +331,8 @@ export function startSearchClayChat(query, host, onBack, onClose) {
311
331
  backHandler = onBack;
312
332
  closeHandler = onClose;
313
333
  var startedAt = Date.now();
314
- state = { requestId: requestId(), mateId: null, sessionId: null, messages: [{ role: "user", text: clean }], queuedUsers: [], pendingTurns: 1, activities: [], activityExpanded: false, stream: "", draft: "", processing: true, awaitingCompletion: true, activity: "thinking", step: 0, startedAt: startedAt, updatedAt: startedAt };
334
+ state = { requestId: requestId(), mateId: null, sessionId: null, messages: [{ role: "user", text: clean }], queuedUsers: [], pendingTurns: 1, activities: [], permissions: [], activityExpanded: false, stream: "", draft: "", processing: true, stopping: false, awaitingCompletion: true, activity: "thinking", step: 0, startedAt: startedAt, updatedAt: startedAt, query: clean };
335
+ store.set({ searchClayChatState: state });
315
336
  startProgressTimer();
316
337
  render();
317
338
  if (!send({ type: "home_clay_ask", requestId: state.requestId, text: clean })) {
@@ -330,6 +351,7 @@ export function attachSearchClayChat(host, onBack, onClose) {
330
351
  hostEl = host;
331
352
  backHandler = onBack;
332
353
  closeHandler = onClose;
354
+ if (state.processing) send({ type: "home_clay_ask", requestId: state.requestId, text: state.query });
333
355
  render();
334
356
  return true;
335
357
  }
@@ -349,7 +371,7 @@ export function getSearchClayChatSummary() {
349
371
  export function detachSearchClayChat() { hostEl = null; }
350
372
 
351
373
  export function handleSearchClayMessage(msg) {
352
- if (!state || !msg || msg.requestId !== state.requestId) return false;
374
+ if (!isExactSearchControlMessage(state, msg)) return false;
353
375
  state.updatedAt = Date.now();
354
376
  if (msg.mateId) state.mateId = msg.mateId;
355
377
  if (msg.type === "home_mate_session_identity" && msg.sessionId) state.sessionId = msg.sessionId;
@@ -366,6 +388,12 @@ export function handleSearchClayMessage(msg) {
366
388
  state.pendingTurns = state.processing ? Math.max(1, state.pendingTurns) : 0;
367
389
  if (state.processing) startProgressTimer();
368
390
  state.stream = "";
391
+ state.stopping = false;
392
+ replaceSearchPermissions(state, msg.pendingPermissions);
393
+ } else if (msg.type === "home_mate_permission_request" || msg.type === "home_mate_permission_resolved" || msg.type === "home_mate_permission_cancelled") {
394
+ applySearchPermissionMessage(state, msg);
395
+ } else if (msg.type === "home_mate_stopping") {
396
+ state.stopping = true;
369
397
  } else if (msg.type === "home_mate_delta") {
370
398
  if (!state.awaitingCompletion) return true;
371
399
  state.sessionId = msg.sessionId || state.sessionId;
@@ -381,6 +409,7 @@ export function handleSearchClayMessage(msg) {
381
409
  state.activityExpanded = false;
382
410
  state.pendingTurns = Math.max(0, state.pendingTurns - 1);
383
411
  state.processing = state.pendingTurns > 0;
412
+ state.stopping = false;
384
413
  state.awaitingCompletion = state.processing;
385
414
  state.startedAt = Date.now();
386
415
  if (!state.processing) stopProgressTimer();
@@ -393,6 +422,7 @@ export function handleSearchClayMessage(msg) {
393
422
  state.pendingTurns = Math.max(0, state.pendingTurns - 1);
394
423
  state.activityExpanded = false;
395
424
  state.processing = state.pendingTurns > 0;
425
+ state.stopping = false;
396
426
  state.awaitingCompletion = state.processing;
397
427
  state.startedAt = Date.now();
398
428
  if (!state.processing) stopProgressTimer();
@@ -400,3 +430,14 @@ export function handleSearchClayMessage(msg) {
400
430
  render();
401
431
  return true;
402
432
  }
433
+
434
+ store.subscribe(function (appState, previous) {
435
+ if (!state || appState.connected === previous.connected) return;
436
+ if (!appState.connected) {
437
+ replaceSearchPermissions(state, []);
438
+ state.stopping = false;
439
+ render();
440
+ return;
441
+ }
442
+ if (state.processing) send({ type: "home_clay_ask", requestId: state.requestId, text: state.query });
443
+ });
@@ -0,0 +1,86 @@
1
+ function cleanText(value, fallback) {
2
+ if (typeof value !== "string") return fallback || "";
3
+ var clean = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
4
+ return clean || fallback || "";
5
+ }
6
+
7
+ export function isExactSearchControlMessage(state, message) {
8
+ if (!state || !message || message.requestId !== state.requestId) return false;
9
+ if (state.mateId && message.mateId && message.mateId !== state.mateId) return false;
10
+ if (state.sessionId) {
11
+ if (message.type === "home_mate_session_identity") return message.previousSessionId === state.sessionId;
12
+ if (message.sessionId !== state.sessionId) return false;
13
+ }
14
+ return true;
15
+ }
16
+
17
+ export function replaceSearchPermissions(state, permissions) {
18
+ state.permissions = Array.isArray(permissions) ? permissions.filter(function (item) {
19
+ return item && typeof item.permissionRequestId === "string" && item.permissionRequestId;
20
+ }).map(function (item) {
21
+ return { permissionRequestId: item.permissionRequestId, toolName: cleanText(item.toolName, "Tool"), toolInput: item.toolInput || {}, decisionReason: cleanText(item.decisionReason, ""), status: "pending" };
22
+ }) : [];
23
+ }
24
+
25
+ export function applySearchPermissionMessage(state, message) {
26
+ if (!isExactSearchControlMessage(state, message)) return false;
27
+ if (!Array.isArray(state.permissions)) state.permissions = [];
28
+ var permissionRequestId = message.permissionRequestId;
29
+ var index = -1;
30
+ for (var i = 0; i < state.permissions.length; i++) {
31
+ if (state.permissions[i].permissionRequestId === permissionRequestId) index = i;
32
+ }
33
+ if (message.type === "home_mate_permission_request") {
34
+ var permission = { permissionRequestId: permissionRequestId, toolName: cleanText(message.toolName, "Tool"), toolInput: message.toolInput || {}, decisionReason: cleanText(message.decisionReason, ""), status: "pending" };
35
+ if (index === -1) state.permissions.push(permission);
36
+ else state.permissions[index] = permission;
37
+ return true;
38
+ }
39
+ if (index === -1) return true;
40
+ if (message.type === "home_mate_permission_resolved") {
41
+ state.permissions[index].status = message.decision === "allow" ? "allowed" : "denied";
42
+ } else if (message.type === "home_mate_permission_cancelled") {
43
+ state.permissions[index].status = "cancelled";
44
+ }
45
+ return true;
46
+ }
47
+
48
+ function permissionTarget(permission) {
49
+ var input = permission.toolInput && typeof permission.toolInput === "object" ? permission.toolInput : {};
50
+ return cleanText(input.file_path || input.command || input.url || input.query || input.pattern || permission.decisionReason, "Clay needs your approval to continue.");
51
+ }
52
+
53
+ export function createSearchPermissionCard(permission, onDecision) {
54
+ var card = document.createElement("section");
55
+ card.className = "search-clay-permission is-" + permission.status;
56
+ card.setAttribute("role", "group");
57
+ card.setAttribute("aria-label", "Permission request for " + permission.toolName);
58
+ var title = document.createElement("strong");
59
+ title.textContent = "Allow " + permission.toolName + "?";
60
+ card.appendChild(title);
61
+ var target = document.createElement("p");
62
+ target.textContent = permissionTarget(permission);
63
+ card.appendChild(target);
64
+ if (permission.status === "pending" || permission.status === "answering") {
65
+ var actions = document.createElement("div");
66
+ var deny = document.createElement("button");
67
+ deny.type = "button";
68
+ deny.textContent = "Deny";
69
+ deny.disabled = permission.status === "answering";
70
+ deny.addEventListener("click", function () { onDecision(permission, "deny"); });
71
+ var allow = document.createElement("button");
72
+ allow.type = "button";
73
+ allow.className = "is-primary";
74
+ allow.textContent = "Approve";
75
+ allow.disabled = permission.status === "answering";
76
+ allow.addEventListener("click", function () { onDecision(permission, "allow"); });
77
+ actions.appendChild(deny);
78
+ actions.appendChild(allow);
79
+ card.appendChild(actions);
80
+ } else {
81
+ var result = document.createElement("small");
82
+ result.textContent = permission.status === "allowed" ? "Approved" : (permission.status === "denied" ? "Denied" : "Cancelled");
83
+ card.appendChild(result);
84
+ }
85
+ return card;
86
+ }
@@ -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
 
@@ -41,7 +41,7 @@
41
41
  @import url("css/session-search.css");
42
42
  @import url("css/tooltip.css");
43
43
  @import url("css/mates.css?v=20260407");
44
- @import url("css/command-palette.css?v=20260908-log-links2");
44
+ @import url("css/command-palette.css?v=20260910-ask-clay-controls1");
45
45
  @import url("css/mention.css");
46
46
  @import url("css/debate.css");
47
47
  @import url("css/home-debate-planning.css");