clay-server 4.0.0 → 4.0.1-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.
@@ -87,6 +87,13 @@ function attachSessions(ctx) {
87
87
  var getLinuxUserForSession = ctx.getLinuxUserForSession;
88
88
  var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
89
89
  var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
90
+ function stopSession(session) {
91
+ if (!session || (!session.isProcessing && !session._queryStarting)) return false;
92
+ if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
93
+ session.taskStopRequested = true;
94
+ if (session.abortController) session.abortController.abort();
95
+ return true;
96
+ }
90
97
  var hydrateImageRefs = ctx.hydrateImageRefs;
91
98
  var onProcessingChanged = ctx.onProcessingChanged;
92
99
  var broadcastPresence = ctx.broadcastPresence;
@@ -1004,11 +1011,7 @@ function attachSessions(ctx) {
1004
1011
 
1005
1012
  if (msg.type === "stop") {
1006
1013
  var session = getSessionForWs(ws);
1007
- if (session && (session.isProcessing || session._queryStarting)) {
1008
- if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
1009
- session.taskStopRequested = true;
1010
- if (session.abortController) session.abortController.abort();
1011
- }
1014
+ stopSession(session);
1012
1015
  return true;
1013
1016
  }
1014
1017
 
@@ -2071,6 +2074,13 @@ function attachSessions(ctx) {
2071
2074
  handleSessionsMessage: handleSessionsMessage,
2072
2075
  resolveSessionForView: resolveSessionForView,
2073
2076
  respondToAskUser: respondToAskUser,
2077
+ respondToHomePermission: function (ws, msg, session) {
2078
+ if (!session || !msg || !session.pendingPermissions || !session.pendingPermissions[msg.requestId]) return false;
2079
+ var indexedSessionId = sm.permissionRequestIndex[msg.requestId];
2080
+ if (indexedSessionId !== session.localId || sm.sessions.get(indexedSessionId) !== session) return false;
2081
+ return handleSessionsMessage(ws, { type: "permission_response", requestId: msg.requestId, decision: msg.decision });
2082
+ },
2083
+ stopHomeSession: stopSession,
2074
2084
  };
2075
2085
  }
2076
2086
 
package/lib/project.js CHANGED
@@ -1331,6 +1331,10 @@ function createProjectContext(opts) {
1331
1331
  if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
1332
1332
  return;
1333
1333
  }
1334
+ if (msg.type === "home_mate_stop" || msg.type === "home_mate_permission_response") {
1335
+ if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
1336
+ return;
1337
+ }
1334
1338
  if (msg.type === "project_assignment_response" || msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite" || msg.type === "mate_create" || msg.type === "mate_list" || msg.type === "mate_delete" || msg.type === "mate_update" || msg.type === "mate_readd_builtin" || msg.type === "mate_list_available_builtins" || msg.type === "email_accounts_list" || msg.type === "email_account_add" || msg.type === "email_account_remove" || msg.type === "email_account_test" || msg.type === "home_debates_list" || msg.type === "home_mate_present" || msg.type === "home_mate_open" || msg.type === "home_mate_sessions_list" || msg.type === "home_mate_session_open" || msg.type === "home_mate_send" || msg.type === "home_mate_new_session" || msg.type === "home_mate_debate_plan" || msg.type === "home_debate_proposal_response" || msg.type === "home_mate_creation_plan" || msg.type === "home_mate_creation_proposal_response" || msg.type === "home_mate_close" || msg.type === "home_mate_memory_list" || msg.type === "home_mate_knowledge_list" || msg.type === "home_mate_models_get" || msg.type === "home_mate_model_set" || msg.type === "home_dock_get" || msg.type === "home_dock_set" || msg.type === "home_surface_get" || msg.type === "home_surface_set" || msg.type === "tools_list" || msg.type === "tool_get" || msg.type === "tool_install" || msg.type === "tool_remove" || msg.type === "tool_storage_op" || msg.type === "tool_llm_op" || msg.type === "tool_llm_config_get" || msg.type === "tool_control_response" || msg.type === "tool_source_get" || msg.type === "tool_mate_access_set" || msg.type === "tool_server_control" || msg.type === "tool_frame_url") {
1335
1339
  if (typeof opts.onDmMessage === "function") {
1336
1340
  opts.onDmMessage(ws, msg, slug);
@@ -2314,6 +2318,8 @@ function createProjectContext(opts) {
2314
2318
  handleHomeDebateProposalResponse: function (ws, msg, session) { return _debateProposal.handleHomeMessage(ws, msg, session); },
2315
2319
  handleHomeMateCreationProposalResponse: function (ws, msg, session) { return _mateCreationProposal.handleHomeMessage(ws, msg, session); },
2316
2320
  handleHomeAskUserResponse: function (msg, session) { return _sessions.respondToAskUser(session, msg); },
2321
+ handleHomePermissionResponse: function (ws, msg, session) { return _sessions.respondToHomePermission(ws, msg, session); },
2322
+ stopHomeSession: function (session) { return _sessions.stopHomeSession(session); },
2317
2323
  handleHomeDebateControl: function (ws, msg, session) { return _debate.handleHomeControl(ws, msg, session); },
2318
2324
  getVendorModelCatalog: _models.getVendorCatalog,
2319
2325
  getVendorModelAvailability: _models.getVendorAvailability,
package/lib/public/app.js CHANGED
@@ -294,6 +294,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
294
294
  savedMainSlug: null,
295
295
  connected: false,
296
296
  pendingOutboundMessages: [],
297
+ searchClayChatState: null,
297
298
  homeChatMateId: null,
298
299
  homeChatSessionId: null,
299
300
  homeChatSessionModel: null,
@@ -392,6 +392,32 @@
392
392
  gap: 12px;
393
393
  }
394
394
 
395
+ .search-clay-permission {
396
+ margin: 0 6px;
397
+ padding: 12px;
398
+ border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border-subtle));
399
+ border-radius: 10px;
400
+ background: color-mix(in srgb, var(--accent) 6%, var(--bg-secondary));
401
+ }
402
+
403
+ .search-clay-permission strong { display: block; color: var(--text); font-size: 13px; }
404
+ .search-clay-permission p { margin: 5px 0 10px; color: var(--text-muted); font-size: 11px; line-height: 1.45; overflow-wrap: anywhere; }
405
+ .search-clay-permission > div { display: flex; justify-content: flex-end; gap: 7px; }
406
+ .search-clay-permission button {
407
+ min-height: 30px;
408
+ padding: 0 10px;
409
+ border: 1px solid var(--border);
410
+ border-radius: 7px;
411
+ background: var(--bg);
412
+ color: var(--text);
413
+ font: inherit;
414
+ font-size: 11px;
415
+ cursor: pointer;
416
+ }
417
+ .search-clay-permission button.is-primary { border-color: var(--accent); background: var(--accent); color: white; }
418
+ .search-clay-permission button:disabled { opacity: 0.55; cursor: default; }
419
+ .search-clay-permission small { display: block; margin-top: 7px; color: var(--text-muted); font-size: 11px; }
420
+
395
421
  .search-clay-message { width: 100%; min-width: 0; }
396
422
  .search-clay-transcript .search-clay-message-user {
397
423
  display: flex;
@@ -31,7 +31,7 @@
31
31
  (function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
32
32
  </script>
33
33
  <script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
34
- <link rel="stylesheet" href="style.css?v=20260908-log-links2">
34
+ <link rel="stylesheet" href="style.css?v=20260910-ask-clay-controls1">
35
35
  <style>
36
36
  @media(max-width:768px){
37
37
  /* User messages: vertical stack, avatar on top, right-aligned */
@@ -2337,7 +2337,7 @@
2337
2337
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2338
2338
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2339
2339
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2340
- <script type="module" src="app.js?v=20260908-log-links2"></script>
2340
+ <script type="module" src="app.js?v=20260910-ask-clay-controls1"></script>
2341
2341
  <div id="pwa-install-modal" class="pwa-modal hidden">
2342
2342
  <div class="pwa-modal-backdrop"></div>
2343
2343
  <div class="pwa-modal-card">
@@ -78,7 +78,7 @@ export function handleHomeProtocolMessage(msg) {
78
78
 
79
79
  export function processMessage(msg) {
80
80
  var searchHandled = handleSearchClayMessage(msg);
81
- if (searchHandled && msg.type === "home_clay_activity") return;
81
+ if (searchHandled && (msg.type === "home_clay_activity" || msg.type === "home_mate_stopping" || msg.type.indexOf("home_mate_permission_") === 0)) return;
82
82
  if (handleHomeProtocolMessage(msg)) return;
83
83
  processAppMessage(msg);
84
84
  }
@@ -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
+ }
@@ -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");
package/lib/sdk-bridge.js CHANGED
@@ -866,7 +866,7 @@ function createSDKBridge(opts) {
866
866
  }
867
867
 
868
868
  var _nm = getNotificationsModule();
869
- if (_nm) {
869
+ if (_nm && session.homeClayEntryMode !== "search") {
870
870
  _nm.notify("permission_request", {
871
871
  title: permissionPushTitle(toolName, input),
872
872
  body: permissionPushBody(toolName, input),
@@ -267,6 +267,9 @@ function transformEvent(event, mateId, session, requestId, stableSessionId) {
267
267
  }
268
268
  if (event.type === "tool_executing") return Object.assign({ type: "home_clay_activity", activityId: session._homeClayToolActivityId || "tool:" + (session._homeClayActivityStep || 1), phase: "searching", status: "active", label: searchToolLabel(event.name, event.input), step: session._homeClayActivityStep || 1 }, metadata);
269
269
  if (event.type === "tool_result") return Object.assign({ type: "home_clay_activity", activityId: session._homeClayToolActivityId || "tool:" + (session._homeClayActivityStep || 1), phase: event.is_error ? "reconsidering" : "reviewing", status: event.is_error ? "error" : "done", label: event.is_error ? "That lead did not work; trying another route" : "Reviewed the search results", step: session._homeClayActivityStep || 1 }, metadata);
270
+ if (event.type === "permission_request") return Object.assign({ type: "home_mate_permission_request", permissionRequestId: event.requestId, toolName: event.toolName, toolInput: event.toolInput, decisionReason: event.decisionReason || "" }, metadata);
271
+ if (event.type === "permission_resolved") return Object.assign({ type: "home_mate_permission_resolved", permissionRequestId: event.requestId, decision: event.decision || "deny" }, metadata);
272
+ if (event.type === "permission_cancel") return Object.assign({ type: "home_mate_permission_cancelled", permissionRequestId: event.requestId }, metadata);
270
273
  }
271
274
  if (session && session.homeDebatePlanning === true && HOME_DEBATE_EVENT_TYPES[event.type]) {
272
275
  var live = { type: "home_debate_event", eventType: event.type };
@@ -224,6 +224,12 @@ function attachHomeChat(deps) {
224
224
  messages: historyToHomeChat(session.history || [], session.debateSetupMode === true, session.mateCreationMode === true),
225
225
  isProcessing: session.isProcessing === true,
226
226
  };
227
+ if (session.homeClayEntryMode === "search") {
228
+ payload.pendingPermissions = Object.keys(session.pendingPermissions || {}).map(function (permissionRequestId) {
229
+ var pending = session.pendingPermissions[permissionRequestId];
230
+ return { permissionRequestId: permissionRequestId, toolName: pending.toolName, toolInput: pending.toolInput, decisionReason: pending.decisionReason || "" };
231
+ });
232
+ }
227
233
  if (session.debateSetupMode === true) payload.debatePlanning = true;
228
234
  if (session.homeDebatePhase) payload.debatePhase = session.homeDebatePhase;
229
235
  if (session.mateCreationMode === true) payload.mateCreation = true;
@@ -388,6 +394,35 @@ function attachHomeChat(deps) {
388
394
 
389
395
  function handleMessage(ws, msg) {
390
396
  if (!msg || typeof msg.type !== "string") return false;
397
+ if (msg.type === "home_mate_stop" || msg.type === "home_mate_permission_response") {
398
+ var controlUserId = ws._clayUser ? ws._clayUser.id : null;
399
+ if (users.isMultiUser() && !controlUserId) {
400
+ sendError(ws, msg.mateId || null, "Not authenticated.", msg.requestId || null, msg.sessionId || null);
401
+ return true;
402
+ }
403
+ var controlFound = findMateProject(controlUserId, msg.mateId, true);
404
+ var controlTap = messageTap(ws, msg);
405
+ var controlManager = controlFound ? controlFound.ctx.getSessionManager() : null;
406
+ var controlSession = controlManager && controlTap && controlTap.mateId === controlFound.mate.id && controlTap.requestId === msg.requestId
407
+ ? controlManager.sessions.get(controlTap.sessionId) : null;
408
+ var controlReference = controlSession ? sessionReference(controlSession) : null;
409
+ if (!controlFound || !controlSession || controlReference !== msg.sessionId || !ownsSession(controlSession, controlUserId) || controlSession.homeClayEntryMode !== "search") {
410
+ sendError(ws, msg.mateId || null, "Conversation control is no longer available.", msg.requestId || null, msg.sessionId || null, "session_not_found");
411
+ return true;
412
+ }
413
+ if (msg.type === "home_mate_permission_response") {
414
+ var controlDecision = msg.decision === "allow" ? "allow" : (msg.decision === "deny" ? "deny" : null);
415
+ if (!controlDecision || typeof msg.permissionRequestId !== "string" || !controlSession.pendingPermissions[msg.permissionRequestId]) {
416
+ sendError(ws, controlFound.mate.id, "This permission request is no longer pending.", msg.requestId || null, msg.sessionId || null, "permission_not_found", { toolId: msg.permissionRequestId });
417
+ return true;
418
+ }
419
+ controlFound.ctx.handleHomePermissionResponse(ws, { requestId: msg.permissionRequestId, decision: controlDecision }, controlSession);
420
+ } else {
421
+ sendMessage(ws, { type: "home_mate_stopping", mateId: controlFound.mate.id, sessionId: controlReference, requestId: msg.requestId || null });
422
+ controlFound.ctx.stopHomeSession(controlSession);
423
+ }
424
+ return true;
425
+ }
391
426
  if (msg.type !== "home_debates_list" && msg.type !== "home_clay_ask" && msg.type !== "home_clay_session_resolve" && msg.type !== "home_clay_log_resolve" && msg.type !== "home_mate_present" && msg.type !== "home_mate_open" && msg.type !== "home_mate_sessions_list" && msg.type !== "home_mate_session_open" && msg.type !== "home_mate_send" && msg.type !== "home_mate_new_session" && msg.type !== "home_mate_debate_plan" && msg.type !== "home_debate_proposal_response" && msg.type !== "home_debate_question_response" && msg.type !== "home_debate_control" && msg.type !== "home_mate_creation_plan" && msg.type !== "home_mate_creation_question_response" && msg.type !== "home_mate_creation_proposal_response" && msg.type !== "home_mate_close" && msg.type !== "home_mate_memory_list" && msg.type !== "home_mate_knowledge_list" && msg.type !== "home_mate_models_get" && msg.type !== "home_mate_model_set") {
392
427
  return false;
393
428
  }
package/lib/ws-schema.js CHANGED
@@ -643,6 +643,8 @@ var schema = {
643
643
  "home_mate_sessions_list": { direction: "c2s", handler: "lib/server-home-chat.js", description: "List the current user's conversations with the selected mate" },
644
644
  "home_mate_session_open": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Open one explicit conversation with the selected mate" },
645
645
  "home_mate_send": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Send a message to the selected mate from the home hub" },
646
+ "home_mate_stop": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Stop the exact query-bound global Ask Clay turn" },
647
+ "home_mate_permission_response": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Answer an exact pending global Ask Clay permission request" },
646
648
  "home_mate_new_session": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Start a fresh embedded session with the selected mate, optionally with a typed creation intent" },
647
649
  "home_mate_debate_plan": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Start a fresh server-seeded debate planning conversation with Clay using an optional prefilled topic" },
648
650
  "home_mate_creation_plan": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Start a fresh server-seeded Mate creation interview with Clay" },
@@ -659,6 +661,10 @@ var schema = {
659
661
  "home_mate_model_set": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Validate and persist the Mate vendor/model, optionally applying it to an exact owned pristine Home draft" },
660
662
  "home_mate_history": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Selected mate's initial or refreshed chat history" },
661
663
  "home_clay_activity": { direction: "s2c", handler: "lib/public/modules/search-clay-chat.js", description: "Sanitized progress stage for a query-bound global-search Clay conversation" },
664
+ "home_mate_permission_request": { direction: "s2c", handler: "lib/public/modules/search-clay-chat.js", description: "Exact pending permission card for a global Ask Clay conversation" },
665
+ "home_mate_permission_resolved": { direction: "s2c", handler: "lib/public/modules/search-clay-chat.js", description: "Server-confirmed Ask Clay permission decision" },
666
+ "home_mate_permission_cancelled": { direction: "s2c", handler: "lib/public/modules/search-clay-chat.js", description: "Server-confirmed Ask Clay permission cancellation" },
667
+ "home_mate_stopping": { direction: "s2c", handler: "lib/public/modules/search-clay-chat.js", description: "Server accepted Stop for the exact Ask Clay conversation" },
662
668
  "home_mate_session_identity": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Promote an active Home session from its local reference to its durable runtime identity" },
663
669
  "home_mate_sessions_state": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Owned visible Mate conversations with sanitized runtime identity, model, status, and timestamps" },
664
670
  "home_mate_delta": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Selected mate's streaming assistant text delta" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.0.0",
3
+ "version": "4.0.1-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",