clay-server 2.34.0-beta.9 → 2.34.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- // Vendor is resolved per-session in session_switched; send default here
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) {
@@ -146,36 +178,15 @@ function attachConnection(ctx) {
146
178
  ownerId: s.ownerId || null,
147
179
  sessionVisibility: s.sessionVisibility || "shared",
148
180
  bookmarked: !!s.bookmarked,
181
+ favoriteOrder: typeof s.favoriteOrder === "number" ? s.favoriteOrder : null,
149
182
  };
150
183
  }),
151
184
  });
152
185
 
153
186
  // Restore active session for this client from server-side presence
154
- var active = null;
187
+ var active = restoredState.active;
155
188
  var presenceKey = wsUser ? wsUser.id : "_default";
156
- var storedPresence = userPresence.getPresence(slug, presenceKey);
157
- if (storedPresence && storedPresence.sessionId) {
158
- if (sm.sessions.has(storedPresence.sessionId)) {
159
- active = sm.sessions.get(storedPresence.sessionId);
160
- } else {
161
- sm.sessions.forEach(function (s) {
162
- if (s.cliSessionId && s.cliSessionId === storedPresence.sessionId) active = s;
163
- });
164
- }
165
- if (active && usersModule.isMultiUser() && wsUser) {
166
- if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) active = null;
167
- } else if (active && !usersModule.isMultiUser() && active.ownerId) {
168
- active = null;
169
- }
170
- }
171
- if (!active && allSessions.length > 0) {
172
- active = allSessions[0];
173
- for (var fi = 1; fi < allSessions.length; fi++) {
174
- if ((allSessions[fi].lastActivity || 0) > (active.lastActivity || 0)) {
175
- active = allSessions[fi];
176
- }
177
- }
178
- }
189
+ var storedPresence = restoredState.storedPresence;
179
190
  var autoCreated = false;
180
191
  if (!active) {
181
192
  var autoOpts = {};
@@ -164,6 +164,39 @@ function attachSessions(ctx) {
164
164
  return true;
165
165
  }
166
166
 
167
+ if (msg.type === "reorder_session_bookmarks") {
168
+ if (typeof msg.sourceId === "number" && typeof msg.targetId === "number" && msg.sourceId !== msg.targetId) {
169
+ var source = sm.sessions.get(msg.sourceId);
170
+ var target = sm.sessions.get(msg.targetId);
171
+ if (!source || !target) return true;
172
+ if (usersModule.isMultiUser() && ws._clayUser) {
173
+ if (!usersModule.canAccessSession(ws._clayUser.id, source, { visibility: "public" })) return true;
174
+ if (!usersModule.canAccessSession(ws._clayUser.id, target, { visibility: "public" })) return true;
175
+ }
176
+ sm.reorderBookmarkedSessions(msg.sourceId, msg.targetId, msg.insertBefore !== false);
177
+ }
178
+ return true;
179
+ }
180
+
181
+ if (msg.type === "bulk_delete_sessions") {
182
+ if (!Array.isArray(msg.sessionIds) || msg.sessionIds.length === 0) return true;
183
+ var deletableIds = [];
184
+ for (var di = 0; di < msg.sessionIds.length; di++) {
185
+ var bulkId = msg.sessionIds[di];
186
+ if (typeof bulkId !== "number") continue;
187
+ var bulkTarget = sm.sessions.get(bulkId);
188
+ if (!bulkTarget) continue;
189
+ if (usersModule.isMultiUser() && ws._clayUser) {
190
+ if (!usersModule.canAccessSession(ws._clayUser.id, bulkTarget, { visibility: "public" })) continue;
191
+ }
192
+ deletableIds.push(bulkId);
193
+ }
194
+ if (deletableIds.length > 0) {
195
+ sm.deleteSessionsBulk(deletableIds, ws);
196
+ }
197
+ return true;
198
+ }
199
+
167
200
  if (msg.type === "transfer_project_owner") {
168
201
  // Home directory projects: ownership is permanently locked
169
202
  if (osUsers && osUsers.length > 0 && /^\/home\/[^/]+\//.test(cwd)) {
@@ -194,6 +227,24 @@ function attachSessions(ctx) {
194
227
  if (msg.type === "resume_session") {
195
228
  if (!msg.cliSessionId) return true;
196
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
+
197
248
  // Try SDK for title first, then fall back to manual parsing
198
249
  var titlePromise = adapter.getSessionInfo(msg.cliSessionId, { dir: cwd }).then(function(info) {
199
250
  return (info && info.summary) ? info.summary.substring(0, 100) : null;
@@ -214,10 +265,10 @@ function attachSessions(ctx) {
214
265
  }
215
266
  }
216
267
  }
217
- 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);
218
269
  if (resumed) ws._clayActiveSession = resumed.localId;
219
270
  }).catch(function() {
220
- var resumed = sm.resumeSession(msg.cliSessionId, undefined, ws);
271
+ var resumed = sm.resumeSession(msg.cliSessionId, persistedVendor ? { vendor: persistedVendor } : undefined, ws);
221
272
  if (resumed) ws._clayActiveSession = resumed.localId;
222
273
  });
223
274
  return true;
@@ -278,7 +329,13 @@ function attachSessions(ctx) {
278
329
  if (switchTargetSess && sm.currentModel) {
279
330
  var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
280
331
  var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
281
- if (tvModels.length > 0 && tvModels.indexOf(sm.currentModel) === -1) {
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) {
282
339
  sm.currentModel = "";
283
340
  }
284
341
  }
@@ -474,14 +531,24 @@ function attachSessions(ctx) {
474
531
  if (msg.type === "set_vendor" && msg.vendor) {
475
532
  var vendorSession = getSessionForWs(ws);
476
533
  if (vendorSession) {
477
- vendorSession.vendor = msg.vendor;
478
- // Clear the shared model so the next query uses the vendor's default
479
- // instead of leaking the previous vendor's model into a fresh session.
480
- if (sm.currentModel) {
481
- sm.currentModel = "";
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();
482
551
  }
483
- sm.saveSessionFile(vendorSession);
484
- sm.broadcastSessionList();
485
552
  }
486
553
  if (msg.vendor) {
487
554
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
@@ -526,20 +526,6 @@
526
526
  white-space: nowrap;
527
527
  }
528
528
 
529
- .mobile-session-bookmark {
530
- display: inline-flex;
531
- align-items: center;
532
- color: var(--accent, #ff7b54);
533
- flex-shrink: 0;
534
- }
535
-
536
- .mobile-session-bookmark .lucide,
537
- .mobile-session-bookmark svg {
538
- width: 13px;
539
- height: 13px;
540
- display: block;
541
- }
542
-
543
529
  .mobile-session-processing {
544
530
  width: 7px;
545
531
  height: 7px;
@@ -309,45 +309,49 @@
309
309
 
310
310
  .light-theme .notif-banner-clear-all {
311
311
  background:
312
- linear-gradient(180deg, rgba(255,255,255,0.88), rgba(255,255,255,0.72)),
313
- rgba(255,255,255,0.62);
314
- border-color: rgba(24, 32, 56, 0.10);
312
+ linear-gradient(180deg, rgba(255,255,255,0.54), rgba(255,255,255,0.28)),
313
+ rgba(248,250,255,0.22);
314
+ backdrop-filter: blur(10px) saturate(1.08);
315
+ -webkit-backdrop-filter: blur(10px) saturate(1.08);
316
+ border-color: rgba(255,255,255,0.52);
315
317
  color: rgba(36, 45, 66, 0.72);
316
318
  box-shadow:
317
- inset 0 1px 0 rgba(255,255,255,0.85),
318
- 0 8px 22px rgba(31, 41, 55, 0.08);
319
+ inset 0 1px 0 rgba(255,255,255,0.78),
320
+ 0 8px 22px rgba(31, 41, 55, 0.07);
319
321
  }
320
322
 
321
323
  .light-theme .notif-banner-clear-all:hover {
322
324
  color: rgba(22, 29, 45, 0.92);
323
325
  background:
324
- linear-gradient(180deg, rgba(255,255,255,0.94), rgba(255,255,255,0.78)),
325
- rgba(255,255,255,0.7);
326
- border-color: rgba(24, 32, 56, 0.14);
326
+ linear-gradient(180deg, rgba(255,255,255,0.62), rgba(255,255,255,0.34)),
327
+ rgba(248,250,255,0.26);
328
+ border-color: rgba(255,255,255,0.6);
327
329
  }
328
330
 
329
331
  .light-theme .notif-banner {
330
332
  background:
331
- linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.74)),
332
- rgba(255,255,255,0.60);
333
- border-color: rgba(27, 35, 56, 0.08);
333
+ linear-gradient(180deg, rgba(255,255,255,0.58), rgba(255,255,255,0.22)),
334
+ rgba(248,250,255,0.2);
335
+ backdrop-filter: blur(12px) saturate(1.05);
336
+ -webkit-backdrop-filter: blur(12px) saturate(1.05);
337
+ border: 1px solid rgba(255,255,255,0.5);
334
338
  box-shadow:
335
- inset 0 1px 0 rgba(255,255,255,0.88),
336
- 0 10px 30px rgba(31, 41, 55, 0.10);
339
+ inset 0 1px 0 rgba(255,255,255,0.72),
340
+ 0 10px 30px rgba(31, 41, 55, 0.09);
337
341
  }
338
342
 
339
343
  .light-theme .notif-banner:hover {
340
- border-color: rgba(27, 35, 56, 0.12);
344
+ border-color: rgba(255,255,255,0.62);
341
345
  box-shadow:
342
- inset 0 1px 0 rgba(255,255,255,0.92),
343
- 0 12px 34px rgba(31, 41, 55, 0.12);
346
+ inset 0 1px 0 rgba(255,255,255,0.8),
347
+ 0 12px 34px rgba(31, 41, 55, 0.11);
344
348
  }
345
349
 
346
350
  .light-theme .notif-banner-icon {
347
- background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(244,247,252,0.82));
348
- border-color: rgba(27, 35, 56, 0.08);
351
+ background: linear-gradient(180deg, rgba(255,255,255,0.72), rgba(246,249,255,0.36));
352
+ border-color: rgba(255,255,255,0.52);
349
353
  color: var(--text-secondary);
350
- box-shadow: inset 0 1px 0 rgba(255,255,255,0.92);
354
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.78);
351
355
  }
352
356
 
353
357
  .light-theme .notif-banner-project {