clay-server 2.27.0-beta.9 → 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 (71) 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-http.js +4 -2
  10. package/lib/project-loop.js +110 -48
  11. package/lib/project-mate-interaction.js +4 -0
  12. package/lib/project-notifications.js +210 -0
  13. package/lib/project-sessions.js +5 -2
  14. package/lib/project-user-message.js +2 -1
  15. package/lib/project.js +26 -2
  16. package/lib/public/app.js +1193 -8517
  17. package/lib/public/css/command-palette.css +14 -0
  18. package/lib/public/css/loop.css +301 -0
  19. package/lib/public/css/notifications-center.css +190 -0
  20. package/lib/public/css/rewind.css +6 -0
  21. package/lib/public/index.html +89 -35
  22. package/lib/public/modules/app-connection.js +160 -0
  23. package/lib/public/modules/app-cursors.js +473 -0
  24. package/lib/public/modules/app-debate-ui.js +389 -0
  25. package/lib/public/modules/app-dm.js +627 -0
  26. package/lib/public/modules/app-favicon.js +212 -0
  27. package/lib/public/modules/app-header.js +229 -0
  28. package/lib/public/modules/app-home-hub.js +600 -0
  29. package/lib/public/modules/app-loop-ui.js +589 -0
  30. package/lib/public/modules/app-loop-wizard.js +439 -0
  31. package/lib/public/modules/app-messages.js +1560 -0
  32. package/lib/public/modules/app-misc.js +299 -0
  33. package/lib/public/modules/app-notifications.js +372 -0
  34. package/lib/public/modules/app-panels.js +888 -0
  35. package/lib/public/modules/app-projects.js +798 -0
  36. package/lib/public/modules/app-rate-limit.js +451 -0
  37. package/lib/public/modules/app-rendering.js +597 -0
  38. package/lib/public/modules/app-skills-install.js +234 -0
  39. package/lib/public/modules/command-palette.js +27 -4
  40. package/lib/public/modules/input.js +31 -20
  41. package/lib/public/modules/scheduler-config.js +1532 -0
  42. package/lib/public/modules/scheduler-history.js +79 -0
  43. package/lib/public/modules/scheduler.js +33 -1554
  44. package/lib/public/modules/session-search.js +13 -1
  45. package/lib/public/modules/sidebar-mates.js +812 -0
  46. package/lib/public/modules/sidebar-mobile.js +1269 -0
  47. package/lib/public/modules/sidebar-projects.js +1449 -0
  48. package/lib/public/modules/sidebar-sessions.js +986 -0
  49. package/lib/public/modules/sidebar.js +232 -4591
  50. package/lib/public/modules/store.js +27 -0
  51. package/lib/public/modules/ws-ref.js +7 -0
  52. package/lib/public/style.css +1 -0
  53. package/lib/sdk-bridge.js +96 -717
  54. package/lib/sdk-message-processor.js +587 -0
  55. package/lib/sdk-message-queue.js +42 -0
  56. package/lib/sdk-skill-discovery.js +131 -0
  57. package/lib/server-admin.js +712 -0
  58. package/lib/server-auth.js +737 -0
  59. package/lib/server-dm.js +221 -0
  60. package/lib/server-mates.js +281 -0
  61. package/lib/server-palette.js +110 -0
  62. package/lib/server-settings.js +479 -0
  63. package/lib/server-skills.js +280 -0
  64. package/lib/server.js +246 -2755
  65. package/lib/sessions.js +11 -4
  66. package/lib/users-auth.js +146 -0
  67. package/lib/users-permissions.js +118 -0
  68. package/lib/users-preferences.js +210 -0
  69. package/lib/users.js +48 -398
  70. package/lib/ws-schema.js +498 -0
  71. package/package.json +1 -1
@@ -0,0 +1,627 @@
1
+ // app-dm.js - DM mode, mate project switching, mate onboarding
2
+ // Extracted from app.js (PR-24)
3
+
4
+ var _ctx = null;
5
+
6
+ var MATE_ONBOARDING_KEY = "clay-mate-onboarding-shown";
7
+ var CLAUDE_CODE_AVATAR = "/claude-code-avatar.png";
8
+ var bgMateIoTimers = {};
9
+ var dmTypingTimer = null;
10
+
11
+ export function initDm(ctx) {
12
+ _ctx = ctx;
13
+
14
+ // --- Mobile mate title bar click handlers ---
15
+ var mobileBack = document.getElementById("mate-mobile-back");
16
+ var mobileTitle = document.getElementById("mate-mobile-title");
17
+ var mobileMore = document.getElementById("mate-mobile-more");
18
+ if (mobileBack) {
19
+ mobileBack.addEventListener("click", function (e) {
20
+ e.stopPropagation();
21
+ exitDmMode();
22
+ });
23
+ }
24
+ if (mobileMore) {
25
+ mobileMore.addEventListener("click", function (e) {
26
+ e.stopPropagation();
27
+ _ctx.openMobileSheet("mate-profile");
28
+ });
29
+ }
30
+ if (mobileTitle) {
31
+ mobileTitle.addEventListener("click", function () {
32
+ _ctx.openMobileSheet("mate-profile");
33
+ });
34
+ }
35
+ }
36
+
37
+ export function openDm(targetUserId) {
38
+ if (!_ctx.ws || _ctx.ws.readyState !== 1) return;
39
+ // Persist DM state for refresh recovery
40
+ try { localStorage.setItem("clay-active-dm", targetUserId); } catch (e) {}
41
+ // Check mate skill updates before opening mate DM
42
+ if (typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
43
+ showMateOnboarding(function () {
44
+ _ctx.requireClayMateInterview(function () {
45
+ _ctx.ws.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
46
+ });
47
+ });
48
+ return;
49
+ }
50
+ _ctx.ws.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
51
+ }
52
+
53
+ function showMateOnboarding(callback) {
54
+ try {
55
+ if (localStorage.getItem(MATE_ONBOARDING_KEY)) { callback(); return; }
56
+ } catch (e) {}
57
+
58
+ var overlay = document.createElement("div");
59
+ overlay.className = "mate-onboarding-overlay";
60
+ overlay.innerHTML =
61
+ '<div class="mate-onboarding-card">' +
62
+ '<h2 class="mate-onboarding-title">Meet your Mates</h2>' +
63
+ '<p class="mate-onboarding-desc">' +
64
+ 'Mates are AI teammates powered by your Claude Code.<br>Each one has a distinct role, builds its own knowledge, and gets sharper over time.' +
65
+ '</p>' +
66
+ '<ul class="mate-onboarding-features">' +
67
+ '<li><span class="mate-onboarding-bullet">\uD83C\uDFAD</span><div><strong>Specialized personas</strong><br><span class="mate-onboarding-sub">Architect, reviewer, researcher, chief of staff, and more</span></div></li>' +
68
+ '<li><span class="mate-onboarding-bullet">\uD83D\uDD04</span><div><strong>Persistent memory</strong><br><span class="mate-onboarding-sub">Every conversation makes them smarter about you and your work</span></div></li>' +
69
+ '<li><span class="mate-onboarding-bullet">\uD83D\uDCAC</span><div><strong>Shared context across the team</strong><br><span class="mate-onboarding-sub">What one mate learns, the others can reference</span></div></li>' +
70
+ '<li><span class="mate-onboarding-bullet">\uD83D\uDCDA</span><div><strong>Self-growing knowledge base</strong><br><span class="mate-onboarding-sub">They accumulate notes, decisions, and observations on their own</span></div></li>' +
71
+ '</ul>' +
72
+ '<button class="mate-onboarding-btn">Let\u2019s go</button>' +
73
+ '</div>';
74
+
75
+ document.body.appendChild(overlay);
76
+
77
+ // Animate in
78
+ requestAnimationFrame(function () {
79
+ overlay.classList.add("visible");
80
+ });
81
+
82
+ function dismissOnboarding() {
83
+ try { localStorage.setItem(MATE_ONBOARDING_KEY, "1"); } catch (e) {}
84
+ fetch("/api/user/mate-onboarded", { method: "POST" }).catch(function () {});
85
+ overlay.classList.remove("visible");
86
+ setTimeout(function () { overlay.remove(); callback(); }, 200);
87
+ }
88
+
89
+ overlay.querySelector(".mate-onboarding-btn").addEventListener("click", dismissOnboarding);
90
+
91
+ // Click outside to dismiss
92
+ overlay.addEventListener("click", function (e) {
93
+ if (e.target === overlay) dismissOnboarding();
94
+ });
95
+ }
96
+
97
+ export function enterDmMode(key, targetUser, messages) {
98
+ console.log("[DEBUG enterDmMode] key=" + key, "isMate=" + (targetUser && targetUser.isMate), "messages=" + (messages ? messages.length : 0));
99
+ // Clean up previous DM/mate state before entering new one
100
+ if (_ctx.dmMode) {
101
+ _ctx.hideMateSidebar();
102
+ _ctx.hideKnowledge();
103
+ _ctx.hideMemory();
104
+ // Reset dm-header-bar
105
+ var prevHeader = document.getElementById("dm-header-bar");
106
+ if (prevHeader) {
107
+ prevHeader.style.display = "";
108
+ prevHeader.style.background = "";
109
+ var prevTag = prevHeader.querySelector(".dm-header-mate-tag");
110
+ if (prevTag) prevTag.remove();
111
+ }
112
+ // Restore terminal button
113
+ var prevTermBtn = document.getElementById("terminal-toggle-btn");
114
+ if (prevTermBtn) prevTermBtn.style.display = "";
115
+ // Remove dm-mode classes
116
+ var prevMain = document.getElementById("main-column");
117
+ if (prevMain) prevMain.classList.remove("dm-mode");
118
+ var prevSidebar = document.getElementById("sidebar-column");
119
+ if (prevSidebar) prevSidebar.classList.remove("dm-mode");
120
+ var prevResize = document.getElementById("sidebar-resize-handle");
121
+ if (prevResize) prevResize.classList.remove("dm-mode");
122
+ // Reset chat title bar
123
+ var prevTitleBar = document.querySelector(".title-bar-content");
124
+ if (prevTitleBar) {
125
+ prevTitleBar.style.background = "";
126
+ prevTitleBar.classList.remove("mate-dm-active");
127
+ }
128
+ }
129
+
130
+ _ctx.dmMode = true;
131
+ _ctx.dmKey = key;
132
+ _ctx.dmTargetUser = targetUser;
133
+
134
+ // Notify server of active mate DM (server-side presence tracking)
135
+ // IMPORTANT: set_mate_dm must go to the MAIN project, not a mate project WS.
136
+ // When switching between mates, ws points to the current mate project,
137
+ // so we defer sending set_mate_dm until we reconnect to the main project's context.
138
+ // The server will also receive it via the mate project's onDmMessage handler,
139
+ // but the presence should only be stored on the main project slug.
140
+ if (targetUser && targetUser.isMate) {
141
+ // Send to the current WS only if it's the main project (not another mate)
142
+ if (!_ctx.mateProjectSlug && _ctx.ws && _ctx.ws.readyState === 1) {
143
+ try { _ctx.ws.send(JSON.stringify({ type: "set_mate_dm", mateId: targetUser.id })); } catch(e) {}
144
+ }
145
+ }
146
+
147
+ // Clear unread for this user
148
+ if (targetUser) {
149
+ _ctx.dmUnread[targetUser.id] = 0;
150
+ _ctx.updateDmBadge(targetUser.id, 0);
151
+ }
152
+
153
+ // Update icon strip active state
154
+ _ctx.setCurrentDmUser(targetUser ? targetUser.id : null);
155
+ var activeProj = document.querySelector("#icon-strip-projects .icon-strip-item.active");
156
+ if (activeProj) activeProj.classList.remove("active");
157
+ var homeIcon = document.querySelector(".icon-strip-home");
158
+ if (homeIcon) homeIcon.classList.remove("active");
159
+ // Re-render user strip to show active state
160
+ if (_ctx.cachedProjects && _ctx.cachedProjects.length > 0) {
161
+ _ctx.renderProjectList();
162
+ }
163
+
164
+ // Hide home hub if visible
165
+ _ctx.hideHomeHub();
166
+
167
+ // Hide sticky notes if visible
168
+ _ctx.hideNotes();
169
+
170
+ var isMate = targetUser && targetUser.isMate;
171
+
172
+ // Hide project UI + sidebar, show DM UI
173
+ var mainCol = document.getElementById("main-column");
174
+ if (mainCol && !isMate) mainCol.classList.add("dm-mode");
175
+ var sidebarCol = document.getElementById("sidebar-column");
176
+ if (sidebarCol) sidebarCol.classList.add("dm-mode");
177
+ var resizeHandle = document.getElementById("sidebar-resize-handle");
178
+ if (resizeHandle) resizeHandle.classList.add("dm-mode");
179
+ if (isMate && targetUser.projectSlug) {
180
+ // Mate DM: switch to mate's project (same as project switching)
181
+ _ctx.showMateSidebar(targetUser.id, targetUser);
182
+ // Close file viewer and terminal panel BEFORE switching WS (needs old WS still open)
183
+ try { _ctx.closeFileViewer(); } catch(e) {}
184
+ _ctx.closeTerminal();
185
+ var termBtn = document.getElementById("terminal-toggle-btn");
186
+ if (termBtn) termBtn.style.display = "none";
187
+ // Apply mate color to chat title bar and panels
188
+ var mateColor = (targetUser.profile && targetUser.profile.avatarColor) || targetUser.avatarColor || "#7c3aed";
189
+ document.body.style.setProperty("--mate-color", mateColor);
190
+ document.body.style.setProperty("--mate-color-tint", mateColor + "0a");
191
+ document.body.classList.add("mate-dm-active");
192
+ // Build mate avatar URL for DM bubble injection
193
+ var mp = targetUser.profile || {};
194
+ var mateAvUrlDm = _ctx.mateAvatarUrl(targetUser, 36);
195
+ var myUser = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
196
+ if (!myUser) {
197
+ try { var cached = JSON.parse(localStorage.getItem("clay_my_user") || "null"); if (cached) myUser = cached; } catch(e) {}
198
+ }
199
+ var myAvatarUrl = _ctx.userAvatarUrl(myUser || { id: _ctx.myUserId }, 36);
200
+ var myDisplayName = (myUser && myUser.displayName) || "";
201
+ document.body.dataset.mateAvatarUrl = mateAvUrlDm;
202
+ document.body.dataset.mateName = mp.displayName || targetUser.displayName || targetUser.name || "";
203
+ document.body.dataset.myAvatarUrl = myAvatarUrl;
204
+ document.body.dataset.myDisplayName = myDisplayName;
205
+ // Cache my info for restore after hard refresh
206
+ if (myUser) {
207
+ try { localStorage.setItem("clay_my_user", JSON.stringify({ displayName: myUser.displayName, avatarStyle: myUser.avatarStyle, avatarSeed: myUser.avatarSeed, avatarCustom: myUser.avatarCustom, username: myUser.username })); } catch(e) {}
208
+ }
209
+ var titleBarContent = document.querySelector(".title-bar-content");
210
+ if (titleBarContent) {
211
+ titleBarContent.style.background = mateColor;
212
+ titleBarContent.classList.add("mate-dm-active");
213
+ }
214
+ // Populate mobile title bar for mate DM (CSS handles visibility via media query)
215
+ var mateMobileTitle = document.getElementById("mate-mobile-title");
216
+ if (mateMobileTitle) {
217
+ var mateMobileAvatar = document.getElementById("mate-mobile-avatar");
218
+ var mateMobileName = document.getElementById("mate-mobile-name");
219
+ var mateMobileStatus = document.getElementById("mate-mobile-status");
220
+ if (mateMobileAvatar) mateMobileAvatar.src = mateAvUrlDm;
221
+ if (mateMobileName) mateMobileName.textContent = (mp.displayName || targetUser.displayName || targetUser.name || "");
222
+ if (mateMobileStatus) mateMobileStatus.textContent = "online";
223
+ mateMobileTitle.classList.remove("hidden");
224
+ // Store mate data for profile sheet
225
+ _ctx.setMobileSheetMateData({
226
+ id: targetUser.id,
227
+ displayName: mp.displayName || targetUser.displayName || targetUser.name || "",
228
+ description: mp.description || targetUser.description || "",
229
+ avatarUrl: mateAvUrlDm,
230
+ color: mateColor
231
+ });
232
+ }
233
+ // Switch to mate project WS LAST, after all UI setup is complete.
234
+ // Must be last because connect() changes ws to CONNECTING state,
235
+ // and earlier code (closeFileViewer etc.) needs the old WS still open.
236
+ connectMateProject(targetUser.projectSlug);
237
+ }
238
+
239
+ // Hide user-island in human DM, keep visible in Mate DM
240
+ var userIsland = document.getElementById("user-island");
241
+ if (userIsland && !isMate) userIsland.classList.add("dm-hidden");
242
+
243
+ // Render DM messages
244
+ _ctx.dmMessageCache = messages ? messages.slice() : [];
245
+ _ctx.messagesEl.innerHTML = "";
246
+ if (messages && messages.length > 0) {
247
+ for (var i = 0; i < messages.length; i++) {
248
+ appendDmMessage(messages[i]);
249
+ }
250
+ }
251
+ _ctx.scrollToBottom();
252
+
253
+ // Focus input
254
+ if (_ctx.inputEl) {
255
+ var targetName = targetUser ? ((targetUser.profile && targetUser.profile.displayName) || targetUser.displayName || targetUser.name || "") : "";
256
+ _ctx.inputEl.placeholder = "Message " + targetName;
257
+ _ctx.inputEl.focus();
258
+ }
259
+
260
+ // Populate DM header bar with user avatar, name, and personal color
261
+ if (targetUser) {
262
+ var dmHeaderBar = document.getElementById("dm-header-bar");
263
+ var dmAvatar = document.getElementById("dm-header-avatar");
264
+ var dmName = document.getElementById("dm-header-name");
265
+ if (isMate) {
266
+ // Mate uses project chat title bar, hide DM header
267
+ if (dmHeaderBar) dmHeaderBar.style.display = "none";
268
+ } else {
269
+ if (dmHeaderBar) dmHeaderBar.style.display = "";
270
+ if (dmAvatar) {
271
+ dmAvatar.src = _ctx.userAvatarUrl(targetUser, 28);
272
+ }
273
+ if (dmName) dmName.textContent = targetUser.displayName;
274
+ if (dmHeaderBar && targetUser.avatarColor) {
275
+ dmHeaderBar.style.background = targetUser.avatarColor;
276
+ }
277
+ // Remove mate tag for regular DM
278
+ var existingTag = dmHeaderBar ? dmHeaderBar.querySelector(".dm-header-mate-tag") : null;
279
+ if (existingTag) existingTag.remove();
280
+ }
281
+ }
282
+ }
283
+
284
+ export function exitDmMode(skipProjectSwitch) {
285
+ if (!_ctx.dmMode) return;
286
+ var wasMate = _ctx.dmTargetUser && _ctx.dmTargetUser.isMate;
287
+ _ctx.dmMode = false;
288
+ _ctx.dmKey = null;
289
+ _ctx.dmTargetUser = null;
290
+ try { localStorage.removeItem("clay-active-dm"); } catch (e) {}
291
+ _ctx.setCurrentDmUser(null);
292
+
293
+ var mainCol = document.getElementById("main-column");
294
+ if (mainCol) mainCol.classList.remove("dm-mode");
295
+ var sidebarCol = document.getElementById("sidebar-column");
296
+ if (sidebarCol) sidebarCol.classList.remove("dm-mode");
297
+ var resizeHandle = document.getElementById("sidebar-resize-handle");
298
+ if (resizeHandle) resizeHandle.classList.remove("dm-mode");
299
+ _ctx.hideMateSidebar();
300
+ _ctx.hideKnowledge();
301
+ _ctx.hideMemory();
302
+ if (_ctx.isSchedulerOpen()) _ctx.closeScheduler();
303
+ // Restore terminal button
304
+ var termBtn = document.getElementById("terminal-toggle-btn");
305
+ if (termBtn) termBtn.style.display = "";
306
+
307
+ // Reset DM header
308
+ var dmHeaderBar = document.getElementById("dm-header-bar");
309
+ if (dmHeaderBar) {
310
+ dmHeaderBar.style.display = "";
311
+ dmHeaderBar.style.background = "";
312
+ var mateTag = dmHeaderBar.querySelector(".dm-header-mate-tag");
313
+ if (mateTag) mateTag.remove();
314
+ }
315
+ // Reset chat title bar and mate color
316
+ document.body.style.removeProperty("--mate-color");
317
+ document.body.style.removeProperty("--mate-color-tint");
318
+ document.body.classList.remove("mate-dm-active");
319
+ delete document.body.dataset.mateAvatarUrl;
320
+ delete document.body.dataset.mateName;
321
+ delete document.body.dataset.myAvatarUrl;
322
+ // Remove injected DM bubble avatars
323
+ var bubbleAvatars = _ctx.messagesEl.querySelectorAll(".dm-bubble-avatar");
324
+ for (var ba = 0; ba < bubbleAvatars.length; ba++) bubbleAvatars[ba].remove();
325
+ var titleBarContent = document.querySelector(".title-bar-content");
326
+ if (titleBarContent) {
327
+ titleBarContent.style.background = "";
328
+ titleBarContent.classList.remove("mate-dm-active");
329
+ }
330
+ // Hide mobile mate title bar
331
+ var mateMobileTitle = document.getElementById("mate-mobile-title");
332
+ if (mateMobileTitle) mateMobileTitle.classList.add("hidden");
333
+
334
+ // Restore user-island (covers my avatar again)
335
+ var userIsland = document.getElementById("user-island");
336
+ if (userIsland) userIsland.classList.remove("dm-hidden");
337
+
338
+ if (_ctx.inputEl) _ctx.inputEl.placeholder = "";
339
+
340
+ // Switch back to main project (same as project switching)
341
+ if (wasMate && !skipProjectSwitch) {
342
+ disconnectMateProject();
343
+ } else if (wasMate && skipProjectSwitch) {
344
+ // Just clean up mate state, caller will handle project switch
345
+ _ctx.returningFromMateDm = true;
346
+ _ctx.mateProjectSlug = null;
347
+ _ctx.savedMainSlug = null;
348
+ _ctx.showDebateSticky("hide", null);
349
+ var debateFloat = document.getElementById("debate-info-float");
350
+ if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
351
+ } else {
352
+ // Human DM: just re-request state from main project
353
+ if (_ctx.ws && _ctx.ws.readyState === 1) {
354
+ _ctx.ws.send(JSON.stringify({ type: "switch_session", id: _ctx.activeSessionId }));
355
+ _ctx.ws.send(JSON.stringify({ type: "note_list_request" }));
356
+ }
357
+ }
358
+ _ctx.renderProjectList();
359
+ }
360
+
361
+ export function handleMateCreatedInApp(mate, msg) {
362
+ if (!mate) return;
363
+ _ctx.cachedMatesList.push(mate);
364
+ if (msg && msg.availableBuiltins) _ctx.cachedAvailableBuiltins = msg.availableBuiltins;
365
+ if (msg && msg.dmFavorites) _ctx.cachedDmFavorites = msg.dmFavorites;
366
+ _ctx.renderUserStrip(_ctx.cachedAllUsers, _ctx.cachedOnlineIds, _ctx.myUserId, _ctx.cachedDmFavorites, _ctx.cachedDmConversations, _ctx.dmUnread, _ctx.dmRemovedUsers, _ctx.cachedMatesList);
367
+ // Built-in mates handle their own onboarding via CLAUDE.md, skip auto-interview
368
+ if (!mate.builtinKey) {
369
+ _ctx.pendingMateInterview = mate;
370
+ }
371
+ openDm(mate.id);
372
+ }
373
+
374
+ export function renderAvailableBuiltins(builtins) {
375
+ // Append deleted built-in mates to the mates list in the picker
376
+ var matesList = document.querySelector(".dm-mates-list");
377
+ if (!matesList) return;
378
+ if (!builtins || builtins.length === 0) return;
379
+
380
+ for (var i = 0; i < builtins.length; i++) {
381
+ (function (b) {
382
+ var item = document.createElement("div");
383
+ item.className = "dm-user-picker-item dm-user-picker-builtin-item";
384
+ item.style.opacity = "0.5";
385
+
386
+ var av = document.createElement("img");
387
+ av.className = "dm-user-picker-avatar";
388
+ av.src = b.avatarCustom || "";
389
+ av.alt = b.displayName;
390
+ item.appendChild(av);
391
+
392
+ var nameWrap = document.createElement("div");
393
+ nameWrap.style.cssText = "flex:1;min-width:0;";
394
+ var nameEl = document.createElement("span");
395
+ nameEl.className = "dm-user-picker-name";
396
+ nameEl.textContent = b.displayName;
397
+ nameWrap.appendChild(nameEl);
398
+ var bioEl = document.createElement("div");
399
+ bioEl.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
400
+ bioEl.textContent = "Deleted";
401
+ nameWrap.appendChild(bioEl);
402
+ item.appendChild(nameWrap);
403
+
404
+ var addBtn = document.createElement("button");
405
+ addBtn.style.cssText = "border:none;background:none;cursor:pointer;padding:2px 6px;color:var(--accent, #6366f1);font-size:12px;font-weight:600;";
406
+ addBtn.textContent = "+ Add";
407
+ addBtn.title = "Re-add " + b.displayName;
408
+ addBtn.addEventListener("click", function (e) {
409
+ e.stopPropagation();
410
+ if (_ctx.ws && _ctx.ws.readyState === 1) {
411
+ _ctx.ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
412
+ }
413
+ _ctx.closeDmUserPicker();
414
+ });
415
+ item.appendChild(addBtn);
416
+
417
+ item.addEventListener("click", function () {
418
+ if (_ctx.ws && _ctx.ws.readyState === 1) {
419
+ _ctx.ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
420
+ }
421
+ _ctx.closeDmUserPicker();
422
+ });
423
+
424
+ matesList.appendChild(item);
425
+ })(builtins[i]);
426
+ }
427
+ }
428
+
429
+ export function buildMateInterviewPrompt(mate) {
430
+ var sd = mate.seedData || {};
431
+ var parts = [];
432
+ var spokenLang = _ctx.getProfileLang() || "en-US";
433
+ parts.push("Spoken Language: " + spokenLang);
434
+ if (sd.relationship) parts.push("Relationship: " + sd.relationship);
435
+ if (sd.activity && sd.activity.length > 0) parts.push("Activities: " + sd.activity.join(", "));
436
+ if (sd.communicationStyle && sd.communicationStyle.length > 0) {
437
+ var styleLabels = {
438
+ direct_concise: "direct and concise",
439
+ soft_detailed: "soft and detailed",
440
+ witty: "witty",
441
+ encouraging: "encouraging",
442
+ formal: "formal",
443
+ no_nonsense: "no-nonsense",
444
+ };
445
+ var styles = sd.communicationStyle.map(function (s) { return styleLabels[s] || s.replace(/_/g, " "); });
446
+ parts.push("Communication: " + styles.join(", "));
447
+ }
448
+ if (sd.autonomy) parts.push("Autonomy: " + sd.autonomy.replace(/_/g, " "));
449
+
450
+ return "Use the /clay-mate-interview skill to start the interview.\n\n" +
451
+ "Mate ID: " + mate.id + "\n" +
452
+ "Mate Directory: .claude/mates/" + mate.id + "\n\n" +
453
+ "Seed Data:\n" + parts.join("\n");
454
+ }
455
+
456
+ export function updateMateIconStatus(msg) {
457
+ if (!_ctx.mateProjectSlug) return;
458
+ var slug = _ctx.mateProjectSlug;
459
+ if (msg.type === "content" || msg.type === "tool" || msg.type === "tool_use" || msg.type === "thinking") {
460
+ var ioDot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
461
+ if (ioDot) {
462
+ ioDot.classList.add("io");
463
+ clearTimeout(bgMateIoTimers[slug]);
464
+ bgMateIoTimers[slug] = setTimeout(function () { ioDot.classList.remove("io"); }, 80);
465
+ }
466
+ }
467
+ if (msg.type === "status" && msg.status === "processing") {
468
+ var dot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
469
+ if (dot) dot.classList.add("processing");
470
+ var mateSessionDot = document.querySelector(".mate-session-item.active .session-processing");
471
+ if (mateSessionDot) mateSessionDot.style.display = "";
472
+ }
473
+ if (msg.type === "done") {
474
+ var dot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
475
+ if (dot) dot.classList.remove("processing");
476
+ var mateSessionDot = document.querySelector(".mate-session-item.active .session-processing");
477
+ if (mateSessionDot) mateSessionDot.style.display = "none";
478
+ }
479
+ }
480
+
481
+ export function connectMateProject(slug) {
482
+ _ctx.mateProjectSlug = slug;
483
+ // Only save the main slug on the FIRST mate switch (preserve original main project)
484
+ if (!_ctx.savedMainSlug) _ctx.savedMainSlug = _ctx.currentSlug;
485
+ _ctx.currentSlug = slug;
486
+ _ctx.wsPath = "/p/" + slug + "/ws";
487
+ _ctx.resetClientState();
488
+ _ctx.connect();
489
+ }
490
+
491
+ export function disconnectMateProject() {
492
+ _ctx.mateProjectSlug = null;
493
+ // Hide debate sticky when leaving mate DM
494
+ _ctx.showDebateSticky("hide", null);
495
+ // Hide debate info float
496
+ var debateFloat = document.getElementById("debate-info-float");
497
+ if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
498
+ // Switch back to main project
499
+ if (_ctx.savedMainSlug) {
500
+ _ctx.returningFromMateDm = true;
501
+ _ctx.currentSlug = _ctx.savedMainSlug;
502
+ _ctx.basePath = "/p/" + _ctx.savedMainSlug + "/";
503
+ _ctx.wsPath = "/p/" + _ctx.savedMainSlug + "/ws";
504
+ _ctx.savedMainSlug = null;
505
+ _ctx.resetClientState();
506
+ _ctx.connect();
507
+ }
508
+ }
509
+
510
+ export function appendDmMessage(msg) {
511
+ if (_ctx.dmMode) _ctx.dmMessageCache.push(msg);
512
+ var isMe = msg.from === _ctx.myUserId;
513
+ var d = new Date(msg.ts);
514
+ var timeStr = d.getHours().toString().padStart(2, "0") + ":" + d.getMinutes().toString().padStart(2, "0");
515
+
516
+ // Check if we can compact (same sender as previous, within 5 min)
517
+ var prev = _ctx.messagesEl.lastElementChild;
518
+ var compact = false;
519
+ if (prev && prev.dataset.from === msg.from) {
520
+ var prevTs = parseInt(prev.dataset.ts || "0", 10);
521
+ if (msg.ts - prevTs < 300000) compact = true;
522
+ }
523
+
524
+ var div = document.createElement("div");
525
+ div.className = "dm-msg" + (compact ? " dm-msg-compact" : "");
526
+ div.dataset.from = msg.from;
527
+ div.dataset.ts = msg.ts;
528
+
529
+ if (compact) {
530
+ // Compact: just hover-time + text, no avatar/name
531
+ var hoverTime = document.createElement("span");
532
+ hoverTime.className = "dm-msg-hover-time";
533
+ hoverTime.textContent = timeStr;
534
+ div.appendChild(hoverTime);
535
+
536
+ var body = document.createElement("div");
537
+ body.className = "dm-msg-body";
538
+ body.textContent = msg.text;
539
+ div.appendChild(body);
540
+ } else {
541
+ // Full: avatar + header(name, time) + text
542
+ var avatar = document.createElement("img");
543
+ avatar.className = "dm-msg-avatar";
544
+ if (isMe) {
545
+ var myUser = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
546
+ avatar.src = _ctx.userAvatarUrl(myUser || { id: _ctx.myUserId }, 36);
547
+ } else if (_ctx.dmTargetUser) {
548
+ avatar.src = _ctx.userAvatarUrl(_ctx.dmTargetUser, 36);
549
+ }
550
+ div.appendChild(avatar);
551
+
552
+ var content = document.createElement("div");
553
+ content.className = "dm-msg-content";
554
+
555
+ var header = document.createElement("div");
556
+ header.className = "dm-msg-header";
557
+
558
+ var name = document.createElement("span");
559
+ name.className = "dm-msg-name";
560
+ if (isMe) {
561
+ var mu = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
562
+ name.textContent = mu ? mu.displayName : "Me";
563
+ } else {
564
+ name.textContent = _ctx.dmTargetUser ? _ctx.dmTargetUser.displayName : "User";
565
+ }
566
+ header.appendChild(name);
567
+
568
+ var time = document.createElement("span");
569
+ time.className = "dm-msg-time";
570
+ time.textContent = timeStr;
571
+ header.appendChild(time);
572
+
573
+ content.appendChild(header);
574
+
575
+ var body = document.createElement("div");
576
+ body.className = "dm-msg-body";
577
+ body.textContent = msg.text;
578
+ content.appendChild(body);
579
+
580
+ div.appendChild(content);
581
+ }
582
+
583
+ _ctx.messagesEl.appendChild(div);
584
+ }
585
+
586
+ export function showDmTypingIndicator(typing) {
587
+ var existing = document.getElementById("dm-typing-indicator");
588
+ if (!typing) {
589
+ if (existing) existing.remove();
590
+ return;
591
+ }
592
+ if (existing) return; // already showing
593
+ if (!_ctx.dmTargetUser) return;
594
+
595
+ var div = document.createElement("div");
596
+ div.id = "dm-typing-indicator";
597
+ div.className = "dm-msg dm-typing-indicator";
598
+
599
+ var avatar = document.createElement("img");
600
+ avatar.className = "dm-msg-avatar";
601
+ avatar.src = _ctx.userAvatarUrl(_ctx.dmTargetUser, 36);
602
+ div.appendChild(avatar);
603
+
604
+ var dots = document.createElement("div");
605
+ dots.className = "dm-typing-dots";
606
+ dots.innerHTML = "<span></span><span></span><span></span>";
607
+ div.appendChild(dots);
608
+
609
+ _ctx.messagesEl.appendChild(div);
610
+ _ctx.scrollToBottom();
611
+
612
+ // Auto-hide after 5s in case stop signal is missed
613
+ clearTimeout(dmTypingTimer);
614
+ dmTypingTimer = setTimeout(function () {
615
+ showDmTypingIndicator(false);
616
+ }, 5000);
617
+ }
618
+
619
+ export function handleDmSend() {
620
+ if (!_ctx.dmMode || !_ctx.dmKey || !_ctx.inputEl) return false;
621
+ var text = _ctx.inputEl.value.trim();
622
+ if (!text) return false;
623
+ _ctx.ws.send(JSON.stringify({ type: "dm_send", dmKey: _ctx.dmKey, text: text }));
624
+ _ctx.inputEl.value = "";
625
+ _ctx.autoResize();
626
+ return true;
627
+ }