clay-server 2.34.0 → 2.34.1-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/project-connection.js +44 -34
- package/lib/project-sessions.js +44 -10
- package/lib/public/modules/app-rendering.js +6 -4
- package/lib/public/modules/input.js +9 -2
- package/lib/public/modules/sidebar-projects.js +12 -4
- package/lib/sdk-bridge.js +27 -2
- package/lib/sessions.js +2 -0
- package/lib/users-preferences.js +43 -1
- package/package.json +2 -2
|
@@ -60,6 +60,35 @@ function attachConnection(ctx) {
|
|
|
60
60
|
// warmup once per reconnect.
|
|
61
61
|
var _warmedUp = false;
|
|
62
62
|
|
|
63
|
+
function findRestoredActiveSession(ws, wsUser, allSessions) {
|
|
64
|
+
var active = null;
|
|
65
|
+
var presenceKey = wsUser ? wsUser.id : "_default";
|
|
66
|
+
var storedPresence = userPresence.getPresence(slug, presenceKey);
|
|
67
|
+
if (storedPresence && storedPresence.sessionId) {
|
|
68
|
+
if (sm.sessions.has(storedPresence.sessionId)) {
|
|
69
|
+
active = sm.sessions.get(storedPresence.sessionId);
|
|
70
|
+
} else {
|
|
71
|
+
sm.sessions.forEach(function (s) {
|
|
72
|
+
if (s.cliSessionId && s.cliSessionId === storedPresence.sessionId) active = s;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (active && usersModule.isMultiUser() && wsUser) {
|
|
76
|
+
if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) active = null;
|
|
77
|
+
} else if (active && !usersModule.isMultiUser() && active.ownerId) {
|
|
78
|
+
active = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!active && allSessions.length > 0) {
|
|
82
|
+
active = allSessions[0];
|
|
83
|
+
for (var fi = 1; fi < allSessions.length; fi++) {
|
|
84
|
+
if ((allSessions[fi].lastActivity || 0) > (active.lastActivity || 0)) {
|
|
85
|
+
active = allSessions[fi];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { active: active, storedPresence: storedPresence };
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
function handleConnection(ws, wsUser, handleMessage, handleDisconnection) {
|
|
64
93
|
ws._clayUser = wsUser || null;
|
|
65
94
|
clients.add(ws);
|
|
@@ -90,6 +119,18 @@ function attachConnection(ctx) {
|
|
|
90
119
|
var title = getTitle();
|
|
91
120
|
var project = getProject();
|
|
92
121
|
var ownerLocked = !!(osUsers && osUsers.length > 0 && /^\/home\/[^/]+\//.test(cwd));
|
|
122
|
+
var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
|
|
123
|
+
if (usersModule.isMultiUser() && wsUser) {
|
|
124
|
+
allSessions = allSessions.filter(function (s) {
|
|
125
|
+
return usersModule.canAccessSession(wsUser.id, s, { visibility: "public" });
|
|
126
|
+
});
|
|
127
|
+
} else if (!usersModule.isMultiUser()) {
|
|
128
|
+
allSessions = allSessions.filter(function (s) { return !s.ownerId; });
|
|
129
|
+
}
|
|
130
|
+
var restoredState = findRestoredActiveSession(ws, wsUser, allSessions);
|
|
131
|
+
var restoredActive = restoredState.active;
|
|
132
|
+
var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
|
|
133
|
+
var initialModels = (sm.modelsByVendor && sm.modelsByVendor[initialVendor]) || sm.availableModels || [];
|
|
93
134
|
sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked });
|
|
94
135
|
// Update notifications are pushed on a scheduled interval (see
|
|
95
136
|
// scheduleUpdateBroadcast). We no longer push on connect to avoid
|
|
@@ -98,8 +139,7 @@ function attachConnection(ctx) {
|
|
|
98
139
|
sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
|
|
99
140
|
}
|
|
100
141
|
if (sm.currentModel) {
|
|
101
|
-
|
|
102
|
-
sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [], vendor: sm.defaultVendor || "claude", availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
|
|
142
|
+
sendTo(ws, { type: "model_info", model: sm.currentModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
|
|
103
143
|
}
|
|
104
144
|
sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
105
145
|
sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
|
|
@@ -116,14 +156,6 @@ function attachConnection(ctx) {
|
|
|
116
156
|
if (_notifications) _notifications.sendConnectionState(ws, sendTo);
|
|
117
157
|
|
|
118
158
|
// Session list (filtered for access control)
|
|
119
|
-
var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
|
|
120
|
-
if (usersModule.isMultiUser() && wsUser) {
|
|
121
|
-
allSessions = allSessions.filter(function (s) {
|
|
122
|
-
return usersModule.canAccessSession(wsUser.id, s, { visibility: "public" });
|
|
123
|
-
});
|
|
124
|
-
} else if (!usersModule.isMultiUser()) {
|
|
125
|
-
allSessions = allSessions.filter(function (s) { return !s.ownerId; });
|
|
126
|
-
}
|
|
127
159
|
sendTo(ws, {
|
|
128
160
|
type: "session_list",
|
|
129
161
|
sessions: allSessions.map(function (s) {
|
|
@@ -152,31 +184,9 @@ function attachConnection(ctx) {
|
|
|
152
184
|
});
|
|
153
185
|
|
|
154
186
|
// Restore active session for this client from server-side presence
|
|
155
|
-
var active =
|
|
187
|
+
var active = restoredState.active;
|
|
156
188
|
var presenceKey = wsUser ? wsUser.id : "_default";
|
|
157
|
-
var storedPresence =
|
|
158
|
-
if (storedPresence && storedPresence.sessionId) {
|
|
159
|
-
if (sm.sessions.has(storedPresence.sessionId)) {
|
|
160
|
-
active = sm.sessions.get(storedPresence.sessionId);
|
|
161
|
-
} else {
|
|
162
|
-
sm.sessions.forEach(function (s) {
|
|
163
|
-
if (s.cliSessionId && s.cliSessionId === storedPresence.sessionId) active = s;
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
if (active && usersModule.isMultiUser() && wsUser) {
|
|
167
|
-
if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) active = null;
|
|
168
|
-
} else if (active && !usersModule.isMultiUser() && active.ownerId) {
|
|
169
|
-
active = null;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (!active && allSessions.length > 0) {
|
|
173
|
-
active = allSessions[0];
|
|
174
|
-
for (var fi = 1; fi < allSessions.length; fi++) {
|
|
175
|
-
if ((allSessions[fi].lastActivity || 0) > (active.lastActivity || 0)) {
|
|
176
|
-
active = allSessions[fi];
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
189
|
+
var storedPresence = restoredState.storedPresence;
|
|
180
190
|
var autoCreated = false;
|
|
181
191
|
if (!active) {
|
|
182
192
|
var autoOpts = {};
|
package/lib/project-sessions.js
CHANGED
|
@@ -227,6 +227,24 @@ function attachSessions(ctx) {
|
|
|
227
227
|
if (msg.type === "resume_session") {
|
|
228
228
|
if (!msg.cliSessionId) return true;
|
|
229
229
|
var cliSess = require("./cli-sessions");
|
|
230
|
+
|
|
231
|
+
// If Clay already has a persisted meta file for this cliSessionId, read
|
|
232
|
+
// its vendor so resumeSession doesn't silently default to the project's
|
|
233
|
+
// primary vendor (which would break codex sessions after server restart).
|
|
234
|
+
var persistedVendor = null;
|
|
235
|
+
try {
|
|
236
|
+
var _fsResume = require("fs");
|
|
237
|
+
var _pathResume = require("path");
|
|
238
|
+
var metaPath = _pathResume.join(sm.sessionsDir, msg.cliSessionId + ".jsonl");
|
|
239
|
+
if (_fsResume.existsSync(metaPath)) {
|
|
240
|
+
var firstLine = _fsResume.readFileSync(metaPath, "utf8").split("\n", 1)[0];
|
|
241
|
+
try {
|
|
242
|
+
var metaObj = JSON.parse(firstLine);
|
|
243
|
+
if (metaObj && metaObj.type === "meta" && metaObj.vendor) persistedVendor = metaObj.vendor;
|
|
244
|
+
} catch (e) {}
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {}
|
|
247
|
+
|
|
230
248
|
// Try SDK for title first, then fall back to manual parsing
|
|
231
249
|
var titlePromise = adapter.getSessionInfo(msg.cliSessionId, { dir: cwd }).then(function(info) {
|
|
232
250
|
return (info && info.summary) ? info.summary.substring(0, 100) : null;
|
|
@@ -247,10 +265,10 @@ function attachSessions(ctx) {
|
|
|
247
265
|
}
|
|
248
266
|
}
|
|
249
267
|
}
|
|
250
|
-
var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title }, ws);
|
|
268
|
+
var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title, vendor: persistedVendor || undefined }, ws);
|
|
251
269
|
if (resumed) ws._clayActiveSession = resumed.localId;
|
|
252
270
|
}).catch(function() {
|
|
253
|
-
var resumed = sm.resumeSession(msg.cliSessionId, undefined, ws);
|
|
271
|
+
var resumed = sm.resumeSession(msg.cliSessionId, persistedVendor ? { vendor: persistedVendor } : undefined, ws);
|
|
254
272
|
if (resumed) ws._clayActiveSession = resumed.localId;
|
|
255
273
|
});
|
|
256
274
|
return true;
|
|
@@ -311,7 +329,13 @@ function attachSessions(ctx) {
|
|
|
311
329
|
if (switchTargetSess && sm.currentModel) {
|
|
312
330
|
var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
|
|
313
331
|
var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
|
|
314
|
-
|
|
332
|
+
var found = false;
|
|
333
|
+
for (var tvi = 0; tvi < tvModels.length; tvi++) {
|
|
334
|
+
var tvEntry = tvModels[tvi];
|
|
335
|
+
var tvVal = typeof tvEntry === "string" ? tvEntry : (tvEntry && (tvEntry.value || tvEntry.id)) || "";
|
|
336
|
+
if (tvVal === sm.currentModel) { found = true; break; }
|
|
337
|
+
}
|
|
338
|
+
if (tvModels.length > 0 && !found) {
|
|
315
339
|
sm.currentModel = "";
|
|
316
340
|
}
|
|
317
341
|
}
|
|
@@ -507,14 +531,24 @@ function attachSessions(ctx) {
|
|
|
507
531
|
if (msg.type === "set_vendor" && msg.vendor) {
|
|
508
532
|
var vendorSession = getSessionForWs(ws);
|
|
509
533
|
if (vendorSession) {
|
|
510
|
-
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
534
|
+
// Refuse to rebind vendor on a session that is already bound to a
|
|
535
|
+
// different CLI (cliSessionId is vendor-specific). This prevents a
|
|
536
|
+
// stale client-side vendor state from clobbering the persisted vendor
|
|
537
|
+
// on page reload / server restart.
|
|
538
|
+
var alreadyBound = vendorSession.cliSessionId && vendorSession.vendor && vendorSession.vendor !== msg.vendor;
|
|
539
|
+
if (alreadyBound) {
|
|
540
|
+
console.warn("[project] set_vendor ignored: session " + vendorSession.localId +
|
|
541
|
+
" is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
|
|
542
|
+
} else {
|
|
543
|
+
vendorSession.vendor = msg.vendor;
|
|
544
|
+
// Clear the shared model so the next query uses the vendor's default
|
|
545
|
+
// instead of leaking the previous vendor's model into a fresh session.
|
|
546
|
+
if (sm.currentModel) {
|
|
547
|
+
sm.currentModel = "";
|
|
548
|
+
}
|
|
549
|
+
sm.saveSessionFile(vendorSession);
|
|
550
|
+
sm.broadcastSessionList();
|
|
515
551
|
}
|
|
516
|
-
sm.saveSessionFile(vendorSession);
|
|
517
|
-
sm.broadcastSessionList();
|
|
518
552
|
}
|
|
519
553
|
if (msg.vendor) {
|
|
520
554
|
var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
|
|
@@ -10,7 +10,7 @@ import { iconHtml, refreshIcons } from './icons.js';
|
|
|
10
10
|
import { userAvatarUrl } from './avatar.js';
|
|
11
11
|
import { closeToolGroup } from './tools.js';
|
|
12
12
|
import { showImageModal, showPasteModal } from './app-misc.js';
|
|
13
|
-
import { sendMessage } from './input.js';
|
|
13
|
+
import { sendMessage, hasSendableContent } from './input.js';
|
|
14
14
|
import { getChatLayout } from './theme.js';
|
|
15
15
|
import { getScheduledMsgEl } from './app-rate-limit.js';
|
|
16
16
|
|
|
@@ -547,9 +547,11 @@ export function getGhostSuggestion() {
|
|
|
547
547
|
|
|
548
548
|
export function showSuggestionChips(suggestion) {
|
|
549
549
|
if (!suggestion || store.get('processing')) return;
|
|
550
|
-
|
|
551
|
-
//
|
|
552
|
-
|
|
550
|
+
// Only show ghost text when there is no sendable content — typed text,
|
|
551
|
+
// pending pastes, pending images, or pending files all suppress the
|
|
552
|
+
// suggestion so Enter can't accidentally send it instead of the user's
|
|
553
|
+
// actual attached content.
|
|
554
|
+
if (hasSendableContent()) return;
|
|
553
555
|
_ghostSuggestionText = suggestion;
|
|
554
556
|
var ghostEl = document.getElementById("ghost-suggestion");
|
|
555
557
|
if (!ghostEl) return;
|
|
@@ -360,6 +360,9 @@ function renderInputPreviews() {
|
|
|
360
360
|
return;
|
|
361
361
|
}
|
|
362
362
|
bar.classList.add("visible");
|
|
363
|
+
// Hide any ghost suggestion as soon as attached content appears — Enter
|
|
364
|
+
// must not silently swallow the user's paste/image/file.
|
|
365
|
+
if (ctx && ctx.hideSuggestionChips) ctx.hideSuggestionChips();
|
|
363
366
|
|
|
364
367
|
// Image thumbnails
|
|
365
368
|
for (var i = 0; i < pendingImages.length; i++) {
|
|
@@ -1040,9 +1043,13 @@ export function initInput(_ctx) {
|
|
|
1040
1043
|
return;
|
|
1041
1044
|
}
|
|
1042
1045
|
e.preventDefault();
|
|
1043
|
-
// If input
|
|
1046
|
+
// If input has no sendable content but ghost suggestion is showing, adopt it.
|
|
1047
|
+
// Use hasSendableContent() instead of checking inputEl.value alone so that
|
|
1048
|
+
// pending images, pastes, or files block the ghost-text adoption — otherwise
|
|
1049
|
+
// pressing Enter with only a pasted image/block queued would send the
|
|
1050
|
+
// suggestion instead of the user's actual content.
|
|
1044
1051
|
var ghost = ctx.getGhostSuggestion ? ctx.getGhostSuggestion() : "";
|
|
1045
|
-
if (!
|
|
1052
|
+
if (!hasSendableContent() && ghost) {
|
|
1046
1053
|
ctx.inputEl.value = ghost;
|
|
1047
1054
|
if (ctx.hideSuggestionChips) ctx.hideSuggestionChips();
|
|
1048
1055
|
}
|
|
@@ -1257,10 +1257,18 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
1257
1257
|
wtEl.href = "/p/" + wt.slug + "/";
|
|
1258
1258
|
wtEl.dataset.slug = wt.slug;
|
|
1259
1259
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1260
|
+
if (wt.icon) {
|
|
1261
|
+
var wtEmoji = document.createElement("span");
|
|
1262
|
+
wtEmoji.className = "wt-branch-abbrev project-emoji";
|
|
1263
|
+
wtEmoji.textContent = wt.icon;
|
|
1264
|
+
parseEmojis(wtEmoji);
|
|
1265
|
+
wtEl.appendChild(wtEmoji);
|
|
1266
|
+
} else {
|
|
1267
|
+
var abbrev = document.createElement("span");
|
|
1268
|
+
abbrev.className = "wt-branch-abbrev";
|
|
1269
|
+
abbrev.textContent = getProjectAbbrev(wt.name);
|
|
1270
|
+
wtEl.appendChild(abbrev);
|
|
1271
|
+
}
|
|
1264
1272
|
|
|
1265
1273
|
var wtStatus = document.createElement("span");
|
|
1266
1274
|
wtStatus.className = "icon-strip-status";
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -182,6 +182,22 @@ function createSDKBridge(opts) {
|
|
|
182
182
|
return sm.availableModels || [];
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// Model list entries may be plain strings (Codex) or { value, displayName }
|
|
186
|
+
// objects (Claude SDK). Normalize to the identifier string.
|
|
187
|
+
function modelEntryValue(entry) {
|
|
188
|
+
if (!entry) return "";
|
|
189
|
+
if (typeof entry === "string") return entry;
|
|
190
|
+
return entry.value || entry.id || "";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function modelListContains(list, modelId) {
|
|
194
|
+
if (!list || !modelId) return false;
|
|
195
|
+
for (var mi = 0; mi < list.length; mi++) {
|
|
196
|
+
if (modelEntryValue(list[mi]) === modelId) return true;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
185
201
|
function sendModelInfoForVendor(vendor, model) {
|
|
186
202
|
send({
|
|
187
203
|
type: "model_info",
|
|
@@ -1221,10 +1237,15 @@ function createSDKBridge(opts) {
|
|
|
1221
1237
|
var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
|
|
1222
1238
|
if (sessionVendor) {
|
|
1223
1239
|
var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
|
|
1224
|
-
if (vendorModels.length > 0 && queryModel && vendorModels
|
|
1225
|
-
queryModel = vendorModels[0];
|
|
1240
|
+
if (vendorModels.length > 0 && queryModel && !modelListContains(vendorModels, queryModel)) {
|
|
1241
|
+
queryModel = modelEntryValue(vendorModels[0]);
|
|
1226
1242
|
}
|
|
1227
1243
|
}
|
|
1244
|
+
// Guard against anything upstream having set queryModel to an object
|
|
1245
|
+
// (e.g. a cached ModelInfo leaked through). Always coerce to string id.
|
|
1246
|
+
if (queryModel && typeof queryModel !== "string") {
|
|
1247
|
+
queryModel = modelEntryValue(queryModel) || undefined;
|
|
1248
|
+
}
|
|
1228
1249
|
|
|
1229
1250
|
var codexConfig = getCodexConfig(sm);
|
|
1230
1251
|
var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
|
|
@@ -1480,6 +1501,10 @@ function createSDKBridge(opts) {
|
|
|
1480
1501
|
}
|
|
1481
1502
|
|
|
1482
1503
|
async function setModel(session, model) {
|
|
1504
|
+
// Normalize to string id in case a { value, displayName } object slips in
|
|
1505
|
+
if (model && typeof model !== "string") {
|
|
1506
|
+
model = modelEntryValue(model);
|
|
1507
|
+
}
|
|
1483
1508
|
if (!session.queryInstance) {
|
|
1484
1509
|
// No active query — just store the model for next startQuery
|
|
1485
1510
|
sm.currentModel = model;
|
package/lib/sessions.js
CHANGED
|
@@ -661,6 +661,8 @@ function createSessionManager(opts) {
|
|
|
661
661
|
bookmarked: false,
|
|
662
662
|
favoriteOrder: null,
|
|
663
663
|
};
|
|
664
|
+
if (opts && opts.vendor) session.vendor = opts.vendor;
|
|
665
|
+
if (opts && opts.ownerId) session.ownerId = opts.ownerId;
|
|
664
666
|
sessions.set(localId, session);
|
|
665
667
|
saveSessionFile(session);
|
|
666
668
|
switchSession(localId, targetWs);
|
package/lib/users-preferences.js
CHANGED
|
@@ -87,6 +87,36 @@ function attachPreferences(deps) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// --- Deleted built-in mate keys tracking ---
|
|
90
|
+
//
|
|
91
|
+
// In single-user mode there is no users.json, so the user row lookup
|
|
92
|
+
// below returns nothing and the key is silently dropped. That made
|
|
93
|
+
// "Remove mate" in the sidebar picker a no-op: the key was never
|
|
94
|
+
// persisted, ensureBuiltinMates re-created the mate on next mate_list,
|
|
95
|
+
// and the user could not actually get rid of built-in mates.
|
|
96
|
+
//
|
|
97
|
+
// Fallback: when the user record isn't found (single-user mode), read
|
|
98
|
+
// and write deletedBuiltinKeys on daemon.json via lib/config.js. This
|
|
99
|
+
// preserves multi-user behavior (users.json row still wins) while
|
|
100
|
+
// giving single-user deploys a place to persist the setting.
|
|
101
|
+
|
|
102
|
+
function loadSingleUserDeletedKeys() {
|
|
103
|
+
try {
|
|
104
|
+
var config = require("./config");
|
|
105
|
+
var cfg = config.loadConfig() || {};
|
|
106
|
+
return Array.isArray(cfg.deletedBuiltinKeys) ? cfg.deletedBuiltinKeys : [];
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function saveSingleUserDeletedKeys(keys) {
|
|
113
|
+
try {
|
|
114
|
+
var config = require("./config");
|
|
115
|
+
var cfg = config.loadConfig() || {};
|
|
116
|
+
cfg.deletedBuiltinKeys = keys;
|
|
117
|
+
config.saveConfig(cfg);
|
|
118
|
+
} catch (e) {}
|
|
119
|
+
}
|
|
90
120
|
|
|
91
121
|
function getDeletedBuiltinKeys(userId) {
|
|
92
122
|
var data = loadUsers();
|
|
@@ -95,7 +125,7 @@ function attachPreferences(deps) {
|
|
|
95
125
|
return data.users[i].deletedBuiltinKeys || [];
|
|
96
126
|
}
|
|
97
127
|
}
|
|
98
|
-
return
|
|
128
|
+
return loadSingleUserDeletedKeys();
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
function addDeletedBuiltinKey(userId, key) {
|
|
@@ -110,6 +140,12 @@ function attachPreferences(deps) {
|
|
|
110
140
|
return;
|
|
111
141
|
}
|
|
112
142
|
}
|
|
143
|
+
// Single-user fallback
|
|
144
|
+
var keys = loadSingleUserDeletedKeys();
|
|
145
|
+
if (keys.indexOf(key) === -1) {
|
|
146
|
+
keys.push(key);
|
|
147
|
+
saveSingleUserDeletedKeys(keys);
|
|
148
|
+
}
|
|
113
149
|
}
|
|
114
150
|
|
|
115
151
|
function removeDeletedBuiltinKey(userId, key) {
|
|
@@ -124,6 +160,12 @@ function attachPreferences(deps) {
|
|
|
124
160
|
return;
|
|
125
161
|
}
|
|
126
162
|
}
|
|
163
|
+
// Single-user fallback
|
|
164
|
+
var keys = loadSingleUserDeletedKeys();
|
|
165
|
+
var filtered = keys.filter(function (k) { return k !== key; });
|
|
166
|
+
if (filtered.length !== keys.length) {
|
|
167
|
+
saveSingleUserDeletedKeys(filtered);
|
|
168
|
+
}
|
|
127
169
|
}
|
|
128
170
|
|
|
129
171
|
// --- Per-user chat layout setting ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clay-server",
|
|
3
|
-
"version": "2.34.
|
|
3
|
+
"version": "2.34.1-beta.2",
|
|
4
4
|
"description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clay-server": "./bin/cli.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@anthropic-ai/claude-agent-sdk": "^0.2.112",
|
|
40
40
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
41
|
-
"@openai/codex": "^0.
|
|
41
|
+
"@openai/codex": "^0.124.0",
|
|
42
42
|
"imapflow": "^1.3.1",
|
|
43
43
|
"nodemailer": "^6.10.1",
|
|
44
44
|
"qrcode-terminal": "^0.12.0",
|