clay-server 2.11.0-beta.8 → 2.11.0

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/sessions.js CHANGED
@@ -10,6 +10,7 @@ function createSessionManager(opts) {
10
10
  var sendTo = opts.sendTo || null; // function(ws, obj) - send to specific client
11
11
  var sendEach = opts.sendEach || null; // function(fn) - call fn(ws) for each connected client
12
12
  var sendAndRecord = null; // set after init via setSendAndRecord
13
+ var onSessionDone = opts.onSessionDone || function () {};
13
14
 
14
15
  // --- Multi-session state ---
15
16
  var nextLocalId = 1;
@@ -17,6 +18,7 @@ function createSessionManager(opts) {
17
18
  var activeSessionId = null; // currently active local ID
18
19
  var slashCommands = null; // shared across sessions
19
20
  var skillNames = null; // Claude-only skills to filter from slash menu
21
+ var singleUserUnread = {}; // sessionLocalId -> unread count (single-user mode)
20
22
 
21
23
  // --- Session persistence (centralized in ~/.clay/sessions/{encoded-cwd}/) ---
22
24
  var sessionsBase = path.join(config.CONFIG_DIR, "sessions");
@@ -191,7 +193,7 @@ function createSessionManager(opts) {
191
193
  resolveLoopInfo = fn;
192
194
  }
193
195
 
194
- function mapSessionForClient(s, clientActiveId) {
196
+ function mapSessionForClient(s, clientActiveId, wsUnread) {
195
197
  var loop = s.loop ? Object.assign({}, s.loop) : null;
196
198
  if (loop && loop.loopId && resolveLoopInfo) {
197
199
  var info = resolveLoopInfo(loop.loopId);
@@ -201,6 +203,7 @@ function createSessionManager(opts) {
201
203
  }
202
204
  }
203
205
  var isActive = (typeof clientActiveId === "number") ? s.localId === clientActiveId : s.localId === activeSessionId;
206
+ var unreadMap = wsUnread || singleUserUnread;
204
207
  return {
205
208
  id: s.localId,
206
209
  cliSessionId: s.cliSessionId || null,
@@ -211,6 +214,7 @@ function createSessionManager(opts) {
211
214
  loop: loop,
212
215
  ownerId: s.ownerId || null,
213
216
  sessionVisibility: s.sessionVisibility || "shared",
217
+ unread: unreadMap[s.localId] || 0,
214
218
  };
215
219
  }
216
220
 
@@ -234,10 +238,11 @@ function createSessionManager(opts) {
234
238
  sendEach(function (ws, filterFn) {
235
239
  var filtered = filterFn ? allVisible.filter(filterFn) : allVisible;
236
240
  var clientActiveId = ws._clayActiveSession;
241
+ var wsUnread = ws._clayUnread || {};
237
242
  if (ws.readyState === 1) {
238
243
  ws.send(JSON.stringify({
239
244
  type: "session_list",
240
- sessions: filtered.map(function (s) { return mapSessionForClient(s, clientActiveId); }),
245
+ sessions: filtered.map(function (s) { return mapSessionForClient(s, clientActiveId, wsUnread); }),
241
246
  }));
242
247
  }
243
248
  });
@@ -325,7 +330,13 @@ function createSessionManager(opts) {
325
330
  if (!session) return;
326
331
 
327
332
  activeSessionId = localId;
328
- if (targetWs) targetWs._clayActiveSession = localId;
333
+ if (targetWs) {
334
+ targetWs._clayActiveSession = localId;
335
+ // Clear unread for this session (multi-user)
336
+ if (targetWs._clayUnread) targetWs._clayUnread[localId] = 0;
337
+ }
338
+ // Clear unread for single-user mode
339
+ singleUserUnread[localId] = 0;
329
340
 
330
341
  // In multi-user mode with a specific client, only send to that client
331
342
  var _send = (targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send;
@@ -357,6 +368,9 @@ function createSessionManager(opts) {
357
368
  var session = sessions.get(localId);
358
369
  if (!session) return;
359
370
 
371
+ // Clean up unread tracking
372
+ delete singleUserUnread[localId];
373
+
360
374
  if (session.abortController) {
361
375
  try { session.abortController.abort(); } catch(e) {}
362
376
  }
@@ -385,6 +399,7 @@ function createSessionManager(opts) {
385
399
  function deleteSessionQuiet(localId) {
386
400
  var session = sessions.get(localId);
387
401
  if (!session) return;
402
+ delete singleUserUnread[localId];
388
403
  if (session.abortController) {
389
404
  try { session.abortController.abort(); } catch(e) {}
390
405
  }
@@ -411,6 +426,14 @@ function createSessionManager(opts) {
411
426
  if (!ioData) ioData = JSON.stringify({ type: "session_io", id: session.localId });
412
427
  if (ws.readyState === 1) ws.send(ioData);
413
428
  }
429
+ // Track unread: increment on "done" for clients not viewing this session
430
+ if (obj.type === "done" && ws._clayActiveSession !== session.localId) {
431
+ if (!ws._clayUnread) ws._clayUnread = {};
432
+ ws._clayUnread[session.localId] = (ws._clayUnread[session.localId] || 0) + 1;
433
+ if (ws.readyState === 1) {
434
+ ws.send(JSON.stringify({ type: "session_unread", id: session.localId, count: ws._clayUnread[session.localId] }));
435
+ }
436
+ }
414
437
  });
415
438
  if (session.isProcessing && !session._ioThrottle && ioData) {
416
439
  session._ioThrottle = true;
@@ -418,11 +441,20 @@ function createSessionManager(opts) {
418
441
  }
419
442
  } else if (session.localId === activeSessionId) {
420
443
  send(obj);
421
- } else if (session.isProcessing && !session._ioThrottle) {
422
- session._ioThrottle = true;
423
- send({ type: "session_io", id: session.localId });
424
- setTimeout(function () { session._ioThrottle = false; }, 80);
444
+ } else {
445
+ // Track unread for single-user mode on "done"
446
+ if (obj.type === "done") {
447
+ singleUserUnread[session.localId] = (singleUserUnread[session.localId] || 0) + 1;
448
+ send({ type: "session_unread", id: session.localId, count: singleUserUnread[session.localId] });
449
+ }
450
+ if (session.isProcessing && !session._ioThrottle) {
451
+ session._ioThrottle = true;
452
+ send({ type: "session_io", id: session.localId });
453
+ setTimeout(function () { session._ioThrottle = false; }, 80);
454
+ }
425
455
  }
456
+ // Notify server for cross-project unread tracking
457
+ if (obj.type === "done") onSessionDone();
426
458
  }
427
459
 
428
460
  function resumeSession(cliSessionId, opts, targetWs) {
@@ -550,6 +582,15 @@ function createSessionManager(opts) {
550
582
  deleteSessionQuiet: deleteSessionQuiet,
551
583
  resumeSession: resumeSession,
552
584
  broadcastSessionList: broadcastSessionList,
585
+ getTotalUnread: function (ws) {
586
+ var unreadMap = ws && ws._clayUnread ? ws._clayUnread : singleUserUnread;
587
+ var total = 0;
588
+ var keys = Object.keys(unreadMap);
589
+ for (var i = 0; i < keys.length; i++) {
590
+ total += unreadMap[keys[i]] || 0;
591
+ }
592
+ return total;
593
+ },
553
594
  saveSessionFile: saveSessionFile,
554
595
  appendToSessionFile: appendToSessionFile,
555
596
  sendAndRecord: doSendAndRecord,
@@ -74,10 +74,12 @@ function createTerminalManager(opts) {
74
74
  var session = terminals.get(id);
75
75
  if (!session) return false;
76
76
 
77
+ // Skip scrollback replay if already subscribed (e.g. create then activate)
78
+ var alreadySubscribed = session.subscribers.has(ws);
77
79
  session.subscribers.add(ws);
78
80
 
79
- // Replay scrollback
80
- if (session.scrollback.length > 0) {
81
+ // Replay scrollback only for newly attached clients
82
+ if (!alreadySubscribed && session.scrollback.length > 0) {
81
83
  var replay = session.scrollback.join("");
82
84
  sendTo(ws, { type: "term_output", id: id, data: replay });
83
85
  }
package/lib/terminal.js CHANGED
@@ -32,7 +32,8 @@ function createTerminal(cwd, cols, rows, osUserInfo) {
32
32
  if (osUserInfo.shell) shell = osUserInfo.shell;
33
33
  }
34
34
 
35
- var term = pty.spawn(shell, [], spawnOpts);
35
+ var args = osUserInfo ? ["-l"] : [];
36
+ var term = pty.spawn(shell, args, spawnOpts);
36
37
 
37
38
  return term;
38
39
  }
package/lib/users.js CHANGED
@@ -455,6 +455,90 @@ function removeExpiredInvites() {
455
455
  if (data.invites.length !== before) saveUsers(data);
456
456
  }
457
457
 
458
+ // --- DM Favorites ---
459
+
460
+ function getDmFavorites(userId) {
461
+ var data = loadUsers();
462
+ for (var i = 0; i < data.users.length; i++) {
463
+ if (data.users[i].id === userId) {
464
+ return data.users[i].dmFavorites || [];
465
+ }
466
+ }
467
+ return [];
468
+ }
469
+
470
+ function addDmFavorite(userId, targetUserId) {
471
+ var data = loadUsers();
472
+ for (var i = 0; i < data.users.length; i++) {
473
+ if (data.users[i].id === userId) {
474
+ if (!data.users[i].dmFavorites) data.users[i].dmFavorites = [];
475
+ if (data.users[i].dmFavorites.indexOf(targetUserId) === -1) {
476
+ data.users[i].dmFavorites.push(targetUserId);
477
+ saveUsers(data);
478
+ }
479
+ return data.users[i].dmFavorites;
480
+ }
481
+ }
482
+ return [];
483
+ }
484
+
485
+ function removeDmFavorite(userId, targetUserId) {
486
+ var data = loadUsers();
487
+ for (var i = 0; i < data.users.length; i++) {
488
+ if (data.users[i].id === userId) {
489
+ if (!data.users[i].dmFavorites) data.users[i].dmFavorites = [];
490
+ data.users[i].dmFavorites = data.users[i].dmFavorites.filter(function (id) {
491
+ return id !== targetUserId;
492
+ });
493
+ saveUsers(data);
494
+ return data.users[i].dmFavorites;
495
+ }
496
+ }
497
+ return [];
498
+ }
499
+
500
+ // --- DM Hidden (dismissed from strip) ---
501
+
502
+ function getDmHidden(userId) {
503
+ var data = loadUsers();
504
+ for (var i = 0; i < data.users.length; i++) {
505
+ if (data.users[i].id === userId) {
506
+ return data.users[i].dmHidden || [];
507
+ }
508
+ }
509
+ return [];
510
+ }
511
+
512
+ function addDmHidden(userId, targetUserId) {
513
+ var data = loadUsers();
514
+ for (var i = 0; i < data.users.length; i++) {
515
+ if (data.users[i].id === userId) {
516
+ if (!data.users[i].dmHidden) data.users[i].dmHidden = [];
517
+ if (data.users[i].dmHidden.indexOf(targetUserId) === -1) {
518
+ data.users[i].dmHidden.push(targetUserId);
519
+ saveUsers(data);
520
+ }
521
+ return data.users[i].dmHidden;
522
+ }
523
+ }
524
+ return [];
525
+ }
526
+
527
+ function removeDmHidden(userId, targetUserId) {
528
+ var data = loadUsers();
529
+ for (var i = 0; i < data.users.length; i++) {
530
+ if (data.users[i].id === userId) {
531
+ if (!data.users[i].dmHidden) data.users[i].dmHidden = [];
532
+ data.users[i].dmHidden = data.users[i].dmHidden.filter(function (id) {
533
+ return id !== targetUserId;
534
+ });
535
+ saveUsers(data);
536
+ return data.users[i].dmHidden;
537
+ }
538
+ }
539
+ return [];
540
+ }
541
+
458
542
  // --- Project access helpers ---
459
543
 
460
544
  function canAccessProject(userId, project) {
@@ -464,6 +548,8 @@ function canAccessProject(userId, project) {
464
548
  // Admin always has access
465
549
  var user = findUserById(userId);
466
550
  if (user && user.role === "admin") return true;
551
+ // Owner always has access to their own project
552
+ if (project.ownerId && project.ownerId === userId) return true;
467
553
  // Private project — check allowedUsers
468
554
  var allowed = project.allowedUsers || [];
469
555
  return allowed.indexOf(userId) >= 0;
@@ -535,4 +621,10 @@ module.exports = {
535
621
  updateLinuxUser: updateLinuxUser,
536
622
  generatePin: generatePin,
537
623
  createUserByAdmin: createUserByAdmin,
624
+ getDmFavorites: getDmFavorites,
625
+ addDmFavorite: addDmFavorite,
626
+ removeDmFavorite: removeDmFavorite,
627
+ getDmHidden: getDmHidden,
628
+ addDmHidden: addDmHidden,
629
+ removeDmHidden: removeDmHidden,
538
630
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.11.0-beta.8",
3
+ "version": "2.11.0",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",