clay-server 2.44.1-beta.2 → 2.45.0-beta.2
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 +1 -1
- package/lib/project-sessions.js +76 -20
- package/lib/public/app.js +1 -1
- package/lib/public/modules/app-messages.js +48 -1
- package/lib/public/modules/app-panels.js +14 -18
- package/lib/public/modules/mcp-ui.js +44 -0
- package/lib/public/modules/profile.js +2 -2
- package/lib/public/modules/skills.js +16 -0
- package/lib/public/modules/tools.js +156 -0
- package/lib/public/modules/user-settings.js +2 -2
- package/lib/sdk-bridge.js +76 -0
- package/lib/sdk-message-processor.js +67 -0
- package/lib/server-settings.js +2 -2
- package/lib/users-preferences.js +8 -7
- package/lib/ws-schema.js +9 -0
- package/lib/yoke/adapters/claude-worker.js +62 -0
- package/lib/yoke/adapters/claude.js +117 -2
- package/package.json +2 -2
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
|
|
706
|
+
matesEnabled: config.matesEnabled === true,
|
|
707
707
|
pinEnabled: !!config.pinHash,
|
|
708
708
|
platform: process.platform,
|
|
709
709
|
hostname: os2.hostname(),
|
package/lib/project-sessions.js
CHANGED
|
@@ -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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 (
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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:
|
|
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);
|
|
@@ -178,25 +178,21 @@ function hasBeta(name) {
|
|
|
178
178
|
function rebuildModelList() {
|
|
179
179
|
if (!configModelList) return;
|
|
180
180
|
// Picker visibility by vendor+mode:
|
|
181
|
-
// Claude TUI -> shown (Claude TUI accepts mid-thread model swaps).
|
|
182
|
-
// Claude
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// active queryInstance, so
|
|
188
|
-
// message
|
|
181
|
+
// Claude TUI -> shown, free (Claude TUI accepts mid-thread model swaps).
|
|
182
|
+
// GUI (Claude SDK + Codex) -> shown, but locked once the session has
|
|
183
|
+
// history. Both bind the model at session creation and
|
|
184
|
+
// changing mid-thread breaks tool schemas / cache reuse,
|
|
185
|
+
// so we allow the pick only before the first message.
|
|
186
|
+
// sdk-bridge setModel stores into sm.currentModel when
|
|
187
|
+
// there's no active queryInstance, so a pick made before
|
|
188
|
+
// the first message takes effect on session start.
|
|
189
189
|
var modelSection = configModelList.parentElement;
|
|
190
190
|
var s = store.snap();
|
|
191
191
|
var vendor = s.currentVendor || "claude";
|
|
192
|
-
|
|
193
|
-
if (modelSection) modelSection.style.display = hideModelPicker ? "none" : "";
|
|
192
|
+
if (modelSection) modelSection.style.display = "";
|
|
194
193
|
configModelList.innerHTML = "";
|
|
195
|
-
if (hideModelPicker) return;
|
|
196
194
|
|
|
197
|
-
var
|
|
198
|
-
&& s.activeSessionMode === "gui"
|
|
199
|
-
&& !!s.sessionHasHistory;
|
|
195
|
+
var lockedForGui = s.activeSessionMode === "gui" && !!s.sessionHasHistory;
|
|
200
196
|
|
|
201
197
|
var list = s.currentModels.length > 0 ? s.currentModels : (s.currentModel ? [{ value: s.currentModel, displayName: s.currentModel }] : []);
|
|
202
198
|
for (var i = 0; i < list.length; i++) {
|
|
@@ -207,12 +203,12 @@ function rebuildModelList() {
|
|
|
207
203
|
var btn = document.createElement("button");
|
|
208
204
|
btn.className = "config-radio-item";
|
|
209
205
|
if (value === s.currentModel) btn.classList.add("active");
|
|
210
|
-
if (
|
|
206
|
+
if (lockedForGui) btn.classList.add("locked");
|
|
211
207
|
btn.dataset.model = value;
|
|
212
208
|
btn.textContent = label;
|
|
213
|
-
if (
|
|
209
|
+
if (lockedForGui) {
|
|
214
210
|
btn.disabled = true;
|
|
215
|
-
btn.title = "Model is locked after the first message in a
|
|
211
|
+
btn.title = "Model is locked after the first message in a GUI session. Start a new session to change it.";
|
|
216
212
|
} else {
|
|
217
213
|
btn.addEventListener("click", function () {
|
|
218
214
|
var model = this.dataset.model;
|
|
@@ -227,7 +223,7 @@ function rebuildModelList() {
|
|
|
227
223
|
configModelList.appendChild(btn);
|
|
228
224
|
}
|
|
229
225
|
|
|
230
|
-
if (
|
|
226
|
+
if (lockedForGui) {
|
|
231
227
|
var hint = document.createElement("div");
|
|
232
228
|
hint.className = "config-model-hint";
|
|
233
229
|
hint.textContent = "Locked after first message — start a new session to change.";
|
|
@@ -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
|
|
594
|
-
var matesOn = data.matesEnabled
|
|
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
|
|
472
|
-
var matesOn = data.matesEnabled
|
|
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.
|
package/lib/server-settings.js
CHANGED
|
@@ -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
|
|
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
|
|
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/users-preferences.js
CHANGED
|
@@ -334,20 +334,21 @@ function attachPreferences(deps) {
|
|
|
334
334
|
|
|
335
335
|
// --- Per-user Mates UI toggle ---
|
|
336
336
|
//
|
|
337
|
-
// When
|
|
338
|
-
// Mate" entry, home-hub mates strip) is hidden for this user
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
// new users (no field set) get the default-
|
|
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
|
|
348
|
+
return data.users[i].matesEnabled === true;
|
|
348
349
|
}
|
|
349
350
|
}
|
|
350
|
-
return
|
|
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.
|
|
3
|
+
"version": "2.45.0-beta.2",
|
|
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.
|
|
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",
|