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/bin/cli.js +282 -115
- package/lib/daemon.js +109 -37
- package/lib/os-users.js +58 -1
- package/lib/pages.js +31 -29
- package/lib/project.js +47 -9
- package/lib/public/app.js +158 -16
- package/lib/public/css/filebrowser.css +6 -0
- package/lib/public/css/icon-strip.css +123 -1
- package/lib/public/css/messages.css +1 -1
- package/lib/public/css/mobile-nav.css +17 -0
- package/lib/public/css/overlays.css +49 -0
- package/lib/public/css/sidebar.css +26 -0
- package/lib/public/css/sticky-notes.css +3 -0
- package/lib/public/index.html +2 -0
- package/lib/public/modules/admin.js +53 -5
- package/lib/public/modules/sidebar.js +299 -21
- package/lib/public/modules/sticky-notes.js +27 -5
- package/lib/public/modules/terminal.js +161 -25
- package/lib/sdk-bridge.js +53 -7
- package/lib/server.js +156 -17
- package/lib/sessions.js +48 -7
- package/lib/terminal-manager.js +4 -2
- package/lib/terminal.js +2 -1
- package/lib/users.js +92 -0
- package/package.json +1 -1
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)
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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,
|
package/lib/terminal-manager.js
CHANGED
|
@@ -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
|
|
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
|
};
|