clay-server 2.44.1-beta.1 → 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.
@@ -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",
package/lib/server.js CHANGED
@@ -153,6 +153,7 @@ function createServer(opts) {
153
153
  var onGetDaemonConfig = opts.onGetDaemonConfig || null;
154
154
  var onSetPin = opts.onSetPin || null;
155
155
  var onSetKeepAwake = opts.onSetKeepAwake || null;
156
+ var onSetInheritGroups = opts.onSetInheritGroups || null;
156
157
  var onSetImageRetention = opts.onSetImageRetention || null;
157
158
  var onShutdown = opts.onShutdown || null;
158
159
  var onRestart = opts.onRestart || null;
@@ -1257,6 +1258,7 @@ function createServer(opts) {
1257
1258
  onGetDaemonConfig: onGetDaemonConfig,
1258
1259
  onSetPin: onSetPin,
1259
1260
  onSetKeepAwake: onSetKeepAwake,
1261
+ onSetInheritGroups: onSetInheritGroups,
1260
1262
  onSetImageRetention: onSetImageRetention,
1261
1263
  onSetUpdateChannel: onSetUpdateChannel,
1262
1264
  updateChannel: onGetDaemonConfig ? (onGetDaemonConfig().updateChannel || "stable") : "stable",
package/lib/terminal.js CHANGED
@@ -39,7 +39,10 @@ function createTerminal(cwd, cols, rows, osUserInfo, opts) {
39
39
  }
40
40
 
41
41
  var args = osUserInfo ? ["-l"] : [];
42
- var term = pty.spawn(shell, args, spawnOpts);
42
+ // Route through setpriv when group inheritance is enabled so the terminal
43
+ // gets the user's supplementary groups (docker, etc.), not just primary gid.
44
+ var ptySpawn = require("./os-users").wrapSpawnAsUser(shell, args, spawnOpts);
45
+ var term = pty.spawn(ptySpawn.command, ptySpawn.args, ptySpawn.options);
43
46
 
44
47
  if (opts && opts.initialInput) {
45
48
  try { term.write(opts.initialInput); } catch (e) {}
@@ -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;