clay-server 2.12.0 → 2.13.0-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.
@@ -376,3 +376,210 @@ export function getProfile() {
376
376
  export function getProfileLang() {
377
377
  return profile.lang;
378
378
  }
379
+
380
+ // --- Mate profile popover (reuses same UI minus language) ---
381
+ var matePopoverEl = null;
382
+ var mateSaveTimer = null;
383
+ var matePreviewSeed = '';
384
+
385
+ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
386
+ if (matePopoverEl) {
387
+ hideMatePopover();
388
+ return;
389
+ }
390
+
391
+ var mp = mateData.profile || {};
392
+ var mateName = mp.displayName || mateData.name || '';
393
+ var mateColor = mp.avatarColor || '#7c3aed';
394
+ var mateStyle = mp.avatarStyle || 'bottts';
395
+ var mateSeed = mp.avatarSeed || mateData.id || 'mate';
396
+ matePreviewSeed = mateSeed;
397
+
398
+ matePopoverEl = document.createElement('div');
399
+ matePopoverEl.className = 'profile-popover mate-profile-popover';
400
+
401
+ var html = '';
402
+
403
+ // Banner + close
404
+ html += '<div class="profile-banner" style="background:' + mateColor + '">';
405
+ html += '<button class="profile-close-btn">&times;</button>';
406
+ html += '</div>';
407
+
408
+ // Avatar row
409
+ html += '<div class="profile-avatar-row">';
410
+ html += '<div class="profile-popover-avatar">';
411
+ html += '<img class="profile-popover-avatar-img" src="' + avatarUrl(mateStyle, mateSeed, 80) + '" alt="avatar">';
412
+ html += '</div>';
413
+ html += '<div class="profile-name-display">' + escapeAttr(mateName || 'New Mate') + '</div>';
414
+ html += '</div>';
415
+
416
+ // Body
417
+ html += '<div class="profile-popover-body">';
418
+
419
+ // Name
420
+ html += '<div class="profile-field">';
421
+ html += '<label class="profile-field-label">Display Name</label>';
422
+ html += '<input type="text" class="profile-field-input" id="mate-profile-name" value="' + escapeAttr(mateName) + '" placeholder="Name your mate..." maxlength="50" spellcheck="false" autocomplete="off">';
423
+ html += '</div>';
424
+
425
+ // Avatar picker
426
+ html += '<div class="profile-field">';
427
+ html += '<label class="profile-field-label">Avatar <button class="profile-shuffle-btn" title="Shuffle">' + iconHtml('shuffle') + '</button></label>';
428
+ html += '<div class="profile-avatar-grid">';
429
+ for (var j = 0; j < AVATAR_STYLES.length; j++) {
430
+ var st = AVATAR_STYLES[j];
431
+ var activeS = (mateStyle === st.id) ? ' profile-avatar-option-active' : '';
432
+ html += '<button class="profile-avatar-option' + activeS + '" data-style="' + st.id + '" title="' + st.name + '">';
433
+ html += '<img src="' + avatarUrl(st.id, mateSeed, 40) + '" alt="' + st.name + '">';
434
+ html += '</button>';
435
+ }
436
+ html += '</div>';
437
+ html += '</div>';
438
+
439
+ // Color
440
+ html += '<div class="profile-field">';
441
+ html += '<label class="profile-field-label">Color</label>';
442
+ html += '<div class="profile-color-grid">';
443
+ for (var k = 0; k < COLORS.length; k++) {
444
+ var c = COLORS[k];
445
+ var activeC = (mateColor === c) ? ' profile-color-active' : '';
446
+ html += '<button class="profile-color-swatch' + activeC + '" data-color="' + c + '" style="background:' + c + '"></button>';
447
+ }
448
+ html += '</div>';
449
+ html += '</div>';
450
+
451
+ html += '</div>'; // close body
452
+
453
+ matePopoverEl.innerHTML = html;
454
+
455
+ // State tracker
456
+ var mateProfile = {
457
+ displayName: mateName,
458
+ avatarStyle: mateStyle,
459
+ avatarSeed: mateSeed,
460
+ avatarColor: mateColor,
461
+ };
462
+
463
+ function debouncedMateUpdate() {
464
+ if (mateSaveTimer) clearTimeout(mateSaveTimer);
465
+ mateSaveTimer = setTimeout(function() {
466
+ if (onUpdate) onUpdate({
467
+ name: mateProfile.displayName,
468
+ profile: {
469
+ displayName: mateProfile.displayName,
470
+ avatarStyle: mateProfile.avatarStyle,
471
+ avatarSeed: mateProfile.avatarSeed,
472
+ avatarColor: mateProfile.avatarColor,
473
+ },
474
+ });
475
+ mateSaveTimer = null;
476
+ }, 400);
477
+ }
478
+
479
+ function updateMatePopoverHeader() {
480
+ if (!matePopoverEl) return;
481
+ var img = matePopoverEl.querySelector('.profile-popover-avatar-img');
482
+ var nd = matePopoverEl.querySelector('.profile-name-display');
483
+ if (img) img.src = avatarUrl(mateProfile.avatarStyle, mateProfile.avatarSeed, 80);
484
+ if (nd) nd.textContent = mateProfile.displayName || 'New Mate';
485
+ }
486
+
487
+ // Events
488
+ matePopoverEl.querySelector('.profile-close-btn').addEventListener('click', function(e) {
489
+ e.stopPropagation();
490
+ hideMatePopover();
491
+ });
492
+
493
+ var nameInput = matePopoverEl.querySelector('#mate-profile-name');
494
+ nameInput.addEventListener('input', function() {
495
+ mateProfile.displayName = nameInput.value.trim();
496
+ updateMatePopoverHeader();
497
+ debouncedMateUpdate();
498
+ });
499
+ nameInput.addEventListener('keydown', function(e) {
500
+ if (e.key === 'Enter') { e.preventDefault(); hideMatePopover(); }
501
+ e.stopPropagation();
502
+ });
503
+ nameInput.addEventListener('keyup', function(e) { e.stopPropagation(); });
504
+ nameInput.addEventListener('keypress', function(e) { e.stopPropagation(); });
505
+
506
+ // Avatar style
507
+ matePopoverEl.querySelectorAll('.profile-avatar-option[data-style]').forEach(function(btn) {
508
+ btn.addEventListener('click', function() {
509
+ mateProfile.avatarStyle = btn.dataset.style;
510
+ mateProfile.avatarSeed = matePreviewSeed;
511
+ updateMatePopoverHeader();
512
+ matePopoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
513
+ b.classList.remove('profile-avatar-option-active');
514
+ });
515
+ btn.classList.add('profile-avatar-option-active');
516
+ debouncedMateUpdate();
517
+ });
518
+ });
519
+
520
+ // Shuffle
521
+ matePopoverEl.querySelector('.profile-shuffle-btn').addEventListener('click', function(e) {
522
+ e.stopPropagation();
523
+ matePreviewSeed = Math.random().toString(36).substring(2, 10);
524
+ if (!matePopoverEl) return;
525
+ matePopoverEl.querySelectorAll('.profile-avatar-option[data-style] img').forEach(function(img) {
526
+ var style = img.closest('.profile-avatar-option').dataset.style;
527
+ img.src = avatarUrl(style, matePreviewSeed, 40);
528
+ });
529
+ });
530
+
531
+ // Color swatches
532
+ matePopoverEl.querySelectorAll('.profile-color-swatch').forEach(function(btn) {
533
+ btn.addEventListener('click', function() {
534
+ mateProfile.avatarColor = btn.dataset.color;
535
+ var bannerEl = matePopoverEl.querySelector('.profile-banner');
536
+ if (bannerEl) bannerEl.style.background = mateProfile.avatarColor;
537
+ matePopoverEl.querySelectorAll('.profile-color-swatch').forEach(function(b) {
538
+ b.classList.remove('profile-color-active');
539
+ });
540
+ btn.classList.add('profile-color-active');
541
+ debouncedMateUpdate();
542
+ });
543
+ });
544
+
545
+ matePopoverEl.addEventListener('click', function(e) { e.stopPropagation(); });
546
+
547
+ // Position near anchor
548
+ document.body.appendChild(matePopoverEl);
549
+ refreshIcons();
550
+
551
+ var rect = anchorEl.getBoundingClientRect();
552
+ matePopoverEl.style.position = 'fixed';
553
+ matePopoverEl.style.left = (rect.right + 8) + 'px';
554
+ matePopoverEl.style.zIndex = '9999';
555
+ // Align bottom of popover with bottom of anchor icon
556
+ var popHeight = matePopoverEl.offsetHeight;
557
+ var bottomAligned = rect.bottom - popHeight;
558
+ matePopoverEl.style.top = Math.max(8, bottomAligned) + 'px';
559
+
560
+ setTimeout(function() {
561
+ document.addEventListener('click', closeMateOnOutside);
562
+ document.addEventListener('keydown', closeMateOnEscape);
563
+ }, 0);
564
+ }
565
+
566
+ function closeMateOnOutside(e) {
567
+ if (matePopoverEl && !matePopoverEl.contains(e.target)) {
568
+ hideMatePopover();
569
+ }
570
+ }
571
+
572
+ function closeMateOnEscape(e) {
573
+ if (e.key === 'Escape' && matePopoverEl) {
574
+ hideMatePopover();
575
+ }
576
+ }
577
+
578
+ function hideMatePopover() {
579
+ if (matePopoverEl) {
580
+ matePopoverEl.remove();
581
+ matePopoverEl = null;
582
+ }
583
+ document.removeEventListener('click', closeMateOnOutside);
584
+ document.removeEventListener('keydown', closeMateOnEscape);
585
+ }
@@ -3,6 +3,7 @@ import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { openProjectSettings } from './project-settings.js';
4
4
  import { triggerShare } from './qrcode.js';
5
5
  import { parseEmojis } from './markdown.js';
6
+ import { showMateProfilePopover } from './profile.js';
6
7
 
7
8
  var ctx;
8
9
 
@@ -1485,7 +1486,7 @@ function showUserCtxMenu(anchorEl, user) {
1485
1486
  // even if the user was only visible via cachedDmConversations (not favorites)
1486
1487
  cachedDmRemovedUsers[user.id] = true;
1487
1488
  if (ctx.onDmRemoveUser) ctx.onDmRemoveUser(user.id);
1488
- renderUserStrip(cachedAllUsers, cachedOnlineUserIds, cachedMyUserId, cachedDmFavorites, cachedDmConversations, cachedDmUnread, cachedDmRemovedUsers);
1489
+ renderUserStrip(cachedAllUsers, cachedOnlineUserIds, cachedMyUserId, cachedDmFavorites, cachedDmConversations, cachedDmUnread, cachedDmRemovedUsers, cachedMates);
1489
1490
  if (ctx.sendWs) {
1490
1491
  ctx.sendWs({ type: "dm_remove_favorite", targetUserId: user.id });
1491
1492
  }
@@ -1522,6 +1523,65 @@ function handleUserCtxOutsideClick(e) {
1522
1523
  }
1523
1524
  }
1524
1525
 
1526
+ function showMateCtxMenu(anchorEl, mate) {
1527
+ closeUserCtxMenu();
1528
+ closeProjectCtxMenu();
1529
+
1530
+ var menu = document.createElement("div");
1531
+ menu.className = "project-ctx-menu";
1532
+
1533
+ // Edit Profile item
1534
+ var editItem = document.createElement("button");
1535
+ editItem.className = "project-ctx-item";
1536
+ editItem.innerHTML = iconHtml("edit-2") + " <span>Edit Profile</span>";
1537
+ editItem.addEventListener("click", function (e) {
1538
+ e.stopPropagation();
1539
+ closeUserCtxMenu();
1540
+ showMateProfilePopover(anchorEl, mate, function (updates) {
1541
+ if (ctx.sendWs) {
1542
+ ctx.sendWs({ type: "mate_update", mateId: mate.id, updates: updates });
1543
+ }
1544
+ });
1545
+ });
1546
+ menu.appendChild(editItem);
1547
+
1548
+ var removeItem = document.createElement("button");
1549
+ removeItem.className = "project-ctx-item project-ctx-delete";
1550
+ removeItem.innerHTML = iconHtml("trash-2") + " <span>Remove Mate</span>";
1551
+ removeItem.addEventListener("click", function (e) {
1552
+ e.stopPropagation();
1553
+ var iconRect = anchorEl.getBoundingClientRect();
1554
+ spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
1555
+ closeUserCtxMenu();
1556
+ if (ctx.sendWs) {
1557
+ ctx.sendWs({ type: "mate_delete", mateId: mate.id });
1558
+ }
1559
+ });
1560
+ menu.appendChild(removeItem);
1561
+
1562
+ document.body.appendChild(menu);
1563
+ userCtxMenu = menu;
1564
+ refreshIcons();
1565
+
1566
+ requestAnimationFrame(function () {
1567
+ var rect = anchorEl.getBoundingClientRect();
1568
+ menu.style.position = "fixed";
1569
+ menu.style.left = (rect.right + 6) + "px";
1570
+ menu.style.top = rect.top + "px";
1571
+ var menuRect = menu.getBoundingClientRect();
1572
+ if (menuRect.right > window.innerWidth - 8) {
1573
+ menu.style.left = (rect.left - menuRect.width - 6) + "px";
1574
+ }
1575
+ if (menuRect.bottom > window.innerHeight - 8) {
1576
+ menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
1577
+ }
1578
+ });
1579
+
1580
+ setTimeout(function () {
1581
+ document.addEventListener("click", handleUserCtxOutsideClick, true);
1582
+ }, 0);
1583
+ }
1584
+
1525
1585
  // --- Project context menu ---
1526
1586
  var projectCtxMenu = null;
1527
1587
 
@@ -2743,8 +2803,10 @@ var currentDmUserId = null;
2743
2803
  var dmPickerOpen = false;
2744
2804
 
2745
2805
  var cachedDmRemovedUsers = {};
2806
+ var cachedMates = [];
2746
2807
 
2747
- export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites, dmConversations, dmUnread, dmRemovedUsers) {
2808
+ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites, dmConversations, dmUnread, dmRemovedUsers, matesList) {
2809
+ cachedMates = matesList || cachedMates || [];
2748
2810
  cachedAllUsers = allUsers || [];
2749
2811
  cachedOnlineUserIds = onlineUserIds || [];
2750
2812
  cachedDmFavorites = dmFavorites || [];
@@ -2825,6 +2887,62 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
2825
2887
  })(others[i]);
2826
2888
  }
2827
2889
 
2890
+ // Render mates
2891
+ for (var mi = 0; mi < cachedMates.length; mi++) {
2892
+ (function (mate) {
2893
+ var mp = mate.profile || {};
2894
+ var el = document.createElement("div");
2895
+ el.className = "icon-strip-user icon-strip-mate";
2896
+ el.dataset.userId = mate.id;
2897
+ if (mate.id === currentDmUserId) el.classList.add("active");
2898
+
2899
+ var pill = document.createElement("span");
2900
+ pill.className = "icon-strip-pill";
2901
+ el.appendChild(pill);
2902
+
2903
+ var avatar = document.createElement("img");
2904
+ avatar.className = "icon-strip-user-avatar";
2905
+ avatar.src = "https://api.dicebear.com/9.x/" + (mp.avatarStyle || "bottts") + "/svg?seed=" + encodeURIComponent(mp.avatarSeed || mate.id) + "&size=34";
2906
+ avatar.alt = mp.displayName || mate.name || "Mate";
2907
+ el.appendChild(avatar);
2908
+
2909
+ // Mate badge (bot icon)
2910
+ var mateBadge = document.createElement("span");
2911
+ mateBadge.className = "icon-strip-user-mate-badge";
2912
+ mateBadge.innerHTML = iconHtml("bot");
2913
+ el.appendChild(mateBadge);
2914
+
2915
+ var badge = document.createElement("span");
2916
+ badge.className = "icon-strip-user-badge";
2917
+ badge.dataset.userId = mate.id;
2918
+ el.appendChild(badge);
2919
+
2920
+ // Tooltip
2921
+ var displayName = mp.displayName || mate.name || "New Mate";
2922
+ el.addEventListener("mouseenter", function () { showIconTooltip(el, displayName); });
2923
+ el.addEventListener("mouseleave", hideIconTooltip);
2924
+
2925
+ // Click: open DM with mate
2926
+ el.addEventListener("click", function () {
2927
+ if (ctx.openDm) ctx.openDm(mate.id);
2928
+ });
2929
+
2930
+ // Right-click: context menu for mate
2931
+ el.addEventListener("contextmenu", function (e) {
2932
+ e.preventDefault();
2933
+ e.stopPropagation();
2934
+ showMateCtxMenu(el, mate);
2935
+ });
2936
+
2937
+ container.appendChild(el);
2938
+ })(cachedMates[mi]);
2939
+ }
2940
+
2941
+ // Show container if we have mates even with no other users
2942
+ if (cachedMates.length > 0) {
2943
+ container.classList.remove("hidden");
2944
+ }
2945
+
2828
2946
  // Add user (+) button
2829
2947
  var addBtn = document.createElement("button");
2830
2948
  addBtn.className = "icon-strip-invite";
@@ -2833,7 +2951,7 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
2833
2951
  e.stopPropagation();
2834
2952
  toggleDmUserPicker(addBtn);
2835
2953
  });
2836
- addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add DM favorite"); });
2954
+ addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add user or create mate"); });
2837
2955
  addBtn.addEventListener("mouseleave", hideIconTooltip);
2838
2956
  container.appendChild(addBtn);
2839
2957
  refreshIcons();
@@ -2917,6 +3035,28 @@ function toggleDmUserPicker(anchorEl) {
2917
3035
  }
2918
3036
  }
2919
3037
 
3038
+ // Create Mate option
3039
+ var createMateEl = document.createElement("div");
3040
+ createMateEl.className = "dm-user-picker-create-mate";
3041
+ createMateEl.innerHTML = iconHtml("bot") + " <span>Create a Mate</span>";
3042
+ createMateEl.addEventListener("click", function () {
3043
+ closeDmUserPicker();
3044
+ if (ctx.openMateWizard) ctx.openMateWizard();
3045
+ });
3046
+ picker.appendChild(createMateEl);
3047
+
3048
+ // Divider
3049
+ var divider = document.createElement("div");
3050
+ divider.style.borderTop = "1px solid var(--border, #333)";
3051
+ divider.style.margin = "4px 0";
3052
+ picker.appendChild(divider);
3053
+
3054
+ // Section label for users
3055
+ var sectionLabel = document.createElement("div");
3056
+ sectionLabel.className = "dm-user-picker-section";
3057
+ sectionLabel.textContent = "Users";
3058
+ picker.appendChild(sectionLabel);
3059
+
2920
3060
  renderPickerList("");
2921
3061
  searchInput.addEventListener("input", function () {
2922
3062
  renderPickerList(searchInput.value);
@@ -24,3 +24,4 @@
24
24
  @import url("css/admin.css");
25
25
  @import url("css/session-search.css");
26
26
  @import url("css/tooltip.css");
27
+ @import url("css/mates.css");
package/lib/sdk-bridge.js CHANGED
@@ -189,6 +189,10 @@ function createSDKBridge(opts) {
189
189
  if (session.responsePreview.length < 200) {
190
190
  session.responsePreview += evt.delta.text;
191
191
  }
192
+ // Accumulate text for mate DM response
193
+ if (typeof session._mateDmResponseText === "string") {
194
+ session._mateDmResponseText += evt.delta.text;
195
+ }
192
196
  sendAndRecord(session, { type: "delta", text: evt.delta.text });
193
197
  } else if (evt.delta.type === "input_json_delta" && session.blocks[idx]) {
194
198
  session.blocks[idx].inputJson += evt.delta.partial_json;
package/lib/server.js CHANGED
@@ -8,6 +8,7 @@ var smtp = require("./smtp");
8
8
  var { createProjectContext } = require("./project");
9
9
  var users = require("./users");
10
10
  var dm = require("./dm");
11
+ var mates = require("./mates");
11
12
 
12
13
  var { CONFIG_DIR } = require("./config");
13
14
  var { provisionLinuxUser } = require("./os-users");
@@ -2135,8 +2136,9 @@ function createServer(opts) {
2135
2136
  }
2136
2137
 
2137
2138
  // --- Project management ---
2138
- function addProject(cwd, slug, title, icon, projectOwnerId, worktreeMeta) {
2139
+ function addProject(cwd, slug, title, icon, projectOwnerId, worktreeMeta, extraOpts) {
2139
2140
  if (projects.has(slug)) return false;
2141
+ var extra = extraOpts || {};
2140
2142
  var ctx = createProjectContext({
2141
2143
  cwd: cwd,
2142
2144
  slug: slug,
@@ -2144,6 +2146,7 @@ function createServer(opts) {
2144
2146
  icon: icon || null,
2145
2147
  projectOwnerId: projectOwnerId || null,
2146
2148
  worktreeMeta: worktreeMeta || null,
2149
+ isMate: extra.isMate || false,
2147
2150
  pushModule: pushModule,
2148
2151
  debug: debug,
2149
2152
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -2285,12 +2288,41 @@ function createServer(opts) {
2285
2288
  };
2286
2289
  }
2287
2290
  }
2288
- ws.send(JSON.stringify({ type: "dm_list", dms: dmList }));
2291
+ // Include mates in the list
2292
+ var mateList = mates.getAllMates();
2293
+ ws.send(JSON.stringify({ type: "dm_list", dms: dmList, mates: mateList }));
2289
2294
  return;
2290
2295
  }
2291
2296
 
2292
2297
  if (msg.type === "dm_open") {
2293
2298
  if (!msg.targetUserId) return;
2299
+
2300
+ // Check if target is a mate
2301
+ if (mates.isMate(msg.targetUserId)) {
2302
+ var mate = mates.getMate(msg.targetUserId);
2303
+ if (!mate) return;
2304
+ var mp = mate.profile || {};
2305
+ ws.send(JSON.stringify({
2306
+ type: "dm_history",
2307
+ dmKey: "mate:" + mate.id,
2308
+ messages: dm.loadHistory("mate:" + mate.id),
2309
+ isMate: true,
2310
+ projectSlug: "mate-" + mate.id,
2311
+ targetUser: {
2312
+ id: mate.id,
2313
+ displayName: mp.displayName || mate.name || "New Mate",
2314
+ username: mate.id,
2315
+ avatarStyle: mp.avatarStyle || "bottts",
2316
+ avatarSeed: mp.avatarSeed || mate.id,
2317
+ avatarColor: mp.avatarColor || "#6c5ce7",
2318
+ isMate: true,
2319
+ mateStatus: mate.status,
2320
+ seedData: mate.seedData || {},
2321
+ },
2322
+ }));
2323
+ return;
2324
+ }
2325
+
2294
2326
  var result = dm.openDm(userId, msg.targetUserId);
2295
2327
  var targetUser = users.findUserById(msg.targetUserId);
2296
2328
  var tp = targetUser ? (targetUser.profile || {}) : {};
@@ -2330,8 +2362,20 @@ function createServer(opts) {
2330
2362
 
2331
2363
  if (msg.type === "dm_send") {
2332
2364
  if (!msg.dmKey || !msg.text) return;
2333
- // Verify sender is a participant
2334
2365
  var parts = msg.dmKey.split(":");
2366
+
2367
+ // Handle mate DM: dmKey is "mate:mate_xxx"
2368
+ if (parts[0] === "mate" && mates.isMate(parts[1])) {
2369
+ var mate = mates.getMate(parts[1]);
2370
+ if (!mate) return;
2371
+ // Verify sender is the mate's creator
2372
+ if (mate.createdBy !== userId) return;
2373
+ var message = dm.sendMessage(msg.dmKey, userId, msg.text);
2374
+ ws.send(JSON.stringify({ type: "dm_message", dmKey: msg.dmKey, message: message }));
2375
+ return;
2376
+ }
2377
+
2378
+ // Regular DM: verify sender is a participant
2335
2379
  if (parts.indexOf(userId) === -1) return;
2336
2380
  var message = dm.sendMessage(msg.dmKey, userId, msg.text);
2337
2381
  // Send confirmation to sender
@@ -2383,6 +2427,69 @@ function createServer(opts) {
2383
2427
  }));
2384
2428
  return;
2385
2429
  }
2430
+
2431
+ // --- Mate handlers ---
2432
+
2433
+ if (msg.type === "mate_create") {
2434
+ if (!msg.seedData) return;
2435
+ try {
2436
+ var mate = mates.createMate(msg.seedData, userId);
2437
+ // Register mate as a project
2438
+ var mateDir = path.join(mates.MATES_DIR, mate.id);
2439
+ var mateSlug = "mate-" + mate.id;
2440
+ var mateName = (mate.profile && mate.profile.displayName) || mate.name || "New Mate";
2441
+ addProject(mateDir, mateSlug, mateName, null, mate.createdBy, null, { isMate: true });
2442
+ ws.send(JSON.stringify({ type: "mate_created", mate: mate, projectSlug: mateSlug }));
2443
+ } catch (e) {
2444
+ ws.send(JSON.stringify({ type: "mate_error", error: "Failed to create mate: " + e.message }));
2445
+ }
2446
+ return;
2447
+ }
2448
+
2449
+ if (msg.type === "mate_list") {
2450
+ var mateList = mates.getAllMates();
2451
+ ws.send(JSON.stringify({ type: "mate_list", mates: mateList }));
2452
+ return;
2453
+ }
2454
+
2455
+ if (msg.type === "mate_delete") {
2456
+ if (!msg.mateId) return;
2457
+ var result = mates.deleteMate(msg.mateId);
2458
+ if (result.error) {
2459
+ ws.send(JSON.stringify({ type: "mate_error", error: result.error }));
2460
+ } else {
2461
+ removeProject("mate-" + msg.mateId);
2462
+ ws.send(JSON.stringify({ type: "mate_deleted", mateId: msg.mateId }));
2463
+ // Broadcast to all clients so strips update
2464
+ projects.forEach(function (ctx) {
2465
+ ctx.forEachClient(function (otherWs) {
2466
+ if (otherWs === ws) return;
2467
+ if (otherWs.readyState !== 1) return;
2468
+ otherWs.send(JSON.stringify({ type: "mate_deleted", mateId: msg.mateId }));
2469
+ });
2470
+ });
2471
+ }
2472
+ return;
2473
+ }
2474
+
2475
+ if (msg.type === "mate_update") {
2476
+ if (!msg.mateId || !msg.updates) return;
2477
+ var updated = mates.updateMate(msg.mateId, msg.updates);
2478
+ if (updated) {
2479
+ ws.send(JSON.stringify({ type: "mate_updated", mate: updated }));
2480
+ // Broadcast update
2481
+ projects.forEach(function (ctx) {
2482
+ ctx.forEachClient(function (otherWs) {
2483
+ if (otherWs === ws) return;
2484
+ if (otherWs.readyState !== 1) return;
2485
+ otherWs.send(JSON.stringify({ type: "mate_updated", mate: updated }));
2486
+ });
2487
+ });
2488
+ } else {
2489
+ ws.send(JSON.stringify({ type: "mate_error", error: "Mate not found" }));
2490
+ }
2491
+ return;
2492
+ }
2386
2493
  }
2387
2494
 
2388
2495
  function removeProject(slug) {
package/lib/sessions.js CHANGED
@@ -280,6 +280,32 @@ function createSessionManager(opts) {
280
280
  return session;
281
281
  }
282
282
 
283
+ // Create a session without switching to it (used for mate/background sessions)
284
+ function createSessionRaw(sessionOpts) {
285
+ var localId = nextLocalId++;
286
+ var session = {
287
+ localId: localId,
288
+ queryInstance: null,
289
+ messageQueue: null,
290
+ cliSessionId: null,
291
+ blocks: {},
292
+ sentToolResults: {},
293
+ pendingPermissions: {},
294
+ pendingAskUser: {},
295
+ allowedTools: {},
296
+ isProcessing: false,
297
+ title: "",
298
+ createdAt: Date.now(),
299
+ lastActivity: Date.now(),
300
+ history: [],
301
+ messageUUIDs: [],
302
+ ownerId: (sessionOpts && sessionOpts.ownerId) || null,
303
+ sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
304
+ };
305
+ sessions.set(localId, session);
306
+ return session;
307
+ }
308
+
283
309
  var HISTORY_PAGE_SIZE = 200;
284
310
 
285
311
  function findTurnBoundary(history, targetIndex) {
@@ -613,6 +639,7 @@ function createSessionManager(opts) {
613
639
  HISTORY_PAGE_SIZE: HISTORY_PAGE_SIZE,
614
640
  getActiveSession: getActiveSession,
615
641
  createSession: createSession,
642
+ createSessionRaw: createSessionRaw,
616
643
  switchSession: switchSession,
617
644
  deleteSession: deleteSession,
618
645
  deleteSessionQuiet: deleteSessionQuiet,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.12.0",
3
+ "version": "2.13.0-beta.1",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",