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.
- package/lib/daemon-project-access.js +118 -0
- package/lib/daemon-projects.js +19 -5
- package/lib/daemon.js +19 -18
- package/lib/project-logs-access-scope.js +94 -0
- package/lib/project-logs-query.js +3 -0
- package/lib/project-logs-service.js +86 -84
- package/lib/project-logs-store.js +5 -3
- package/lib/project-pair-lifecycle.js +71 -61
- package/lib/project-pair-message.js +50 -0
- package/lib/project-pair-replacement-state.js +72 -0
- package/lib/project-pair-task-control.js +264 -0
- package/lib/project-pair-usage.js +73 -0
- package/lib/project-session-pair.js +115 -116
- package/lib/project-sessions.js +40 -6
- package/lib/project-worker-proposal.js +54 -57
- package/lib/project.js +27 -4
- package/lib/public/app.js +1 -0
- package/lib/public/css/command-palette.css +26 -0
- package/lib/public/css/icon-strip.css +40 -0
- package/lib/public/index.html +2 -2
- package/lib/public/modules/app-message-router.js +1 -1
- package/lib/public/modules/app-messages.js +9 -0
- package/lib/public/modules/project-access.js +141 -0
- package/lib/public/modules/project-grouping.js +22 -0
- package/lib/public/modules/search-clay-chat.js +47 -6
- package/lib/public/modules/search-clay-controls.js +86 -0
- package/lib/public/modules/sidebar-projects.js +14 -171
- package/lib/public/modules/worker-proposal.js +4 -0
- package/lib/public/style.css +1 -1
- package/lib/sdk-bridge.js +21 -1
- package/lib/server-admin.js +3 -3
- package/lib/server-global-ws.js +10 -0
- package/lib/server-home-chat-events.js +3 -0
- package/lib/server-home-chat.js +35 -0
- package/lib/server-mates.js +4 -3
- package/lib/server-palette.js +17 -14
- package/lib/server.js +184 -102
- package/lib/session-pair-factory.js +3 -0
- package/lib/session-pair-history.js +40 -0
- package/lib/session-pair-mcp-server.js +12 -1
- package/lib/session-pair-operation.js +11 -0
- package/lib/session-pair-prompts.js +24 -2
- package/lib/session-pair-turn-control.js +8 -3
- package/lib/worker-proposal-control.js +69 -0
- package/lib/worker-proposal-decision.js +7 -0
- package/lib/worker-runtime-catalog.js +52 -0
- package/lib/workspace-query-access.js +3 -2
- package/lib/workspace-query-service.js +1 -1
- package/lib/worktree.js +8 -4
- package/lib/ws-schema.js +7 -0
- package/lib/yoke/adapters/codex.js +6 -2
- 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">×</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
|
|
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">×</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')
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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/public/style.css
CHANGED
|
@@ -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=
|
|
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");
|