clay-server 2.44.1-beta.2 → 2.45.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.js CHANGED
@@ -703,7 +703,7 @@ var relay = createServer({
703
703
  osUsers: !!config.osUsers,
704
704
  inheritGroups: config.inheritGroups !== false,
705
705
  chatLayout: config.chatLayout || "channel",
706
- matesEnabled: config.matesEnabled !== false,
706
+ matesEnabled: config.matesEnabled === true,
707
707
  pinEnabled: !!config.pinHash,
708
708
  platform: process.platform,
709
709
  hostname: os2.hostname(),
@@ -169,19 +169,32 @@ function attachSessions(ctx) {
169
169
  var termId = (typeof s.runtimeTerminalId === "number")
170
170
  ? s.runtimeTerminalId
171
171
  : (typeof s.terminalId === "number" ? s.terminalId : null);
172
- try {
173
- ctx._notifications && ctx._notifications.notify("tui_attention", {
174
- slug: slug,
175
- sessionId: s.localId,
176
- ownerId: s.ownerId || null,
177
- targetUserId: s.ownerId || null,
178
- title: "Claude responded",
179
- body: firstLine,
180
- terminalId: termId,
181
- sessionTitle: s.title || "",
182
- cliSessionId: s.cliSessionId || null,
183
- });
184
- } catch (e) {}
172
+ // Don't banner a session someone is already watching — they can see
173
+ // the reply in the TUI. Gate at the source (server) rather than relying
174
+ // on the client's activeSessionId suppression, which isn't reliable for
175
+ // TUI sessions and lets banners pile up for the session in view.
176
+ var beingViewed = false;
177
+ for (var vws of clients) {
178
+ if (vws.readyState === 1 && vws._clayActiveSession === s.localId) {
179
+ beingViewed = true;
180
+ break;
181
+ }
182
+ }
183
+ if (!beingViewed) {
184
+ try {
185
+ ctx._notifications && ctx._notifications.notify("tui_attention", {
186
+ slug: slug,
187
+ sessionId: s.localId,
188
+ ownerId: s.ownerId || null,
189
+ targetUserId: s.ownerId || null,
190
+ title: "Claude responded",
191
+ body: firstLine,
192
+ terminalId: termId,
193
+ sessionTitle: s.title || "",
194
+ cliSessionId: s.cliSessionId || null,
195
+ });
196
+ } catch (e) {}
197
+ }
185
198
  // Re-read the assistant text index and broadcast it so any client
186
199
  // with this session in view can wire hover-to-grab onto the new
187
200
  // message right away. The transcript is small enough that a full
@@ -904,6 +917,24 @@ function attachSessions(ctx) {
904
917
  return true;
905
918
  }
906
919
 
920
+ if (msg.type === "reload_skills") {
921
+ var session = getSessionForWs(ws);
922
+ if (session && sdk.reloadSkills) {
923
+ sdk.reloadSkills(session);
924
+ }
925
+ return true;
926
+ }
927
+
928
+ if (msg.type === "set_mcp_permission_mode_override" && msg.serverName) {
929
+ var session = getSessionForWs(ws);
930
+ if (session && sdk.setMcpPermissionModeOverride) {
931
+ // mode: "default" | "auto" | null (null clears the override)
932
+ var mcpMode = (msg.mode === "auto" || msg.mode === "default") ? msg.mode : null;
933
+ sdk.setMcpPermissionModeOverride(session, msg.serverName, mcpMode);
934
+ }
935
+ return true;
936
+ }
937
+
907
938
  if (msg.type === "set_vendor" && msg.vendor) {
908
939
  var vendorSession = getSessionForWs(ws);
909
940
  if (vendorSession) {
@@ -1256,13 +1287,18 @@ function attachSessions(ctx) {
1256
1287
  sdk.pushMessage(session, answerText);
1257
1288
  }
1258
1289
  } else {
1259
- // Claude native AskUserQuestion path (canUseTool). The SDK is
1260
- // synchronously blocked on the permission callback, so we must
1261
- // resolve it with the standard permission shape.
1262
- pending.resolve({
1263
- behavior: "allow",
1264
- updatedInput: Object.assign({}, pending.input, { answers: answers }),
1265
- });
1290
+ // Claude native AskUserQuestion path (built-in tool, intercepted via
1291
+ // canUseTool). In headless SDK mode the built-in can't render its own
1292
+ // answer UI: resolving "allow" lets it return an EMPTY result, so the
1293
+ // model continues the same turn with no answer ("I don't see an
1294
+ // answer"). Injected user messages also lose the race against that
1295
+ // empty result. Instead, deliver the answer as the tool_result by
1296
+ // denying with the formatted answer text — the SDK surfaces a deny
1297
+ // `message` to the model as the tool result, so the model receives the
1298
+ // user's choice in the same turn. (updatedInput.answers is not read by
1299
+ // the 0.3.x built-in, which is what caused answers to be dropped.)
1300
+ var nativeAnswerText = formatAskUserAnswerAsMessage(pending.input, answers);
1301
+ pending.resolve({ behavior: "deny", message: nativeAnswerText });
1266
1302
  }
1267
1303
  return true;
1268
1304
  }
@@ -1457,6 +1493,26 @@ function attachSessions(ctx) {
1457
1493
  return true;
1458
1494
  }
1459
1495
 
1496
+ // --- Host user dialog response (SDK request_user_dialog) ---
1497
+ if (msg.type === "user_dialog_response") {
1498
+ var session = getSessionForWs(ws);
1499
+ if (!session) return true;
1500
+ var pending = session.pendingUserDialogs && session.pendingUserDialogs[msg.requestId];
1501
+ if (!pending) return true;
1502
+ delete session.pendingUserDialogs[msg.requestId];
1503
+ if (msg.behavior === "completed") {
1504
+ pending.resolve({ behavior: "completed", result: msg.result });
1505
+ } else {
1506
+ pending.resolve({ behavior: "cancelled" });
1507
+ }
1508
+ sm.sendAndRecord(session, {
1509
+ type: "user_dialog_resolved",
1510
+ requestId: msg.requestId,
1511
+ behavior: msg.behavior === "completed" ? "completed" : "cancelled",
1512
+ });
1513
+ return true;
1514
+ }
1515
+
1460
1516
  // --- Browse directories (for add-project autocomplete) ---
1461
1517
  if (msg.type === "browse_dir") {
1462
1518
  var rawPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
package/lib/public/app.js CHANGED
@@ -287,7 +287,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
287
287
  cachedMatesList: [],
288
288
  cachedDmFavorites: [],
289
289
  cachedAvailableBuiltins: [],
290
- matesEnabled: true,
290
+ matesEnabled: false,
291
291
  returningFromMateDm: false,
292
292
  pendingMateInterview: null,
293
293
  pendingTermCommand: null,
@@ -21,7 +21,7 @@ import { renderMemoryList } from './mate-memory.js';
21
21
  import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
22
22
  import { handleFindInSessionResults } from './session-search.js';
23
23
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
24
- import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved } from './tools.js';
24
+ import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
25
25
  import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
26
26
  import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch } from './filebrowser.js';
27
27
  import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
@@ -992,6 +992,16 @@ export function processMessage(msg) {
992
992
  stopUrgentBlink();
993
993
  break;
994
994
 
995
+ case "user_dialog_request":
996
+ renderUserDialogRequest(msg);
997
+ startUrgentBlink();
998
+ break;
999
+
1000
+ case "user_dialog_resolved":
1001
+ markUserDialogResolved(msg.requestId, msg.behavior);
1002
+ stopUrgentBlink();
1003
+ break;
1004
+
995
1005
  case "slash_command_result":
996
1006
  finalizeAssistantBlock();
997
1007
  var cmdBlock = document.createElement("div");
@@ -1088,6 +1098,43 @@ export function processMessage(msg) {
1088
1098
  addSystemMessage(msg.text, false);
1089
1099
  break;
1090
1100
 
1101
+ case "thinking_tokens":
1102
+ updateThinkingTokens(msg.estimatedTokens);
1103
+ break;
1104
+
1105
+ case "informational":
1106
+ // level: info | notice | suggestion | warning
1107
+ if (msg.content) addSystemMessage(msg.content, msg.level === "warning");
1108
+ break;
1109
+
1110
+ case "permission_denied": {
1111
+ var deniedText = (msg.toolName ? msg.toolName + ": " : "")
1112
+ + "tool call denied"
1113
+ + (msg.reason ? " — " + msg.reason : (msg.message ? " — " + msg.message : ""))
1114
+ + ".";
1115
+ addSystemMessage(deniedText, false);
1116
+ break;
1117
+ }
1118
+
1119
+ case "model_refusal": {
1120
+ var refusalText;
1121
+ if (msg.refusalKind === "fallback") {
1122
+ refusalText = "The model declined this request and automatically retried"
1123
+ + (msg.fallbackModel ? " with " + msg.fallbackModel : " with a fallback model")
1124
+ + (msg.originalModel ? " (originally " + msg.originalModel + ")" : "")
1125
+ + ".";
1126
+ if (msg.category) refusalText += " Category: " + msg.category + ".";
1127
+ addSystemMessage(refusalText, false);
1128
+ } else {
1129
+ refusalText = "The model declined to respond to this request.";
1130
+ if (msg.explanation) refusalText += " " + msg.explanation;
1131
+ else if (msg.content) refusalText += " " + msg.content;
1132
+ if (msg.category) refusalText += " (Category: " + msg.category + ")";
1133
+ addSystemMessage(refusalText, true);
1134
+ }
1135
+ break;
1136
+ }
1137
+
1091
1138
  case "process_conflict":
1092
1139
  removeMatePreThinking();
1093
1140
  setActivity(null);
@@ -137,6 +137,28 @@ function renderMcpServerList() {
137
137
 
138
138
  row.appendChild(cb);
139
139
  row.appendChild(info);
140
+
141
+ // Per-server permission mode override (applies to the active session only).
142
+ if (server.projectEnabled) {
143
+ var modeSel = document.createElement("select");
144
+ modeSel.className = "mcp-server-perm-mode";
145
+ modeSel.dataset.serverName = server.name;
146
+ modeSel.title = "Permission mode for this server (this session only)";
147
+ var optAsk = document.createElement("option");
148
+ optAsk.value = "default";
149
+ optAsk.textContent = "Ask";
150
+ var optAuto = document.createElement("option");
151
+ optAuto.value = "auto";
152
+ optAuto.textContent = "Auto-approve";
153
+ modeSel.appendChild(optAsk);
154
+ modeSel.appendChild(optAuto);
155
+ modeSel.value = server.permissionModeOverride === "auto" ? "auto" : "default";
156
+ // The row is a <label>; keep select interactions from toggling the checkbox.
157
+ modeSel.addEventListener("click", function (e) { e.stopPropagation(); e.preventDefault(); });
158
+ modeSel.addEventListener("change", onPermissionModeChange);
159
+ row.appendChild(modeSel);
160
+ }
161
+
140
162
  contentEl.appendChild(row);
141
163
  }
142
164
  refreshIcons(contentEl);
@@ -293,3 +315,25 @@ function onToggle(e) {
293
315
  }));
294
316
  }
295
317
  }
318
+
319
+ function onPermissionModeChange(e) {
320
+ var name = e.target.dataset.serverName;
321
+ var mode = e.target.value; // "default" | "auto"
322
+
323
+ // Remember locally so a re-render keeps the selection.
324
+ for (var i = 0; i < _mcpServers.length; i++) {
325
+ if (_mcpServers[i].name === name) {
326
+ _mcpServers[i].permissionModeOverride = mode;
327
+ break;
328
+ }
329
+ }
330
+
331
+ var ws = getWs();
332
+ if (ws && ws.readyState === 1) {
333
+ ws.send(JSON.stringify({
334
+ type: "set_mcp_permission_mode_override",
335
+ serverName: name,
336
+ mode: mode,
337
+ }));
338
+ }
339
+ }
@@ -590,8 +590,8 @@ export function initProfile(_ctx) {
590
590
 
591
591
  // Apply Mates UI gate at boot so the sidebar avatars / DM picker
592
592
  // entry / home-hub strip are hidden before the user opens settings.
593
- // Default true: only flip the body class off when explicitly false.
594
- var matesOn = data.matesEnabled !== false;
593
+ // Default OFF: Mates is opt-in, so only enable when explicitly true.
594
+ var matesOn = data.matesEnabled === true;
595
595
  store.set({ matesEnabled: matesOn });
596
596
  if (document.body) document.body.classList.toggle('mates-disabled', !matesOn);
597
597
  }).catch(function(err) {
@@ -1,6 +1,16 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { escapeHtml, copyToClipboard, showToast } from './utils.js';
3
3
  import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+
6
+ // Ask the active session to hot-reload skills so a just-installed/uninstalled
7
+ // skill takes effect without restarting the session. No-op if no live session.
8
+ function reloadActiveSessionSkills() {
9
+ var ws = getWs();
10
+ if (ws && ws.readyState === 1) {
11
+ ws.send(JSON.stringify({ type: "reload_skills" }));
12
+ }
13
+ }
4
14
 
5
15
  var modal;
6
16
  var contentEl;
@@ -607,6 +617,9 @@ export function handleSkillInstalled(msg) {
607
617
  if (activeTab === "installed" && currentView === "list") {
608
618
  loadInstalledSkills();
609
619
  }
620
+
621
+ // Hot-reload skills in the active session so it's usable immediately
622
+ reloadActiveSessionSkills();
610
623
  } else {
611
624
  // Show error toast
612
625
  showToast("Failed to install " + skill + (msg.error ? ": " + msg.error : ""), "error");
@@ -676,6 +689,9 @@ export function handleSkillUninstalled(msg) {
676
689
  if (activeTab === "installed" && currentView === "list") {
677
690
  loadInstalledSkills();
678
691
  }
692
+
693
+ // Hot-reload skills in the active session so the change applies immediately
694
+ reloadActiveSessionSkills();
679
695
  } else {
680
696
  showToast("Failed to uninstall " + skill + (msg.error ? ": " + msg.error : ""), "error");
681
697
  // Re-enable the uninstall button
@@ -1181,6 +1181,159 @@ export function markElicitationResolved(requestId, action) {
1181
1181
  delete pendingElicitations[requestId];
1182
1182
  }
1183
1183
 
1184
+ // --- Host user dialog (SDK request_user_dialog) ---
1185
+ // Generic, defensive renderer. We declare only kinds we handle in
1186
+ // supportedDialogKinds (server side), but treat the payload as opaque: render
1187
+ // a human message + any provided choices, fall back to Proceed/Cancel, and
1188
+ // log the raw payload so unseen shapes can be refined. Cancel is always safe.
1189
+
1190
+ var pendingUserDialogs = {};
1191
+
1192
+ function sendUserDialogResponse(container, requestId, behavior, result) {
1193
+ if (container.classList.contains("resolved")) return;
1194
+ container.classList.add("resolved");
1195
+ if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
1196
+
1197
+ var isCancel = behavior !== "completed";
1198
+ container.classList.add(isCancel ? "resolved-denied" : "resolved-allowed");
1199
+
1200
+ var actions = container.querySelector(".permission-actions");
1201
+ if (actions) {
1202
+ actions.innerHTML = '<span class="permission-decision-label">' + (isCancel ? "Cancelled" : "Submitted") + '</span>';
1203
+ }
1204
+
1205
+ if (ctx.ws && ctx.connected) {
1206
+ var msg = {
1207
+ type: "user_dialog_response",
1208
+ requestId: requestId,
1209
+ behavior: isCancel ? "cancelled" : "completed",
1210
+ };
1211
+ if (!isCancel) msg.result = result;
1212
+ ctx.ws.send(JSON.stringify(msg));
1213
+ }
1214
+
1215
+ delete pendingUserDialogs[requestId];
1216
+ }
1217
+
1218
+ export function renderUserDialogRequest(msg) {
1219
+ if (pendingUserDialogs[msg.requestId]) return;
1220
+ ctx.finalizeAssistantBlock();
1221
+ stopThinking();
1222
+ closeToolGroup();
1223
+
1224
+ var payload = msg.payload || {};
1225
+ try { console.log("[user_dialog] kind=" + msg.dialogKind + " payload=", payload); } catch (e) {}
1226
+
1227
+ var container = document.createElement("div");
1228
+ container.className = "permission-container user-dialog-container";
1229
+ container.dataset.requestId = msg.requestId;
1230
+
1231
+ // Header
1232
+ var titleText = payload.title || "Claude needs a decision";
1233
+ var header = document.createElement("div");
1234
+ header.className = "permission-header";
1235
+ header.innerHTML =
1236
+ '<span class="permission-icon">' + iconHtml("key") + '</span>' +
1237
+ '<span class="permission-title">' + escapeHtml(titleText) + '</span>';
1238
+
1239
+ // Body: best-effort human message from common payload fields
1240
+ var body = document.createElement("div");
1241
+ body.className = "permission-body";
1242
+ var messageText = payload.message || payload.prompt || payload.question || payload.text || "";
1243
+ if (messageText) {
1244
+ var messageEl = document.createElement("div");
1245
+ messageEl.className = "permission-reason";
1246
+ messageEl.textContent = String(messageText);
1247
+ body.appendChild(messageEl);
1248
+ }
1249
+ var kindEl = document.createElement("div");
1250
+ kindEl.className = "user-dialog-kind";
1251
+ kindEl.style.cssText = "margin-top: 6px; font-size: 11px; color: var(--text-muted);";
1252
+ kindEl.textContent = msg.dialogKind || "dialog";
1253
+ body.appendChild(kindEl);
1254
+
1255
+ // Actions
1256
+ var actions = document.createElement("div");
1257
+ actions.className = "permission-actions";
1258
+
1259
+ // If the payload carries explicit choices, render one button per choice.
1260
+ var choices = Array.isArray(payload.options) ? payload.options
1261
+ : Array.isArray(payload.choices) ? payload.choices
1262
+ : null;
1263
+
1264
+ if (choices && choices.length) {
1265
+ for (var i = 0; i < choices.length; i++) {
1266
+ (function (choice) {
1267
+ var label = (choice && (choice.label || choice.title || choice.name)) || String(choice);
1268
+ var value = (choice && ("value" in choice)) ? choice.value : choice;
1269
+ var btn = document.createElement("button");
1270
+ btn.className = "permission-btn permission-allow";
1271
+ btn.textContent = label;
1272
+ btn.addEventListener("click", function () {
1273
+ sendUserDialogResponse(container, msg.requestId, "completed", value);
1274
+ });
1275
+ actions.appendChild(btn);
1276
+ })(choices[i]);
1277
+ }
1278
+ } else {
1279
+ var proceedBtn = document.createElement("button");
1280
+ proceedBtn.className = "permission-btn permission-allow";
1281
+ proceedBtn.textContent = "Proceed";
1282
+ proceedBtn.addEventListener("click", function () {
1283
+ sendUserDialogResponse(container, msg.requestId, "completed", {});
1284
+ });
1285
+ actions.appendChild(proceedBtn);
1286
+ }
1287
+
1288
+ var cancelBtn = document.createElement("button");
1289
+ cancelBtn.className = "permission-btn permission-deny";
1290
+ cancelBtn.textContent = "Cancel";
1291
+ cancelBtn.addEventListener("click", function () {
1292
+ sendUserDialogResponse(container, msg.requestId, "cancelled", null);
1293
+ });
1294
+ actions.appendChild(cancelBtn);
1295
+
1296
+ container.appendChild(header);
1297
+ container.appendChild(body);
1298
+ container.appendChild(actions);
1299
+ ctx.addToMessages(container);
1300
+
1301
+ pendingUserDialogs[msg.requestId] = container;
1302
+ refreshIcons();
1303
+ ctx.setActivity(null);
1304
+ maybeScrollToBottom();
1305
+ }
1306
+
1307
+ export function markUserDialogResolved(requestId, behavior) {
1308
+ var container = pendingUserDialogs[requestId];
1309
+ if (!container) {
1310
+ container = ctx.messagesEl.querySelector('.user-dialog-container[data-request-id="' + requestId + '"]');
1311
+ }
1312
+ if (!container || container.classList.contains("resolved")) return;
1313
+
1314
+ container.classList.add("resolved");
1315
+ var isCancel = behavior !== "completed";
1316
+ container.classList.add(isCancel ? "resolved-denied" : "resolved-allowed");
1317
+
1318
+ var actionsEl = container.querySelector(".permission-actions");
1319
+ if (actionsEl) {
1320
+ actionsEl.innerHTML = '<span class="permission-decision-label">' + (isCancel ? "Cancelled" : "Submitted") + '</span>';
1321
+ }
1322
+ delete pendingUserDialogs[requestId];
1323
+ }
1324
+
1325
+ // --- Live thinking-token estimate ---
1326
+ // Annotates the active thinking block's label with a running token estimate.
1327
+ // No-op when no thinking block is active.
1328
+ export function updateThinkingTokens(estimatedTokens) {
1329
+ if (!currentThinking || !currentThinking.el) return;
1330
+ var label = currentThinking.el.querySelector(".thinking-label");
1331
+ if (!label) return;
1332
+ var n = estimatedTokens || 0;
1333
+ var disp = n >= 1000 ? ("~" + (Math.round(n / 100) / 10) + "k") : ("~" + n);
1334
+ label.textContent = "Thinking " + disp + " tokens";
1335
+ }
1336
+
1184
1337
  // --- Plan mode rendering ---
1185
1338
  export function renderPlanBanner(type) {
1186
1339
  ctx.finalizeAssistantBlock();
@@ -1554,6 +1707,8 @@ export function startThinking() {
1554
1707
  var el = thinkingGroup.el;
1555
1708
  el.classList.remove("done");
1556
1709
  el.querySelector(".thinking-content").textContent = "";
1710
+ var reuseLabel = el.querySelector(".thinking-label");
1711
+ if (reuseLabel) reuseLabel.textContent = "Thinking";
1557
1712
  // Mate mode: restore dots activity row, hide thinking header
1558
1713
  if (el.classList.contains("mate-thinking")) {
1559
1714
  var actRow = el.querySelector(".mate-thinking-activity");
@@ -2379,6 +2534,7 @@ export function resetToolState() {
2379
2534
  if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
2380
2535
  pendingPermissions = {};
2381
2536
  pendingElicitations = {};
2537
+ pendingUserDialogs = {};
2382
2538
  currentToolGroup = null;
2383
2539
  toolGroupCounter = 0;
2384
2540
  toolGroups = {};
@@ -468,8 +468,8 @@ function populateAccount() {
468
468
  if (data.mateOnboardingShown) {
469
469
  try { localStorage.setItem("clay-mate-onboarding-shown", "1"); } catch (e) {}
470
470
  }
471
- // Mates UI toggle: default true; treat any missing/non-false value as on
472
- var matesOn = data.matesEnabled !== false;
471
+ // Mates UI toggle: default OFF (opt-in); enable only when explicitly true
472
+ var matesOn = data.matesEnabled === true;
473
473
  var matesToggle = document.getElementById('us-mates-enabled');
474
474
  if (matesToggle) matesToggle.checked = matesOn;
475
475
  store.set({ matesEnabled: matesOn });
package/lib/sdk-bridge.js CHANGED
@@ -392,6 +392,54 @@ function createSDKBridge(opts) {
392
392
  });
393
393
  }
394
394
 
395
+ // --- Host user dialog ---
396
+ // Mirrors handleElicitation: the adapter invokes onUserDialog with an opaque
397
+ // { dialogKind, payload }, we forward a user_dialog_request to the client, and
398
+ // resolve once the client answers. Which dialog kinds exist is the adapter's
399
+ // concern; the relay just transports them. Unknown kinds / autonomous mode
400
+ // resolve to { behavior: "cancelled" } (the no-dialog default).
401
+
402
+ function handleUserDialog(session, request, opts) {
403
+ // Ralph Loop: auto-cancel host dialogs in autonomous mode (CLI falls back
404
+ // to the no-dialog default, e.g. the classic refusal error).
405
+ if (session.loop && session.loop.active && session.loop.role !== "crafting") {
406
+ return Promise.resolve({ behavior: "cancelled" });
407
+ }
408
+
409
+ return new Promise(function(resolve) {
410
+ var requestId = crypto.randomUUID();
411
+ if (!session.pendingUserDialogs) session.pendingUserDialogs = {};
412
+ session.pendingUserDialogs[requestId] = {
413
+ resolve: resolve,
414
+ request: request,
415
+ };
416
+ sendAndRecord(session, {
417
+ type: "user_dialog_request",
418
+ requestId: requestId,
419
+ dialogKind: request.dialogKind,
420
+ payload: request.payload || {},
421
+ toolUseId: request.toolUseID || null,
422
+ });
423
+
424
+ if (pushModule) {
425
+ pushModule.sendPush({
426
+ type: "user_dialog",
427
+ slug: slug,
428
+ title: "Claude needs a decision",
429
+ body: "Waiting for your response",
430
+ tag: "claude-user-dialog",
431
+ });
432
+ }
433
+
434
+ if (opts.signal) {
435
+ opts.signal.addEventListener("abort", function() {
436
+ delete session.pendingUserDialogs[requestId];
437
+ resolve({ behavior: "cancelled" });
438
+ });
439
+ }
440
+ });
441
+ }
442
+
395
443
 
396
444
  // --- Linux user project directory setup ---
397
445
  // Ensures the linux user's .claude project directory exists and is writable,
@@ -1281,6 +1329,9 @@ function createSDKBridge(opts) {
1281
1329
  onElicitation: function(request, elicitOpts) {
1282
1330
  return handleElicitation(session, request, elicitOpts);
1283
1331
  },
1332
+ onUserDialog: function(dialogRequest, dialogOpts) {
1333
+ return handleUserDialog(session, dialogRequest, dialogOpts);
1334
+ },
1284
1335
  callMcpTool: function(serverName, toolName, args) {
1285
1336
  return callMcpToolHandler(mergedMcpServers, serverName, toolName, args);
1286
1337
  },
@@ -1603,6 +1654,29 @@ function createSDKBridge(opts) {
1603
1654
  }
1604
1655
  }
1605
1656
 
1657
+ // Hot-reload skills/MCP/agents on the active query without a restart.
1658
+ async function reloadSkills(session) {
1659
+ if (!session || !session.queryInstance) return;
1660
+ try {
1661
+ // Route through QueryHandle (works for both in-process and worker paths)
1662
+ await session.queryInstance.reloadSkills();
1663
+ send({ type: "system_info", text: "Skills reloaded." });
1664
+ } catch (e) {
1665
+ send({ type: "error", text: "Failed to reload skills: " + (e.message || e) });
1666
+ }
1667
+ }
1668
+
1669
+ // Override the permission mode for one MCP server on the active query.
1670
+ async function setMcpPermissionModeOverride(session, serverName, mode) {
1671
+ if (!session || !session.queryInstance) return;
1672
+ try {
1673
+ // Route through QueryHandle (works for both in-process and worker paths)
1674
+ await session.queryInstance.setMcpPermissionModeOverride(serverName, mode);
1675
+ } catch (e) {
1676
+ send({ type: "error", text: "Failed to set MCP permission mode: " + (e.message || e) });
1677
+ }
1678
+ }
1679
+
1606
1680
  // --- @Mention: persistent read-only session for a mentioned Mate ---
1607
1681
  // Creates a mention session that can be reused across multiple mentions
1608
1682
  // within a conversation flow (session continuity).
@@ -1789,6 +1863,8 @@ function createSDKBridge(opts) {
1789
1863
  permissionPushBody: permissionPushBody,
1790
1864
  warmup: warmup,
1791
1865
  stopTask: stopTask,
1866
+ reloadSkills: reloadSkills,
1867
+ setMcpPermissionModeOverride: setMcpPermissionModeOverride,
1792
1868
  createMentionSession: createMentionSession,
1793
1869
  startIdleReaper: startIdleReaper,
1794
1870
  stopIdleReaper: stopIdleReaper,
@@ -698,6 +698,73 @@ function attachMessageProcessor(ctx) {
698
698
  var retryText = parsed.message || parsed.error || "Retrying API request...";
699
699
  sendToSession(session, { type: "system_info", text: retryText });
700
700
 
701
+ } else if (parsed.yokeType === "commands_changed") {
702
+ // Mid-session command list change: rebuild the union with skills (same as
703
+ // init) and push a full replacement to the client.
704
+ var cmdSeen = new Set();
705
+ var cmdCombined = [];
706
+ var cmdAll = (parsed.commandNames || []).concat(Array.from(sm.skillNames || []));
707
+ for (var ci = 0; ci < cmdAll.length; ci++) {
708
+ if (!cmdSeen.has(cmdAll[ci])) {
709
+ cmdSeen.add(cmdAll[ci]);
710
+ cmdCombined.push(cmdAll[ci]);
711
+ }
712
+ }
713
+ sm.slashCommands = cmdCombined;
714
+ send({ type: "slash_commands", commands: sm.slashCommands });
715
+
716
+ } else if (parsed.yokeType === "worker_shutting_down") {
717
+ // Surface non-routine teardown reasons; "host_exit" is the normal close.
718
+ if (parsed.reason && parsed.reason !== "host_exit") {
719
+ sendToSession(session, { type: "system_info", text: "Session worker shutting down: " + parsed.reason });
720
+ }
721
+
722
+ } else if (parsed.yokeType === "thinking_tokens") {
723
+ // Live thinking-token estimate; ephemeral, do not persist in history.
724
+ sendToSession(session, {
725
+ type: "thinking_tokens",
726
+ estimatedTokens: parsed.estimatedTokens || 0,
727
+ estimatedTokensDelta: parsed.estimatedTokensDelta || 0,
728
+ });
729
+
730
+ } else if (parsed.yokeType === "informational") {
731
+ // Leveled informational message from the agent (info/notice/suggestion/warning).
732
+ if (parsed.content) {
733
+ sendAndRecord(session, {
734
+ type: "informational",
735
+ level: parsed.level || "info",
736
+ content: parsed.content,
737
+ toolUseId: parsed.toolUseId || null,
738
+ preventContinuation: !!parsed.preventContinuation,
739
+ });
740
+ }
741
+
742
+ } else if (parsed.yokeType === "permission_denied") {
743
+ // A tool call was blocked; surface why.
744
+ sendAndRecord(session, {
745
+ type: "permission_denied",
746
+ toolName: parsed.toolName || "",
747
+ toolUseId: parsed.toolUseId || null,
748
+ agentId: parsed.agentId || null,
749
+ reasonType: parsed.reasonType || null,
750
+ reason: parsed.reason || null,
751
+ message: parsed.message || "",
752
+ });
753
+
754
+ } else if (parsed.yokeType === "model_refusal") {
755
+ // Model declined the request. "fallback" => the CLI retried on another
756
+ // model; "no_fallback" => the turn ended with a refusal.
757
+ sendAndRecord(session, {
758
+ type: "model_refusal",
759
+ refusalKind: parsed.refusalKind || "no_fallback",
760
+ originalModel: parsed.originalModel || null,
761
+ fallbackModel: parsed.fallbackModel || null,
762
+ direction: parsed.direction || null,
763
+ category: parsed.category || null,
764
+ explanation: parsed.explanation || null,
765
+ content: parsed.content || null,
766
+ });
767
+
701
768
  } else if (parsed.yokeType === "system") {
702
769
  // Catch-all for unhandled system subtypes (e.g. hook-block errors).
703
770
  // Extract any error text and surface it in the UI.
@@ -29,7 +29,7 @@ function attachSettings(ctx) {
29
29
  profile.autoContinueOnRateLimit = !!mu.autoContinueOnRateLimit;
30
30
  profile.chatLayout = mu.chatLayout || "channel";
31
31
  profile.mateOnboardingShown = !!mu.mateOnboardingShown;
32
- profile.matesEnabled = mu.matesEnabled !== false;
32
+ profile.matesEnabled = mu.matesEnabled === true;
33
33
  try { profile.terminalFont = users.getTerminalFont(mu.id); } catch (e) {}
34
34
  res.writeHead(200, { "Content-Type": "application/json" });
35
35
  res.end(JSON.stringify(profile));
@@ -52,7 +52,7 @@ function attachSettings(ctx) {
52
52
  profile.autoContinueOnRateLimit = !!dc.autoContinueOnRateLimit;
53
53
  profile.chatLayout = dc.chatLayout || "channel";
54
54
  profile.mateOnboardingShown = !!dc.mateOnboardingShown;
55
- profile.matesEnabled = dc.matesEnabled !== false;
55
+ profile.matesEnabled = dc.matesEnabled === true;
56
56
  if (dc.terminalFont && typeof dc.terminalFont === "object") {
57
57
  profile.terminalFont = {
58
58
  family: dc.terminalFont.family || "'SF Mono', Menlo, Monaco, 'Courier New', monospace",
@@ -334,20 +334,21 @@ function attachPreferences(deps) {
334
334
 
335
335
  // --- Per-user Mates UI toggle ---
336
336
  //
337
- // When false, the entire Mates surface (sidebar avatars, DM "Create a
338
- // Mate" entry, home-hub mates strip) is hidden for this user. Default
339
- // is ON: Mates is opt-out, not opt-in. Stored as `matesEnabled`; we
340
- // treat any value other than the literal `false` as enabled so brand-
341
- // new users (no field set) get the default-on experience.
337
+ // When disabled, the entire Mates surface (sidebar avatars, DM "Create a
338
+ // Mate" entry, home-hub mates strip) is hidden for this user; multi-user
339
+ // and the rest of the sidebar are unaffected. Default is OFF: Mates is
340
+ // opt-in, not opt-out. Stored as `matesEnabled`; only the literal `true`
341
+ // enables it, so brand-new users (no field set) get the default-off
342
+ // experience and must turn Mates on explicitly in settings.
342
343
 
343
344
  function getMatesEnabled(userId) {
344
345
  var data = loadUsers();
345
346
  for (var i = 0; i < data.users.length; i++) {
346
347
  if (data.users[i].id === userId) {
347
- return data.users[i].matesEnabled !== false;
348
+ return data.users[i].matesEnabled === true;
348
349
  }
349
350
  }
350
- return true;
351
+ return false;
351
352
  }
352
353
 
353
354
  function setMatesEnabled(userId, enabled) {
package/lib/ws-schema.js CHANGED
@@ -67,6 +67,10 @@ var schema = {
67
67
  "compacting": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Conversation compaction in progress (active: true/false)" },
68
68
  "message_uuid": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "UUID assigned to a user or assistant message" },
69
69
  "prompt_suggestion": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Suggested follow-up prompt from the assistant" },
70
+ "model_refusal": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Model declined the request (fallback to another model, or no fallback)" },
71
+ "informational": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Leveled informational message from the agent (info/notice/suggestion/warning)" },
72
+ "permission_denied": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "A tool call was denied; explains why" },
73
+ "thinking_tokens": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Live thinking-token estimate (ephemeral)" },
70
74
  "context_preview": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Preview of context sources included in a query" },
71
75
 
72
76
  // -----------------------------------------------------------------------
@@ -115,6 +119,9 @@ var schema = {
115
119
  "elicitation_request": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Structured form elicitation request from assistant" },
116
120
  "elicitation_response": { direction: "c2s", handler: "lib/project-sessions.js", description: "User response to an elicitation form" },
117
121
  "elicitation_resolved": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Elicitation request resolved" },
122
+ "user_dialog_request": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Host dialog request from assistant (SDK request_user_dialog)" },
123
+ "user_dialog_response": { direction: "c2s", handler: "lib/project-sessions.js", description: "User response to a host dialog request" },
124
+ "user_dialog_resolved": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Host dialog request resolved" },
118
125
 
119
126
  // -----------------------------------------------------------------------
120
127
  // Stop / control
@@ -157,6 +164,8 @@ var schema = {
157
164
  "set_server_default_model": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the server-wide default model" },
158
165
  "set_project_default_model": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the project default model" },
159
166
  "set_permission_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set permission mode for the current session" },
167
+ "reload_skills": { direction: "c2s", handler: "lib/project-sessions.js", description: "Hot-reload skills/MCP/agents on the active query without a restart" },
168
+ "set_mcp_permission_mode_override": { direction: "c2s", handler: "lib/project-sessions.js", description: "Override permission mode (default/auto/null) for one MCP server" },
160
169
  "set_server_default_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the server-wide default permission mode" },
161
170
  "set_project_default_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the project default permission mode" },
162
171
  "set_effort": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set effort level for the current session" },
@@ -99,6 +99,7 @@ var abortController = null;
99
99
  var pendingPermissions = {}; // requestId -> resolve
100
100
  var pendingAskUser = {}; // toolUseId -> resolve
101
101
  var pendingElicitations = {}; // requestId -> resolve
102
+ var pendingUserDialogs = {}; // requestId -> resolve
102
103
  var pendingMcpToolCalls = {}; // requestId -> { resolve, reject }
103
104
  var conn = null;
104
105
  var buffer = "";
@@ -271,6 +272,12 @@ function handleMessage(msg) {
271
272
  case "stop_task":
272
273
  handleStopTask(msg);
273
274
  break;
275
+ case "reload_skills":
276
+ handleReloadSkills(msg);
277
+ break;
278
+ case "set_mcp_permission_mode_override":
279
+ handleSetMcpPermissionModeOverride(msg);
280
+ break;
274
281
  case "rewind_files":
275
282
  handleRewindFiles(msg);
276
283
  break;
@@ -283,6 +290,9 @@ function handleMessage(msg) {
283
290
  case "elicitation_response":
284
291
  handleElicitationResponse(msg);
285
292
  break;
293
+ case "user_dialog_response":
294
+ handleUserDialogResponse(msg);
295
+ break;
286
296
  case "mcp_tool_result":
287
297
  handleMcpToolResult(msg);
288
298
  break;
@@ -343,6 +353,27 @@ function onElicitation(request, opts) {
343
353
  });
344
354
  }
345
355
 
356
+ // --- onUserDialog: delegates to daemon via IPC ---
357
+ function onUserDialog(request, opts) {
358
+ var requestId = crypto.randomUUID();
359
+ sendToDaemon({
360
+ type: "user_dialog_request",
361
+ requestId: requestId,
362
+ dialogKind: request.dialogKind,
363
+ payload: request.payload || {},
364
+ toolUseId: request.toolUseID || null,
365
+ });
366
+ return new Promise(function(resolve) {
367
+ pendingUserDialogs[requestId] = resolve;
368
+ if (opts.signal) {
369
+ opts.signal.addEventListener("abort", function() {
370
+ delete pendingUserDialogs[requestId];
371
+ resolve({ behavior: "cancelled" });
372
+ });
373
+ }
374
+ });
375
+ }
376
+
346
377
  function handlePermissionResponse(msg) {
347
378
  var resolve = pendingPermissions[msg.requestId];
348
379
  if (resolve) {
@@ -367,6 +398,14 @@ function handleElicitationResponse(msg) {
367
398
  }
368
399
  }
369
400
 
401
+ function handleUserDialogResponse(msg) {
402
+ var resolve = pendingUserDialogs[msg.requestId];
403
+ if (resolve) {
404
+ delete pendingUserDialogs[msg.requestId];
405
+ resolve(msg.result);
406
+ }
407
+ }
408
+
370
409
  function handleMcpToolResult(msg) {
371
410
  var pending = pendingMcpToolCalls[msg.requestId];
372
411
  if (!pending) return;
@@ -477,6 +516,11 @@ async function handleQueryStart(msg) {
477
516
  options.onElicitation = function(request, elicitOpts) {
478
517
  return onElicitation(request, elicitOpts);
479
518
  };
519
+ // supportedDialogKinds arrives serialized via msg.options (already on `options`);
520
+ // the callback is non-serializable so we wire it here.
521
+ options.onUserDialog = function(request, dialogOpts) {
522
+ return onUserDialog(request, dialogOpts);
523
+ };
480
524
 
481
525
  perf("creating query instance");
482
526
  try {
@@ -630,6 +674,24 @@ async function handleStopTask(msg) {
630
674
  }
631
675
  }
632
676
 
677
+ async function handleReloadSkills(msg) {
678
+ if (!queryInstance || typeof queryInstance.reloadSkills !== "function") return;
679
+ try {
680
+ await queryInstance.reloadSkills();
681
+ } catch (e) {
682
+ console.error("[sdk-worker] reloadSkills error:", e.message);
683
+ }
684
+ }
685
+
686
+ async function handleSetMcpPermissionModeOverride(msg) {
687
+ if (!queryInstance || typeof queryInstance.setMcpPermissionModeOverride !== "function") return;
688
+ try {
689
+ await queryInstance.setMcpPermissionModeOverride(msg.serverName, msg.mode);
690
+ } catch (e) {
691
+ console.error("[sdk-worker] setMcpPermissionModeOverride error:", e.message);
692
+ }
693
+ }
694
+
633
695
  // --- Warmup ---
634
696
  async function handleWarmup(msg) {
635
697
  var sdk;
@@ -12,6 +12,12 @@ var crypto = require("crypto");
12
12
  var { spawn } = require("child_process");
13
13
  var { resolveOsUserInfo } = require("../../os-users");
14
14
 
15
+ // SDK-specific knowledge stays in the adapter (not the relay): the host dialog
16
+ // kinds this client can render, declared to the SDK when an onUserDialog
17
+ // callback is wired. The CLI fails closed — kinds not listed here are never
18
+ // emitted. Keep in sync with the client's generic user-dialog renderer.
19
+ var SUPPORTED_DIALOG_KINDS = ["refusal_fallback_prompt"];
20
+
15
21
  // --- SDK loading ---
16
22
  // Async loader (ESM dynamic import, same pattern as current project.js getSDK)
17
23
  var _sdkPromise = null;
@@ -182,6 +188,68 @@ function flattenEvent(raw) {
182
188
  base.summary = raw.summary || null;
183
189
  return base;
184
190
  }
191
+ // Model refusal: the model declined and the CLI either fell back to
192
+ // another model or ended the turn. Translate to a vendor-neutral
193
+ // yokeType so the relay never sees SDK subtype strings.
194
+ if (raw.subtype === "model_refusal_fallback") {
195
+ base.yokeType = "model_refusal";
196
+ base.refusalKind = "fallback";
197
+ base.originalModel = raw.original_model || null;
198
+ base.fallbackModel = raw.fallback_model || null;
199
+ base.direction = raw.direction || null;
200
+ base.category = raw.api_refusal_category || null;
201
+ return base;
202
+ }
203
+ if (raw.subtype === "model_refusal_no_fallback") {
204
+ base.yokeType = "model_refusal";
205
+ base.refusalKind = "no_fallback";
206
+ base.originalModel = raw.original_model || null;
207
+ base.category = raw.api_refusal_category || null;
208
+ base.explanation = raw.api_refusal_explanation || null;
209
+ base.content = raw.content || null;
210
+ return base;
211
+ }
212
+ // Slash-command list changed mid-session (e.g. skills discovered dynamically).
213
+ if (raw.subtype === "commands_changed") {
214
+ base.yokeType = "commands_changed";
215
+ base.commandNames = Array.isArray(raw.commands)
216
+ ? raw.commands.map(function(c) { return (c && typeof c === "object") ? c.name : c; }).filter(Boolean)
217
+ : [];
218
+ return base;
219
+ }
220
+ // The session worker is shutting down (reason is a host-set snake_case code).
221
+ if (raw.subtype === "worker_shutting_down") {
222
+ base.yokeType = "worker_shutting_down";
223
+ base.reason = raw.reason || "";
224
+ return base;
225
+ }
226
+ // Live thinking-token estimate (ephemeral progress).
227
+ if (raw.subtype === "thinking_tokens") {
228
+ base.yokeType = "thinking_tokens";
229
+ base.estimatedTokens = raw.estimated_tokens || 0;
230
+ base.estimatedTokensDelta = raw.estimated_tokens_delta || 0;
231
+ return base;
232
+ }
233
+ // Informational system message (render-leveled: info/notice/suggestion/warning).
234
+ if (raw.subtype === "informational") {
235
+ base.yokeType = "informational";
236
+ base.level = raw.level || "info";
237
+ base.content = raw.content || "";
238
+ base.toolUseId = raw.tool_use_id || null;
239
+ base.preventContinuation = !!raw.prevent_continuation;
240
+ return base;
241
+ }
242
+ // A tool call was denied (by classifier, rule, mode, or async agent).
243
+ if (raw.subtype === "permission_denied") {
244
+ base.yokeType = "permission_denied";
245
+ base.toolName = raw.tool_name || "";
246
+ base.toolUseId = raw.tool_use_id || null;
247
+ base.agentId = raw.agent_id || null;
248
+ base.reasonType = raw.decision_reason_type || null;
249
+ base.reason = raw.decision_reason || null;
250
+ base.message = raw.message || "";
251
+ return base;
252
+ }
185
253
  // Catch-all system event
186
254
  base.yokeType = "system";
187
255
  base.subtype = raw.subtype;
@@ -338,6 +406,20 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
338
406
  return Promise.resolve();
339
407
  },
340
408
 
409
+ reloadSkills: function() {
410
+ if (rawQuery && typeof rawQuery.reloadSkills === "function") {
411
+ return rawQuery.reloadSkills();
412
+ }
413
+ return Promise.resolve(null);
414
+ },
415
+
416
+ setMcpPermissionModeOverride: function(serverName, mode) {
417
+ if (rawQuery && typeof rawQuery.setMcpPermissionModeOverride === "function") {
418
+ return rawQuery.setMcpPermissionModeOverride(serverName, mode);
419
+ }
420
+ return Promise.resolve(null);
421
+ },
422
+
341
423
  getContextUsage: function() {
342
424
  if (rawQuery && typeof rawQuery.getContextUsage === "function") {
343
425
  return rawQuery.getContextUsage();
@@ -684,7 +766,7 @@ function serializeWorkerValue(value, seen) {
684
766
  // in-process QueryHandle. This allows processQueryStream to iterate a worker
685
767
  // query identically to an in-process query.
686
768
 
687
- function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool) {
769
+ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool, onUserDialog) {
688
770
  // Async iterable state
689
771
  var iterQueue = [];
690
772
  var iterWaiting = null;
@@ -782,6 +864,24 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool)
782
864
  }
783
865
  break;
784
866
 
867
+ case "user_dialog_request":
868
+ if (onUserDialog) {
869
+ onUserDialog({
870
+ dialogKind: msg.dialogKind,
871
+ payload: msg.payload || {},
872
+ toolUseID: msg.toolUseId || undefined,
873
+ }, {
874
+ signal: { addEventListener: function() {} },
875
+ }).then(function(result) {
876
+ worker.send({ type: "user_dialog_response", requestId: msg.requestId, result: result });
877
+ }).catch(function(e) {
878
+ console.error("[yoke/claude] user_dialog_response send failed:", e.message || e);
879
+ });
880
+ } else {
881
+ worker.send({ type: "user_dialog_response", requestId: msg.requestId, result: { behavior: "cancelled" } });
882
+ }
883
+ break;
884
+
785
885
  case "mcp_tool_call":
786
886
  if (callMcpTool) {
787
887
  callMcpTool(msg.serverName, msg.toolName, msg.args || {}).then(function(result) {
@@ -919,6 +1019,16 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool)
919
1019
  return Promise.resolve();
920
1020
  },
921
1021
 
1022
+ reloadSkills: function() {
1023
+ worker.send({ type: "reload_skills" });
1024
+ return Promise.resolve(null);
1025
+ },
1026
+
1027
+ setMcpPermissionModeOverride: function(serverName, mode) {
1028
+ worker.send({ type: "set_mcp_permission_mode_override", serverName: serverName, mode: mode });
1029
+ return Promise.resolve(null);
1030
+ },
1031
+
922
1032
  getContextUsage: function() {
923
1033
  return Promise.resolve(null);
924
1034
  },
@@ -1218,6 +1328,10 @@ function createClaudeAdapter(opts) {
1218
1328
  if (queryOpts.toolServers) sdkOptions.mcpServers = queryOpts.toolServers;
1219
1329
  if (queryOpts.canUseTool) sdkOptions.canUseTool = queryOpts.canUseTool;
1220
1330
  if (queryOpts.onElicitation) sdkOptions.onElicitation = queryOpts.onElicitation;
1331
+ if (queryOpts.onUserDialog) {
1332
+ sdkOptions.onUserDialog = queryOpts.onUserDialog;
1333
+ sdkOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
1334
+ }
1221
1335
  if (queryOpts.resumeSessionId) sdkOptions.resume = queryOpts.resumeSessionId;
1222
1336
 
1223
1337
  // Claude-specific options from adapterOptions.CLAUDE
@@ -1366,7 +1480,7 @@ function createClaudeAdapter(opts) {
1366
1480
  }
1367
1481
 
1368
1482
  // Create the worker query handle (sets up message handler on worker)
1369
- var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, queryOpts.onElicitation, queryOpts.callMcpTool);
1483
+ var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, queryOpts.onElicitation, queryOpts.callMcpTool, queryOpts.onUserDialog);
1370
1484
 
1371
1485
  // Wait for worker to be ready before sending query_start
1372
1486
  if (!reusingWorker) {
@@ -1393,6 +1507,7 @@ function createClaudeAdapter(opts) {
1393
1507
  if (claudeOpts.settings) queryOptions.settings = claudeOpts.settings;
1394
1508
 
1395
1509
  if (queryOpts.toolServerDescriptors) queryOptions.mcpServerDescriptors = queryOpts.toolServerDescriptors;
1510
+ if (queryOpts.onUserDialog) queryOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
1396
1511
  if (queryOpts.model) queryOptions.model = queryOpts.model;
1397
1512
  if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
1398
1513
  if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.44.1-beta.2",
3
+ "version": "2.45.0-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",
@@ -48,7 +48,7 @@
48
48
  "homepage": "https://github.com/chadbyte/clay#readme",
49
49
  "author": "Chad",
50
50
  "dependencies": {
51
- "@anthropic-ai/claude-agent-sdk": "^0.2.132",
51
+ "@anthropic-ai/claude-agent-sdk": "^0.3.196",
52
52
  "@lydell/node-pty": "^1.2.0-beta.3",
53
53
  "@openai/codex": "^0.124.0",
54
54
  "imapflow": "^1.3.1",