clay-server 2.27.0-beta.8 → 2.27.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.
Files changed (72) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-debate.js +19 -12
  10. package/lib/project-http.js +4 -2
  11. package/lib/project-loop.js +110 -48
  12. package/lib/project-mate-interaction.js +4 -0
  13. package/lib/project-notifications.js +210 -0
  14. package/lib/project-sessions.js +5 -2
  15. package/lib/project-user-message.js +2 -1
  16. package/lib/project.js +26 -2
  17. package/lib/public/app.js +1193 -8521
  18. package/lib/public/css/command-palette.css +14 -0
  19. package/lib/public/css/loop.css +301 -0
  20. package/lib/public/css/notifications-center.css +190 -0
  21. package/lib/public/css/rewind.css +6 -0
  22. package/lib/public/index.html +89 -35
  23. package/lib/public/modules/app-connection.js +160 -0
  24. package/lib/public/modules/app-cursors.js +473 -0
  25. package/lib/public/modules/app-debate-ui.js +389 -0
  26. package/lib/public/modules/app-dm.js +627 -0
  27. package/lib/public/modules/app-favicon.js +212 -0
  28. package/lib/public/modules/app-header.js +229 -0
  29. package/lib/public/modules/app-home-hub.js +600 -0
  30. package/lib/public/modules/app-loop-ui.js +589 -0
  31. package/lib/public/modules/app-loop-wizard.js +439 -0
  32. package/lib/public/modules/app-messages.js +1560 -0
  33. package/lib/public/modules/app-misc.js +299 -0
  34. package/lib/public/modules/app-notifications.js +372 -0
  35. package/lib/public/modules/app-panels.js +888 -0
  36. package/lib/public/modules/app-projects.js +798 -0
  37. package/lib/public/modules/app-rate-limit.js +451 -0
  38. package/lib/public/modules/app-rendering.js +597 -0
  39. package/lib/public/modules/app-skills-install.js +234 -0
  40. package/lib/public/modules/command-palette.js +27 -4
  41. package/lib/public/modules/input.js +31 -20
  42. package/lib/public/modules/scheduler-config.js +1532 -0
  43. package/lib/public/modules/scheduler-history.js +79 -0
  44. package/lib/public/modules/scheduler.js +33 -1554
  45. package/lib/public/modules/session-search.js +13 -1
  46. package/lib/public/modules/sidebar-mates.js +812 -0
  47. package/lib/public/modules/sidebar-mobile.js +1269 -0
  48. package/lib/public/modules/sidebar-projects.js +1449 -0
  49. package/lib/public/modules/sidebar-sessions.js +986 -0
  50. package/lib/public/modules/sidebar.js +232 -4591
  51. package/lib/public/modules/store.js +27 -0
  52. package/lib/public/modules/ws-ref.js +7 -0
  53. package/lib/public/style.css +1 -0
  54. package/lib/sdk-bridge.js +96 -717
  55. package/lib/sdk-message-processor.js +587 -0
  56. package/lib/sdk-message-queue.js +42 -0
  57. package/lib/sdk-skill-discovery.js +131 -0
  58. package/lib/server-admin.js +712 -0
  59. package/lib/server-auth.js +737 -0
  60. package/lib/server-dm.js +221 -0
  61. package/lib/server-mates.js +281 -0
  62. package/lib/server-palette.js +110 -0
  63. package/lib/server-settings.js +479 -0
  64. package/lib/server-skills.js +280 -0
  65. package/lib/server.js +246 -2755
  66. package/lib/sessions.js +11 -4
  67. package/lib/users-auth.js +146 -0
  68. package/lib/users-permissions.js +118 -0
  69. package/lib/users-preferences.js +210 -0
  70. package/lib/users.js +48 -398
  71. package/lib/ws-schema.js +498 -0
  72. package/package.json +1 -1
@@ -0,0 +1,812 @@
1
+ // sidebar-mates.js - User/mate strip, DM picker, context menus, tooltips, presence
2
+ // Extracted from sidebar.js (PR-37)
3
+
4
+ import { userAvatarUrl, mateAvatarUrl } from './avatar.js';
5
+ import { escapeHtml } from './utils.js';
6
+ import { iconHtml, refreshIcons } from './icons.js';
7
+ import { showMateProfilePopover } from './profile.js';
8
+
9
+ var _ctx = null;
10
+
11
+ // --- User strip state ---
12
+ var cachedAllUsers = [];
13
+ var cachedOnlineUserIds = [];
14
+ var cachedDmFavorites = [];
15
+ var cachedDmConversations = [];
16
+ var cachedDmUnread = {};
17
+ var cachedMyUserId = null;
18
+ var currentDmUserId = null;
19
+ var dmPickerOpen = false;
20
+ var cachedDmRemovedUsers = {};
21
+ var cachedMates = [];
22
+
23
+ // --- Icon strip tooltip ---
24
+ var iconStripTooltip = null;
25
+
26
+ // --- DM user context menu ---
27
+ var userCtxMenu = null;
28
+
29
+ export function initSidebarMates(ctx) {
30
+ _ctx = ctx;
31
+ }
32
+
33
+ export function showIconTooltip(el, text) {
34
+ hideIconTooltip();
35
+ var tip = document.createElement("div");
36
+ tip.className = "icon-strip-tooltip";
37
+ tip.textContent = text;
38
+ document.body.appendChild(tip);
39
+ iconStripTooltip = tip;
40
+
41
+ requestAnimationFrame(function () {
42
+ var rect = el.getBoundingClientRect();
43
+ tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
44
+ tip.classList.add("visible");
45
+ });
46
+ }
47
+
48
+ export function showIconTooltipHtml(el, html) {
49
+ hideIconTooltip();
50
+ var tip = document.createElement("div");
51
+ tip.className = "icon-strip-tooltip";
52
+ tip.style.whiteSpace = "normal";
53
+ tip.style.maxWidth = "260px";
54
+ tip.innerHTML = html;
55
+ document.body.appendChild(tip);
56
+ iconStripTooltip = tip;
57
+
58
+ requestAnimationFrame(function () {
59
+ var rect = el.getBoundingClientRect();
60
+ tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
61
+ tip.classList.add("visible");
62
+ });
63
+ }
64
+
65
+ export function hideIconTooltip() {
66
+ if (iconStripTooltip) {
67
+ iconStripTooltip.remove();
68
+ iconStripTooltip = null;
69
+ }
70
+ }
71
+
72
+ export function closeUserCtxMenu() {
73
+ if (userCtxMenu) {
74
+ userCtxMenu.remove();
75
+ userCtxMenu = null;
76
+ }
77
+ document.removeEventListener("click", handleUserCtxOutsideClick, true);
78
+ }
79
+
80
+ function showUserCtxMenu(anchorEl, user) {
81
+ closeUserCtxMenu();
82
+ if (_ctx.closeProjectCtxMenu) _ctx.closeProjectCtxMenu();
83
+
84
+ var menu = document.createElement("div");
85
+ menu.className = "project-ctx-menu";
86
+
87
+ var removeItem = document.createElement("button");
88
+ removeItem.className = "project-ctx-item project-ctx-delete";
89
+ removeItem.innerHTML = iconHtml("user-minus") + " <span>Remove from favorites</span>";
90
+ removeItem.addEventListener("click", function (e) {
91
+ e.stopPropagation();
92
+ // Spawn dust particles at the user icon position
93
+ var iconRect = anchorEl.getBoundingClientRect();
94
+ if (_ctx.spawnDustParticles) _ctx.spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
95
+ closeUserCtxMenu();
96
+ // Immediately mark as removed so strip re-render hides the icon,
97
+ // even if the user was only visible via cachedDmConversations (not favorites)
98
+ cachedDmRemovedUsers[user.id] = true;
99
+ if (_ctx.onDmRemoveUser) _ctx.onDmRemoveUser(user.id);
100
+ renderUserStrip(cachedAllUsers, cachedOnlineUserIds, cachedMyUserId, cachedDmFavorites, cachedDmConversations, cachedDmUnread, cachedDmRemovedUsers, cachedMates);
101
+ if (_ctx.sendWs) {
102
+ _ctx.sendWs({ type: "dm_remove_favorite", targetUserId: user.id });
103
+ }
104
+ });
105
+ menu.appendChild(removeItem);
106
+
107
+ document.body.appendChild(menu);
108
+ userCtxMenu = menu;
109
+ refreshIcons();
110
+
111
+ requestAnimationFrame(function () {
112
+ var rect = anchorEl.getBoundingClientRect();
113
+ menu.style.position = "fixed";
114
+ menu.style.left = (rect.right + 6) + "px";
115
+ menu.style.top = rect.top + "px";
116
+ var menuRect = menu.getBoundingClientRect();
117
+ if (menuRect.right > window.innerWidth - 8) {
118
+ menu.style.left = (rect.left - menuRect.width - 6) + "px";
119
+ }
120
+ if (menuRect.bottom > window.innerHeight - 8) {
121
+ menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
122
+ }
123
+ });
124
+
125
+ // Close on outside click
126
+ setTimeout(function () {
127
+ document.addEventListener("click", handleUserCtxOutsideClick, true);
128
+ }, 0);
129
+ }
130
+
131
+ function handleUserCtxOutsideClick(e) {
132
+ if (userCtxMenu && !userCtxMenu.contains(e.target)) {
133
+ closeUserCtxMenu();
134
+ }
135
+ }
136
+
137
+ function showMateCtxMenu(anchorEl, mate) {
138
+ // Primary mates cannot be edited or removed
139
+ if (mate.primary) return;
140
+
141
+ closeUserCtxMenu();
142
+ if (_ctx.closeProjectCtxMenu) _ctx.closeProjectCtxMenu();
143
+
144
+ var menu = document.createElement("div");
145
+ menu.className = "project-ctx-menu";
146
+
147
+ // Edit Profile item
148
+ var editItem = document.createElement("button");
149
+ editItem.className = "project-ctx-item";
150
+ editItem.innerHTML = iconHtml("edit-2") + " <span>Edit Profile</span>";
151
+ editItem.addEventListener("click", function (e) {
152
+ e.stopPropagation();
153
+ closeUserCtxMenu();
154
+ showMateProfilePopover(anchorEl, mate, function (updates) {
155
+ if (_ctx.sendWs) {
156
+ _ctx.sendWs({ type: "mate_update", mateId: mate.id, updates: updates });
157
+ }
158
+ });
159
+ });
160
+ menu.appendChild(editItem);
161
+
162
+ var removeItem = document.createElement("button");
163
+ removeItem.className = "project-ctx-item";
164
+ removeItem.innerHTML = iconHtml("star-off") + " <span>Remove from favorites</span>";
165
+ removeItem.addEventListener("click", function (e) {
166
+ e.stopPropagation();
167
+ closeUserCtxMenu();
168
+ // Spawn dust particles at the mate icon position
169
+ var iconRect = anchorEl.getBoundingClientRect();
170
+ if (_ctx.spawnDustParticles) _ctx.spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
171
+ if (_ctx.sendWs) {
172
+ _ctx.sendWs({ type: "dm_remove_favorite", targetUserId: mate.id });
173
+ }
174
+ });
175
+ menu.appendChild(removeItem);
176
+
177
+ document.body.appendChild(menu);
178
+ userCtxMenu = menu;
179
+ refreshIcons();
180
+
181
+ requestAnimationFrame(function () {
182
+ var rect = anchorEl.getBoundingClientRect();
183
+ menu.style.position = "fixed";
184
+ menu.style.left = (rect.right + 6) + "px";
185
+ menu.style.top = rect.top + "px";
186
+ var menuRect = menu.getBoundingClientRect();
187
+ if (menuRect.right > window.innerWidth - 8) {
188
+ menu.style.left = (rect.left - menuRect.width - 6) + "px";
189
+ }
190
+ if (menuRect.bottom > window.innerHeight - 8) {
191
+ menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
192
+ }
193
+ });
194
+
195
+ setTimeout(function () {
196
+ document.addEventListener("click", handleUserCtxOutsideClick, true);
197
+ }, 0);
198
+ }
199
+
200
+ var _lastSidebarPresenceIds = [];
201
+ export function renderSidebarPresence(onlineUsers) {
202
+ var container = document.getElementById("sidebar-presence");
203
+ if (!container) return;
204
+ if (!onlineUsers || onlineUsers.length < 2) {
205
+ if (_lastSidebarPresenceIds.length > 0) {
206
+ _lastSidebarPresenceIds = [];
207
+ container.innerHTML = "";
208
+ }
209
+ return;
210
+ }
211
+ // Skip re-render if same users
212
+ var newIds = onlineUsers.map(function (u) { return u.id; }).sort();
213
+ if (newIds.length === _lastSidebarPresenceIds.length && newIds.every(function (id, i) { return id === _lastSidebarPresenceIds[i]; })) return;
214
+ _lastSidebarPresenceIds = newIds;
215
+ container.innerHTML = "";
216
+ var maxShow = 4;
217
+ for (var i = 0; i < Math.min(onlineUsers.length, maxShow); i++) {
218
+ var ou = onlineUsers[i];
219
+ var img = document.createElement("img");
220
+ img.className = "sidebar-presence-avatar";
221
+ img.src = presenceAvatarUrl(ou);
222
+ img.alt = ou.displayName;
223
+ img.dataset.tip = ou.displayName + " (@" + ou.username + ")";
224
+ container.appendChild(img);
225
+ }
226
+ if (onlineUsers.length > maxShow) {
227
+ var more = document.createElement("span");
228
+ more.className = "sidebar-presence-more";
229
+ more.textContent = "+" + (onlineUsers.length - maxShow);
230
+ container.appendChild(more);
231
+ }
232
+ }
233
+
234
+ // Presence avatar URL helper
235
+ function presenceAvatarUrl(userOrStyle) {
236
+ if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
237
+ return userAvatarUrl({ avatarStyle: userOrStyle || "thumbs" }, 24);
238
+ }
239
+
240
+ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites, dmConversations, dmUnread, dmRemovedUsers, matesList) {
241
+ cachedMates = matesList || cachedMates || [];
242
+ cachedAllUsers = allUsers || [];
243
+ cachedOnlineUserIds = onlineUserIds || [];
244
+ cachedDmFavorites = dmFavorites || [];
245
+ cachedDmConversations = dmConversations || [];
246
+ cachedDmUnread = dmUnread || {};
247
+ cachedDmRemovedUsers = dmRemovedUsers || {};
248
+ cachedMyUserId = myUserId;
249
+ var container = document.getElementById("icon-strip-users");
250
+ if (!container) return;
251
+
252
+ // All other users
253
+ var allOthers = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
254
+
255
+ // Hide section if no other users and no mates
256
+ if (allOthers.length === 0 && cachedMates.length === 0) {
257
+ container.innerHTML = "";
258
+ container.classList.add("hidden");
259
+ return;
260
+ }
261
+
262
+ // Filter to show only: favorites + users with unread + users with DM conversations
263
+ // But exclude users explicitly removed from favorites
264
+ var others = allOthers.filter(function (u) {
265
+ if (cachedDmRemovedUsers[u.id]) return false;
266
+ if (cachedDmFavorites.indexOf(u.id) !== -1) return true;
267
+ if (cachedDmUnread[u.id] && cachedDmUnread[u.id] > 0) return true;
268
+ if (cachedDmConversations.indexOf(u.id) !== -1) return true;
269
+ return false;
270
+ });
271
+
272
+ container.classList.remove("hidden");
273
+ container.innerHTML = "";
274
+
275
+ for (var i = 0; i < others.length; i++) {
276
+ (function (u) {
277
+ var el = document.createElement("div");
278
+ el.className = "icon-strip-user";
279
+ el.dataset.userId = u.id;
280
+ if (u.id === currentDmUserId) el.classList.add("active");
281
+ if (onlineUserIds.indexOf(u.id) !== -1) el.classList.add("online");
282
+
283
+ var pill = document.createElement("span");
284
+ pill.className = "icon-strip-pill";
285
+ el.appendChild(pill);
286
+
287
+ var avatar = document.createElement("img");
288
+ avatar.className = "icon-strip-user-avatar";
289
+ avatar.src = userAvatarUrl(u, 34);
290
+ avatar.alt = u.displayName;
291
+ el.appendChild(avatar);
292
+
293
+ var onlineDot = document.createElement("span");
294
+ onlineDot.className = "icon-strip-user-online";
295
+ el.appendChild(onlineDot);
296
+
297
+ var badge = document.createElement("span");
298
+ badge.className = "icon-strip-user-badge";
299
+ badge.dataset.userId = u.id;
300
+ el.appendChild(badge);
301
+
302
+ // Tooltip
303
+ el.addEventListener("mouseenter", function () { showIconTooltip(el, u.displayName); });
304
+ el.addEventListener("mouseleave", hideIconTooltip);
305
+
306
+ // Click: open DM
307
+ el.addEventListener("click", function () {
308
+ if (_ctx.openDm) _ctx.openDm(u.id);
309
+ });
310
+
311
+ // Right-click: show context menu
312
+ el.addEventListener("contextmenu", function (e) {
313
+ e.preventDefault();
314
+ e.stopPropagation();
315
+ showUserCtxMenu(el, u);
316
+ });
317
+
318
+ container.appendChild(el);
319
+ })(others[i]);
320
+ }
321
+
322
+ // Build mate project status lookup from project list
323
+ var mateProjectStatus = {};
324
+ if (_ctx && _ctx.projectList) {
325
+ var allProjects = _ctx.projectList;
326
+ for (var pi = 0; pi < allProjects.length; pi++) {
327
+ if (allProjects[pi].isMate) {
328
+ mateProjectStatus[allProjects[pi].slug] = allProjects[pi];
329
+ }
330
+ }
331
+ }
332
+
333
+ // Render mates (only favorites, built-in first, then user-created)
334
+ var favoriteMates = cachedMates.filter(function (m) {
335
+ if (cachedDmRemovedUsers[m.id]) return false;
336
+ if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
337
+ if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
338
+ return false;
339
+ });
340
+ var sortedMates = favoriteMates.sort(function (a, b) {
341
+ var aBuiltin = a.builtinKey ? 1 : 0;
342
+ var bBuiltin = b.builtinKey ? 1 : 0;
343
+ if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
344
+ return (a.createdAt || 0) - (b.createdAt || 0);
345
+ });
346
+ for (var mi = 0; mi < sortedMates.length; mi++) {
347
+ (function (mate) {
348
+ var mp = mate.profile || {};
349
+ var mateSlug = "mate-" + mate.id;
350
+ var mateProj = mateProjectStatus[mateSlug] || {};
351
+ var isActive = mate.id === currentDmUserId;
352
+ var el = document.createElement("div");
353
+ el.className = "icon-strip-user icon-strip-mate";
354
+ el.dataset.userId = mate.id;
355
+ el.dataset.mateSlug = mateSlug;
356
+ if (isActive) el.classList.add("active");
357
+
358
+ // Pending permission shake
359
+ if (mateProj.pendingPermissions > 0 && !isActive) {
360
+ el.classList.add("has-pending-perm");
361
+ }
362
+
363
+ var pill = document.createElement("span");
364
+ pill.className = "icon-strip-pill";
365
+ el.appendChild(pill);
366
+
367
+ var avatar = document.createElement("img");
368
+ avatar.className = "icon-strip-user-avatar" + (mate.primary ? " icon-strip-primary-mate" : "");
369
+ avatar.src = mateAvatarUrl(mate, 34);
370
+ avatar.alt = mp.displayName || mate.name || "Mate";
371
+ var mateColor = (mp.avatarColor) || mate.avatarColor || "#7c3aed";
372
+ avatar.style.background = mateColor + "30";
373
+ el.appendChild(avatar);
374
+
375
+ // Processing status dot (IO blink)
376
+ var statusDot = document.createElement("span");
377
+ statusDot.className = "icon-strip-status";
378
+ if (mateProj.isProcessing) statusDot.classList.add("processing");
379
+ el.appendChild(statusDot);
380
+
381
+ // Mate badge (bot icon)
382
+ var mateBadge = document.createElement("span");
383
+ mateBadge.className = "icon-strip-user-mate-badge";
384
+ mateBadge.innerHTML = iconHtml("bot");
385
+ el.appendChild(mateBadge);
386
+
387
+ var badge = document.createElement("span");
388
+ badge.className = "icon-strip-user-badge";
389
+ badge.dataset.userId = mate.id;
390
+ el.appendChild(badge);
391
+
392
+ // Restore unread badge if cached
393
+ var unreadCount = cachedDmUnread[mate.id] || 0;
394
+ if (unreadCount > 0 && !isActive) {
395
+ badge.textContent = unreadCount > 99 ? "99+" : String(unreadCount);
396
+ badge.classList.add("has-unread");
397
+ }
398
+
399
+ // Tooltip
400
+ var displayName = mp.displayName || mate.name || "New Mate";
401
+ el.addEventListener("mouseenter", function () {
402
+ var html = '<div style="font-weight:600">' + escapeHtml(displayName);
403
+ if (mate.primary) {
404
+ html += ' <span style="font-size:10px;font-weight:600;color:#00b894;background:rgba(0,184,148,0.1);padding:1px 5px;border-radius:3px;margin-left:4px">SYSTEM</span>';
405
+ }
406
+ html += '</div>';
407
+ if (mate.bio) {
408
+ html += '<div style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-top:2px">' + escapeHtml(mate.bio) + '</div>';
409
+ }
410
+ showIconTooltipHtml(el, html);
411
+ });
412
+ el.addEventListener("mouseleave", hideIconTooltip);
413
+
414
+ // Click: open DM with mate
415
+ el.addEventListener("click", function () {
416
+ if (_ctx.openDm) _ctx.openDm(mate.id);
417
+ });
418
+
419
+ // Right-click: context menu for mate
420
+ el.addEventListener("contextmenu", function (e) {
421
+ e.preventDefault();
422
+ e.stopPropagation();
423
+ showMateCtxMenu(el, mate);
424
+ });
425
+
426
+ container.appendChild(el);
427
+ })(sortedMates[mi]);
428
+ }
429
+
430
+ // Show container if we have mates even with no other users
431
+ if (cachedMates.length > 0) {
432
+ container.classList.remove("hidden");
433
+ }
434
+
435
+ // Add user (+) button
436
+ var addBtn = document.createElement("button");
437
+ addBtn.className = "icon-strip-invite";
438
+ addBtn.innerHTML = iconHtml("user-plus");
439
+ addBtn.addEventListener("click", function (e) {
440
+ e.stopPropagation();
441
+ toggleDmUserPicker(addBtn);
442
+ });
443
+ addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add user or create mate"); });
444
+ addBtn.addEventListener("mouseleave", hideIconTooltip);
445
+ container.appendChild(addBtn);
446
+ refreshIcons();
447
+ }
448
+
449
+ function toggleDmUserPicker(anchorEl) {
450
+ if (dmPickerOpen) {
451
+ closeDmUserPicker();
452
+ return;
453
+ }
454
+ dmPickerOpen = true;
455
+
456
+ var picker = document.createElement("div");
457
+ picker.className = "dm-user-picker";
458
+ picker.id = "dm-user-picker";
459
+
460
+ // Search input
461
+ var searchInput = document.createElement("input");
462
+ searchInput.className = "dm-user-picker-search";
463
+ searchInput.type = "text";
464
+ searchInput.placeholder = "Search mates and users...";
465
+ picker.appendChild(searchInput);
466
+
467
+ // User list element (appended later, after USERS label)
468
+ var listEl = document.createElement("div");
469
+ listEl.className = "dm-user-picker-list";
470
+
471
+ // Position the picker above the + button
472
+ document.body.appendChild(picker);
473
+ var rect = anchorEl.getBoundingClientRect();
474
+ picker.style.left = (rect.right + 8) + "px";
475
+ picker.style.bottom = (window.innerHeight - rect.bottom) + "px";
476
+
477
+ function renderPickerList(filter) {
478
+ listEl.innerHTML = "";
479
+ var allOthers = cachedAllUsers.filter(function (u) { return u.id !== cachedMyUserId; });
480
+ // Exclude already-favorited users
481
+ var available = allOthers.filter(function (u) {
482
+ return cachedDmFavorites.indexOf(u.id) === -1;
483
+ });
484
+ if (filter) {
485
+ var lf = filter.toLowerCase();
486
+ available = available.filter(function (u) {
487
+ return (u.displayName && u.displayName.toLowerCase().indexOf(lf) !== -1) ||
488
+ (u.username && u.username.toLowerCase().indexOf(lf) !== -1);
489
+ });
490
+ }
491
+ if (available.length === 0) {
492
+ var emptyEl = document.createElement("div");
493
+ emptyEl.className = "dm-user-picker-empty";
494
+ emptyEl.textContent = filter ? "No users found" : "No more users to add";
495
+ listEl.appendChild(emptyEl);
496
+ return;
497
+ }
498
+ for (var i = 0; i < available.length; i++) {
499
+ (function (u) {
500
+ var item = document.createElement("div");
501
+ item.className = "dm-user-picker-item";
502
+
503
+ var av = document.createElement("img");
504
+ av.className = "dm-user-picker-avatar";
505
+ av.src = userAvatarUrl(u, 28);
506
+ av.alt = u.displayName;
507
+ item.appendChild(av);
508
+
509
+ var name = document.createElement("span");
510
+ name.className = "dm-user-picker-name";
511
+ name.textContent = u.displayName;
512
+ item.appendChild(name);
513
+
514
+ item.addEventListener("click", function () {
515
+ if (_ctx.sendWs) {
516
+ _ctx.sendWs({ type: "dm_add_favorite", targetUserId: u.id });
517
+ }
518
+ closeDmUserPicker();
519
+ });
520
+
521
+ listEl.appendChild(item);
522
+ })(available[i]);
523
+ }
524
+ }
525
+
526
+ // --- MATES section ---
527
+ var matesSectionLabel = document.createElement("div");
528
+ matesSectionLabel.className = "dm-user-picker-section";
529
+ matesSectionLabel.textContent = "Mates";
530
+ picker.appendChild(matesSectionLabel);
531
+
532
+ var matesListEl = document.createElement("div");
533
+ matesListEl.className = "dm-user-picker-list dm-mates-list";
534
+ picker.appendChild(matesListEl);
535
+
536
+ // Update scroll gradient hint
537
+ function updateMatesScrollHint() {
538
+ var isOverflow = matesListEl.scrollHeight > matesListEl.clientHeight + 2;
539
+ if (!isOverflow) {
540
+ matesListEl.classList.add("no-overflow");
541
+ matesListEl.classList.remove("scrolled-bottom");
542
+ return;
543
+ }
544
+ matesListEl.classList.remove("no-overflow");
545
+ var atBottom = matesListEl.scrollTop + matesListEl.clientHeight >= matesListEl.scrollHeight - 4;
546
+ if (atBottom) {
547
+ matesListEl.classList.add("scrolled-bottom");
548
+ } else {
549
+ matesListEl.classList.remove("scrolled-bottom");
550
+ }
551
+ }
552
+ matesListEl.addEventListener("scroll", updateMatesScrollHint);
553
+
554
+ function renderMatesList(filter) {
555
+ matesListEl.innerHTML = "";
556
+ var allMates = cachedMates || [];
557
+ if (filter) {
558
+ var lf = filter.toLowerCase();
559
+ allMates = allMates.filter(function (m) {
560
+ var name = (m.profile && m.profile.displayName) || m.name || "";
561
+ return name.toLowerCase().indexOf(lf) !== -1;
562
+ });
563
+ }
564
+ // Build unified list: installed builtins, deleted builtins, user-created
565
+ var availBuiltins = (_ctx.availableBuiltins && _ctx.availableBuiltins()) || [];
566
+ var entries = [];
567
+ // 1. Installed builtin mates
568
+ for (var si = 0; si < allMates.length; si++) {
569
+ if (allMates[si].builtinKey) entries.push({ type: "mate", data: allMates[si] });
570
+ }
571
+ // 2. Deleted builtins (only when not filtering)
572
+ if (!filter) {
573
+ for (var di = 0; di < availBuiltins.length; di++) {
574
+ entries.push({ type: "deleted", data: availBuiltins[di] });
575
+ }
576
+ }
577
+ // 3. User-created mates
578
+ var userMates = allMates.filter(function (m) { return !m.builtinKey; });
579
+ userMates.sort(function (a, b) { return (a.createdAt || 0) - (b.createdAt || 0); });
580
+ for (var ui = 0; ui < userMates.length; ui++) {
581
+ entries.push({ type: "mate", data: userMates[ui] });
582
+ }
583
+
584
+ for (var i = 0; i < entries.length; i++) {
585
+ var entry = entries[i];
586
+ if (entry.type === "deleted") {
587
+ // Deleted builtin: show with "+ Add" button
588
+ (function (b) {
589
+ var bItem = document.createElement("div");
590
+ bItem.className = "dm-user-picker-item dm-user-picker-builtin-item";
591
+ bItem.style.opacity = "0.7";
592
+ var bAv = document.createElement("img");
593
+ bAv.className = "dm-user-picker-avatar";
594
+ bAv.src = mateAvatarUrl({ avatarCustom: b.avatarCustom, avatarStyle: b.avatarStyle || "bottts", avatarSeed: b.displayName, id: b.key }, 28);
595
+ bAv.alt = b.displayName;
596
+ bItem.appendChild(bAv);
597
+ var bNameWrap = document.createElement("div");
598
+ bNameWrap.style.cssText = "flex:1;min-width:0;";
599
+ var bName = document.createElement("span");
600
+ bName.className = "dm-user-picker-name";
601
+ bName.textContent = b.displayName;
602
+ bNameWrap.appendChild(bName);
603
+ var bBio = document.createElement("div");
604
+ bBio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
605
+ bBio.textContent = b.bio || b.displayName;
606
+ bNameWrap.appendChild(bBio);
607
+ bItem.appendChild(bNameWrap);
608
+ var bAddBtn = document.createElement("button");
609
+ bAddBtn.style.cssText = "border:none;background:none;cursor:pointer;padding:2px 6px;color:var(--accent, #6366f1);font-size:12px;font-weight:600;white-space:nowrap;";
610
+ bAddBtn.textContent = "+ Add";
611
+ bAddBtn.title = "Re-add " + b.displayName;
612
+ bAddBtn.addEventListener("click", function (e) {
613
+ e.stopPropagation();
614
+ if (_ctx.sendWs) _ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
615
+ closeDmUserPicker();
616
+ });
617
+ bItem.appendChild(bAddBtn);
618
+ bItem.addEventListener("click", function () {
619
+ if (_ctx.sendWs) _ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
620
+ closeDmUserPicker();
621
+ });
622
+ matesListEl.appendChild(bItem);
623
+ })(entry.data);
624
+ } else {
625
+ // Normal mate
626
+ (function (m) {
627
+ var mp = m.profile || {};
628
+ var isFav = cachedDmFavorites.indexOf(m.id) !== -1;
629
+ var item = document.createElement("div");
630
+ item.className = "dm-user-picker-item";
631
+ if (isFav) item.classList.add("dm-picker-fav");
632
+ var av = document.createElement("img");
633
+ av.className = "dm-user-picker-avatar";
634
+ av.src = mateAvatarUrl(m, 28);
635
+ av.alt = mp.displayName || m.name || "Mate";
636
+ item.appendChild(av);
637
+ var nameWrap = document.createElement("div");
638
+ nameWrap.style.cssText = "flex:1;min-width:0;";
639
+ var name = document.createElement("span");
640
+ name.className = "dm-user-picker-name";
641
+ name.textContent = mp.displayName || m.name || "Mate";
642
+ nameWrap.appendChild(name);
643
+ if (m.bio) {
644
+ var bio = document.createElement("div");
645
+ bio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
646
+ bio.textContent = m.bio;
647
+ nameWrap.appendChild(bio);
648
+ }
649
+ item.appendChild(nameWrap);
650
+ // Delete button with inline confirm
651
+ var delBtn = document.createElement("button");
652
+ delBtn.className = "dm-picker-del-btn";
653
+ delBtn.innerHTML = m.builtinKey ? iconHtml("minus-circle") : iconHtml("trash-2");
654
+ delBtn.title = m.builtinKey ? "Remove mate" : "Delete mate";
655
+ delBtn.addEventListener("click", function (e) {
656
+ e.stopPropagation();
657
+ var origHtml = item.innerHTML;
658
+ item.innerHTML = "";
659
+ item.style.justifyContent = "center";
660
+ item.style.gap = "6px";
661
+ var confirmMsg = document.createElement("span");
662
+ confirmMsg.style.cssText = "font-size:12px;color:var(--text-dimmer);";
663
+ confirmMsg.textContent = m.builtinKey ? "Remove? You can add back anytime." : "Delete permanently?";
664
+ item.appendChild(confirmMsg);
665
+ var yesBtn = document.createElement("button");
666
+ yesBtn.style.cssText = "border:none;background:var(--danger,#e74c3c);color:#fff;padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
667
+ yesBtn.textContent = m.builtinKey ? "Remove" : "Delete";
668
+ yesBtn.addEventListener("click", function (e2) {
669
+ e2.stopPropagation();
670
+ if (_ctx.sendWs) _ctx.sendWs({ type: "mate_delete", mateId: m.id });
671
+ closeDmUserPicker();
672
+ });
673
+ item.appendChild(yesBtn);
674
+ var noBtn = document.createElement("button");
675
+ noBtn.style.cssText = "border:1px solid var(--border);background:none;color:var(--text);padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
676
+ noBtn.textContent = "Cancel";
677
+ noBtn.addEventListener("click", function (e2) {
678
+ e2.stopPropagation();
679
+ item.innerHTML = origHtml;
680
+ item.style.justifyContent = "";
681
+ item.style.gap = "";
682
+ refreshIcons();
683
+ });
684
+ item.appendChild(noBtn);
685
+ });
686
+ item.appendChild(delBtn);
687
+ item.addEventListener("click", function () {
688
+ if (_ctx.openDm) _ctx.openDm(m.id);
689
+ if (!isFav && _ctx.sendWs) _ctx.sendWs({ type: "dm_add_favorite", targetUserId: m.id });
690
+ closeDmUserPicker();
691
+ });
692
+ matesListEl.appendChild(item);
693
+ })(entry.data);
694
+ }
695
+ }
696
+
697
+ if (entries.length === 0 && filter) {
698
+ var emptyEl = document.createElement("div");
699
+ emptyEl.className = "dm-user-picker-empty";
700
+ emptyEl.textContent = "No mates found";
701
+ matesListEl.appendChild(emptyEl);
702
+ }
703
+ refreshIcons();
704
+ requestAnimationFrame(updateMatesScrollHint);
705
+ }
706
+
707
+ // Create Mate option
708
+ var createMateEl = document.createElement("div");
709
+ createMateEl.className = "dm-user-picker-create-mate";
710
+ var hasCustomMates = (cachedMates || []).some(function (m) { return !m.builtinKey; });
711
+ var createMateLabel = hasCustomMates ? "Create a Mate" : "Create a Mate for what you're doing";
712
+ createMateEl.innerHTML = iconHtml("bot") + " <span>" + createMateLabel + "</span>";
713
+ createMateEl.addEventListener("click", function () {
714
+ closeDmUserPicker();
715
+ if (_ctx.openMateWizard) _ctx.openMateWizard();
716
+ });
717
+ picker.appendChild(createMateEl);
718
+
719
+ // Divider
720
+ var divider = document.createElement("div");
721
+ divider.style.borderTop = "1px solid var(--border, #333)";
722
+ divider.style.margin = "4px 0";
723
+ picker.appendChild(divider);
724
+
725
+ // Section label for users
726
+ var sectionLabel = document.createElement("div");
727
+ sectionLabel.className = "dm-user-picker-section";
728
+ sectionLabel.textContent = "Users";
729
+ picker.appendChild(sectionLabel);
730
+ picker.appendChild(listEl);
731
+
732
+ renderMatesList("");
733
+ renderPickerList("");
734
+ searchInput.addEventListener("input", function () {
735
+ var val = searchInput.value;
736
+ renderMatesList(val);
737
+ renderPickerList(val);
738
+ });
739
+
740
+ // Focus search
741
+ setTimeout(function () { searchInput.focus(); }, 50);
742
+
743
+ // Close on click outside
744
+ function onDocClick(e) {
745
+ if (!picker.contains(e.target) && e.target !== anchorEl && !anchorEl.contains(e.target)) {
746
+ closeDmUserPicker();
747
+ document.removeEventListener("click", onDocClick, true);
748
+ }
749
+ }
750
+ setTimeout(function () {
751
+ document.addEventListener("click", onDocClick, true);
752
+ }, 10);
753
+ picker._docClickHandler = onDocClick;
754
+ }
755
+
756
+ export function closeDmUserPicker() {
757
+ dmPickerOpen = false;
758
+ var picker = document.getElementById("dm-user-picker");
759
+ if (picker) {
760
+ if (picker._docClickHandler) {
761
+ document.removeEventListener("click", picker._docClickHandler, true);
762
+ }
763
+ picker.remove();
764
+ }
765
+ }
766
+
767
+ export function setCurrentDmUser(userId) {
768
+ currentDmUserId = userId;
769
+ // Update active state on user icons immediately
770
+ var container = document.getElementById("icon-strip-users");
771
+ if (!container) return;
772
+ var items = container.querySelectorAll(".icon-strip-user");
773
+ for (var i = 0; i < items.length; i++) {
774
+ if (items[i].dataset.userId === userId) {
775
+ items[i].classList.add("active");
776
+ } else {
777
+ items[i].classList.remove("active");
778
+ }
779
+ }
780
+ }
781
+
782
+ export function updateDmBadge(userId, count) {
783
+ var badge = document.querySelector('.icon-strip-user-badge[data-user-id="' + userId + '"]');
784
+ if (!badge) return;
785
+ if (count > 0) {
786
+ badge.textContent = count > 99 ? "99+" : String(count);
787
+ badge.classList.add("has-unread");
788
+ } else {
789
+ badge.textContent = "";
790
+ badge.classList.remove("has-unread");
791
+ }
792
+ }
793
+
794
+ export function getCurrentDmUserId() {
795
+ return currentDmUserId;
796
+ }
797
+
798
+ export function getCachedMates() {
799
+ return cachedMates;
800
+ }
801
+
802
+ export function getCachedDmFavorites() {
803
+ return cachedDmFavorites;
804
+ }
805
+
806
+ export function getCachedDmUnread() {
807
+ return cachedDmUnread;
808
+ }
809
+
810
+ export function getCachedDmRemovedUsers() {
811
+ return cachedDmRemovedUsers;
812
+ }