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
@@ -1,4662 +1,303 @@
1
- import { avatarUrl, userAvatarUrl, mateAvatarUrl } from './avatar.js';
2
- import { escapeHtml, copyToClipboard } from './utils.js';
3
- import { iconHtml, refreshIcons } from './icons.js';
4
- import { openProjectSettings } from './project-settings.js';
5
- import { triggerShare } from './qrcode.js';
6
- import { parseEmojis } from './markdown.js';
7
- import { getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
8
- import { showMateProfilePopover } from './profile.js';
9
1
  import { closeArchive } from './sticky-notes.js';
10
2
  import { closeScheduler } from './scheduler.js';
11
- import { openSearch as openSessionSearch } from './session-search.js';
12
- import { openCommandPalette } from './command-palette.js';
13
- import { getMateSessions } from './mate-sidebar.js';
3
+ import { initSidebarSessions } from './sidebar-sessions.js';
4
+ import { initSidebarProjects, closeProjectCtxMenu } from './sidebar-projects.js';
5
+ import {
6
+ initSidebarMates,
7
+ showIconTooltip,
8
+ showIconTooltipHtml,
9
+ hideIconTooltip,
10
+ closeUserCtxMenu,
11
+ getCurrentDmUserId
12
+ } from './sidebar-mates.js';
13
+ import { initSidebarMobile } from './sidebar-mobile.js';
14
14
 
15
15
  var ctx;
16
16
 
17
- // --- Session search ---
18
- var searchQuery = "";
19
- var searchMatchIds = null; // null = no search, Set of matched session IDs
20
- var searchDebounce = null;
21
- var cachedSessions = [];
22
- var expandedLoopGroups = new Set();
23
- var expandedLoopRuns = new Set();
24
- var expandedMobileLoopGroups = new Set();
25
- var expandedMobileLoopRuns = new Set();
26
-
27
- // --- Cached project data for mobile sheet ---
28
- var cachedProjectList = [];
29
- var cachedCurrentSlug = null;
30
- var mobileChatSheetOpen = false; // track if chat sheet is showing
31
-
32
17
  function dismissOverlayPanels() {
33
18
  closeArchive();
34
19
  closeScheduler();
35
20
  }
36
21
 
37
- // --- Session presence (multi-user: who is viewing which session) ---
38
- var sessionPresence = {}; // { sessionId: [{ id, displayName, avatarStyle, avatarSeed }] }
39
-
40
- // --- Countdown timer for upcoming schedules ---
41
- var countdownTimer = null;
42
- var countdownContainer = null;
43
-
44
- // --- Session context menu ---
45
- var sessionCtxMenu = null;
46
- var sessionCtxSessionId = null;
47
-
48
- function closeSessionCtxMenu() {
49
- if (sessionCtxMenu) {
50
- sessionCtxMenu.remove();
51
- sessionCtxMenu = null;
52
- sessionCtxSessionId = null;
22
+ export function updatePageTitle() {
23
+ var sessionTitle = "";
24
+ var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
25
+ if (activeItem) sessionTitle = activeItem.textContent;
26
+ if (ctx.headerTitleEl) {
27
+ ctx.headerTitleEl.textContent = sessionTitle || ctx.projectName || "Clay";
53
28
  }
54
- }
55
-
56
- function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
57
- closeSessionCtxMenu();
58
- sessionCtxSessionId = sessionId;
59
-
60
- var menu = document.createElement("div");
61
- menu.className = "session-ctx-menu";
62
-
63
- var renameItem = document.createElement("button");
64
- renameItem.className = "session-ctx-item";
65
- renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
66
- renameItem.addEventListener("click", function (e) {
67
- e.stopPropagation();
68
- closeSessionCtxMenu();
69
- startInlineRename(sessionId, title);
70
- });
71
- menu.appendChild(renameItem);
72
-
73
- // Session visibility toggle (only the session owner can change)
74
- if (ctx.multiUser && sessionData && sessionData.ownerId && sessionData.ownerId === ctx.myUserId) {
75
- var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
76
- var isPrivate = currentVis === "private";
77
- var visItem = document.createElement("button");
78
- visItem.className = "session-ctx-item";
79
- visItem.innerHTML = iconHtml(isPrivate ? "eye" : "eye-off") + " <span>" + (isPrivate ? "Make Shared" : "Make Private") + "</span>";
80
- visItem.addEventListener("click", function (e) {
81
- e.stopPropagation();
82
- closeSessionCtxMenu();
83
- var newVis = isPrivate ? "shared" : "private";
84
- if (ctx.ws && ctx.connected) {
85
- ctx.ws.send(JSON.stringify({ type: "set_session_visibility", sessionId: sessionId, visibility: newVis }));
86
- }
87
- });
88
- menu.appendChild(visItem);
29
+ var tbProjectName = ctx.$("title-bar-project-name");
30
+ if (tbProjectName && ctx.projectName) {
31
+ tbProjectName.textContent = ctx.projectName;
32
+ } else if (tbProjectName && !tbProjectName.textContent) {
33
+ // Fallback: derive name from URL slug when projectName not yet available
34
+ var _m = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
35
+ if (_m) tbProjectName.textContent = _m[1];
89
36
  }
90
-
91
- if (!ctx.permissions || ctx.permissions.sessionDelete !== false) {
92
- var deleteItem = document.createElement("button");
93
- deleteItem.className = "session-ctx-item session-ctx-delete";
94
- deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
95
- deleteItem.addEventListener("click", function (e) {
96
- e.stopPropagation();
97
- closeSessionCtxMenu();
98
- ctx.showConfirm('Delete "' + (title || "New Session") + '"? This session and its history will be permanently removed.', function () {
99
- var ws = ctx.ws;
100
- if (ws && ctx.connected) {
101
- ws.send(JSON.stringify({ type: "delete_session", id: sessionId }));
102
- }
103
- });
104
- });
105
- menu.appendChild(deleteItem);
37
+ if (ctx.projectName && sessionTitle) {
38
+ document.title = sessionTitle + " - " + ctx.projectName;
39
+ } else if (ctx.projectName) {
40
+ document.title = ctx.projectName + " - Clay";
41
+ } else {
42
+ document.title = "Clay";
106
43
  }
107
-
108
- document.body.appendChild(menu);
109
- sessionCtxMenu = menu;
110
- refreshIcons();
111
-
112
- // Position: fixed relative to the anchor button
113
- requestAnimationFrame(function () {
114
- var btnRect = anchorBtn.getBoundingClientRect();
115
- menu.style.position = "fixed";
116
- menu.style.top = (btnRect.bottom + 2) + "px";
117
- menu.style.right = (window.innerWidth - btnRect.right) + "px";
118
- menu.style.left = "auto";
119
- // If menu overflows below viewport, flip up
120
- var menuRect = menu.getBoundingClientRect();
121
- if (menuRect.bottom > window.innerHeight - 8) {
122
- menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
123
- }
124
- });
125
44
  }
126
45
 
127
- function startInlineRename(sessionId, currentTitle) {
128
- var el = ctx.sessionListEl.querySelector('.session-item[data-session-id="' + sessionId + '"]');
129
- if (!el) return;
130
- var textSpan = el.querySelector(".session-item-text");
131
- if (!textSpan) return;
132
-
133
- var input = document.createElement("input");
134
- input.type = "text";
135
- input.className = "session-rename-input";
136
- input.value = currentTitle || "New Session";
137
-
138
- var originalHtml = textSpan.innerHTML;
139
- textSpan.innerHTML = "";
140
- textSpan.appendChild(input);
141
- input.focus();
142
- input.select();
143
-
144
- function commitRename() {
145
- var newTitle = input.value.trim();
146
- if (newTitle && newTitle !== currentTitle && ctx.ws && ctx.connected) {
147
- ctx.ws.send(JSON.stringify({ type: "rename_session", id: sessionId, title: newTitle }));
148
- }
149
- // Restore text (server will send updated session_list)
150
- textSpan.innerHTML = originalHtml;
151
- if (newTitle && newTitle !== currentTitle) {
152
- textSpan.textContent = newTitle;
153
- }
154
- }
46
+ export function openSidebar() {
47
+ ctx.sidebar.classList.add("open");
48
+ ctx.sidebarOverlay.classList.add("visible");
49
+ }
155
50
 
156
- input.addEventListener("keydown", function (e) {
157
- if (e.key === "Enter") { e.preventDefault(); commitRename(); }
158
- if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
159
- });
160
- input.addEventListener("blur", commitRename);
161
- input.addEventListener("click", function (e) { e.stopPropagation(); });
51
+ export function closeSidebar() {
52
+ ctx.sidebar.classList.remove("open");
53
+ ctx.sidebarOverlay.classList.remove("visible");
162
54
  }
163
55
 
164
- function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
165
- closeSessionCtxMenu();
56
+ export function initSidebar(_ctx) {
57
+ ctx = _ctx;
166
58
 
167
- var menu = document.createElement("div");
168
- menu.className = "session-ctx-menu";
59
+ // Initialize session sub-module with sidebar callbacks
60
+ ctx.closeSidebar = closeSidebar;
61
+ ctx.dismissOverlayPanels = dismissOverlayPanels;
62
+ ctx.updatePageTitle = updatePageTitle;
63
+ ctx.showIconTooltip = showIconTooltip;
64
+ ctx.showIconTooltipHtml = showIconTooltipHtml;
65
+ ctx.hideIconTooltip = hideIconTooltip;
66
+ ctx.closeUserCtxMenu = closeUserCtxMenu;
67
+ ctx.closeProjectCtxMenu = closeProjectCtxMenu;
68
+ ctx.getCurrentDmUserId = getCurrentDmUserId;
69
+ ctx.spawnDustParticles = spawnDustParticles;
70
+ initSidebarSessions(ctx);
71
+ initSidebarProjects(ctx);
72
+ initSidebarMates(ctx);
73
+ initSidebarMobile(ctx);
169
74
 
170
- var renameItem = document.createElement("button");
171
- renameItem.className = "session-ctx-item";
172
- renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
173
- renameItem.addEventListener("click", function (e) {
174
- e.stopPropagation();
175
- closeSessionCtxMenu();
176
- startLoopInlineRename(loopId, loopName);
75
+ ctx.hamburgerBtn.addEventListener("click", function () {
76
+ ctx.sidebar.classList.contains("open") ? closeSidebar() : openSidebar();
177
77
  });
178
- menu.appendChild(renameItem);
179
78
 
180
- if (!ctx.permissions || ctx.permissions.sessionDelete !== false) {
181
- var deleteItem = document.createElement("button");
182
- deleteItem.className = "session-ctx-item session-ctx-delete";
183
- deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
184
- deleteItem.addEventListener("click", function (e) {
185
- e.stopPropagation();
186
- closeSessionCtxMenu();
187
- var msg = 'Delete "' + (loopName || "Ralph Loop") + '"';
188
- if (childCount > 1) msg += " and its " + childCount + " sessions";
189
- msg += "? This cannot be undone.";
190
- ctx.showConfirm(msg, function () {
191
- if (ctx.ws && ctx.connected) {
192
- ctx.ws.send(JSON.stringify({ type: "delete_loop_group", loopId: loopId }));
193
- }
194
- });
195
- });
196
- menu.appendChild(deleteItem);
79
+ ctx.sidebarOverlay.addEventListener("click", closeSidebar);
80
+
81
+ // --- Desktop sidebar collapse/expand ---
82
+ function toggleSidebarCollapse() {
83
+ var layout = ctx.$("layout");
84
+ var collapsed = layout.classList.toggle("sidebar-collapsed");
85
+ try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
86
+ setTimeout(function () { syncUserIslandWidth(); syncResizeHandle(); }, 210);
197
87
  }
198
88
 
199
- document.body.appendChild(menu);
200
- sessionCtxMenu = menu;
201
- refreshIcons();
89
+ if (ctx.sidebarToggleBtn) ctx.sidebarToggleBtn.addEventListener("click", toggleSidebarCollapse);
90
+ if (ctx.sidebarExpandBtn) ctx.sidebarExpandBtn.addEventListener("click", toggleSidebarCollapse);
91
+ var mateSidebarToggle = document.getElementById("mate-sidebar-toggle-btn");
92
+ if (mateSidebarToggle) mateSidebarToggle.addEventListener("click", toggleSidebarCollapse);
202
93
 
203
- requestAnimationFrame(function () {
204
- var btnRect = anchorBtn.getBoundingClientRect();
205
- menu.style.position = "fixed";
206
- menu.style.top = (btnRect.bottom + 2) + "px";
207
- menu.style.right = (window.innerWidth - btnRect.right) + "px";
208
- menu.style.left = "auto";
209
- var menuRect = menu.getBoundingClientRect();
210
- if (menuRect.bottom > window.innerHeight - 8) {
211
- menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
94
+ // Restore collapsed state from localStorage
95
+ try {
96
+ if (localStorage.getItem("sidebar-collapsed") === "1") {
97
+ ctx.$("layout").classList.add("sidebar-collapsed");
212
98
  }
213
- });
214
- }
215
-
216
- function startLoopInlineRename(loopId, currentName) {
217
- var el = ctx.sessionListEl.querySelector('.session-loop-group[data-loop-id="' + loopId + '"]');
218
- if (!el) return;
219
- var textSpan = el.querySelector(".session-item-text");
220
- if (!textSpan) return;
221
-
222
- var input = document.createElement("input");
223
- input.type = "text";
224
- input.className = "session-rename-input";
225
- input.value = currentName || "Ralph Loop";
226
-
227
- var originalHtml = textSpan.innerHTML;
228
- textSpan.innerHTML = "";
229
- textSpan.appendChild(input);
230
- input.focus();
231
- input.select();
99
+ } catch (e) {}
232
100
 
233
- function commitRename() {
234
- var newName = input.value.trim();
235
- if (newName && newName !== currentName && ctx.ws && ctx.connected) {
236
- ctx.ws.send(JSON.stringify({ type: "loop_registry_rename", id: loopId, name: newName }));
237
- }
238
- textSpan.innerHTML = originalHtml;
239
- if (newName && newName !== currentName) {
240
- // Update text inline immediately
241
- var nameNode = textSpan.querySelector(".session-loop-name");
242
- if (nameNode) nameNode.textContent = newName;
101
+ ctx.newSessionBtn.addEventListener("click", function () {
102
+ if (ctx.ws && ctx.connected) {
103
+ ctx.ws.send(JSON.stringify({ type: "new_session" }));
104
+ closeSidebar();
243
105
  }
244
- }
245
-
246
- input.addEventListener("keydown", function (e) {
247
- if (e.key === "Enter") { e.preventDefault(); commitRename(); }
248
- if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
249
106
  });
250
- input.addEventListener("blur", commitRename);
251
- input.addEventListener("click", function (e) { e.stopPropagation(); });
252
- }
253
-
254
- function getDateGroup(ts) {
255
- var now = new Date();
256
- var d = new Date(ts);
257
- var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
258
- var yesterday = new Date(today.getTime() - 86400000);
259
- var weekAgo = new Date(today.getTime() - 7 * 86400000);
260
- if (d >= today) return "Today";
261
- if (d >= yesterday) return "Yesterday";
262
- if (d >= weekAgo) return "This Week";
263
- return "Older";
264
- }
265
-
266
- function highlightMatch(text, query) {
267
- if (!query) return escapeHtml(text);
268
- var lower = text.toLowerCase();
269
- var qLower = query.toLowerCase();
270
- var idx = lower.indexOf(qLower);
271
- if (idx === -1) return escapeHtml(text);
272
- var before = text.substring(0, idx);
273
- var match = text.substring(idx, idx + query.length);
274
- var after = text.substring(idx + query.length);
275
- return escapeHtml(before) + '<mark class="session-highlight">' + escapeHtml(match) + '</mark>' + escapeHtml(after);
276
- }
277
-
278
- function renderLoopChild(s) {
279
- var el = document.createElement("div");
280
- var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
281
- var dimmed = searchMatchIds !== null && !isMatch;
282
- el.className = "session-loop-child" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
283
- el.dataset.sessionId = s.id;
284
107
 
285
- var textSpan = document.createElement("span");
286
- textSpan.className = "session-item-text";
287
- var textHtml = "";
288
- if (s.isProcessing) {
289
- textHtml += '<span class="session-processing"></span>';
290
- }
291
- if (s.loop) {
292
- var isRalphChild = s.loop.source === "ralph";
293
- var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
294
- var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
295
- var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
296
- textHtml += '<span class="session-loop-role-badge' + roleCls + '">' + roleName + iterSuffix + '</span>';
108
+ // --- New Ralph Loop button ---
109
+ var newRalphBtn = ctx.$("new-ralph-btn");
110
+ if (newRalphBtn) {
111
+ newRalphBtn.addEventListener("click", function () {
112
+ if (ctx.openRalphWizard) ctx.openRalphWizard();
113
+ });
297
114
  }
298
- textSpan.innerHTML = textHtml;
299
- el.appendChild(textSpan);
300
-
301
- el.addEventListener("click", (function (id) {
302
- return function () {
303
- if (ctx.ws && ctx.connected) {
304
- ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
305
- dismissOverlayPanels();
306
- closeSidebar();
307
- }
308
- };
309
- })(s.id));
310
-
311
- return el;
312
- }
313
115
 
314
- function renderLoopGroup(loopId, children, groupKey) {
315
- var gk = groupKey || loopId;
116
+ // --- Panel switch (sessions / files / projects) ---
117
+ var fileBrowserBtn = ctx.$("file-browser-btn");
118
+ var projectsPanel = ctx.$("sidebar-panel-projects");
119
+ var sessionsPanel = ctx.$("sidebar-panel-sessions");
120
+ var filesPanel = ctx.$("sidebar-panel-files");
121
+ var sessionsHeaderContent = ctx.$("sessions-header-content");
122
+ var filesHeaderContent = ctx.$("files-header-content");
123
+ var filePanelClose = ctx.$("file-panel-close");
316
124
 
317
- // Sub-group children by startedAt (each run)
318
- var runMap = {};
319
- for (var i = 0; i < children.length; i++) {
320
- var runKey = String(children[i].loop && children[i].loop.startedAt || 0);
321
- if (!runMap[runKey]) runMap[runKey] = [];
322
- runMap[runKey].push(children[i]);
125
+ function hideAllPanels() {
126
+ if (projectsPanel) projectsPanel.classList.add("hidden");
127
+ if (sessionsPanel) sessionsPanel.classList.add("hidden");
128
+ if (filesPanel) filesPanel.classList.add("hidden");
129
+ if (sessionsHeaderContent) sessionsHeaderContent.classList.add("hidden");
130
+ if (filesHeaderContent) filesHeaderContent.classList.add("hidden");
323
131
  }
324
- var runKeys = Object.keys(runMap);
325
132
 
326
- // Sort each run's children by iteration then role
327
- for (var ri = 0; ri < runKeys.length; ri++) {
328
- runMap[runKeys[ri]].sort(function (a, b) {
329
- var ai = (a.loop && a.loop.iteration) || 0;
330
- var bi = (b.loop && b.loop.iteration) || 0;
331
- if (ai !== bi) return ai - bi;
332
- var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
333
- var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
334
- return ar - br;
335
- });
133
+ function showProjectsPanel() {
134
+ hideAllPanels();
135
+ if (projectsPanel) projectsPanel.classList.remove("hidden");
336
136
  }
337
137
 
338
- // Sort runs by startedAt descending (newest first)
339
- runKeys.sort(function (a, b) { return Number(b) - Number(a); });
340
-
341
- var expanded = expandedLoopGroups.has(gk);
342
- var hasActive = false;
343
- var anyProcessing = false;
344
- var latestSession = children[0];
345
- for (var ci = 0; ci < children.length; ci++) {
346
- if (children[ci].active) hasActive = true;
347
- if (children[ci].isProcessing) anyProcessing = true;
348
- if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
349
- latestSession = children[ci];
350
- }
138
+ function showSessionsPanel() {
139
+ hideAllPanels();
140
+ if (sessionsPanel) sessionsPanel.classList.remove("hidden");
141
+ if (sessionsHeaderContent) sessionsHeaderContent.classList.remove("hidden");
351
142
  }
352
143
 
353
- var loopName = (children[0].loop && children[0].loop.name) || "Ralph Loop";
354
- var isRalph = children[0].loop && children[0].loop.source === "ralph";
355
- var isDebate = children[0].loop && children[0].loop.source === "debate";
356
- var isCrafting = false;
357
- for (var j = 0; j < children.length; j++) {
358
- if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
144
+ function showFilesPanel() {
145
+ hideAllPanels();
146
+ if (filesPanel) filesPanel.classList.remove("hidden");
147
+ if (filesHeaderContent) filesHeaderContent.classList.remove("hidden");
148
+ if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
359
149
  }
360
150
 
361
- var runCount = runKeys.length;
362
-
363
- var wrapper = document.createElement("div");
364
- wrapper.className = "session-loop-wrapper";
365
-
366
- // Group header row
367
- var el = document.createElement("div");
368
- var groupClass = "session-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "");
369
- if (isDebate) groupClass += " debate";
370
- else if (!isRalph) groupClass += " scheduled";
371
- el.className = groupClass;
372
- el.dataset.loopId = loopId;
373
-
374
- var chevron = document.createElement("button");
375
- chevron.className = "session-loop-chevron";
376
- chevron.innerHTML = iconHtml("chevron-right");
377
- chevron.addEventListener("click", (function (lid) {
378
- return function (e) {
379
- e.stopPropagation();
380
- if (expandedLoopGroups.has(lid)) {
381
- expandedLoopGroups.delete(lid);
382
- } else {
383
- expandedLoopGroups.add(lid);
384
- }
385
- renderSessionList(null);
386
- };
387
- })(gk));
388
- el.appendChild(chevron);
389
-
390
- var textSpan = document.createElement("span");
391
- textSpan.className = "session-item-text";
392
- var textHtml = "";
393
- if (anyProcessing) {
394
- textHtml += '<span class="session-processing"></span>';
151
+ if (fileBrowserBtn) {
152
+ fileBrowserBtn.addEventListener("click", showFilesPanel);
395
153
  }
396
- var groupIcon = isDebate ? "mic" : (isRalph ? "repeat" : "calendar-clock");
397
- var iconClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
398
- textHtml += '<span class="session-loop-icon' + iconClass + '">' + iconHtml(groupIcon) + '</span>';
399
- textHtml += '<span class="session-loop-name">' + escapeHtml(loopName) + '</span>';
400
- if (isCrafting && children.length === 1) {
401
- textHtml += '<span class="session-loop-badge crafting">Crafting</span>';
402
- } else {
403
- var countLabel = runCount === 1 ? children.length : runCount + (runCount === 1 ? " run" : " runs");
404
- var countClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
405
- textHtml += '<span class="session-loop-count' + countClass + '">' + countLabel + '</span>';
154
+ if (filePanelClose) {
155
+ filePanelClose.addEventListener("click", showSessionsPanel);
406
156
  }
407
- textSpan.innerHTML = textHtml;
408
- el.appendChild(textSpan);
409
-
410
- // More button (ellipsis)
411
- var moreBtn = document.createElement("button");
412
- moreBtn.className = "session-more-btn";
413
- moreBtn.innerHTML = iconHtml("ellipsis");
414
- moreBtn.title = "More options";
415
- moreBtn.addEventListener("click", (function (lid, name, count, btn) {
416
- return function (e) {
417
- e.stopPropagation();
418
- showLoopCtxMenu(btn, lid, name, count);
419
- };
420
- })(loopId, loopName, children.length, moreBtn));
421
- el.appendChild(moreBtn);
422
-
423
- // Click row (not chevron/more) -> switch to latest session
424
- el.addEventListener("click", (function (id) {
425
- return function () {
426
- if (ctx.ws && ctx.connected) {
427
- ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
428
- dismissOverlayPanels();
429
- closeSidebar();
430
- }
431
- };
432
- })(latestSession.id));
433
157
 
434
- wrapper.appendChild(el);
158
+ // --- User island width sync ---
159
+ var userIsland = document.getElementById("user-island");
160
+ var sidebarColumn = document.getElementById("sidebar-column");
435
161
 
436
- // Expanded: show runs as sub-groups
437
- if (expanded) {
438
- var childContainer = document.createElement("div");
439
- childContainer.className = "session-loop-children";
162
+ function syncUserIslandWidth() {
163
+ if (!userIsland) return;
164
+ var mateSidebarColumn = document.getElementById("mate-sidebar-column");
165
+ var isMateDM = document.body.classList.contains("mate-dm-active");
166
+ var col = (isMateDM && mateSidebarColumn && !mateSidebarColumn.classList.contains("hidden")) ? mateSidebarColumn : sidebarColumn;
167
+ if (!col) return;
168
+ var rect = col.getBoundingClientRect();
169
+ userIsland.style.width = (rect.right - 8 - 8) + "px";
170
+ }
440
171
 
441
- if (runCount === 1) {
442
- // Single run: show sessions directly (no extra nesting)
443
- var singleRun = runMap[runKeys[0]];
444
- for (var sk = 0; sk < singleRun.length; sk++) {
445
- childContainer.appendChild(renderLoopChild(singleRun[sk]));
446
- }
447
- } else {
448
- // Multiple runs: render each run as a collapsible sub-group
449
- for (var rk = 0; rk < runKeys.length; rk++) {
450
- childContainer.appendChild(renderLoopRun(gk, runKeys[rk], runMap[runKeys[rk]], isRalph));
451
- }
452
- }
172
+ // --- Sidebar resize handle ---
173
+ var resizeHandle = document.getElementById("sidebar-resize-handle");
453
174
 
454
- wrapper.appendChild(childContainer);
175
+ function syncResizeHandle() {
176
+ if (!resizeHandle || !sidebarColumn) return;
177
+ var rect = sidebarColumn.getBoundingClientRect();
178
+ var parentRect = sidebarColumn.parentElement.getBoundingClientRect();
179
+ resizeHandle.style.left = (rect.right - parentRect.left) + "px";
455
180
  }
456
181
 
457
- return wrapper;
458
- }
182
+ if (resizeHandle && sidebarColumn) {
183
+ var dragging = false;
459
184
 
460
- function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
461
- var runGk = parentGk + ":" + startedAtKey;
462
- var expanded = expandedLoopRuns.has(runGk);
463
- var startedAt = Number(startedAtKey);
464
- var timeLabel = startedAt ? new Date(startedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "Unknown";
185
+ function onResizeMove(e) {
186
+ if (!dragging) return;
187
+ e.preventDefault();
188
+ var clientX = e.touches ? e.touches[0].clientX : e.clientX;
189
+ var iconStrip = document.getElementById("icon-strip");
190
+ var stripWidth = iconStrip ? iconStrip.offsetWidth : 72;
191
+ var newWidth = clientX - stripWidth;
192
+ if (newWidth < 192) newWidth = 192;
193
+ if (newWidth > 320) newWidth = 320;
194
+ sidebarColumn.style.width = newWidth + "px";
195
+ syncResizeHandle();
196
+ syncUserIslandWidth();
197
+ }
465
198
 
466
- var hasActive = false;
467
- var anyProcessing = false;
468
- var latestSession = sessions[0];
469
- for (var i = 0; i < sessions.length; i++) {
470
- if (sessions[i].active) hasActive = true;
471
- if (sessions[i].isProcessing) anyProcessing = true;
472
- if ((sessions[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
473
- latestSession = sessions[i];
199
+ function onResizeEnd() {
200
+ if (!dragging) return;
201
+ dragging = false;
202
+ resizeHandle.classList.remove("dragging");
203
+ document.body.style.cursor = "";
204
+ document.body.style.userSelect = "";
205
+ document.removeEventListener("mousemove", onResizeMove);
206
+ document.removeEventListener("mouseup", onResizeEnd);
207
+ document.removeEventListener("touchmove", onResizeMove);
208
+ document.removeEventListener("touchend", onResizeEnd);
209
+ try { localStorage.setItem("sidebar-width", sidebarColumn.style.width); } catch (e) {}
474
210
  }
475
- }
476
211
 
477
- var wrapper = document.createElement("div");
478
- wrapper.className = "session-loop-run-wrapper";
212
+ function onResizeStart(e) {
213
+ e.preventDefault();
214
+ dragging = true;
215
+ resizeHandle.classList.add("dragging");
216
+ document.body.style.cursor = "col-resize";
217
+ document.body.style.userSelect = "none";
218
+ document.addEventListener("mousemove", onResizeMove);
219
+ document.addEventListener("mouseup", onResizeEnd);
220
+ document.addEventListener("touchmove", onResizeMove, { passive: false });
221
+ document.addEventListener("touchend", onResizeEnd);
222
+ }
479
223
 
480
- var el = document.createElement("div");
481
- el.className = "session-loop-run" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
224
+ resizeHandle.addEventListener("mousedown", onResizeStart);
225
+ resizeHandle.addEventListener("touchstart", onResizeStart, { passive: false });
482
226
 
483
- var chevron = document.createElement("button");
484
- chevron.className = "session-loop-chevron";
485
- chevron.innerHTML = iconHtml("chevron-right");
486
- chevron.addEventListener("click", (function (rk) {
487
- return function (e) {
488
- e.stopPropagation();
489
- if (expandedLoopRuns.has(rk)) {
490
- expandedLoopRuns.delete(rk);
491
- } else {
492
- expandedLoopRuns.add(rk);
227
+ // Restore saved width (skip transition so user-island syncs immediately)
228
+ try {
229
+ var savedWidth = localStorage.getItem("sidebar-width");
230
+ if (savedWidth) {
231
+ var px = parseInt(savedWidth, 10);
232
+ if (px >= 192 && px <= 320) {
233
+ sidebarColumn.style.transition = "none";
234
+ sidebarColumn.style.width = px + "px";
235
+ sidebarColumn.offsetWidth; // force reflow
236
+ sidebarColumn.style.transition = "";
237
+ }
493
238
  }
494
- renderSessionList(null);
495
- };
496
- })(runGk));
497
- el.appendChild(chevron);
239
+ } catch (e) {}
498
240
 
499
- var textSpan = document.createElement("span");
500
- textSpan.className = "session-item-text";
501
- var textHtml = "";
502
- if (anyProcessing) {
503
- textHtml += '<span class="session-processing"></span>';
241
+ syncResizeHandle();
242
+ syncUserIslandWidth();
504
243
  }
505
- textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
506
- textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
507
- textSpan.innerHTML = textHtml;
508
- el.appendChild(textSpan);
509
-
510
- // Click row -> switch to latest session of this run
511
- el.addEventListener("click", (function (id) {
512
- return function () {
513
- if (ctx.ws && ctx.connected) {
514
- ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
515
- dismissOverlayPanels();
516
- closeSidebar();
517
- }
518
- };
519
- })(latestSession.id));
520
244
 
521
- wrapper.appendChild(el);
245
+ // Initial sync even if no resize handle
246
+ syncUserIslandWidth();
522
247
 
523
- if (expanded) {
524
- var childContainer = document.createElement("div");
525
- childContainer.className = "session-loop-children";
526
- for (var k = 0; k < sessions.length; k++) {
527
- childContainer.appendChild(renderLoopChild(sessions[k]));
248
+ // --- User island tooltip on hover (collapsed sidebar) ---
249
+ if (userIsland) {
250
+ var profileArea = userIsland.querySelector(".user-island-profile");
251
+ if (profileArea) {
252
+ profileArea.addEventListener("mouseenter", function () {
253
+ var layout = document.getElementById("layout");
254
+ if (!layout || !layout.classList.contains("sidebar-collapsed")) return;
255
+ var nameEl = userIsland.querySelector(".user-island-name");
256
+ var text = nameEl ? nameEl.textContent : "";
257
+ if (text) showIconTooltip(profileArea, text);
258
+ });
259
+ profileArea.addEventListener("mouseleave", function () {
260
+ hideIconTooltip();
261
+ });
528
262
  }
529
- wrapper.appendChild(childContainer);
530
263
  }
531
264
 
532
- return wrapper;
533
265
  }
534
266
 
535
- function renderSessionItem(s) {
536
- var el = document.createElement("div");
537
- var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
538
- var dimmed = searchMatchIds !== null && !isMatch;
539
- el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
540
- el.dataset.sessionId = s.id;
267
+ export function spawnDustParticles(cx, cy) {
268
+ var colors = ["#8B7355", "#A0522D", "#D2B48C", "#C4A882", "#9E9E9E", "#B8860B", "#BC8F8F"];
269
+ var count = 24;
270
+ var container = document.createElement("div");
271
+ container.style.position = "fixed";
272
+ container.style.top = "0";
273
+ container.style.left = "0";
274
+ container.style.width = "0";
275
+ container.style.height = "0";
276
+ container.style.pointerEvents = "none";
277
+ container.style.zIndex = "10000";
278
+ document.body.appendChild(container);
541
279
 
542
- var textSpan = document.createElement("span");
543
- textSpan.className = "session-item-text";
544
- var textHtml = "";
545
- if (s.isProcessing) {
546
- textHtml += '<span class="session-processing"></span>';
547
- }
548
- if (s.loop && s.loop.source === "debate") {
549
- textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
550
- }
551
- if (ctx.multiUser && s.sessionVisibility === "private") {
552
- textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
553
- }
554
- textHtml += highlightMatch(s.title || "New Session", searchQuery);
555
- textSpan.innerHTML = textHtml;
556
- el.appendChild(textSpan);
557
-
558
- // Right-click / long-press: context menu
559
- el.addEventListener("contextmenu", (function(id, title, cliSid, anchor, sData) {
560
- return function(e) {
561
- e.preventDefault();
562
- e.stopPropagation();
563
- showSessionCtxMenu(anchor, id, title, cliSid, sData);
564
- };
565
- })(s.id, s.title, s.cliSessionId, el, s));
566
-
567
- // Unread badge
568
- var unreadBadge = document.createElement("span");
569
- unreadBadge.className = "session-unread-badge";
570
- unreadBadge.dataset.sessionId = s.id;
571
- if (s.unread > 0) {
572
- unreadBadge.textContent = s.unread > 99 ? "99+" : String(s.unread);
573
- unreadBadge.classList.add("has-unread");
574
- }
575
- el.appendChild(unreadBadge);
576
-
577
- el.addEventListener("click", (function (id) {
578
- return function () {
579
- if (ctx.ws && ctx.connected) {
580
- var pendingQuery = searchQuery || "";
581
- ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
582
- dismissOverlayPanels();
583
- closeSidebar();
584
- if (pendingQuery) {
585
- setTimeout(function () { openSessionSearch(pendingQuery); }, 400);
586
- }
587
- }
588
- };
589
- })(s.id));
590
-
591
- // Presence avatars (multi-user)
592
- renderPresenceAvatars(el, String(s.id));
593
-
594
- return el;
595
- }
596
-
597
- export function renderSessionList(sessions) {
598
- if (sessions) cachedSessions = sessions;
599
-
600
- // If mobile chat sheet is open, refresh its session list
601
- refreshMobileChatSheet();
602
-
603
- ctx.sessionListEl.innerHTML = "";
604
-
605
- // Partition: loop sessions vs normal sessions
606
- // Group by loopId + date so all runs of the same task on the same day are merged
607
- var loopGroups = {}; // groupKey -> [sessions]
608
- var normalSessions = [];
609
- for (var i = 0; i < cachedSessions.length; i++) {
610
- var s = cachedSessions[i];
611
- if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
612
- // Task crafting sessions live in the scheduler calendar, not the main list (except debate)
613
- continue;
614
- } else if (s.loop && s.loop.loopId) {
615
- var startedAt = s.loop.startedAt || 0;
616
- var dateStr = startedAt ? new Date(startedAt).toISOString().slice(0, 10) : "unknown";
617
- var groupKey = s.loop.loopId + ":" + dateStr;
618
- if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
619
- loopGroups[groupKey].push(s);
620
- } else {
621
- normalSessions.push(s);
622
- }
623
- }
624
-
625
- // Build virtual items: normal sessions + one entry per loop group (using latest child's lastActivity)
626
- var items = [];
627
- for (var j = 0; j < normalSessions.length; j++) {
628
- items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
629
- }
630
- var groupKeys = Object.keys(loopGroups);
631
- for (var k = 0; k < groupKeys.length; k++) {
632
- var gk = groupKeys[k];
633
- var children = loopGroups[gk];
634
- var realLoopId = children[0].loop.loopId;
635
- var maxActivity = 0;
636
- for (var m = 0; m < children.length; m++) {
637
- var act = children[m].lastActivity || 0;
638
- if (act > maxActivity) maxActivity = act;
639
- }
640
- items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
641
- }
642
-
643
- // Sort by lastActivity descending
644
- items.sort(function (a, b) {
645
- return (b.lastActivity || 0) - (a.lastActivity || 0);
646
- });
647
-
648
- var currentGroup = "";
649
- for (var n = 0; n < items.length; n++) {
650
- var item = items[n];
651
- var group = getDateGroup(item.lastActivity || 0);
652
- if (group !== currentGroup) {
653
- currentGroup = group;
654
- var header = document.createElement("div");
655
- header.className = "session-group-header";
656
- header.textContent = group;
657
- ctx.sessionListEl.appendChild(header);
658
- }
659
- if (item.type === "loop") {
660
- ctx.sessionListEl.appendChild(renderLoopGroup(item.loopId, item.children, item.groupKey));
661
- } else {
662
- ctx.sessionListEl.appendChild(renderSessionItem(item.data));
663
- }
664
- }
665
- refreshIcons();
666
- updatePageTitle();
667
- }
668
-
669
- export function handleSearchResults(msg) {
670
- if (msg.query !== searchQuery) return; // stale response
671
- var ids = new Set();
672
- for (var i = 0; i < msg.results.length; i++) {
673
- ids.add(msg.results[i].id);
674
- }
675
- searchMatchIds = ids;
676
- renderSessionList(null);
677
- }
678
-
679
- export function updateSessionPresence(presence) {
680
- sessionPresence = presence;
681
- // Update presence avatars on existing session items without full re-render
682
- var items = ctx.sessionListEl.querySelectorAll("[data-session-id]");
683
- for (var i = 0; i < items.length; i++) {
684
- renderPresenceAvatars(items[i], items[i].dataset.sessionId);
685
- }
686
- }
687
-
688
- function presenceAvatarUrl(userOrStyle, seed) {
689
- if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
690
- return avatarUrl(userOrStyle || "thumbs", seed, 24);
691
- }
692
-
693
- function renderPresenceAvatars(el, sessionId) {
694
- // Remove existing presence container
695
- var existing = el.querySelector(".session-presence");
696
- if (existing) existing.remove();
697
-
698
- var users = sessionPresence[sessionId];
699
- if (!users || users.length === 0) return;
700
-
701
- var container = document.createElement("span");
702
- container.className = "session-presence";
703
-
704
- var max = 3;
705
- var shown = users.length > max ? max : users.length;
706
- for (var i = 0; i < shown; i++) {
707
- var u = users[i];
708
- var img = document.createElement("img");
709
- img.className = "session-presence-avatar";
710
- img.src = presenceAvatarUrl(u);
711
- img.alt = u.displayName;
712
- img.dataset.tip = u.displayName + (u.username ? " (@" + u.username + ")" : "");
713
- if (i > 0) img.style.marginLeft = "-6px";
714
- container.appendChild(img);
715
- }
716
- if (users.length > max) {
717
- var more = document.createElement("span");
718
- more.className = "session-presence-more";
719
- more.textContent = "+" + (users.length - max);
720
- container.appendChild(more);
721
- }
722
-
723
- // Insert before the more-btn
724
- var moreBtn = el.querySelector(".session-more-btn");
725
- if (moreBtn) {
726
- el.insertBefore(container, moreBtn);
727
- } else {
728
- el.appendChild(container);
729
- }
730
- }
731
-
732
- export function updatePageTitle() {
733
- var sessionTitle = "";
734
- var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
735
- if (activeItem) sessionTitle = activeItem.textContent;
736
- if (ctx.headerTitleEl) {
737
- ctx.headerTitleEl.textContent = sessionTitle || ctx.projectName || "Clay";
738
- }
739
- var tbProjectName = ctx.$("title-bar-project-name");
740
- if (tbProjectName && ctx.projectName) {
741
- tbProjectName.textContent = ctx.projectName;
742
- } else if (tbProjectName && !tbProjectName.textContent) {
743
- // Fallback: derive name from URL slug when projectName not yet available
744
- var _m = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
745
- if (_m) tbProjectName.textContent = _m[1];
746
- }
747
- if (ctx.projectName && sessionTitle) {
748
- document.title = sessionTitle + " - " + ctx.projectName;
749
- } else if (ctx.projectName) {
750
- document.title = ctx.projectName + " - Clay";
751
- } else {
752
- document.title = "Clay";
753
- }
754
- }
755
-
756
- export function openSidebar() {
757
- ctx.sidebar.classList.add("open");
758
- ctx.sidebarOverlay.classList.add("visible");
759
- }
760
-
761
- export function closeSidebar() {
762
- ctx.sidebar.classList.remove("open");
763
- ctx.sidebarOverlay.classList.remove("visible");
764
- }
765
-
766
- // --- Mobile sheet (fullscreen overlay for Projects / Sessions / Mate Profile) ---
767
-
768
- var mobileSheetMateData = null;
769
-
770
- export function setMobileSheetMateData(data) {
771
- mobileSheetMateData = data;
772
- }
773
-
774
- export function openMobileSheet(type) {
775
- var sheet = document.getElementById("mobile-sheet");
776
- if (!sheet) return;
777
-
778
- var titleEl = sheet.querySelector(".mobile-sheet-title");
779
- var listEl = sheet.querySelector(".mobile-sheet-list");
780
- if (!titleEl || !listEl) return;
781
-
782
- // Return file tree to sidebar before clearing (prevents destroying it)
783
- if (sheet.classList.contains("sheet-files")) {
784
- var prevFileTree = document.getElementById("file-tree");
785
- var prevPanel = document.getElementById("sidebar-panel-files");
786
- if (prevFileTree && prevPanel) prevPanel.appendChild(prevFileTree);
787
- }
788
- // Return knowledge files to mate sidebar before clearing
789
- if (sheet.classList.contains("sheet-knowledge")) {
790
- var prevKnowledge = document.getElementById("mate-knowledge-files");
791
- var prevKnowledgePanel = document.getElementById("mate-sidebar-knowledge");
792
- if (prevKnowledge && prevKnowledgePanel) prevKnowledgePanel.appendChild(prevKnowledge);
793
- }
794
-
795
- listEl.innerHTML = "";
796
- sheet.classList.remove("sheet-files", "sheet-knowledge");
797
-
798
- if (type === "projects") {
799
- titleEl.textContent = "Projects";
800
- renderSheetProjects(listEl);
801
- } else if (type === "sessions") {
802
- titleEl.textContent = "Chat";
803
- renderSheetSessions(listEl);
804
- } else if (type === "files") {
805
- titleEl.textContent = "Files";
806
- sheet.classList.add("sheet-files");
807
- var fileTree = document.getElementById("file-tree");
808
- if (fileTree) {
809
- listEl.appendChild(fileTree);
810
- fileTree.classList.remove("hidden");
811
- }
812
- if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
813
- } else if (type === "mate-knowledge") {
814
- titleEl.textContent = "Knowledge";
815
- sheet.classList.add("sheet-knowledge");
816
- var knowledgeFiles = document.getElementById("mate-knowledge-files");
817
- if (knowledgeFiles) {
818
- listEl.appendChild(knowledgeFiles);
819
- knowledgeFiles.classList.remove("hidden");
820
- }
821
- // Request knowledge list if not loaded
822
- if (ctx.requestKnowledgeList) ctx.requestKnowledgeList();
823
- } else if (type === "mate-profile") {
824
- titleEl.textContent = "";
825
- renderSheetMateProfile(listEl);
826
- } else if (type === "search") {
827
- titleEl.textContent = "Search";
828
- renderSheetSearch(listEl);
829
- } else if (type === "tools") {
830
- titleEl.textContent = "Tools";
831
- renderSheetTools(listEl);
832
- } else if (type === "settings") {
833
- titleEl.textContent = "Settings";
834
- renderSheetSettings(listEl);
835
- }
836
-
837
- sheet.classList.remove("hidden", "closing");
838
- refreshIcons();
839
- }
840
-
841
- function closeMobileSheet() {
842
- var sheet = document.getElementById("mobile-sheet");
843
- if (!sheet || sheet.classList.contains("hidden")) return;
844
-
845
- mobileChatSheetOpen = false;
846
-
847
- // Return file tree to sidebar if it was moved
848
- if (sheet.classList.contains("sheet-files")) {
849
- var fileTree = document.getElementById("file-tree");
850
- var sidebarFilesPanel = document.getElementById("sidebar-panel-files");
851
- if (fileTree && sidebarFilesPanel) {
852
- sidebarFilesPanel.appendChild(fileTree);
853
- }
854
- }
855
- // Return knowledge files to mate sidebar if moved
856
- if (sheet.classList.contains("sheet-knowledge")) {
857
- var knowledgeFiles = document.getElementById("mate-knowledge-files");
858
- var knowledgePanel = document.getElementById("mate-sidebar-knowledge");
859
- if (knowledgeFiles && knowledgePanel) {
860
- knowledgePanel.appendChild(knowledgeFiles);
861
- }
862
- }
863
-
864
- sheet.classList.add("closing");
865
- setTimeout(function () {
866
- sheet.classList.add("hidden");
867
- sheet.classList.remove("closing", "sheet-files");
868
- }, 230);
869
- }
870
-
871
- function renderSheetProjects(listEl) {
872
- for (var i = 0; i < cachedProjectList.length; i++) {
873
- (function (p) {
874
- var el = document.createElement("button");
875
- el.className = "mobile-project-item" + (p.slug === cachedCurrentSlug ? " active" : "");
876
-
877
- var abbrev = document.createElement("span");
878
- abbrev.className = "mobile-project-abbrev";
879
- if (p.icon) {
880
- abbrev.textContent = p.icon;
881
- parseEmojis(abbrev);
882
- } else {
883
- abbrev.textContent = getProjectAbbrev(p.name);
884
- }
885
- el.appendChild(abbrev);
886
-
887
- var name = document.createElement("span");
888
- name.className = "mobile-project-name";
889
- name.textContent = p.name;
890
- el.appendChild(name);
891
-
892
- if (p.isProcessing) {
893
- var dot = document.createElement("span");
894
- dot.className = "mobile-project-processing";
895
- el.appendChild(dot);
896
- }
897
-
898
- if (p.unread > 0 && p.slug !== cachedCurrentSlug) {
899
- var mBadge = document.createElement("span");
900
- mBadge.className = "mobile-project-unread";
901
- mBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
902
- el.appendChild(mBadge);
903
- }
904
-
905
- el.addEventListener("click", function () {
906
- if (ctx.switchProject) ctx.switchProject(p.slug);
907
- closeMobileSheet();
908
- });
909
-
910
- listEl.appendChild(el);
911
- })(cachedProjectList[i]);
912
- }
913
- }
914
-
915
- function renderSheetSessions(listEl) {
916
- // --- Context filter bar (horizontal scroll) ---
917
- var filterBar = document.createElement("div");
918
- filterBar.className = "mobile-chat-filter-bar";
919
-
920
- // Current project chip (always first, pre-selected)
921
- var currentProject = null;
922
- for (var pi = 0; pi < cachedProjectList.length; pi++) {
923
- if (cachedProjectList[pi].slug === cachedCurrentSlug) {
924
- currentProject = cachedProjectList[pi];
925
- break;
926
- }
927
- }
928
-
929
- // Build chips: projects first, then mates
930
- var chips = [];
931
-
932
- for (var ci = 0; ci < cachedProjectList.length; ci++) {
933
- (function (p) {
934
- var chip = document.createElement("button");
935
- chip.className = "mobile-chat-chip";
936
- var isDmActive = document.body.classList.contains("mate-dm-active");
937
- if (p.slug === cachedCurrentSlug && !isDmActive) chip.classList.add("active");
938
- chip.dataset.type = "project";
939
- chip.dataset.slug = p.slug;
940
-
941
- var abbrev = document.createElement("span");
942
- abbrev.className = "mobile-chat-chip-icon";
943
- if (p.icon) {
944
- abbrev.textContent = p.icon;
945
- parseEmojis(abbrev);
946
- } else {
947
- abbrev.textContent = getProjectAbbrev(p.name);
948
- }
949
- chip.appendChild(abbrev);
950
-
951
- var label = document.createElement("span");
952
- label.textContent = p.name;
953
- chip.appendChild(label);
954
-
955
- // Processing dot: same class as icon strip
956
- var statusDot = document.createElement("span");
957
- statusDot.className = "icon-strip-status";
958
- if (p.isProcessing) statusDot.classList.add("processing");
959
- chip.appendChild(statusDot);
960
-
961
- if (p.unread > 0 && p.slug !== cachedCurrentSlug) {
962
- var badge = document.createElement("span");
963
- badge.className = "mobile-chat-chip-badge";
964
- badge.textContent = p.unread > 99 ? "99+" : String(p.unread);
965
- chip.appendChild(badge);
966
- }
967
-
968
- chips.push(chip);
969
- })(cachedProjectList[ci]);
970
- }
971
-
972
- var favoriteChipMates = cachedMates.filter(function (m) {
973
- if (cachedDmRemovedUsers[m.id]) return false;
974
- if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
975
- if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
976
- return false;
977
- });
978
- var sortedChipMates = favoriteChipMates.sort(function (a, b) {
979
- var aBuiltin = a.builtinKey ? 1 : 0;
980
- var bBuiltin = b.builtinKey ? 1 : 0;
981
- if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
982
- return (a.createdAt || 0) - (b.createdAt || 0);
983
- });
984
- for (var mi = 0; mi < sortedChipMates.length; mi++) {
985
- (function (mate) {
986
- var mp = mate.profile || {};
987
- var chip = document.createElement("button");
988
- chip.className = "mobile-chat-chip";
989
- if (currentDmUserId === mate.id) chip.classList.add("active");
990
- chip.dataset.type = "mate";
991
- chip.dataset.mateId = mate.id;
992
-
993
- var avatarEl = document.createElement("img");
994
- avatarEl.className = "mobile-chat-chip-avatar";
995
- avatarEl.src = mateAvatarUrl(mate, 20);
996
- avatarEl.alt = mp.displayName || mate.name || "";
997
- chip.appendChild(avatarEl);
998
-
999
- var label = document.createElement("span");
1000
- label.textContent = mp.displayName || mate.name || "Mate";
1001
- chip.appendChild(label);
1002
-
1003
- // Processing dot: same class as icon strip, same data source
1004
- var mateSlug = "mate-" + mate.id;
1005
- var mateProj = null;
1006
- var allProjects = (ctx && ctx.projectList) || [];
1007
- for (var pi = 0; pi < allProjects.length; pi++) {
1008
- if (allProjects[pi].slug === mateSlug) { mateProj = allProjects[pi]; break; }
1009
- }
1010
- var statusDot = document.createElement("span");
1011
- statusDot.className = "icon-strip-status";
1012
- if (mateProj && mateProj.isProcessing) statusDot.classList.add("processing");
1013
- chip.appendChild(statusDot);
1014
-
1015
- var unreadCount = cachedDmUnread[mate.id] || 0;
1016
- if (unreadCount > 0) {
1017
- var badge = document.createElement("span");
1018
- badge.className = "mobile-chat-chip-badge";
1019
- badge.textContent = unreadCount > 99 ? "99+" : String(unreadCount);
1020
- chip.appendChild(badge);
1021
- }
1022
-
1023
- chips.push(chip);
1024
- })(sortedChipMates[mi]);
1025
- }
1026
-
1027
- for (var i = 0; i < chips.length; i++) {
1028
- filterBar.appendChild(chips[i]);
1029
- }
1030
- listEl.appendChild(filterBar);
1031
-
1032
- // --- Session list container ---
1033
- var sessionListEl = document.createElement("div");
1034
- sessionListEl.className = "mobile-chat-session-list";
1035
- listEl.appendChild(sessionListEl);
1036
-
1037
- // --- Render sessions for a context ---
1038
- function renderSessionsForContext(type, slug, mateId) {
1039
- sessionListEl.innerHTML = "";
1040
-
1041
- if (type === "project") {
1042
- renderMobileSessionsInto(sessionListEl);
1043
- } else if (type === "mate") {
1044
- // Mate DM: open the DM and show mate actions
1045
- if (ctx.openDm) ctx.openDm(mateId);
1046
- renderMateMobileActions(sessionListEl);
1047
- }
1048
-
1049
- refreshIcons();
1050
- }
1051
-
1052
- // --- Chip click handlers ---
1053
- for (var j = 0; j < chips.length; j++) {
1054
- (function (chip) {
1055
- chip.addEventListener("click", function () {
1056
- // Deactivate all chips
1057
- for (var k = 0; k < chips.length; k++) {
1058
- chips[k].classList.remove("active");
1059
- }
1060
- chip.classList.add("active");
1061
-
1062
- var type = chip.dataset.type;
1063
- if (type === "project") {
1064
- var slug = chip.dataset.slug;
1065
- var isDmNow = !!currentDmUserId;
1066
- if (slug !== cachedCurrentSlug || isDmNow) {
1067
- // Switch project (or exit DM back to same project)
1068
- sessionListEl.innerHTML = "";
1069
- if (slug !== cachedCurrentSlug) {
1070
- var loading = document.createElement("div");
1071
- loading.className = "mobile-chat-context-note";
1072
- loading.textContent = "Loading sessions...";
1073
- sessionListEl.appendChild(loading);
1074
- }
1075
- if (ctx.switchProject) ctx.switchProject(slug);
1076
- if (!isDmNow || slug !== cachedCurrentSlug) {
1077
- // renderSessionList will be called by WS, which calls refreshMobileChatSheet
1078
- } else {
1079
- // Exited DM, same project - render sessions now
1080
- renderSessionsForContext("project", slug, null);
1081
- }
1082
- } else {
1083
- renderSessionsForContext("project", slug, null);
1084
- }
1085
- } else if (type === "mate") {
1086
- renderSessionsForContext("mate", null, chip.dataset.mateId);
1087
- }
1088
- });
1089
- })(chips[j]);
1090
- }
1091
-
1092
- // Track that chat sheet is open
1093
- mobileChatSheetOpen = true;
1094
-
1095
- // --- Initial render: show mate actions if DM active, otherwise project sessions ---
1096
- if (currentDmUserId) {
1097
- renderSessionsForContext("mate", null, currentDmUserId);
1098
- } else {
1099
- renderSessionsForContext("project", cachedCurrentSlug, null);
1100
- }
1101
- }
1102
-
1103
- // Helper: create a mobile session item element
1104
- function createMobileSessionItem(s) {
1105
- var el = document.createElement("button");
1106
- el.className = "mobile-session-item" + (s.active ? " active" : "");
1107
-
1108
- // Processing dot (left side, before title)
1109
- if (s.isProcessing) {
1110
- var dot = document.createElement("span");
1111
- dot.className = "mobile-session-processing";
1112
- el.appendChild(dot);
1113
- }
1114
-
1115
- var titleSpan = document.createElement("span");
1116
- titleSpan.className = "mobile-session-title";
1117
- titleSpan.textContent = s.title || "New Session";
1118
- el.appendChild(titleSpan);
1119
-
1120
- // Unread badge (right side)
1121
- if (s.unread > 0 && !s.active) {
1122
- var badge = document.createElement("span");
1123
- badge.className = "mobile-session-unread";
1124
- badge.textContent = s.unread > 99 ? "99+" : String(s.unread);
1125
- el.appendChild(badge);
1126
- }
1127
-
1128
- (function (id) {
1129
- el.addEventListener("click", function () {
1130
- if (ctx.ws && ctx.connected) {
1131
- ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
1132
- }
1133
- dismissOverlayPanels();
1134
- closeMobileSheet();
1135
- });
1136
- })(s.id);
1137
-
1138
- return el;
1139
- }
1140
-
1141
- // Helper: create a mobile loop child element (individual session inside a group)
1142
- function createMobileLoopChild(s) {
1143
- var el = document.createElement("button");
1144
- el.className = "mobile-loop-child" + (s.active ? " active" : "");
1145
-
1146
- if (s.isProcessing) {
1147
- var dot = document.createElement("span");
1148
- dot.className = "mobile-session-processing";
1149
- el.appendChild(dot);
1150
- }
1151
-
1152
- var textSpan = document.createElement("span");
1153
- textSpan.className = "mobile-session-title";
1154
- if (s.loop) {
1155
- var isRalphChild = s.loop.source === "ralph";
1156
- var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
1157
- var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
1158
- var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
1159
- var badge = document.createElement("span");
1160
- badge.className = "mobile-loop-role-badge" + roleCls;
1161
- badge.textContent = roleName + iterSuffix;
1162
- textSpan.appendChild(badge);
1163
- }
1164
- el.appendChild(textSpan);
1165
-
1166
- (function (id) {
1167
- el.addEventListener("click", function () {
1168
- if (ctx.ws && ctx.connected) {
1169
- ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
1170
- }
1171
- dismissOverlayPanels();
1172
- closeMobileSheet();
1173
- });
1174
- })(s.id);
1175
-
1176
- return el;
1177
- }
1178
-
1179
- // Helper: create a mobile loop run sub-group (collapsible time group)
1180
- function createMobileLoopRun(parentGk, startedAtKey, sessions, isRalph) {
1181
- var runGk = parentGk + ":" + startedAtKey;
1182
- var expanded = expandedMobileLoopRuns.has(runGk);
1183
- var startedAt = Number(startedAtKey);
1184
- var timeLabel = startedAt ? new Date(startedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "Unknown";
1185
-
1186
- var hasActive = false;
1187
- var anyProcessing = false;
1188
- var latestSession = sessions[0];
1189
- for (var i = 0; i < sessions.length; i++) {
1190
- if (sessions[i].active) hasActive = true;
1191
- if (sessions[i].isProcessing) anyProcessing = true;
1192
- if ((sessions[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
1193
- latestSession = sessions[i];
1194
- }
1195
- }
1196
-
1197
- var wrapper = document.createElement("div");
1198
- wrapper.className = "mobile-loop-run-wrapper";
1199
-
1200
- var header = document.createElement("button");
1201
- header.className = "mobile-loop-run" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
1202
-
1203
- var chevron = document.createElement("span");
1204
- chevron.className = "mobile-loop-chevron";
1205
- chevron.innerHTML = iconHtml("chevron-right");
1206
- header.appendChild(chevron);
1207
-
1208
- var label = document.createElement("span");
1209
- label.className = "mobile-loop-run-time";
1210
- var labelHtml = "";
1211
- if (anyProcessing) {
1212
- labelHtml += '<span class="mobile-session-processing"></span> ';
1213
- }
1214
- labelHtml += escapeHtml(timeLabel);
1215
- label.innerHTML = labelHtml;
1216
- header.appendChild(label);
1217
-
1218
- var countBadge = document.createElement("span");
1219
- countBadge.className = "mobile-loop-count" + (isRalph ? "" : " scheduled");
1220
- countBadge.textContent = String(sessions.length);
1221
- header.appendChild(countBadge);
1222
-
1223
- header.addEventListener("click", (function (rk) {
1224
- return function (e) {
1225
- e.stopPropagation();
1226
- if (expandedMobileLoopRuns.has(rk)) {
1227
- expandedMobileLoopRuns.delete(rk);
1228
- } else {
1229
- expandedMobileLoopRuns.add(rk);
1230
- }
1231
- refreshMobileChatSheet();
1232
- };
1233
- })(runGk));
1234
-
1235
- wrapper.appendChild(header);
1236
-
1237
- if (expanded) {
1238
- var childContainer = document.createElement("div");
1239
- childContainer.className = "mobile-loop-children";
1240
- for (var k = 0; k < sessions.length; k++) {
1241
- childContainer.appendChild(createMobileLoopChild(sessions[k]));
1242
- }
1243
- wrapper.appendChild(childContainer);
1244
- }
1245
-
1246
- return wrapper;
1247
- }
1248
-
1249
- // Helper: create a mobile loop group element (collapsible group header)
1250
- function createMobileLoopGroup(loopId, children, groupKey) {
1251
- var gk = groupKey || loopId;
1252
-
1253
- // Sub-group children by startedAt (each run)
1254
- var runMap = {};
1255
- for (var i = 0; i < children.length; i++) {
1256
- var runKey = String(children[i].loop && children[i].loop.startedAt || 0);
1257
- if (!runMap[runKey]) runMap[runKey] = [];
1258
- runMap[runKey].push(children[i]);
1259
- }
1260
- var runKeys = Object.keys(runMap);
1261
-
1262
- // Sort each run's children by iteration then role
1263
- for (var ri = 0; ri < runKeys.length; ri++) {
1264
- runMap[runKeys[ri]].sort(function (a, b) {
1265
- var ai = (a.loop && a.loop.iteration) || 0;
1266
- var bi = (b.loop && b.loop.iteration) || 0;
1267
- if (ai !== bi) return ai - bi;
1268
- var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
1269
- var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
1270
- return ar - br;
1271
- });
1272
- }
1273
-
1274
- // Sort runs by startedAt descending (newest first)
1275
- runKeys.sort(function (a, b) { return Number(b) - Number(a); });
1276
-
1277
- var expanded = expandedMobileLoopGroups.has(gk);
1278
- var hasActive = false;
1279
- var anyProcessing = false;
1280
- var latestSession = children[0];
1281
- for (var ci = 0; ci < children.length; ci++) {
1282
- if (children[ci].active) hasActive = true;
1283
- if (children[ci].isProcessing) anyProcessing = true;
1284
- if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
1285
- latestSession = children[ci];
1286
- }
1287
- }
1288
-
1289
- var loopName = (children[0].loop && children[0].loop.name) || "Ralph Loop";
1290
- var isRalph = children[0].loop && children[0].loop.source === "ralph";
1291
- var isCrafting = false;
1292
- for (var j = 0; j < children.length; j++) {
1293
- if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
1294
- }
1295
- var runCount = runKeys.length;
1296
-
1297
- var wrapper = document.createElement("div");
1298
- wrapper.className = "mobile-loop-wrapper";
1299
-
1300
- // Group header row
1301
- var header = document.createElement("button");
1302
- header.className = "mobile-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
1303
-
1304
- var chevron = document.createElement("span");
1305
- chevron.className = "mobile-loop-chevron";
1306
- chevron.innerHTML = iconHtml("chevron-right");
1307
- header.appendChild(chevron);
1308
-
1309
- var iconSpan = document.createElement("span");
1310
- var groupIcon = isRalph ? "repeat" : "calendar-clock";
1311
- iconSpan.className = "mobile-loop-icon" + (isRalph ? "" : " scheduled");
1312
- iconSpan.innerHTML = iconHtml(groupIcon);
1313
- header.appendChild(iconSpan);
1314
-
1315
- if (anyProcessing) {
1316
- var dot = document.createElement("span");
1317
- dot.className = "mobile-session-processing";
1318
- header.appendChild(dot);
1319
- }
1320
-
1321
- var nameSpan = document.createElement("span");
1322
- nameSpan.className = "mobile-loop-name";
1323
- nameSpan.textContent = loopName;
1324
- header.appendChild(nameSpan);
1325
-
1326
- if (isCrafting && children.length === 1) {
1327
- var craftBadge = document.createElement("span");
1328
- craftBadge.className = "mobile-loop-badge crafting";
1329
- craftBadge.textContent = "Crafting";
1330
- header.appendChild(craftBadge);
1331
- } else {
1332
- var countBadge = document.createElement("span");
1333
- countBadge.className = "mobile-loop-count" + (isRalph ? "" : " scheduled");
1334
- var countLabel = runCount === 1 ? String(children.length) : runCount + (runCount === 1 ? " run" : " runs");
1335
- countBadge.textContent = countLabel;
1336
- header.appendChild(countBadge);
1337
- }
1338
-
1339
- // Chevron toggles expansion
1340
- header.addEventListener("click", (function (lid) {
1341
- return function (e) {
1342
- e.stopPropagation();
1343
- if (expandedMobileLoopGroups.has(lid)) {
1344
- expandedMobileLoopGroups.delete(lid);
1345
- } else {
1346
- expandedMobileLoopGroups.add(lid);
1347
- }
1348
- refreshMobileChatSheet();
1349
- };
1350
- })(gk));
1351
-
1352
- wrapper.appendChild(header);
1353
-
1354
- // Expanded: show runs
1355
- if (expanded) {
1356
- var childContainer = document.createElement("div");
1357
- childContainer.className = "mobile-loop-children";
1358
-
1359
- if (runCount === 1) {
1360
- var singleRun = runMap[runKeys[0]];
1361
- for (var sk = 0; sk < singleRun.length; sk++) {
1362
- childContainer.appendChild(createMobileLoopChild(singleRun[sk]));
1363
- }
1364
- } else {
1365
- for (var rk = 0; rk < runKeys.length; rk++) {
1366
- childContainer.appendChild(createMobileLoopRun(gk, runKeys[rk], runMap[runKeys[rk]], isRalph));
1367
- }
1368
- }
1369
-
1370
- wrapper.appendChild(childContainer);
1371
- }
1372
-
1373
- return wrapper;
1374
- }
1375
-
1376
- function renderMateMobileActions(container) {
1377
- var newSessionBtn = document.createElement("button");
1378
- newSessionBtn.className = "mobile-session-new";
1379
- newSessionBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
1380
- newSessionBtn.addEventListener("click", function () {
1381
- if (ctx.ws && ctx.connected) {
1382
- ctx.ws.send(JSON.stringify({ type: "new_session" }));
1383
- }
1384
- closeMobileSheet();
1385
- });
1386
- container.appendChild(newSessionBtn);
1387
-
1388
- var debateBtn = document.createElement("button");
1389
- debateBtn.className = "mobile-session-new";
1390
- debateBtn.innerHTML = '<i data-lucide="mic" style="width:16px;height:16px"></i> New debate';
1391
- debateBtn.addEventListener("click", function () {
1392
- closeMobileSheet();
1393
- var targetBtn = document.getElementById("mate-debate-btn");
1394
- if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
1395
- });
1396
- container.appendChild(debateBtn);
1397
-
1398
- // Render mate session list
1399
- var mateSessions = getMateSessions();
1400
- if (mateSessions.length > 0) {
1401
- var sorted = mateSessions.slice().sort(function (a, b) {
1402
- return (b.lastActivity || 0) - (a.lastActivity || 0);
1403
- });
1404
-
1405
- var currentGroup = "";
1406
- for (var i = 0; i < sorted.length; i++) {
1407
- var s = sorted[i];
1408
- var group = getDateGroup(s.lastActivity || 0);
1409
- if (group !== currentGroup) {
1410
- currentGroup = group;
1411
- var header = document.createElement("div");
1412
- header.className = "mobile-sheet-group";
1413
- header.textContent = group;
1414
- container.appendChild(header);
1415
- }
1416
- var mateItem = createMobileSessionItem(s);
1417
- container.appendChild(mateItem);
1418
- }
1419
- }
1420
-
1421
- refreshIcons();
1422
- }
1423
-
1424
- // Helper: render sorted sessions into a container with date groups (with loop session grouping)
1425
- function renderMobileSessionsInto(container) {
1426
- var newBtn = document.createElement("button");
1427
- newBtn.className = "mobile-session-new";
1428
- newBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
1429
- newBtn.addEventListener("click", function () {
1430
- if (ctx.ws && ctx.connected) {
1431
- ctx.ws.send(JSON.stringify({ type: "new_session" }));
1432
- }
1433
- closeMobileSheet();
1434
- });
1435
- container.appendChild(newBtn);
1436
-
1437
- // Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
1438
- var loopGroups = {};
1439
- var normalSessions = [];
1440
- for (var i = 0; i < cachedSessions.length; i++) {
1441
- var s = cachedSessions[i];
1442
- if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
1443
- continue;
1444
- } else if (s.loop && s.loop.loopId) {
1445
- var startedAt = s.loop.startedAt || 0;
1446
- var dateStr = startedAt ? new Date(startedAt).toISOString().slice(0, 10) : "unknown";
1447
- var groupKey = s.loop.loopId + ":" + dateStr;
1448
- if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
1449
- loopGroups[groupKey].push(s);
1450
- } else {
1451
- normalSessions.push(s);
1452
- }
1453
- }
1454
-
1455
- // Build virtual items
1456
- var items = [];
1457
- for (var j = 0; j < normalSessions.length; j++) {
1458
- items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
1459
- }
1460
- var groupKeys = Object.keys(loopGroups);
1461
- for (var k = 0; k < groupKeys.length; k++) {
1462
- var gk = groupKeys[k];
1463
- var children = loopGroups[gk];
1464
- var realLoopId = children[0].loop.loopId;
1465
- var maxActivity = 0;
1466
- for (var m = 0; m < children.length; m++) {
1467
- var act = children[m].lastActivity || 0;
1468
- if (act > maxActivity) maxActivity = act;
1469
- }
1470
- items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
1471
- }
1472
-
1473
- // Sort by lastActivity descending
1474
- items.sort(function (a, b) {
1475
- return (b.lastActivity || 0) - (a.lastActivity || 0);
1476
- });
1477
-
1478
- var currentGroup = "";
1479
- for (var n = 0; n < items.length; n++) {
1480
- var item = items[n];
1481
- var group = getDateGroup(item.lastActivity || 0);
1482
- if (group !== currentGroup) {
1483
- currentGroup = group;
1484
- var header = document.createElement("div");
1485
- header.className = "mobile-sheet-group";
1486
- header.textContent = group;
1487
- container.appendChild(header);
1488
- }
1489
- if (item.type === "loop") {
1490
- container.appendChild(createMobileLoopGroup(item.loopId, item.children, item.groupKey));
1491
- } else {
1492
- container.appendChild(createMobileSessionItem(item.data));
1493
- }
1494
- }
1495
- }
1496
-
1497
- // Refresh mobile chat sheet when session data updates (called from renderSessionList)
1498
- export function refreshMobileChatSheet() {
1499
- if (!mobileChatSheetOpen) return;
1500
- var sheet = document.getElementById("mobile-sheet");
1501
- if (!sheet || sheet.classList.contains("hidden")) {
1502
- mobileChatSheetOpen = false;
1503
- return;
1504
- }
1505
- var sessionListEl = sheet.querySelector(".mobile-chat-session-list");
1506
- if (!sessionListEl) return;
1507
-
1508
- // Update chips: active state and processing dots
1509
- var chips = sheet.querySelectorAll(".mobile-chat-chip");
1510
- for (var i = 0; i < chips.length; i++) {
1511
- var chip = chips[i];
1512
- chip.classList.remove("active");
1513
-
1514
- // Update active state
1515
- var isDmActive = !!currentDmUserId;
1516
- if (chip.dataset.type === "project" && chip.dataset.slug === cachedCurrentSlug && !isDmActive) {
1517
- chip.classList.add("active");
1518
- } else if (chip.dataset.type === "mate" && chip.dataset.mateId === currentDmUserId) {
1519
- chip.classList.add("active");
1520
- }
1521
-
1522
- // Update processing dot: same class as icon strip
1523
- var statusDot = chip.querySelector(".icon-strip-status");
1524
- if (statusDot) {
1525
- var isProcessing = false;
1526
- var allProjects = (ctx && ctx.projectList) || [];
1527
- var lookupSlug = chip.dataset.type === "mate" ? ("mate-" + chip.dataset.mateId) : chip.dataset.slug;
1528
- for (var pi = 0; pi < allProjects.length; pi++) {
1529
- if (allProjects[pi].slug === lookupSlug && allProjects[pi].isProcessing) {
1530
- isProcessing = true;
1531
- break;
1532
- }
1533
- }
1534
- statusDot.classList.toggle("processing", isProcessing);
1535
- }
1536
- }
1537
-
1538
- // Re-render sessions for current context
1539
- sessionListEl.innerHTML = "";
1540
- if (currentDmUserId) {
1541
- renderMateMobileActions(sessionListEl);
1542
- } else {
1543
- renderMobileSessionsInto(sessionListEl);
1544
- }
1545
-
1546
- refreshIcons();
1547
- }
1548
-
1549
- function renderSheetMateProfile(listEl) {
1550
- if (!mobileSheetMateData) return;
1551
- var data = mobileSheetMateData;
1552
-
1553
- // Profile header
1554
- var header = document.createElement("div");
1555
- header.className = "mate-profile-header";
1556
-
1557
- var avatar = document.createElement("img");
1558
- avatar.className = "mate-profile-avatar";
1559
- avatar.src = data.avatarUrl || "";
1560
- avatar.alt = data.displayName || "";
1561
- header.appendChild(avatar);
1562
-
1563
- var info = document.createElement("div");
1564
- info.className = "mate-profile-info";
1565
- var nameEl = document.createElement("div");
1566
- nameEl.className = "mate-profile-name";
1567
- nameEl.textContent = data.displayName || "";
1568
- info.appendChild(nameEl);
1569
- if (data.description) {
1570
- var descEl = document.createElement("div");
1571
- descEl.className = "mate-profile-desc";
1572
- descEl.textContent = data.description;
1573
- info.appendChild(descEl);
1574
- }
1575
- header.appendChild(info);
1576
- listEl.appendChild(header);
1577
-
1578
- // Action buttons
1579
- var actions = [
1580
- { icon: "book-open", label: "Knowledge", btnId: "mate-knowledge-btn", countId: "mate-knowledge-count" },
1581
- { icon: "sticky-note", label: "Sticky Notes", btnId: "sticky-notes-toggle-btn", countId: "sticky-notes-sidebar-count" },
1582
- { icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" },
1583
- { icon: "calendar", label: "Scheduled Tasks", btnId: "mate-scheduler-btn" }
1584
- ];
1585
-
1586
- for (var i = 0; i < actions.length; i++) {
1587
- (function (action) {
1588
- var btn = document.createElement("button");
1589
- btn.className = "mate-profile-action";
1590
- var countHtml = "";
1591
- if (action.countId) {
1592
- var countEl = document.getElementById(action.countId);
1593
- if (countEl && !countEl.classList.contains("hidden") && countEl.textContent) {
1594
- countHtml = '<span class="mate-profile-action-count">' + escapeHtml(countEl.textContent) + '</span>';
1595
- }
1596
- }
1597
- btn.innerHTML = '<i data-lucide="' + action.icon + '"></i><span>' + action.label + '</span>' + countHtml;
1598
- btn.addEventListener("click", function () {
1599
- closeMobileSheet();
1600
- var targetBtn = document.getElementById(action.btnId);
1601
- if (targetBtn) {
1602
- setTimeout(function () { targetBtn.click(); }, 250);
1603
- }
1604
- });
1605
- listEl.appendChild(btn);
1606
- })(actions[i]);
1607
- }
1608
- }
1609
-
1610
- function renderSheetSearch(listEl) {
1611
- // Search input at top
1612
- var wrap = document.createElement("div");
1613
- wrap.className = "mobile-search-input-wrap";
1614
- var input = document.createElement("input");
1615
- input.className = "mobile-search-input";
1616
- input.type = "text";
1617
- input.placeholder = "Search sessions, messages...";
1618
- input.autocomplete = "off";
1619
- input.spellcheck = false;
1620
- wrap.appendChild(input);
1621
- listEl.appendChild(wrap);
1622
-
1623
- // Results container
1624
- var resultsEl = document.createElement("div");
1625
- resultsEl.style.padding = "0 8px";
1626
- listEl.appendChild(resultsEl);
1627
-
1628
- // Auto-focus
1629
- setTimeout(function () { input.focus(); }, 300);
1630
-
1631
- // Show all sessions initially
1632
- renderSearchResults(resultsEl, "");
1633
-
1634
- input.addEventListener("input", function () {
1635
- var q = input.value.trim().toLowerCase();
1636
- renderSearchResults(resultsEl, q);
1637
- });
1638
- input.addEventListener("keydown", function (e) { e.stopPropagation(); });
1639
- input.addEventListener("keyup", function (e) { e.stopPropagation(); });
1640
- input.addEventListener("keypress", function (e) { e.stopPropagation(); });
1641
- }
1642
-
1643
- function renderSearchResults(container, query) {
1644
- container.innerHTML = "";
1645
- var sorted = cachedSessions.slice().sort(function (a, b) {
1646
- return (b.lastActivity || 0) - (a.lastActivity || 0);
1647
- });
1648
-
1649
- var found = 0;
1650
- for (var i = 0; i < sorted.length; i++) {
1651
- var s = sorted[i];
1652
- var title = s.title || "New Session";
1653
- if (query && title.toLowerCase().indexOf(query) === -1) continue;
1654
- found++;
1655
-
1656
- var el = document.createElement("button");
1657
- el.className = "mobile-session-item";
1658
- if (s.active) el.classList.add("active");
1659
-
1660
- var titleSpan = document.createElement("span");
1661
- titleSpan.className = "mobile-session-title";
1662
- titleSpan.textContent = title;
1663
- el.appendChild(titleSpan);
1664
-
1665
- if (s.isProcessing) {
1666
- var dot = document.createElement("span");
1667
- dot.className = "mobile-session-processing";
1668
- el.appendChild(dot);
1669
- }
1670
-
1671
- (function (id) {
1672
- el.addEventListener("click", function () {
1673
- if (ctx.ws && ctx.connected) {
1674
- ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
1675
- }
1676
- dismissOverlayPanels();
1677
- closeMobileSheet();
1678
- });
1679
- })(s.id);
1680
-
1681
- container.appendChild(el);
1682
- }
1683
-
1684
- if (found === 0 && query) {
1685
- var empty = document.createElement("div");
1686
- empty.className = "mobile-alert-empty";
1687
- empty.textContent = 'No results for "' + query + '"';
1688
- container.appendChild(empty);
1689
- }
1690
- }
1691
-
1692
- function renderSheetTools(listEl) {
1693
- var isMateDm = document.body.classList.contains("mate-dm-active");
1694
-
1695
- var items = isMateDm ? [
1696
- { icon: "brain", label: "Memory", action: "mate-memory" },
1697
- { icon: "book-open", label: "Knowledge", action: "mate-knowledge" },
1698
- { icon: "sticky-note", label: "Sticky Notes", action: "mate-sticky" },
1699
- { icon: "puzzle", label: "Skills", action: "mate-skills" },
1700
- { icon: "calendar-clock", label: "Scheduled Tasks", action: "mate-scheduler" }
1701
- ] : [
1702
- { icon: "folder-tree", label: "Files", action: "files" },
1703
- { icon: "square-terminal", label: "Terminal", action: "terminal" },
1704
- { icon: "calendar-clock", label: "Scheduled Tasks", action: "scheduler" }
1705
- ];
1706
-
1707
- for (var i = 0; i < items.length; i++) {
1708
- (function (item) {
1709
- var btn = document.createElement("button");
1710
- btn.className = "mobile-more-item";
1711
- btn.innerHTML = '<i data-lucide="' + item.icon + '"></i><span class="mobile-more-item-label">' + item.label + '</span>';
1712
- btn.addEventListener("click", function () {
1713
- closeMobileSheet();
1714
- var targetId = null;
1715
- if (item.action === "files") {
1716
- setTimeout(function () { openMobileSheet("files"); }, 250);
1717
- } else if (item.action === "terminal") {
1718
- if (ctx.openTerminal) ctx.openTerminal();
1719
- } else if (item.action === "scheduler") {
1720
- targetId = "scheduler-btn";
1721
- } else if (item.action === "mate-knowledge") {
1722
- setTimeout(function () { openMobileSheet("mate-knowledge"); }, 250);
1723
- return;
1724
- } else if (item.action === "mate-sticky") {
1725
- targetId = "mate-sticky-notes-btn";
1726
- } else if (item.action === "mate-skills") {
1727
- targetId = "mate-skills-btn";
1728
- } else if (item.action === "mate-memory") {
1729
- targetId = "mate-memory-btn";
1730
- } else if (item.action === "mate-scheduler") {
1731
- targetId = "mate-scheduler-btn";
1732
- } else if (item.action === "mate-debate") {
1733
- targetId = "mate-debate-btn";
1734
- }
1735
- if (targetId) {
1736
- var targetBtn = document.getElementById(targetId);
1737
- if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
1738
- }
1739
- });
1740
- listEl.appendChild(btn);
1741
- })(items[i]);
1742
- }
1743
- }
1744
-
1745
- function renderSheetSettings(listEl) {
1746
- var items = [
1747
- { icon: "folder-cog", label: "Project Settings", action: "project-settings" },
1748
- { icon: "settings", label: "Server Settings", action: "server-settings" }
1749
- ];
1750
-
1751
- for (var i = 0; i < items.length; i++) {
1752
- (function (item) {
1753
- var btn = document.createElement("button");
1754
- btn.className = "mobile-more-item";
1755
- btn.innerHTML = '<i data-lucide="' + item.icon + '"></i><span class="mobile-more-item-label">' + item.label + '</span>';
1756
- btn.addEventListener("click", function () {
1757
- closeMobileSheet();
1758
- if (item.action === "project-settings") {
1759
- setTimeout(function () {
1760
- // Find current project data
1761
- var proj = null;
1762
- for (var pi = 0; pi < cachedAllProjects.length; pi++) {
1763
- if (cachedAllProjects[pi].slug === cachedCurrentSlug) {
1764
- proj = cachedAllProjects[pi];
1765
- break;
1766
- }
1767
- }
1768
- // For mate projects, use mate display name instead of slug
1769
- if (proj && proj.isMate && cachedMates.length > 0) {
1770
- var mateId = cachedCurrentSlug.replace("mate-", "");
1771
- for (var mi = 0; mi < cachedMates.length; mi++) {
1772
- var mp = cachedMates[mi].profile || {};
1773
- if (cachedMates[mi].id === mateId) {
1774
- proj = Object.assign({}, proj, { name: mp.displayName || cachedMates[mi].name || proj.name });
1775
- break;
1776
- }
1777
- }
1778
- }
1779
- openProjectSettings(cachedCurrentSlug, proj);
1780
- }, 250);
1781
- } else if (item.action === "server-settings") {
1782
- var settingsBtn = document.getElementById("server-settings-btn");
1783
- if (settingsBtn) setTimeout(function () { settingsBtn.click(); }, 250);
1784
- }
1785
- });
1786
- listEl.appendChild(btn);
1787
- })(items[i]);
1788
- }
1789
-
1790
- // Dark/Light switch button
1791
- var isDark = getCurrentTheme().variant === "dark";
1792
- var themeBtn = document.createElement("button");
1793
- themeBtn.className = "mobile-more-item";
1794
- themeBtn.innerHTML = '<i data-lucide="' + (isDark ? "sun" : "moon") + '"></i><span class="mobile-more-item-label">Switch to ' + (isDark ? "Light" : "Dark") + '</span>';
1795
-
1796
- themeBtn.addEventListener("click", function () {
1797
- var themeToggle = document.getElementById("theme-toggle-check");
1798
- if (themeToggle) themeToggle.click();
1799
- // Update button text after a tick (theme applies async)
1800
- setTimeout(function () {
1801
- var nowDark = getCurrentTheme().variant === "dark";
1802
- themeBtn.innerHTML = '<i data-lucide="' + (nowDark ? "sun" : "moon") + '"></i><span class="mobile-more-item-label">Switch to ' + (nowDark ? "Light" : "Dark") + '</span>';
1803
- refreshIcons();
1804
- }, 50);
1805
- });
1806
-
1807
- listEl.appendChild(themeBtn);
1808
-
1809
- // Chat Layout switch button
1810
- var currentLayout = getChatLayout();
1811
- var isBubble = currentLayout === "bubble";
1812
- var layoutBtn = document.createElement("button");
1813
- layoutBtn.className = "mobile-more-item";
1814
- layoutBtn.innerHTML = '<i data-lucide="' + (isBubble ? "monitor" : "message-circle") + '"></i>'
1815
- + '<span class="mobile-more-item-label">Switch to ' + (isBubble ? "Channel" : "Bubble") + '</span>';
1816
-
1817
- layoutBtn.addEventListener("click", function () {
1818
- var next = getChatLayout() === "bubble" ? "channel" : "bubble";
1819
- setChatLayout(next);
1820
- fetch('/api/user/chat-layout', {
1821
- method: 'PUT',
1822
- headers: { 'Content-Type': 'application/json' },
1823
- body: JSON.stringify({ layout: next })
1824
- });
1825
- closeMobileSheet();
1826
- });
1827
-
1828
- listEl.appendChild(layoutBtn);
1829
-
1830
- // "Open as app" — only show if not already in PWA standalone mode
1831
- if (!document.documentElement.classList.contains("pwa-standalone")) {
1832
- var pwaBtn = document.createElement("button");
1833
- pwaBtn.className = "mobile-more-item";
1834
- pwaBtn.innerHTML = '<i data-lucide="smartphone"></i><span class="mobile-more-item-label">Open as app</span>';
1835
- pwaBtn.addEventListener("click", function () {
1836
- closeMobileSheet();
1837
- // Trigger the existing PWA install modal
1838
- var installPill = document.getElementById("pwa-install-pill");
1839
- if (installPill) {
1840
- setTimeout(function () { installPill.click(); }, 250);
1841
- }
1842
- });
1843
- listEl.appendChild(pwaBtn);
1844
- }
1845
- }
1846
-
1847
- export function initSidebar(_ctx) {
1848
- ctx = _ctx;
1849
-
1850
- document.addEventListener("click", function () { closeSessionCtxMenu(); });
1851
-
1852
- ctx.hamburgerBtn.addEventListener("click", function () {
1853
- ctx.sidebar.classList.contains("open") ? closeSidebar() : openSidebar();
1854
- });
1855
-
1856
- ctx.sidebarOverlay.addEventListener("click", closeSidebar);
1857
-
1858
- // --- Desktop sidebar collapse/expand ---
1859
- function toggleSidebarCollapse() {
1860
- var layout = ctx.$("layout");
1861
- var collapsed = layout.classList.toggle("sidebar-collapsed");
1862
- try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
1863
- setTimeout(function () { syncUserIslandWidth(); syncResizeHandle(); }, 210);
1864
- }
1865
-
1866
- if (ctx.sidebarToggleBtn) ctx.sidebarToggleBtn.addEventListener("click", toggleSidebarCollapse);
1867
- if (ctx.sidebarExpandBtn) ctx.sidebarExpandBtn.addEventListener("click", toggleSidebarCollapse);
1868
- var mateSidebarToggle = document.getElementById("mate-sidebar-toggle-btn");
1869
- if (mateSidebarToggle) mateSidebarToggle.addEventListener("click", toggleSidebarCollapse);
1870
-
1871
- // Restore collapsed state from localStorage
1872
- try {
1873
- if (localStorage.getItem("sidebar-collapsed") === "1") {
1874
- ctx.$("layout").classList.add("sidebar-collapsed");
1875
- }
1876
- } catch (e) {}
1877
-
1878
- ctx.newSessionBtn.addEventListener("click", function () {
1879
- if (ctx.ws && ctx.connected) {
1880
- ctx.ws.send(JSON.stringify({ type: "new_session" }));
1881
- closeSidebar();
1882
- }
1883
- });
1884
-
1885
- // --- New Ralph Loop button ---
1886
- var newRalphBtn = ctx.$("new-ralph-btn");
1887
- if (newRalphBtn) {
1888
- newRalphBtn.addEventListener("click", function () {
1889
- if (ctx.openRalphWizard) ctx.openRalphWizard();
1890
- });
1891
- }
1892
-
1893
- // --- Session search ---
1894
- var searchBtn = ctx.$("search-session-btn");
1895
- var searchBox = ctx.$("session-search");
1896
- var searchInput = ctx.$("session-search-input");
1897
- var searchClear = ctx.$("session-search-clear");
1898
-
1899
- function openSearch() {
1900
- searchBox.classList.remove("hidden");
1901
- searchBtn.classList.add("active");
1902
- searchInput.value = "";
1903
- searchQuery = "";
1904
- setTimeout(function () { searchInput.focus(); }, 50);
1905
- }
1906
-
1907
- function closeSearch() {
1908
- searchBox.classList.add("hidden");
1909
- searchBtn.classList.remove("active");
1910
- searchInput.value = "";
1911
- searchQuery = "";
1912
- searchMatchIds = null;
1913
- if (searchDebounce) { clearTimeout(searchDebounce); searchDebounce = null; }
1914
- renderSessionList(null);
1915
- }
1916
-
1917
- searchBtn.addEventListener("click", function () {
1918
- if (searchBox.classList.contains("hidden")) {
1919
- openSearch();
1920
- } else {
1921
- closeSearch();
1922
- }
1923
- });
1924
-
1925
- if (searchClear) {
1926
- searchClear.addEventListener("click", function () {
1927
- closeSearch();
1928
- });
1929
- }
1930
-
1931
- searchInput.addEventListener("input", function () {
1932
- searchQuery = searchInput.value.trim();
1933
- if (searchDebounce) clearTimeout(searchDebounce);
1934
- if (!searchQuery) {
1935
- searchMatchIds = null;
1936
- renderSessionList(null);
1937
- return;
1938
- }
1939
- searchDebounce = setTimeout(function () {
1940
- if (ctx.ws && ctx.connected) {
1941
- ctx.ws.send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
1942
- }
1943
- }, 200);
1944
- });
1945
-
1946
- searchInput.addEventListener("keydown", function (e) {
1947
- if (e.key === "Escape") {
1948
- e.preventDefault();
1949
- closeSearch();
1950
- }
1951
- });
1952
-
1953
- // --- Resume session picker ---
1954
- var resumeModal = ctx.$("resume-modal");
1955
- var resumeCancel = ctx.$("resume-cancel");
1956
- var pickerLoading = ctx.$("resume-picker-loading");
1957
- var pickerEmpty = ctx.$("resume-picker-empty");
1958
- var pickerList = ctx.$("resume-picker-list");
1959
-
1960
- function openResumeModal() {
1961
- resumeModal.classList.remove("hidden");
1962
- pickerLoading.classList.remove("hidden");
1963
- pickerEmpty.classList.add("hidden");
1964
- pickerList.classList.add("hidden");
1965
- pickerList.innerHTML = "";
1966
- if (ctx.ws && ctx.connected) {
1967
- ctx.ws.send(JSON.stringify({ type: "list_cli_sessions" }));
1968
- }
1969
- }
1970
-
1971
- function closeResumeModal() {
1972
- resumeModal.classList.add("hidden");
1973
- }
1974
-
1975
- ctx.resumeSessionBtn.addEventListener("click", openResumeModal);
1976
- resumeCancel.addEventListener("click", closeResumeModal);
1977
- resumeModal.querySelector(".confirm-backdrop").addEventListener("click", closeResumeModal);
1978
-
1979
- // --- Panel switch (sessions / files / projects) ---
1980
- var fileBrowserBtn = ctx.$("file-browser-btn");
1981
- var projectsPanel = ctx.$("sidebar-panel-projects");
1982
- var sessionsPanel = ctx.$("sidebar-panel-sessions");
1983
- var filesPanel = ctx.$("sidebar-panel-files");
1984
- var sessionsHeaderContent = ctx.$("sessions-header-content");
1985
- var filesHeaderContent = ctx.$("files-header-content");
1986
- var filePanelClose = ctx.$("file-panel-close");
1987
-
1988
- function hideAllPanels() {
1989
- if (projectsPanel) projectsPanel.classList.add("hidden");
1990
- if (sessionsPanel) sessionsPanel.classList.add("hidden");
1991
- if (filesPanel) filesPanel.classList.add("hidden");
1992
- if (sessionsHeaderContent) sessionsHeaderContent.classList.add("hidden");
1993
- if (filesHeaderContent) filesHeaderContent.classList.add("hidden");
1994
- }
1995
-
1996
- function showProjectsPanel() {
1997
- hideAllPanels();
1998
- if (projectsPanel) projectsPanel.classList.remove("hidden");
1999
- }
2000
-
2001
- function showSessionsPanel() {
2002
- hideAllPanels();
2003
- if (sessionsPanel) sessionsPanel.classList.remove("hidden");
2004
- if (sessionsHeaderContent) sessionsHeaderContent.classList.remove("hidden");
2005
- }
2006
-
2007
- function showFilesPanel() {
2008
- hideAllPanels();
2009
- if (filesPanel) filesPanel.classList.remove("hidden");
2010
- if (filesHeaderContent) filesHeaderContent.classList.remove("hidden");
2011
- if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
2012
- }
2013
-
2014
- if (fileBrowserBtn) {
2015
- fileBrowserBtn.addEventListener("click", showFilesPanel);
2016
- }
2017
- if (filePanelClose) {
2018
- filePanelClose.addEventListener("click", showSessionsPanel);
2019
- }
2020
-
2021
- // --- Mobile sheet close handlers ---
2022
- var mobileSheet = document.getElementById("mobile-sheet");
2023
- if (mobileSheet) {
2024
- var sheetBackdrop = mobileSheet.querySelector(".mobile-sheet-backdrop");
2025
- var sheetCloseBtn = mobileSheet.querySelector(".mobile-sheet-close");
2026
- if (sheetBackdrop) sheetBackdrop.addEventListener("click", closeMobileSheet);
2027
- if (sheetCloseBtn) sheetCloseBtn.addEventListener("click", closeMobileSheet);
2028
-
2029
- // --- Drag to dismiss sheet ---
2030
- var sheetHandle = mobileSheet.querySelector(".mobile-sheet-handle");
2031
- var sheetContent = mobileSheet.querySelector(".mobile-sheet-content");
2032
- if (sheetHandle && sheetContent) {
2033
- var dragStartY = 0;
2034
- var dragging = false;
2035
-
2036
- sheetHandle.addEventListener("touchstart", function (e) {
2037
- dragStartY = e.touches[0].clientY;
2038
- dragging = true;
2039
- sheetContent.style.transition = "none";
2040
- }, { passive: true });
2041
-
2042
- mobileSheet.addEventListener("touchmove", function (e) {
2043
- if (!dragging) return;
2044
- var deltaY = e.touches[0].clientY - dragStartY;
2045
- if (deltaY < 0) deltaY = 0;
2046
- sheetContent.style.transform = "translateY(" + deltaY + "px)";
2047
- if (sheetBackdrop) {
2048
- var opacity = Math.max(0, 1 - deltaY / (sheetContent.offsetHeight * 0.5));
2049
- sheetBackdrop.style.opacity = opacity;
2050
- }
2051
- }, { passive: true });
2052
-
2053
- mobileSheet.addEventListener("touchend", function () {
2054
- if (!dragging) return;
2055
- dragging = false;
2056
- var currentY = parseFloat(sheetContent.style.transform.replace(/[^0-9.-]/g, "")) || 0;
2057
- var threshold = sheetContent.offsetHeight * 0.3;
2058
-
2059
- if (currentY > threshold) {
2060
- sheetContent.style.transition = "transform 0.22s ease-in";
2061
- sheetContent.style.transform = "translateY(100%)";
2062
- if (sheetBackdrop) {
2063
- sheetBackdrop.style.transition = "opacity 0.22s ease-in";
2064
- sheetBackdrop.style.opacity = "0";
2065
- }
2066
- setTimeout(function () {
2067
- sheetContent.style.transition = "";
2068
- sheetContent.style.transform = "";
2069
- if (sheetBackdrop) {
2070
- sheetBackdrop.style.transition = "";
2071
- sheetBackdrop.style.opacity = "";
2072
- }
2073
- // Close without animation since we already animated
2074
- var sheet = document.getElementById("mobile-sheet");
2075
- if (sheet) {
2076
- if (sheet.classList.contains("sheet-files")) {
2077
- var fileTree = document.getElementById("file-tree");
2078
- var sidebarFilesPanel = document.getElementById("sidebar-panel-files");
2079
- if (fileTree && sidebarFilesPanel) {
2080
- sidebarFilesPanel.appendChild(fileTree);
2081
- }
2082
- }
2083
- sheet.classList.add("hidden");
2084
- sheet.classList.remove("closing", "sheet-files");
2085
- }
2086
- }, 230);
2087
- } else {
2088
- sheetContent.style.transition = "transform 0.2s ease-out";
2089
- sheetContent.style.transform = "translateY(0)";
2090
- if (sheetBackdrop) {
2091
- sheetBackdrop.style.transition = "opacity 0.2s ease-out";
2092
- sheetBackdrop.style.opacity = "";
2093
- }
2094
- setTimeout(function () {
2095
- sheetContent.style.transition = "";
2096
- sheetContent.style.transform = "";
2097
- if (sheetBackdrop) {
2098
- sheetBackdrop.style.transition = "";
2099
- sheetBackdrop.style.opacity = "";
2100
- }
2101
- }, 200);
2102
- }
2103
- }, { passive: true });
2104
- }
2105
- }
2106
-
2107
- // --- Mobile tab bar ---
2108
- var mobileTabBar = document.getElementById("mobile-tab-bar");
2109
- var mobileTabs = mobileTabBar ? mobileTabBar.querySelectorAll(".mobile-tab") : [];
2110
- var mobileHomeBtn = document.getElementById("mobile-home-btn");
2111
-
2112
- function setMobileTabActive(tabName) {
2113
- for (var i = 0; i < mobileTabs.length; i++) {
2114
- if (mobileTabs[i].dataset.tab === tabName) {
2115
- mobileTabs[i].classList.add("active");
2116
- } else {
2117
- mobileTabs[i].classList.remove("active");
2118
- }
2119
- }
2120
- if (mobileHomeBtn) {
2121
- if (tabName === "home") {
2122
- mobileHomeBtn.classList.add("active");
2123
- } else {
2124
- mobileHomeBtn.classList.remove("active");
2125
- }
2126
- }
2127
- }
2128
-
2129
- for (var t = 0; t < mobileTabs.length; t++) {
2130
- (function (tab) {
2131
- tab.addEventListener("click", function () {
2132
- var name = tab.dataset.tab;
2133
-
2134
- if (name === "chat") {
2135
- openMobileSheet("sessions");
2136
- setMobileTabActive("chat");
2137
- } else if (name === "search") {
2138
- openCommandPalette();
2139
- setMobileTabActive("search");
2140
- } else if (name === "tools") {
2141
- openMobileSheet("tools");
2142
- setMobileTabActive("tools");
2143
- } else if (name === "settings") {
2144
- openMobileSheet("settings");
2145
- setMobileTabActive("settings");
2146
- }
2147
- });
2148
- })(mobileTabs[t]);
2149
- }
2150
-
2151
- if (mobileHomeBtn) {
2152
- mobileHomeBtn.addEventListener("click", function () {
2153
- closeSidebar();
2154
- setMobileTabActive("home");
2155
- if (ctx.showHomeHub) ctx.showHomeHub();
2156
- });
2157
- }
2158
-
2159
- // --- User island width sync ---
2160
- var userIsland = document.getElementById("user-island");
2161
- var sidebarColumn = document.getElementById("sidebar-column");
2162
-
2163
- function syncUserIslandWidth() {
2164
- if (!userIsland) return;
2165
- var mateSidebarColumn = document.getElementById("mate-sidebar-column");
2166
- var isMateDM = document.body.classList.contains("mate-dm-active");
2167
- var col = (isMateDM && mateSidebarColumn && !mateSidebarColumn.classList.contains("hidden")) ? mateSidebarColumn : sidebarColumn;
2168
- if (!col) return;
2169
- var rect = col.getBoundingClientRect();
2170
- userIsland.style.width = (rect.right - 8 - 8) + "px";
2171
- }
2172
-
2173
- // --- Sidebar resize handle ---
2174
- var resizeHandle = document.getElementById("sidebar-resize-handle");
2175
-
2176
- function syncResizeHandle() {
2177
- if (!resizeHandle || !sidebarColumn) return;
2178
- var rect = sidebarColumn.getBoundingClientRect();
2179
- var parentRect = sidebarColumn.parentElement.getBoundingClientRect();
2180
- resizeHandle.style.left = (rect.right - parentRect.left) + "px";
2181
- }
2182
-
2183
- if (resizeHandle && sidebarColumn) {
2184
- var dragging = false;
2185
-
2186
- function onResizeMove(e) {
2187
- if (!dragging) return;
2188
- e.preventDefault();
2189
- var clientX = e.touches ? e.touches[0].clientX : e.clientX;
2190
- var iconStrip = document.getElementById("icon-strip");
2191
- var stripWidth = iconStrip ? iconStrip.offsetWidth : 72;
2192
- var newWidth = clientX - stripWidth;
2193
- if (newWidth < 192) newWidth = 192;
2194
- if (newWidth > 320) newWidth = 320;
2195
- sidebarColumn.style.width = newWidth + "px";
2196
- syncResizeHandle();
2197
- syncUserIslandWidth();
2198
- }
2199
-
2200
- function onResizeEnd() {
2201
- if (!dragging) return;
2202
- dragging = false;
2203
- resizeHandle.classList.remove("dragging");
2204
- document.body.style.cursor = "";
2205
- document.body.style.userSelect = "";
2206
- document.removeEventListener("mousemove", onResizeMove);
2207
- document.removeEventListener("mouseup", onResizeEnd);
2208
- document.removeEventListener("touchmove", onResizeMove);
2209
- document.removeEventListener("touchend", onResizeEnd);
2210
- try { localStorage.setItem("sidebar-width", sidebarColumn.style.width); } catch (e) {}
2211
- }
2212
-
2213
- function onResizeStart(e) {
2214
- e.preventDefault();
2215
- dragging = true;
2216
- resizeHandle.classList.add("dragging");
2217
- document.body.style.cursor = "col-resize";
2218
- document.body.style.userSelect = "none";
2219
- document.addEventListener("mousemove", onResizeMove);
2220
- document.addEventListener("mouseup", onResizeEnd);
2221
- document.addEventListener("touchmove", onResizeMove, { passive: false });
2222
- document.addEventListener("touchend", onResizeEnd);
2223
- }
2224
-
2225
- resizeHandle.addEventListener("mousedown", onResizeStart);
2226
- resizeHandle.addEventListener("touchstart", onResizeStart, { passive: false });
2227
-
2228
- // Restore saved width (skip transition so user-island syncs immediately)
2229
- try {
2230
- var savedWidth = localStorage.getItem("sidebar-width");
2231
- if (savedWidth) {
2232
- var px = parseInt(savedWidth, 10);
2233
- if (px >= 192 && px <= 320) {
2234
- sidebarColumn.style.transition = "none";
2235
- sidebarColumn.style.width = px + "px";
2236
- sidebarColumn.offsetWidth; // force reflow
2237
- sidebarColumn.style.transition = "";
2238
- }
2239
- }
2240
- } catch (e) {}
2241
-
2242
- syncResizeHandle();
2243
- syncUserIslandWidth();
2244
- }
2245
-
2246
- // Initial sync even if no resize handle
2247
- syncUserIslandWidth();
2248
-
2249
- // --- User island tooltip on hover (collapsed sidebar) ---
2250
- if (userIsland) {
2251
- var profileArea = userIsland.querySelector(".user-island-profile");
2252
- if (profileArea) {
2253
- profileArea.addEventListener("mouseenter", function () {
2254
- var layout = document.getElementById("layout");
2255
- if (!layout || !layout.classList.contains("sidebar-collapsed")) return;
2256
- var nameEl = userIsland.querySelector(".user-island-name");
2257
- var text = nameEl ? nameEl.textContent : "";
2258
- if (text) showIconTooltip(profileArea, text);
2259
- });
2260
- profileArea.addEventListener("mouseleave", function () {
2261
- hideIconTooltip();
2262
- });
2263
- }
2264
- }
2265
-
2266
- // --- Schedule countdown timer ---
2267
- startCountdownTimer();
2268
- }
2269
-
2270
- function startCountdownTimer() {
2271
- if (countdownTimer) clearInterval(countdownTimer);
2272
- countdownTimer = setInterval(updateCountdowns, 1000);
2273
- }
2274
-
2275
- function updateCountdowns() {
2276
- if (!ctx || !ctx.getUpcomingSchedules || !ctx.sessionListEl) return;
2277
- var upcoming = ctx.getUpcomingSchedules(3 * 60 * 1000); // 3 minutes
2278
-
2279
- // Remove stale container
2280
- if (countdownContainer && !ctx.sessionListEl.contains(countdownContainer)) {
2281
- countdownContainer = null;
2282
- }
2283
-
2284
- if (upcoming.length === 0) {
2285
- if (countdownContainer) {
2286
- countdownContainer.remove();
2287
- countdownContainer = null;
2288
- }
2289
- return;
2290
- }
2291
-
2292
- if (!countdownContainer) {
2293
- countdownContainer = document.createElement("div");
2294
- countdownContainer.className = "session-countdown-group";
2295
- ctx.sessionListEl.insertBefore(countdownContainer, ctx.sessionListEl.firstChild);
2296
- }
2297
-
2298
- var html = "";
2299
- var now = Date.now();
2300
- for (var i = 0; i < upcoming.length; i++) {
2301
- var u = upcoming[i];
2302
- var remaining = Math.max(0, Math.ceil((u.nextRunAt - now) / 1000));
2303
- var min = Math.floor(remaining / 60);
2304
- var sec = remaining % 60;
2305
- var timeStr = min + ":" + (sec < 10 ? "0" : "") + sec;
2306
- var colorStyle = u.color ? " style=\"border-left-color:" + u.color + "\"" : "";
2307
- html += '<div class="session-countdown-item"' + colorStyle + '>';
2308
- html += '<span class="session-countdown-name">' + escapeHtml(u.name) + '</span>';
2309
- html += '<span class="session-countdown-badge">' + timeStr + '</span>';
2310
- html += '</div>';
2311
- }
2312
- countdownContainer.innerHTML = html;
2313
- }
2314
-
2315
- // --- CLI session picker ---
2316
- function relativeTime(isoString) {
2317
- if (!isoString) return "";
2318
- var ms = Date.now() - new Date(isoString).getTime();
2319
- var sec = Math.floor(ms / 1000);
2320
- if (sec < 60) return "just now";
2321
- var min = Math.floor(sec / 60);
2322
- if (min < 60) return min + "m ago";
2323
- var hr = Math.floor(min / 60);
2324
- if (hr < 24) return hr + "h ago";
2325
- var days = Math.floor(hr / 24);
2326
- if (days < 30) return days + "d ago";
2327
- return new Date(isoString).toLocaleDateString();
2328
- }
2329
-
2330
- export function populateCliSessionList(sessions) {
2331
- var pickerLoading = ctx.$("resume-picker-loading");
2332
- var pickerEmpty = ctx.$("resume-picker-empty");
2333
- var pickerList = ctx.$("resume-picker-list");
2334
- if (!pickerLoading || !pickerList) return;
2335
-
2336
- pickerLoading.classList.add("hidden");
2337
-
2338
- if (!sessions || sessions.length === 0) {
2339
- pickerEmpty.classList.remove("hidden");
2340
- pickerList.classList.add("hidden");
2341
- return;
2342
- }
2343
-
2344
- pickerEmpty.classList.add("hidden");
2345
- pickerList.classList.remove("hidden");
2346
- pickerList.innerHTML = "";
2347
-
2348
- for (var i = 0; i < sessions.length; i++) {
2349
- var s = sessions[i];
2350
- var item = document.createElement("div");
2351
- item.className = "cli-session-item";
2352
-
2353
- var title = document.createElement("div");
2354
- title.className = "cli-session-title";
2355
- title.textContent = s.firstPrompt || "Untitled session";
2356
- item.appendChild(title);
2357
-
2358
- var meta = document.createElement("div");
2359
- meta.className = "cli-session-meta";
2360
- if (s.lastActivity) {
2361
- var time = document.createElement("span");
2362
- time.textContent = relativeTime(s.lastActivity);
2363
- meta.appendChild(time);
2364
- }
2365
- if (s.model) {
2366
- var model = document.createElement("span");
2367
- model.className = "badge";
2368
- model.textContent = s.model;
2369
- meta.appendChild(model);
2370
- }
2371
- if (s.gitBranch) {
2372
- var branch = document.createElement("span");
2373
- branch.className = "badge";
2374
- branch.textContent = s.gitBranch;
2375
- meta.appendChild(branch);
2376
- }
2377
- item.appendChild(meta);
2378
-
2379
- (function (sessionId) {
2380
- item.addEventListener("click", function () {
2381
- if (ctx.ws && ctx.connected) {
2382
- ctx.ws.send(JSON.stringify({ type: "resume_session", cliSessionId: sessionId }));
2383
- }
2384
- var modal = ctx.$("resume-modal");
2385
- if (modal) modal.classList.add("hidden");
2386
- closeSidebar();
2387
- });
2388
- })(s.sessionId);
2389
-
2390
- pickerList.appendChild(item);
2391
- }
2392
- }
2393
-
2394
- // --- Icon Strip (Discord-style project icons) ---
2395
- var iconStripTooltip = null;
2396
-
2397
- function getProjectAbbrev(name) {
2398
- if (!name) return "?";
2399
- // Take first letter of each word, max 2 chars
2400
- var words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
2401
- if (words.length >= 2) {
2402
- return (words[0][0] + words[1][0]).toUpperCase();
2403
- }
2404
- return name.substring(0, 2).toUpperCase();
2405
- }
2406
-
2407
- function showIconTooltip(el, text) {
2408
- hideIconTooltip();
2409
- var tip = document.createElement("div");
2410
- tip.className = "icon-strip-tooltip";
2411
- tip.textContent = text;
2412
- document.body.appendChild(tip);
2413
- iconStripTooltip = tip;
2414
-
2415
- requestAnimationFrame(function () {
2416
- var rect = el.getBoundingClientRect();
2417
- tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
2418
- tip.classList.add("visible");
2419
- });
2420
- }
2421
-
2422
- function showIconTooltipHtml(el, html) {
2423
- hideIconTooltip();
2424
- var tip = document.createElement("div");
2425
- tip.className = "icon-strip-tooltip";
2426
- tip.style.whiteSpace = "normal";
2427
- tip.style.maxWidth = "260px";
2428
- tip.innerHTML = html;
2429
- document.body.appendChild(tip);
2430
- iconStripTooltip = tip;
2431
-
2432
- requestAnimationFrame(function () {
2433
- var rect = el.getBoundingClientRect();
2434
- tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
2435
- tip.classList.add("visible");
2436
- });
2437
- }
2438
-
2439
- function hideIconTooltip() {
2440
- if (iconStripTooltip) {
2441
- iconStripTooltip.remove();
2442
- iconStripTooltip = null;
2443
- }
2444
- }
2445
-
2446
- // --- DM user context menu ---
2447
- var userCtxMenu = null;
2448
-
2449
- function closeUserCtxMenu() {
2450
- if (userCtxMenu) {
2451
- userCtxMenu.remove();
2452
- userCtxMenu = null;
2453
- }
2454
- document.removeEventListener("click", handleUserCtxOutsideClick, true);
2455
- }
2456
-
2457
- function showUserCtxMenu(anchorEl, user) {
2458
- closeUserCtxMenu();
2459
- closeProjectCtxMenu();
2460
-
2461
- var menu = document.createElement("div");
2462
- menu.className = "project-ctx-menu";
2463
-
2464
- var removeItem = document.createElement("button");
2465
- removeItem.className = "project-ctx-item project-ctx-delete";
2466
- removeItem.innerHTML = iconHtml("user-minus") + " <span>Remove from favorites</span>";
2467
- removeItem.addEventListener("click", function (e) {
2468
- e.stopPropagation();
2469
- // Spawn dust particles at the user icon position
2470
- var iconRect = anchorEl.getBoundingClientRect();
2471
- spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
2472
- closeUserCtxMenu();
2473
- // Immediately mark as removed so strip re-render hides the icon,
2474
- // even if the user was only visible via cachedDmConversations (not favorites)
2475
- cachedDmRemovedUsers[user.id] = true;
2476
- if (ctx.onDmRemoveUser) ctx.onDmRemoveUser(user.id);
2477
- renderUserStrip(cachedAllUsers, cachedOnlineUserIds, cachedMyUserId, cachedDmFavorites, cachedDmConversations, cachedDmUnread, cachedDmRemovedUsers, cachedMates);
2478
- if (ctx.sendWs) {
2479
- ctx.sendWs({ type: "dm_remove_favorite", targetUserId: user.id });
2480
- }
2481
- });
2482
- menu.appendChild(removeItem);
2483
-
2484
- document.body.appendChild(menu);
2485
- userCtxMenu = menu;
2486
- refreshIcons();
2487
-
2488
- requestAnimationFrame(function () {
2489
- var rect = anchorEl.getBoundingClientRect();
2490
- menu.style.position = "fixed";
2491
- menu.style.left = (rect.right + 6) + "px";
2492
- menu.style.top = rect.top + "px";
2493
- var menuRect = menu.getBoundingClientRect();
2494
- if (menuRect.right > window.innerWidth - 8) {
2495
- menu.style.left = (rect.left - menuRect.width - 6) + "px";
2496
- }
2497
- if (menuRect.bottom > window.innerHeight - 8) {
2498
- menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
2499
- }
2500
- });
2501
-
2502
- // Close on outside click
2503
- setTimeout(function () {
2504
- document.addEventListener("click", handleUserCtxOutsideClick, true);
2505
- }, 0);
2506
- }
2507
-
2508
- function handleUserCtxOutsideClick(e) {
2509
- if (userCtxMenu && !userCtxMenu.contains(e.target)) {
2510
- closeUserCtxMenu();
2511
- }
2512
- }
2513
-
2514
- function showMateCtxMenu(anchorEl, mate) {
2515
- // Primary mates cannot be edited or removed
2516
- if (mate.primary) return;
2517
-
2518
- closeUserCtxMenu();
2519
- closeProjectCtxMenu();
2520
-
2521
- var menu = document.createElement("div");
2522
- menu.className = "project-ctx-menu";
2523
-
2524
- // Edit Profile item
2525
- var editItem = document.createElement("button");
2526
- editItem.className = "project-ctx-item";
2527
- editItem.innerHTML = iconHtml("edit-2") + " <span>Edit Profile</span>";
2528
- editItem.addEventListener("click", function (e) {
2529
- e.stopPropagation();
2530
- closeUserCtxMenu();
2531
- showMateProfilePopover(anchorEl, mate, function (updates) {
2532
- if (ctx.sendWs) {
2533
- ctx.sendWs({ type: "mate_update", mateId: mate.id, updates: updates });
2534
- }
2535
- });
2536
- });
2537
- menu.appendChild(editItem);
2538
-
2539
- var removeItem = document.createElement("button");
2540
- removeItem.className = "project-ctx-item";
2541
- removeItem.innerHTML = iconHtml("star-off") + " <span>Remove from favorites</span>";
2542
- removeItem.addEventListener("click", function (e) {
2543
- e.stopPropagation();
2544
- closeUserCtxMenu();
2545
- // Spawn dust particles at the mate icon position
2546
- var iconRect = anchorEl.getBoundingClientRect();
2547
- spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
2548
- if (ctx.sendWs) {
2549
- ctx.sendWs({ type: "dm_remove_favorite", targetUserId: mate.id });
2550
- }
2551
- });
2552
- menu.appendChild(removeItem);
2553
-
2554
- document.body.appendChild(menu);
2555
- userCtxMenu = menu;
2556
- refreshIcons();
2557
-
2558
- requestAnimationFrame(function () {
2559
- var rect = anchorEl.getBoundingClientRect();
2560
- menu.style.position = "fixed";
2561
- menu.style.left = (rect.right + 6) + "px";
2562
- menu.style.top = rect.top + "px";
2563
- var menuRect = menu.getBoundingClientRect();
2564
- if (menuRect.right > window.innerWidth - 8) {
2565
- menu.style.left = (rect.left - menuRect.width - 6) + "px";
2566
- }
2567
- if (menuRect.bottom > window.innerHeight - 8) {
2568
- menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
2569
- }
2570
- });
2571
-
2572
- setTimeout(function () {
2573
- document.addEventListener("click", handleUserCtxOutsideClick, true);
2574
- }, 0);
2575
- }
2576
-
2577
- // --- Project context menu ---
2578
- var projectCtxMenu = null;
2579
-
2580
- var EMOJI_CATEGORIES = [
2581
- { id: "frequent", icon: "🕐", label: "Frequent", emojis: [
2582
- "😀","😎","🤓","🧠","💡","🔥","⚡","🚀",
2583
- "🎯","🎮","🎨","🎵","📦","📁","📝","💻",
2584
- "🖥️","⌨️","🔧","🛠️","⚙️","🧪","🔬","🧬",
2585
- "🌍","🌱","🌊","🌸","🍀","🌈","☀️","🌙",
2586
- "🐱","🐶","🐼","🦊","🦋","🐝","🐙","🦄",
2587
- "🍕","🍔","☕","🍩","🍎","🍇","🧁","🍣",
2588
- "❤️","💜","💙","💚","💛","🧡","🤍","🖤",
2589
- "⭐","✨","💎","🏆","👑","🎪","🎭","🃏",
2590
- ]},
2591
- { id: "smileys", icon: "😀", label: "Smileys & People", emojis: [
2592
- "😀","😃","😄","😁","😆","😅","🤣","😂",
2593
- "🙂","😊","😇","🥰","😍","🤩","😘","😗",
2594
- "😚","😙","🥲","😋","😛","😜","🤪","😝",
2595
- "🤑","🤗","🤭","🫢","🤫","🤔","🫡","🤐",
2596
- "🤨","😐","😑","😶","🫥","😏","😒","🙄",
2597
- "😬","🤥","😌","😔","😪","🤤","😴","😷",
2598
- "🤒","🤕","🤢","🤮","🥴","😵","🤯","🥳",
2599
- "🥸","😎","🤓","🧐","😕","🫤","😟","🙁",
2600
- "😮","😯","😲","😳","🥺","🥹","😦","😧",
2601
- "😨","😰","😥","😢","😭","😱","😖","😣",
2602
- "😞","😓","😩","😫","🥱","😤","😡","😠",
2603
- "🤬","😈","👿","💀","☠️","💩","🤡","👹",
2604
- "👺","👻","👽","👾","🤖","😺","😸","😹",
2605
- "😻","😼","😽","🙀","😿","😾","🙈","🙉",
2606
- "🙊","👋","🤚","🖐️","✋","🖖","🫱","🫲",
2607
- "🫳","🫴","👌","🤌","🤏","✌️","🤞","🫰",
2608
- "🤟","🤘","🤙","👈","👉","👆","🖕","👇",
2609
- "☝️","🫵","👍","👎","✊","👊","🤛","🤜",
2610
- "👏","🙌","🫶","👐","🤲","🤝","🙏","💪",
2611
- ]},
2612
- { id: "animals", icon: "🐻", label: "Animals & Nature", emojis: [
2613
- "🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼",
2614
- "🐻‍❄️","🐨","🐯","🦁","🐮","🐷","🐽","🐸",
2615
- "🐵","🙈","🙉","🙊","🐒","🐔","🐧","🐦",
2616
- "🐤","🐣","🐥","🦆","🦅","🦉","🦇","🐺",
2617
- "🐗","🐴","🦄","🐝","🪱","🐛","🦋","🐌",
2618
- "🐞","🐜","🪰","🪲","🪳","🦟","🦗","🕷️",
2619
- "🦂","🐢","🐍","🦎","🦖","🦕","🐙","🦑",
2620
- "🦐","🦞","🦀","🪸","🐡","🐠","🐟","🐬",
2621
- "🐳","🐋","🦈","🐊","🐅","🐆","🦓","🫏",
2622
- "🦍","🦧","🦣","🐘","🦛","🦏","🐪","🐫",
2623
- "🦒","🦘","🦬","🐃","🐂","🐄","🐎","🐖",
2624
- "🐏","🐑","🦙","🐐","🦌","🫎","🐕","🐩",
2625
- "🦮","🐕‍🦺","🐈","🐈‍⬛","🪶","🐓","🦃","🦤",
2626
- "🦚","🦜","🦢","🪿","🦩","🕊️","🐇","🦝",
2627
- "🦨","🦡","🦫","🦦","🦥","🐁","🐀","🐿️",
2628
- "🦔","🌵","🎄","🌲","🌳","🌴","🪵","🌱",
2629
- "🌿","☘️","🍀","🎍","🪴","🎋","🍃","🍂",
2630
- "🍁","🪺","🪹","🍄","🌾","💐","🌷","🌹",
2631
- "🥀","🪻","🌺","🌸","🌼","🌻","🌞","🌝",
2632
- "🌛","🌜","🌚","🌕","🌖","🌗","🌘","🌑",
2633
- "🌒","🌓","🌔","🌙","🌎","🌍","🌏","🪐",
2634
- "💫","⭐","🌟","✨","⚡","☄️","💥","🔥",
2635
- "🌪️","🌈","☀️","🌤️","⛅","🌥️","☁️","🌦️",
2636
- "🌧️","⛈️","🌩️","❄️","☃️","⛄","🌬️","💨",
2637
- "💧","💦","🫧","☔","☂️","🌊","🌫️",
2638
- ]},
2639
- { id: "food", icon: "🍔", label: "Food & Drink", emojis: [
2640
- "🍇","🍈","🍉","🍊","🍋","🍌","🍍","🥭",
2641
- "🍎","🍏","🍐","🍑","🍒","🍓","🫐","🥝",
2642
- "🍅","🫒","🥥","🥑","🍆","🥔","🥕","🌽",
2643
- "🌶️","🫑","🥒","🥬","🥦","🧄","🧅","🥜",
2644
- "🫘","🌰","🫚","🫛","🍞","🥐","🥖","🫓",
2645
- "🥨","🥯","🥞","🧇","🧀","🍖","🍗","🥩",
2646
- "🥓","🍔","🍟","🍕","🌭","🥪","🌮","🌯",
2647
- "🫔","🥙","🧆","🥚","🍳","🥘","🍲","🫕",
2648
- "🥣","🥗","🍿","🧈","🧂","🥫","🍱","🍘",
2649
- "🍙","🍚","🍛","🍜","🍝","🍠","🍢","🍣",
2650
- "🍤","🍥","🥮","🍡","🥟","🥠","🥡","🦀",
2651
- "🦞","🦐","🦑","🦪","🍦","🍧","🍨","🍩",
2652
- "🍪","🎂","🍰","🧁","🥧","🍫","🍬","🍭",
2653
- "🍮","🍯","🍼","🥛","☕","🫖","🍵","🍶",
2654
- "🍾","🍷","🍸","🍹","🍺","🍻","🥂","🥃",
2655
- "🫗","🥤","🧋","🧃","🧉","🧊",
2656
- ]},
2657
- { id: "activity", icon: "⚽", label: "Activity", emojis: [
2658
- "⚽","🏀","🏈","⚾","🥎","🎾","🏐","🏉",
2659
- "🥏","🎱","🪀","🏓","🏸","🏒","🏑","🥍",
2660
- "🏏","🪃","🥅","⛳","🪁","🛝","🏹","🎣",
2661
- "🤿","🥊","🥋","🎽","🛹","🛼","🛷","⛸️",
2662
- "🥌","🎿","⛷️","🏂","🪂","🏋️","🤸","🤺",
2663
- "⛹️","🤾","🏌️","🏇","🧘","🏄","🏊","🤽",
2664
- "🚣","🧗","🚵","🚴","🎪","🤹","🎭","🎨",
2665
- "🎬","🎤","🎧","🎼","🎹","🥁","🪘","🎷",
2666
- "🎺","🪗","🎸","🪕","🎻","🪈","🎲","♟️",
2667
- "🎯","🎳","🎮","🕹️","🧩","🪩",
2668
- ]},
2669
- { id: "travel", icon: "🚗", label: "Travel & Places", emojis: [
2670
- "🚗","🚕","🚙","🚌","🚎","🏎️","🚓","🚑",
2671
- "🚒","🚐","🛻","🚚","🚛","🚜","🛵","🏍️",
2672
- "🛺","🚲","🛴","🛹","🚏","🛣️","🛤️","⛽",
2673
- "🛞","🚨","🚥","🚦","🛑","🚧","⚓","🛟",
2674
- "⛵","🛶","🚤","🛳️","⛴️","🛥️","🚢","✈️",
2675
- "🛩️","🛫","🛬","🪂","💺","🚁","🚟","🚠",
2676
- "🚡","🛰️","🚀","🛸","🏠","🏡","🏘️","🏚️",
2677
- "🏗️","🏭","🏢","🏬","🏣","🏤","🏥","🏦",
2678
- "🏨","🏪","🏫","🏩","💒","🏛️","⛪","🕌",
2679
- "🛕","🕍","⛩️","🕋","⛲","⛺","🌁","🌃",
2680
- "🏙️","🌄","🌅","🌆","🌇","🌉","🗼","🗽",
2681
- "🗻","🏕️","🎠","🎡","🎢","🏖️","🏝️","🏜️",
2682
- "🌋","⛰️","🗺️","🧭","🏔️",
2683
- ]},
2684
- { id: "objects", icon: "💡", label: "Objects", emojis: [
2685
- "⌚","📱","📲","💻","⌨️","🖥️","🖨️","🖱️",
2686
- "🖲️","🕹️","🗜️","💽","💾","💿","📀","📼",
2687
- "📷","📸","📹","🎥","📽️","🎞️","📞","☎️",
2688
- "📟","📠","📺","📻","🎙️","🎚️","🎛️","🧭",
2689
- "⏱️","⏲️","⏰","🕰️","⌛","⏳","📡","🔋",
2690
- "🪫","🔌","💡","🔦","🕯️","🪔","🧯","🛢️",
2691
- "🛍️","💰","💴","💵","💶","💷","🪙","💸",
2692
- "💳","🧾","💹","✉️","📧","📨","📩","📤",
2693
- "📥","📦","📫","📬","📭","📮","🗳️","✏️",
2694
- "✒️","🖋️","🖊️","🖌️","🖍️","📝","💼","📁",
2695
- "📂","🗂️","📅","📆","🗒️","🗓️","📇","📈",
2696
- "📉","📊","📋","📌","📍","📎","🖇️","📏",
2697
- "📐","✂️","🗃️","🗄️","🗑️","🔒","🔓","🔏",
2698
- "🔐","🔑","🗝️","🔨","🪓","⛏️","⚒️","🛠️",
2699
- "🗡️","⚔️","💣","🪃","🏹","🛡️","🪚","🔧",
2700
- "🪛","🔩","⚙️","🗜️","⚖️","🦯","🔗","⛓️",
2701
- "🪝","🧰","🧲","🪜","⚗️","🧪","🧫","🧬",
2702
- "🔬","🔭","📡","💉","🩸","💊","🩹","🩼",
2703
- "🩺","🩻","🚪","🛗","🪞","🪟","🛏️","🛋️",
2704
- "🪑","🚽","🪠","🚿","🛁","🪤","🪒","🧴",
2705
- "🧷","🧹","🧺","🧻","🪣","🧼","🫧","🪥",
2706
- "🧽","🧯","🛒","🚬","⚰️","🪦","⚱️","🧿",
2707
- "🪬","🗿","🪧","🪪",
2708
- ]},
2709
- { id: "symbols", icon: "❤️", label: "Symbols", emojis: [
2710
- "❤️","🧡","💛","💚","💙","💜","🖤","🤍",
2711
- "🤎","💔","❤️‍🔥","❤️‍🩹","❣️","💕","💞","💓",
2712
- "💗","💖","💘","💝","💟","☮️","✝️","☪️",
2713
- "🕉️","☸️","🪯","✡️","🔯","🕎","☯️","☦️",
2714
- "🛐","⛎","♈","♉","♊","♋","♌","♍",
2715
- "♎","♏","♐","♑","♒","♓","🆔","⚛️",
2716
- "🉑","☢️","☣️","📴","📳","🈶","🈚","🈸",
2717
- "🈺","🈷️","✴️","🆚","💮","🉐","㊙️","㊗️",
2718
- "🈴","🈵","🈹","🈲","🅰️","🅱️","🆎","🆑",
2719
- "🅾️","🆘","❌","⭕","🛑","⛔","📛","🚫",
2720
- "💯","💢","♨️","🚷","🚯","🚳","🚱","🔞",
2721
- "📵","🚭","❗","❕","❓","❔","‼️","⁉️",
2722
- "🔅","🔆","〽️","⚠️","🚸","🔱","⚜️","🔰",
2723
- "♻️","✅","🈯","💹","❇️","✳️","❎","🌐",
2724
- "💠","Ⓜ️","🌀","💤","🏧","🚾","♿","🅿️",
2725
- "🛗","🈳","🈂️","🛂","🛃","🛄","🛅","🚹",
2726
- "🚺","🚼","⚧️","🚻","🚮","🎦","📶","🈁",
2727
- "🔣","ℹ️","🔤","🔡","🔠","🆖","🆗","🆙",
2728
- "🆒","🆕","🆓","0️⃣","1️⃣","2️⃣","3️⃣","4️⃣",
2729
- "5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🔟","🔢","#️⃣",
2730
- "*️⃣","⏏️","▶️","⏸️","⏯️","⏹️","⏺️","⏭️",
2731
- "⏮️","⏩","⏪","⏫","⏬","◀️","🔼","🔽",
2732
- "➡️","⬅️","⬆️","⬇️","↗️","↘️","↙️","↖️",
2733
- "↕️","↔️","↩️","↪️","⤴️","⤵️","🔀","🔁",
2734
- "🔂","🔄","🔃","🎵","🎶","✖️","➕","➖",
2735
- "➗","🟰","♾️","💲","💱","™️","©️","®️",
2736
- "〰️","➰","➿","🔚","🔙","🔛","🔝","🔜",
2737
- "✔️","☑️","🔘","🔴","🟠","🟡","🟢","🔵",
2738
- "🟣","⚫","⚪","🟤","🔺","🔻","🔸","🔹",
2739
- "🔶","🔷","🔳","🔲","▪️","▫️","◾","◽",
2740
- "◼️","◻️","🟥","🟧","🟨","🟩","🟦","🟪",
2741
- "⬛","⬜","🟫","🔈","🔇","🔉","🔊","🔔",
2742
- "🔕","📣","📢","👁️‍🗨️","💬","💭","🗯️","♠️",
2743
- "♣️","♥️","♦️","🃏","🎴","🀄","🕐","🕑",
2744
- "🕒","🕓","🕔","🕕","🕖","🕗","🕘","🕙","🕚","🕛",
2745
- ]},
2746
- { id: "flags", icon: "🏁", label: "Flags", emojis: [
2747
- "🏁","🚩","🎌","🏴","🏳️","🏳️‍🌈","🏳️‍⚧️","🏴‍☠️",
2748
- "🇦🇨","🇦🇩","🇦🇪","🇦🇫","🇦🇬","🇦🇮","🇦🇱","🇦🇲",
2749
- "🇦🇴","🇦🇶","🇦🇷","🇦🇸","🇦🇹","🇦🇺","🇦🇼","🇦🇽",
2750
- "🇦🇿","🇧🇦","🇧🇧","🇧🇩","🇧🇪","🇧🇫","🇧🇬","🇧🇭",
2751
- "🇧🇮","🇧🇯","🇧🇱","🇧🇲","🇧🇳","🇧🇴","🇧🇶","🇧🇷",
2752
- "🇧🇸","🇧🇹","🇧🇻","🇧🇼","🇧🇾","🇧🇿","🇨🇦","🇨🇨",
2753
- "🇨🇩","🇨🇫","🇨🇬","🇨🇭","🇨🇮","🇨🇰","🇨🇱","🇨🇲",
2754
- "🇨🇳","🇨🇴","🇨🇵","🇨🇷","🇨🇺","🇨🇻","🇨🇼","🇨🇽",
2755
- "🇨🇾","🇨🇿","🇩🇪","🇩🇬","🇩🇯","🇩🇰","🇩🇲","🇩🇴",
2756
- "🇩🇿","🇪🇦","🇪🇨","🇪🇪","🇪🇬","🇪🇭","🇪🇷","🇪🇸",
2757
- "🇪🇹","🇪🇺","🇫🇮","🇫🇯","🇫🇰","🇫🇲","🇫🇴","🇫🇷",
2758
- "🇬🇦","🇬🇧","🇬🇩","🇬🇪","🇬🇫","🇬🇬","🇬🇭","🇬🇮",
2759
- "🇬🇱","🇬🇲","🇬🇳","🇬🇵","🇬🇶","🇬🇷","🇬🇸","🇬🇹",
2760
- "🇬🇺","🇬🇼","🇬🇾","🇭🇰","🇭🇲","🇭🇳","🇭🇷","🇭🇹",
2761
- "🇭🇺","🇮🇨","🇮🇩","🇮🇪","🇮🇱","🇮🇲","🇮🇳","🇮🇴",
2762
- "🇮🇶","🇮🇷","🇮🇸","🇮🇹","🇯🇪","🇯🇲","🇯🇴","🇯🇵",
2763
- "🇰🇪","🇰🇬","🇰🇭","🇰🇮","🇰🇲","🇰🇳","🇰🇵","🇰🇷",
2764
- "🇰🇼","🇰🇾","🇰🇿","🇱🇦","🇱🇧","🇱🇨","🇱🇮","🇱🇰",
2765
- "🇱🇷","🇱🇸","🇱🇹","🇱🇺","🇱🇻","🇱🇾","🇲🇦","🇲🇨",
2766
- "🇲🇩","🇲🇪","🇲🇫","🇲🇬","🇲🇭","🇲🇰","🇲🇱","🇲🇲",
2767
- "🇲🇳","🇲🇴","🇲🇵","🇲🇶","🇲🇷","🇲🇸","🇲🇹","🇲🇺",
2768
- "🇲🇻","🇲🇼","🇲🇽","🇲🇾","🇲🇿","🇳🇦","🇳🇨","🇳🇪",
2769
- "🇳🇫","🇳🇬","🇳🇮","🇳🇱","🇳🇴","🇳🇵","🇳🇷","🇳🇺",
2770
- "🇳🇿","🇴🇲","🇵🇦","🇵🇪","🇵🇫","🇵🇬","🇵🇭","🇵🇰",
2771
- "🇵🇱","🇵🇲","🇵🇳","🇵🇷","🇵🇸","🇵🇹","🇵🇼","🇵🇾",
2772
- "🇶🇦","🇷🇪","🇷🇴","🇷🇸","🇷🇺","🇷🇼","🇸🇦","🇸🇧",
2773
- "🇸🇨","🇸🇩","🇸🇪","🇸🇬","🇸🇭","🇸🇮","🇸🇯","🇸🇰",
2774
- "🇸🇱","🇸🇲","🇸🇳","🇸🇴","🇸🇷","🇸🇸","🇸🇹","🇸🇻",
2775
- "🇸🇽","🇸🇾","🇸🇿","🇹🇦","🇹🇨","🇹🇩","🇹🇫","🇹🇬",
2776
- "🇹🇭","🇹🇯","🇹🇰","🇹🇱","🇹🇲","🇹🇳","🇹🇴","🇹🇷",
2777
- "🇹🇹","🇹🇻","🇹🇼","🇹🇿","🇺🇦","🇺🇬","🇺🇲","🇺🇳",
2778
- "🇺🇸","🇺🇾","🇺🇿","🇻🇦","🇻🇨","🇻🇪","🇻🇬","🇻🇮",
2779
- "🇻🇳","🇻🇺","🇼🇫","🇼🇸","🇽🇰","🇾🇪","🇾🇹","🇿🇦",
2780
- "🇿🇲","🇿🇼",
2781
- ]},
2782
- ];
2783
-
2784
- // --- Project Access Popover ---
2785
- var projectAccessPopover = null;
2786
-
2787
- function closeProjectAccessPopover() {
2788
- if (projectAccessPopover) {
2789
- projectAccessPopover.remove();
2790
- projectAccessPopover = null;
2791
- document.removeEventListener("click", closeAccessOnOutside);
2792
- document.removeEventListener("keydown", closeAccessOnEscape);
2793
- }
2794
- }
2795
-
2796
- function closeAccessOnOutside(e) {
2797
- if (projectAccessPopover && !projectAccessPopover.contains(e.target)) closeProjectAccessPopover();
2798
- }
2799
- function closeAccessOnEscape(e) {
2800
- if (e.key === "Escape") closeProjectAccessPopover();
2801
- }
2802
-
2803
- function showProjectAccessPopover(anchorEl, slug) {
2804
- closeProjectAccessPopover();
2805
-
2806
- var popover = document.createElement("div");
2807
- popover.className = "project-access-popover";
2808
- popover.innerHTML = '<div class="project-access-loading">Loading...</div>';
2809
- popover.addEventListener("click", function (e) { e.stopPropagation(); });
2810
- document.body.appendChild(popover);
2811
- projectAccessPopover = popover;
2812
-
2813
- // Position near anchor
2814
- requestAnimationFrame(function () {
2815
- var rect = anchorEl.getBoundingClientRect();
2816
- popover.style.position = "fixed";
2817
- popover.style.left = (rect.right + 8) + "px";
2818
- popover.style.top = rect.top + "px";
2819
- popover.style.zIndex = "9999";
2820
- var popRect = popover.getBoundingClientRect();
2821
- if (popRect.right > window.innerWidth - 8) {
2822
- popover.style.left = (rect.left - popRect.width - 8) + "px";
2823
- }
2824
- if (popRect.bottom > window.innerHeight - 8) {
2825
- popover.style.top = (window.innerHeight - popRect.height - 8) + "px";
2826
- }
2827
- });
2828
-
2829
- setTimeout(function () {
2830
- document.addEventListener("click", closeAccessOnOutside);
2831
- document.addEventListener("keydown", closeAccessOnEscape);
2832
- }, 0);
2833
-
2834
- // Fetch access info and user list in parallel
2835
- Promise.all([
2836
- fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/access").then(function (r) { return r.json(); }),
2837
- fetch("/api/admin/users").then(function (r) { return r.json(); }),
2838
- ]).then(function (results) {
2839
- var access = results[0];
2840
- var usersData = results[1];
2841
- if (access.error || usersData.error) {
2842
- popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
2843
- return;
2844
- }
2845
- renderAccessPopover(popover, slug, access, usersData.users || []);
2846
- }).catch(function () {
2847
- popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
2848
- });
2849
- }
2850
-
2851
- function renderAccessPopover(popover, slug, access, allUsers) {
2852
- var visibility = access.visibility || "public";
2853
- var allowedUsers = access.allowedUsers || [];
2854
- var ownerId = access.ownerId;
2855
-
2856
- // Filter out the owner from the user list (owner always has access)
2857
- var selectableUsers = allUsers.filter(function (u) { return u.id !== ownerId; });
2858
-
2859
- var html = '';
2860
- html += '<div class="project-access-header">';
2861
- html += '<span class="project-access-title">Project Access</span>';
2862
- html += '<button class="project-access-close">&times;</button>';
2863
- html += '</div>';
2864
-
2865
- // Visibility toggle
2866
- html += '<div class="project-access-section">';
2867
- html += '<label class="project-access-label">Visibility</label>';
2868
- html += '<div class="project-access-vis-row">';
2869
- html += '<button class="project-access-vis-btn' + (visibility === "private" ? ' active' : '') + '" data-vis="private">';
2870
- html += iconHtml("lock") + ' Private';
2871
- html += '</button>';
2872
- html += '<button class="project-access-vis-btn' + (visibility === "public" ? ' active' : '') + '" data-vis="public">';
2873
- html += iconHtml("globe") + ' Public';
2874
- html += '</button>';
2875
- html += '</div>';
2876
- html += '</div>';
2877
-
2878
- // Allowed users (only when private)
2879
- html += '<div class="project-access-section project-access-users-section"' + (visibility !== "private" ? ' style="display:none"' : '') + '>';
2880
- html += '<label class="project-access-label">Allowed Users</label>';
2881
- html += '<div class="project-access-user-list">';
2882
- for (var i = 0; i < selectableUsers.length; i++) {
2883
- var u = selectableUsers[i];
2884
- var checked = allowedUsers.indexOf(u.id) !== -1 ? " checked" : "";
2885
- html += '<label class="project-access-user-item">';
2886
- html += '<input type="checkbox" data-uid="' + u.id + '"' + checked + '>';
2887
- html += '<span>' + escapeHtml(u.displayName || u.username || u.id) + '</span>';
2888
- html += '</label>';
2889
- }
2890
- if (selectableUsers.length === 0) {
2891
- html += '<div class="project-access-empty">No other users</div>';
2892
- }
2893
- html += '</div>';
2894
- html += '</div>';
2895
-
2896
- popover.innerHTML = html;
2897
- refreshIcons();
2898
-
2899
- // Close button
2900
- popover.querySelector(".project-access-close").addEventListener("click", function () {
2901
- closeProjectAccessPopover();
2902
- });
2903
-
2904
- // Visibility toggle
2905
- popover.querySelectorAll(".project-access-vis-btn").forEach(function (btn) {
2906
- btn.addEventListener("click", function () {
2907
- var newVis = btn.dataset.vis;
2908
- popover.querySelectorAll(".project-access-vis-btn").forEach(function (b) { b.classList.remove("active"); });
2909
- btn.classList.add("active");
2910
- var usersSection = popover.querySelector(".project-access-users-section");
2911
- if (usersSection) usersSection.style.display = newVis === "private" ? "" : "none";
2912
- fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/visibility", {
2913
- method: "PUT",
2914
- headers: { "Content-Type": "application/json" },
2915
- body: JSON.stringify({ visibility: newVis }),
2916
- });
2917
- });
2918
- });
2919
-
2920
- // User checkboxes
2921
- popover.querySelectorAll('.project-access-user-item input[type="checkbox"]').forEach(function (cb) {
2922
- cb.addEventListener("change", function () {
2923
- var selected = [];
2924
- popover.querySelectorAll('.project-access-user-item input[type="checkbox"]:checked').forEach(function (c) {
2925
- selected.push(c.dataset.uid);
2926
- });
2927
- fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/users", {
2928
- method: "PUT",
2929
- headers: { "Content-Type": "application/json" },
2930
- body: JSON.stringify({ allowedUsers: selected }),
2931
- });
2932
- });
2933
- });
2934
- }
2935
-
2936
- function closeProjectCtxMenu() {
2937
- if (projectCtxMenu) {
2938
- projectCtxMenu.remove();
2939
- projectCtxMenu = null;
2940
- }
2941
- }
2942
-
2943
- function showIconCtxMenu(anchorEl, slug, name) {
2944
- closeProjectCtxMenu();
2945
- closeUserCtxMenu();
2946
- closeEmojiPicker();
2947
-
2948
- var menu = document.createElement("div");
2949
- menu.className = "project-ctx-menu";
2950
-
2951
- var isWorktree = slug.indexOf("--") !== -1;
2952
-
2953
- if (isWorktree) {
2954
- // Worktree context menu: only "Remove Worktree"
2955
- var removeWtItem = document.createElement("button");
2956
- removeWtItem.className = "project-ctx-item project-ctx-delete";
2957
- removeWtItem.innerHTML = iconHtml("trash-2") + " <span>Remove Worktree</span>";
2958
- removeWtItem.addEventListener("click", function (e) {
2959
- e.stopPropagation();
2960
- closeProjectCtxMenu();
2961
- if (ctx.ws && ctx.connected) {
2962
- ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
2963
- }
2964
- });
2965
- menu.appendChild(removeWtItem);
2966
- } else {
2967
- // Regular project context menu
2968
- var iconItem = document.createElement("button");
2969
- iconItem.className = "project-ctx-item";
2970
- iconItem.innerHTML = iconHtml("smile") + " <span>Set Icon</span>";
2971
- iconItem.addEventListener("click", function (e) {
2972
- e.stopPropagation();
2973
- closeProjectCtxMenu();
2974
- showEmojiPicker(slug, anchorEl);
2975
- });
2976
- menu.appendChild(iconItem);
2977
-
2978
- // --- Add Worktree ---
2979
- var wtItem = document.createElement("button");
2980
- wtItem.className = "project-ctx-item";
2981
- wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
2982
- wtItem.addEventListener("click", function (e) {
2983
- e.stopPropagation();
2984
- closeProjectCtxMenu();
2985
- showWorktreeModal(slug, name || slug);
2986
- });
2987
- menu.appendChild(wtItem);
2988
- // Remove Project intentionally omitted from right-click.
2989
- // Destructive actions only live in the chevron menu.
2990
- }
2991
-
2992
- document.body.appendChild(menu);
2993
- projectCtxMenu = menu;
2994
- refreshIcons();
2995
-
2996
- requestAnimationFrame(function () {
2997
- var rect = anchorEl.getBoundingClientRect();
2998
- menu.style.position = "fixed";
2999
- menu.style.left = (rect.right + 6) + "px";
3000
- menu.style.top = rect.top + "px";
3001
- var menuRect = menu.getBoundingClientRect();
3002
- if (menuRect.right > window.innerWidth - 8) {
3003
- menu.style.left = (rect.left - menuRect.width - 6) + "px";
3004
- }
3005
- if (menuRect.bottom > window.innerHeight - 8) {
3006
- menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
3007
- }
3008
- });
3009
- }
3010
-
3011
- function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
3012
- closeProjectCtxMenu();
3013
- closeUserCtxMenu();
3014
- closeEmojiPicker();
3015
-
3016
- var menu = document.createElement("div");
3017
- menu.className = "project-ctx-menu";
3018
-
3019
- // --- Set Icon ---
3020
- var iconItem = document.createElement("button");
3021
- iconItem.className = "project-ctx-item";
3022
- iconItem.innerHTML = iconHtml("smile") + " <span>Set Icon</span>";
3023
- iconItem.addEventListener("click", function (e) {
3024
- e.stopPropagation();
3025
- closeProjectCtxMenu();
3026
- showEmojiPicker(slug, anchorEl);
3027
- });
3028
- menu.appendChild(iconItem);
3029
-
3030
- // --- Project Settings ---
3031
- if (!ctx.permissions || ctx.permissions.projectSettings !== false) {
3032
- var settingsItem = document.createElement("button");
3033
- settingsItem.className = "project-ctx-item";
3034
- settingsItem.innerHTML = iconHtml("settings") + " <span>Project Settings</span>";
3035
- settingsItem.addEventListener("click", function (e) {
3036
- e.stopPropagation();
3037
- closeProjectCtxMenu();
3038
- openProjectSettings(slug, { slug: slug, name: name, icon: icon, projectOwnerId: ctx.projectOwnerId });
3039
- });
3040
- menu.appendChild(settingsItem);
3041
- }
3042
-
3043
- // --- Separator: collaboration ---
3044
- var sep1 = document.createElement("div");
3045
- sep1.className = "project-ctx-separator";
3046
- menu.appendChild(sep1);
3047
-
3048
- // --- Share ---
3049
- var shareItem = document.createElement("button");
3050
- shareItem.className = "project-ctx-item";
3051
- shareItem.innerHTML = iconHtml("share") + " <span>Share</span>";
3052
- shareItem.addEventListener("click", function (e) {
3053
- e.stopPropagation();
3054
- closeProjectCtxMenu();
3055
- triggerShare();
3056
- });
3057
- menu.appendChild(shareItem);
3058
-
3059
- // --- Manage Access (owner or admin, multi-user only) ---
3060
- if (ctx.multiUser && slug.indexOf("--") === -1) {
3061
- var isProjectOwner = ctx.myUserId && ctx.projectOwnerId && ctx.myUserId === ctx.projectOwnerId;
3062
- var isAdmin = ctx.permissions && ctx.permissions.projectSettings !== false;
3063
- if (isProjectOwner || isAdmin) {
3064
- var accessItem = document.createElement("button");
3065
- accessItem.className = "project-ctx-item";
3066
- accessItem.innerHTML = iconHtml("users") + " <span>Manage Access</span>";
3067
- accessItem.addEventListener("click", function (e) {
3068
- e.stopPropagation();
3069
- closeProjectCtxMenu();
3070
- showProjectAccessPopover(anchorEl, slug);
3071
- });
3072
- menu.appendChild(accessItem);
3073
- }
3074
- }
3075
-
3076
- // --- Separator: development ---
3077
- var sep2 = document.createElement("div");
3078
- sep2.className = "project-ctx-separator";
3079
- menu.appendChild(sep2);
3080
-
3081
- // --- Add Worktree ---
3082
- var wtItem = document.createElement("button");
3083
- wtItem.className = "project-ctx-item";
3084
- wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
3085
- wtItem.addEventListener("click", function (e) {
3086
- e.stopPropagation();
3087
- closeProjectCtxMenu();
3088
- showWorktreeModal(slug, name || slug);
3089
- });
3090
- menu.appendChild(wtItem);
3091
-
3092
- if (!ctx.permissions || ctx.permissions.deleteProject !== false) {
3093
- // --- Separator: danger zone ---
3094
- var sep3 = document.createElement("div");
3095
- sep3.className = "project-ctx-separator";
3096
- menu.appendChild(sep3);
3097
-
3098
- // --- Remove Project ---
3099
- var deleteItem = document.createElement("button");
3100
- deleteItem.className = "project-ctx-item project-ctx-delete";
3101
- deleteItem.innerHTML = iconHtml("trash-2") + " <span>Remove Project</span>";
3102
- deleteItem.addEventListener("click", function (e) {
3103
- e.stopPropagation();
3104
- closeProjectCtxMenu();
3105
- if (ctx.ws && ctx.connected) {
3106
- ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name }));
3107
- }
3108
- });
3109
- menu.appendChild(deleteItem);
3110
- }
3111
-
3112
- document.body.appendChild(menu);
3113
- projectCtxMenu = menu;
3114
- refreshIcons();
3115
-
3116
- // Position
3117
- requestAnimationFrame(function () {
3118
- var rect = anchorEl.getBoundingClientRect();
3119
- menu.style.position = "fixed";
3120
- if (position === "below") {
3121
- // Chevron dropdown: directly below the anchor
3122
- menu.style.left = rect.left + "px";
3123
- menu.style.top = (rect.bottom + 4) + "px";
3124
- } else {
3125
- // Icon strip right-click: to the right of the anchor
3126
- menu.style.left = (rect.right + 6) + "px";
3127
- menu.style.top = rect.top + "px";
3128
- }
3129
- var menuRect = menu.getBoundingClientRect();
3130
- if (menuRect.right > window.innerWidth - 8) {
3131
- menu.style.left = (rect.left - menuRect.width - 6) + "px";
3132
- }
3133
- if (menuRect.bottom > window.innerHeight - 8) {
3134
- menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
3135
- }
3136
- });
3137
- }
3138
-
3139
- // --- Emoji picker ---
3140
- var emojiPickerEl = null;
3141
-
3142
- function closeEmojiPicker() {
3143
- if (emojiPickerEl) {
3144
- emojiPickerEl.remove();
3145
- emojiPickerEl = null;
3146
- }
3147
- }
3148
-
3149
- function showEmojiPicker(slug, anchorEl) {
3150
- closeEmojiPicker();
3151
-
3152
- var picker = document.createElement("div");
3153
- picker.className = "emoji-picker";
3154
- picker.addEventListener("click", function (e) { e.stopPropagation(); });
3155
-
3156
- // --- Header ---
3157
- var header = document.createElement("div");
3158
- header.className = "emoji-picker-header";
3159
- header.textContent = "Choose Icon";
3160
-
3161
- var removeBtn = document.createElement("button");
3162
- removeBtn.className = "emoji-picker-remove";
3163
- removeBtn.textContent = "Remove";
3164
- removeBtn.addEventListener("click", function (e) {
3165
- e.stopPropagation();
3166
- closeEmojiPicker();
3167
- if (ctx.ws && ctx.connected) {
3168
- ctx.ws.send(JSON.stringify({ type: "set_project_icon", slug: slug, icon: null }));
3169
- }
3170
- });
3171
- header.appendChild(removeBtn);
3172
- picker.appendChild(header);
3173
-
3174
- // --- Category tabs ---
3175
- var tabBar = document.createElement("div");
3176
- tabBar.className = "emoji-picker-tabs";
3177
- var tabBtns = [];
3178
-
3179
- for (var t = 0; t < EMOJI_CATEGORIES.length; t++) {
3180
- (function (cat, idx) {
3181
- var tab = document.createElement("button");
3182
- tab.className = "emoji-picker-tab" + (idx === 0 ? " active" : "");
3183
- tab.textContent = cat.icon;
3184
- tab.title = cat.label;
3185
- tab.addEventListener("click", function (e) {
3186
- e.stopPropagation();
3187
- switchCategory(idx);
3188
- });
3189
- tabBar.appendChild(tab);
3190
- tabBtns.push(tab);
3191
- })(EMOJI_CATEGORIES[t], t);
3192
- }
3193
- parseEmojis(tabBar);
3194
- picker.appendChild(tabBar);
3195
-
3196
- // --- Scrollable grid area ---
3197
- var scrollArea = document.createElement("div");
3198
- scrollArea.className = "emoji-picker-scroll";
3199
-
3200
- var grid = document.createElement("div");
3201
- grid.className = "emoji-picker-grid";
3202
- scrollArea.appendChild(grid);
3203
- picker.appendChild(scrollArea);
3204
-
3205
- function buildGrid(emojis) {
3206
- grid.innerHTML = "";
3207
- for (var i = 0; i < emojis.length; i++) {
3208
- (function (emoji) {
3209
- var btn = document.createElement("button");
3210
- btn.className = "emoji-picker-item";
3211
- btn.textContent = emoji;
3212
- btn.addEventListener("click", function (e) {
3213
- e.stopPropagation();
3214
- closeEmojiPicker();
3215
- if (ctx.ws && ctx.connected) {
3216
- ctx.ws.send(JSON.stringify({ type: "set_project_icon", slug: slug, icon: emoji }));
3217
- }
3218
- });
3219
- grid.appendChild(btn);
3220
- })(emojis[i]);
3221
- }
3222
- parseEmojis(grid);
3223
- scrollArea.scrollTop = 0;
3224
- }
3225
-
3226
- function switchCategory(idx) {
3227
- for (var j = 0; j < tabBtns.length; j++) {
3228
- tabBtns[j].classList.toggle("active", j === idx);
3229
- }
3230
- buildGrid(EMOJI_CATEGORIES[idx].emojis);
3231
- }
3232
-
3233
- // Start with first category (Frequent)
3234
- buildGrid(EMOJI_CATEGORIES[0].emojis);
3235
-
3236
-
3237
-
3238
- document.body.appendChild(picker);
3239
- emojiPickerEl = picker;
3240
-
3241
- // Position
3242
- requestAnimationFrame(function () {
3243
- var rect = anchorEl.getBoundingClientRect();
3244
- picker.style.left = (rect.right + 6) + "px";
3245
- picker.style.top = rect.top + "px";
3246
- var pRect = picker.getBoundingClientRect();
3247
- if (pRect.right > window.innerWidth - 8) {
3248
- picker.style.left = (rect.left - pRect.width - 6) + "px";
3249
- }
3250
- if (pRect.bottom > window.innerHeight - 8) {
3251
- picker.style.top = (window.innerHeight - pRect.height - 8) + "px";
3252
- }
3253
- });
3254
- }
3255
-
3256
- // --- Rename prompt ---
3257
- function showProjectRename(slug, currentName) {
3258
- var nameEl = document.getElementById("title-bar-project-name");
3259
- if (!nameEl) return;
3260
-
3261
- var input = document.createElement("input");
3262
- input.type = "text";
3263
- input.className = "project-rename-input";
3264
- input.value = currentName || "";
3265
-
3266
- var originalText = nameEl.textContent;
3267
- nameEl.textContent = "";
3268
- nameEl.appendChild(input);
3269
- input.focus();
3270
- input.select();
3271
-
3272
- var committed = false;
3273
-
3274
- function commitRename() {
3275
- if (committed) return;
3276
- committed = true;
3277
- var newName = input.value.trim();
3278
- if (newName && newName !== currentName && ctx.ws && ctx.connected) {
3279
- ctx.ws.send(JSON.stringify({ type: "set_project_title", slug: slug, title: newName }));
3280
- nameEl.textContent = newName;
3281
- } else {
3282
- nameEl.textContent = originalText;
3283
- }
3284
- }
3285
-
3286
- input.addEventListener("keydown", function (e) {
3287
- e.stopPropagation();
3288
- if (e.key === "Enter") { e.preventDefault(); commitRename(); }
3289
- if (e.key === "Escape") { e.preventDefault(); committed = true; nameEl.textContent = originalText; }
3290
- });
3291
- input.addEventListener("blur", commitRename);
3292
- input.addEventListener("click", function (e) { e.stopPropagation(); });
3293
- }
3294
-
3295
- // Click outside to close
3296
- document.addEventListener("click", function () {
3297
- closeProjectCtxMenu();
3298
- closeEmojiPicker();
3299
- });
3300
-
3301
- // --- Drag-and-drop state ---
3302
- var draggedSlug = null;
3303
- var draggedEl = null;
3304
-
3305
- function showTrashZone() {
3306
- var addBtn = document.getElementById("icon-strip-add");
3307
- if (!addBtn) return;
3308
- addBtn.style.display = "none";
3309
-
3310
- var existing = document.getElementById("icon-strip-trash");
3311
- if (existing) existing.remove();
3312
-
3313
- var trash = document.createElement("div");
3314
- trash.id = "icon-strip-trash";
3315
- trash.className = "icon-strip-trash";
3316
- trash.innerHTML = iconHtml("trash-2");
3317
- addBtn.parentNode.insertBefore(trash, addBtn.nextSibling);
3318
- refreshIcons();
3319
-
3320
- // Tooltip
3321
- trash.addEventListener("mouseenter", function () { showIconTooltip(trash, "Remove project"); });
3322
- trash.addEventListener("mouseleave", hideIconTooltip);
3323
-
3324
- trash.addEventListener("dragover", function (e) {
3325
- e.preventDefault();
3326
- e.dataTransfer.dropEffect = "move";
3327
- trash.classList.add("drag-hover");
3328
- });
3329
- trash.addEventListener("dragleave", function () {
3330
- trash.classList.remove("drag-hover");
3331
- });
3332
- trash.addEventListener("drop", function (e) {
3333
- e.preventDefault();
3334
- trash.classList.remove("drag-hover");
3335
- var slug = e.dataTransfer.getData("text/plain");
3336
- if (slug && ctx.ws && ctx.connected) {
3337
- ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug }));
3338
- }
3339
- });
3340
- }
3341
-
3342
- function hideTrashZone() {
3343
- var trash = document.getElementById("icon-strip-trash");
3344
- if (trash) trash.remove();
3345
- var addBtn = document.getElementById("icon-strip-add");
3346
- if (addBtn) addBtn.style.display = "";
3347
- }
3348
-
3349
- export function spawnDustParticles(cx, cy) {
3350
- var colors = ["#8B7355", "#A0522D", "#D2B48C", "#C4A882", "#9E9E9E", "#B8860B", "#BC8F8F"];
3351
- var count = 24;
3352
- var container = document.createElement("div");
3353
- container.style.position = "fixed";
3354
- container.style.top = "0";
3355
- container.style.left = "0";
3356
- container.style.width = "0";
3357
- container.style.height = "0";
3358
- container.style.pointerEvents = "none";
3359
- container.style.zIndex = "10000";
3360
- document.body.appendChild(container);
3361
-
3362
- for (var i = 0; i < count; i++) {
3363
- var dot = document.createElement("div");
3364
- dot.className = "dust-particle";
3365
- var size = 3 + Math.random() * 5;
3366
- var angle = Math.random() * Math.PI * 2;
3367
- var dist = 30 + Math.random() * 60;
3368
- var dx = Math.cos(angle) * dist;
3369
- var dy = Math.sin(angle) * dist - 20; // bias upward
3370
- var duration = 600 + Math.random() * 500;
3371
-
3372
- dot.style.width = size + "px";
3373
- dot.style.height = size + "px";
3374
- dot.style.left = cx + "px";
3375
- dot.style.top = cy + "px";
3376
- dot.style.background = colors[Math.floor(Math.random() * colors.length)];
3377
- dot.style.setProperty("--dust-x", dx + "px");
3378
- dot.style.setProperty("--dust-y", dy + "px");
3379
- dot.style.setProperty("--dust-duration", duration + "ms");
3380
-
3381
- container.appendChild(dot);
3382
- }
3383
-
3384
- setTimeout(function () { container.remove(); }, 1200);
3385
- }
3386
-
3387
- function clearDragIndicators() {
3388
- var items = document.querySelectorAll(".icon-strip-item.drag-over-above, .icon-strip-item.drag-over-below");
3389
- for (var i = 0; i < items.length; i++) {
3390
- items[i].classList.remove("drag-over-above", "drag-over-below");
3391
- }
3392
- }
3393
-
3394
- function setupDragHandlers(el, slug) {
3395
- el.setAttribute("draggable", "true");
3396
-
3397
- el.addEventListener("dragstart", function (e) {
3398
- draggedSlug = slug;
3399
- draggedEl = el;
3400
- e.dataTransfer.effectAllowed = "move";
3401
- e.dataTransfer.setData("text/plain", slug);
3402
-
3403
- // Custom drag image — just the 38px rounded icon, no pill/status
3404
- var ghost = document.createElement("div");
3405
- ghost.textContent = el.textContent.trim().split("\n")[0]; // abbreviation only
3406
- ghost.style.cssText = "position:fixed;left:-200px;top:-200px;width:38px;height:38px;border-radius:12px;" +
3407
- "background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;" +
3408
- "font-size:15px;font-weight:600;pointer-events:none;z-index:-1;";
3409
- document.body.appendChild(ghost);
3410
- e.dataTransfer.setDragImage(ghost, 19, 19);
3411
- setTimeout(function () { ghost.remove(); }, 0);
3412
-
3413
- setTimeout(function () { el.classList.add("dragging"); }, 0);
3414
- hideIconTooltip();
3415
- showTrashZone();
3416
- });
3417
-
3418
- el.addEventListener("dragover", function (e) {
3419
- e.preventDefault();
3420
- if (!draggedSlug || draggedSlug === slug) return;
3421
- e.dataTransfer.dropEffect = "move";
3422
-
3423
- clearDragIndicators();
3424
- var rect = el.getBoundingClientRect();
3425
- var midY = rect.top + rect.height / 2;
3426
- if (e.clientY < midY) {
3427
- el.classList.add("drag-over-above");
3428
- } else {
3429
- el.classList.add("drag-over-below");
3430
- }
3431
- });
3432
-
3433
- el.addEventListener("dragleave", function () {
3434
- el.classList.remove("drag-over-above", "drag-over-below");
3435
- });
3436
-
3437
- el.addEventListener("drop", function (e) {
3438
- e.preventDefault();
3439
- clearDragIndicators();
3440
- if (!draggedSlug || draggedSlug === slug) return;
3441
-
3442
- var rect = el.getBoundingClientRect();
3443
- var midY = rect.top + rect.height / 2;
3444
- var insertBefore = e.clientY < midY;
3445
-
3446
- // Build new slug order
3447
- var container = document.getElementById("icon-strip-projects");
3448
- var items = container.querySelectorAll(".icon-strip-item");
3449
- var slugs = [];
3450
- for (var i = 0; i < items.length; i++) {
3451
- if (items[i].dataset.slug !== draggedSlug) {
3452
- slugs.push(items[i].dataset.slug);
3453
- }
3454
- }
3455
- // Insert dragged slug at correct position
3456
- var targetIdx = slugs.indexOf(slug);
3457
- if (!insertBefore) targetIdx++;
3458
- slugs.splice(targetIdx, 0, draggedSlug);
3459
-
3460
- // Send reorder to server
3461
- if (ctx.ws && ctx.connected) {
3462
- ctx.ws.send(JSON.stringify({ type: "reorder_projects", slugs: slugs }));
3463
- }
3464
- });
3465
-
3466
- el.addEventListener("dragend", function () {
3467
- el.classList.remove("dragging");
3468
- clearDragIndicators();
3469
- draggedSlug = null;
3470
- draggedEl = null;
3471
- hideTrashZone();
3472
- });
3473
- }
3474
-
3475
- export function renderSidebarPresence(onlineUsers) {
3476
- var container = document.getElementById("sidebar-presence");
3477
- if (!container) return;
3478
- container.innerHTML = "";
3479
- if (!onlineUsers || onlineUsers.length < 2) return;
3480
- var maxShow = 4;
3481
- for (var i = 0; i < Math.min(onlineUsers.length, maxShow); i++) {
3482
- var ou = onlineUsers[i];
3483
- var img = document.createElement("img");
3484
- img.className = "sidebar-presence-avatar";
3485
- img.src = presenceAvatarUrl(ou);
3486
- img.alt = ou.displayName;
3487
- img.dataset.tip = ou.displayName + " (@" + ou.username + ")";
3488
- container.appendChild(img);
3489
- }
3490
- if (onlineUsers.length > maxShow) {
3491
- var more = document.createElement("span");
3492
- more.className = "sidebar-presence-more";
3493
- more.textContent = "+" + (onlineUsers.length - maxShow);
3494
- container.appendChild(more);
3495
- }
3496
- }
3497
-
3498
- // --- Worktree folder collapse state (persisted in localStorage) ---
3499
- var wtCollapsed = {};
3500
- try {
3501
- wtCollapsed = JSON.parse(localStorage.getItem("clay-wt-collapsed") || "{}");
3502
- } catch (e) {}
3503
- function setWtCollapsed(slug, collapsed) {
3504
- wtCollapsed[slug] = collapsed;
3505
- try { localStorage.setItem("clay-wt-collapsed", JSON.stringify(wtCollapsed)); } catch (e) {}
3506
- }
3507
-
3508
- // Group projects by parent/worktree relationship
3509
- function groupProjects(projects) {
3510
- var parents = [];
3511
- var wtByParent = {};
3512
- for (var i = 0; i < projects.length; i++) {
3513
- var p = projects[i];
3514
- if (p.isWorktree && p.parentSlug) {
3515
- if (!wtByParent[p.parentSlug]) wtByParent[p.parentSlug] = [];
3516
- wtByParent[p.parentSlug].push(p);
3517
- } else {
3518
- parents.push(p);
3519
- }
3520
- }
3521
- return { parents: parents, wtByParent: wtByParent };
3522
- }
3523
-
3524
- // Create a standard icon-strip item element (shared between parent and worktree rendering)
3525
- function createIconItem(p, currentSlug) {
3526
- var el = document.createElement("a");
3527
- var isActive = p.slug === currentSlug && !currentDmUserId;
3528
- el.className = "icon-strip-item" + (isActive ? " active" : "");
3529
- el.href = "/p/" + p.slug + "/";
3530
- el.dataset.slug = p.slug;
3531
-
3532
- if (p.icon) {
3533
- var emojiSpan = document.createElement("span");
3534
- emojiSpan.className = "project-emoji";
3535
- emojiSpan.textContent = p.icon;
3536
- parseEmojis(emojiSpan);
3537
- el.appendChild(emojiSpan);
3538
- } else {
3539
- el.appendChild(document.createTextNode(getProjectAbbrev(p.name)));
3540
- }
3541
-
3542
- var pill = document.createElement("span");
3543
- pill.className = "icon-strip-pill";
3544
- el.appendChild(pill);
3545
-
3546
- var statusDot = document.createElement("span");
3547
- statusDot.className = "icon-strip-status";
3548
- if (p.isProcessing) statusDot.classList.add("processing");
3549
- el.appendChild(statusDot);
3550
-
3551
- var projectBadge = document.createElement("span");
3552
- projectBadge.className = "icon-strip-project-badge";
3553
- if (p.unread > 0 && !isActive) {
3554
- projectBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
3555
- projectBadge.classList.add("has-unread");
3556
- }
3557
- el.appendChild(projectBadge);
3558
-
3559
- // Pending permission shake for non-active projects
3560
- if (p.pendingPermissions > 0 && !isActive) {
3561
- el.classList.add("has-pending-perm");
3562
- }
3563
-
3564
- (function (name, elem) {
3565
- elem.addEventListener("mouseenter", function () { showIconTooltip(elem, name); });
3566
- elem.addEventListener("mouseleave", hideIconTooltip);
3567
- })(p.name, el);
3568
-
3569
- (function (slug) {
3570
- el.addEventListener("click", function (e) {
3571
- e.preventDefault();
3572
- if (ctx.switchProject) ctx.switchProject(slug);
3573
- });
3574
- })(p.slug);
3575
-
3576
- return el;
3577
- }
3578
-
3579
- // Worktree creation modal
3580
- function showWorktreeModal(parentSlug, parentName) {
3581
- // Remove existing modal if any
3582
- var existing = document.getElementById("wt-modal-container");
3583
- if (existing) existing.remove();
3584
-
3585
- var container = document.createElement("div");
3586
- container.id = "wt-modal-container";
3587
-
3588
- var overlay = document.createElement("div");
3589
- overlay.className = "wt-modal-overlay";
3590
- container.appendChild(overlay);
3591
-
3592
- var modal = document.createElement("div");
3593
- modal.className = "wt-modal";
3594
-
3595
- var title = document.createElement("div");
3596
- title.className = "wt-modal-title";
3597
- title.textContent = "Add Worktree \u2014 " + parentName;
3598
- modal.appendChild(title);
3599
-
3600
- var branchLabel = document.createElement("label");
3601
- branchLabel.className = "wt-modal-label";
3602
- branchLabel.textContent = "Branch name";
3603
- modal.appendChild(branchLabel);
3604
-
3605
- var branchInput = document.createElement("input");
3606
- branchInput.type = "text";
3607
- branchInput.className = "wt-modal-input";
3608
- branchInput.placeholder = "feat/my-feature";
3609
- branchInput.autocomplete = "off";
3610
- branchInput.spellcheck = false;
3611
- modal.appendChild(branchInput);
3612
-
3613
- var baseLabel = document.createElement("label");
3614
- baseLabel.className = "wt-modal-label";
3615
- baseLabel.textContent = "Base branch";
3616
- modal.appendChild(baseLabel);
3617
-
3618
- var baseSelect = document.createElement("select");
3619
- baseSelect.className = "wt-modal-input";
3620
- // Add "main" as default while loading
3621
- var defaultOpt = document.createElement("option");
3622
- defaultOpt.value = "main";
3623
- defaultOpt.textContent = "main";
3624
- baseSelect.appendChild(defaultOpt);
3625
- modal.appendChild(baseSelect);
3626
-
3627
- // Fetch branches from target project via HTTP API
3628
- fetch("/p/" + parentSlug + "/api/branches")
3629
- .then(function (res) { return res.json(); })
3630
- .then(function (data) {
3631
- baseSelect.innerHTML = "";
3632
- var branches = data.branches || ["main"];
3633
- var defBranch = data.defaultBranch || "main";
3634
- for (var i = 0; i < branches.length; i++) {
3635
- var opt = document.createElement("option");
3636
- opt.value = branches[i];
3637
- opt.textContent = branches[i];
3638
- if (branches[i] === defBranch) opt.selected = true;
3639
- baseSelect.appendChild(opt);
3640
- }
3641
- })
3642
- .catch(function () {});
3643
-
3644
- var errorDiv = document.createElement("div");
3645
- errorDiv.className = "wt-modal-error";
3646
- modal.appendChild(errorDiv);
3647
-
3648
- var actions = document.createElement("div");
3649
- actions.className = "wt-modal-actions";
3650
-
3651
- var cancelBtn = document.createElement("button");
3652
- cancelBtn.className = "wt-modal-btn";
3653
- cancelBtn.textContent = "Cancel";
3654
- actions.appendChild(cancelBtn);
3655
-
3656
- var createBtn = document.createElement("button");
3657
- createBtn.className = "wt-modal-btn primary";
3658
- createBtn.textContent = "Create";
3659
- actions.appendChild(createBtn);
3660
-
3661
- modal.appendChild(actions);
3662
- container.appendChild(modal);
3663
- document.body.appendChild(container);
3664
- branchInput.focus();
3665
-
3666
- function closeModal() { container.remove(); }
3667
-
3668
- function doCreate() {
3669
- var branch = branchInput.value.trim();
3670
- var base = baseSelect.value.trim() || null;
3671
- if (!branch) {
3672
- errorDiv.textContent = "Branch name is required";
3673
- errorDiv.classList.add("visible");
3674
- return;
3675
- }
3676
- // Sanitize: replace slashes with dashes for directory name
3677
- var dirName = branch.replace(/\//g, "-");
3678
- createBtn.disabled = true;
3679
- createBtn.textContent = "Creating...";
3680
- errorDiv.classList.remove("visible");
3681
-
3682
- if (ctx.ws && ctx.connected) {
3683
- ctx.ws.send(JSON.stringify({
3684
- type: "create_worktree",
3685
- branch: branch,
3686
- dirName: dirName,
3687
- baseBranch: base
3688
- }));
3689
- }
3690
-
3691
- // Listen for the result
3692
- var handler = function (event) {
3693
- var msg;
3694
- try { msg = JSON.parse(event.data); } catch (e) { return; }
3695
- if (msg.type === "create_worktree_result") {
3696
- ctx.ws.removeEventListener("message", handler);
3697
- if (msg.ok) {
3698
- closeModal();
3699
- if (msg.slug && ctx.switchProject) ctx.switchProject(msg.slug);
3700
- } else {
3701
- createBtn.disabled = false;
3702
- createBtn.textContent = "Create";
3703
- errorDiv.textContent = msg.error || "Failed to create worktree";
3704
- errorDiv.classList.add("visible");
3705
- }
3706
- }
3707
- };
3708
- ctx.ws.addEventListener("message", handler);
3709
- }
3710
-
3711
- overlay.addEventListener("click", closeModal);
3712
- cancelBtn.addEventListener("click", closeModal);
3713
- createBtn.addEventListener("click", doCreate);
3714
- branchInput.addEventListener("keydown", function (e) {
3715
- if (e.key === "Enter") doCreate();
3716
- if (e.key === "Escape") closeModal();
3717
- });
3718
- baseSelect.addEventListener("keydown", function (e) {
3719
- if (e.key === "Enter") doCreate();
3720
- if (e.key === "Escape") closeModal();
3721
- });
3722
- }
3723
-
3724
- export function renderIconStrip(projects, currentSlug) {
3725
- cachedProjectList = projects;
3726
- cachedCurrentSlug = currentSlug;
3727
-
3728
- var container = document.getElementById("icon-strip-projects");
3729
- if (!container) return;
3730
- container.innerHTML = "";
3731
-
3732
- var grouped = groupProjects(projects);
3733
-
3734
- for (var i = 0; i < grouped.parents.length; i++) {
3735
- var p = grouped.parents[i];
3736
- var worktrees = grouped.wtByParent[p.slug] || [];
3737
- var hasWorktrees = worktrees.length > 0;
3738
-
3739
- if (!hasWorktrees) {
3740
- // Regular project, render as before
3741
- var el = createIconItem(p, currentSlug);
3742
- (function (slug, name, elem) {
3743
- elem.addEventListener("contextmenu", function (e) {
3744
- e.preventDefault();
3745
- e.stopPropagation();
3746
- showIconCtxMenu(elem, slug, name);
3747
- });
3748
- })(p.slug, p.name || p.slug, el);
3749
- setupDragHandlers(el, p.slug);
3750
- container.appendChild(el);
3751
- continue;
3752
- }
3753
-
3754
- // Folder group for parent + worktrees
3755
- var folder = document.createElement("div");
3756
- folder.className = "icon-strip-group";
3757
- folder.dataset.parentSlug = p.slug;
3758
- if (wtCollapsed[p.slug]) folder.classList.add("collapsed");
3759
-
3760
- // Bubble up worktree processing state to parent
3761
- if (!p.isProcessing) {
3762
- for (var wpi = 0; wpi < worktrees.length; wpi++) {
3763
- if (worktrees[wpi].isProcessing) { p.isProcessing = true; break; }
3764
- }
3765
- }
3766
-
3767
- // Parent icon as folder header
3768
- var header = createIconItem(p, currentSlug);
3769
- header.classList.add("folder-header");
3770
- (function (slug, name, elem) {
3771
- elem.addEventListener("contextmenu", function (e) {
3772
- e.preventDefault();
3773
- e.stopPropagation();
3774
- showIconCtxMenu(elem, slug, name);
3775
- });
3776
- })(p.slug, p.name || p.slug, header);
3777
- setupDragHandlers(header, p.slug);
3778
-
3779
- // Chevron toggle
3780
- var chevron = document.createElement("span");
3781
- chevron.className = "icon-strip-group-chevron";
3782
- chevron.innerHTML = '<i data-lucide="git-branch"></i>';
3783
- (function (parentSlug, folderEl) {
3784
- chevron.addEventListener("click", function (e) {
3785
- e.preventDefault();
3786
- e.stopPropagation();
3787
- var nowCollapsed = folderEl.classList.toggle("collapsed");
3788
- setWtCollapsed(parentSlug, nowCollapsed);
3789
- });
3790
- chevron.addEventListener("contextmenu", function (e) {
3791
- e.preventDefault();
3792
- e.stopPropagation();
3793
- });
3794
- })(p.slug, folder);
3795
- chevron.setAttribute("data-tip", "Toggle worktrees");
3796
- header.appendChild(chevron);
3797
- folder.appendChild(header);
3798
-
3799
- // Worktree items container
3800
- var itemsContainer = document.createElement("div");
3801
- itemsContainer.className = "icon-strip-group-items";
3802
-
3803
- for (var wi = 0; wi < worktrees.length; wi++) {
3804
- (function (wt) {
3805
- var wtEl = document.createElement("a");
3806
- var isWtActive = wt.slug === currentSlug && !currentDmUserId;
3807
- var isAccessible = wt.worktreeAccessible !== false;
3808
- wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (!isAccessible ? " wt-disabled" : "");
3809
- wtEl.href = "/p/" + wt.slug + "/";
3810
- wtEl.dataset.slug = wt.slug;
3811
-
3812
- var abbrev = document.createElement("span");
3813
- abbrev.className = "wt-branch-abbrev";
3814
- abbrev.textContent = getProjectAbbrev(wt.name);
3815
- wtEl.appendChild(abbrev);
3816
-
3817
- var wtStatus = document.createElement("span");
3818
- wtStatus.className = "icon-strip-status";
3819
- if (wt.isProcessing) wtStatus.classList.add("processing");
3820
- wtEl.appendChild(wtStatus);
3821
-
3822
- var tooltipText = wt.name;
3823
- if (!isAccessible) {
3824
- tooltipText += " (outside project path, cannot be accessed)";
3825
- }
3826
- (function (text, elem) {
3827
- elem.addEventListener("mouseenter", function () { showIconTooltip(elem, text); });
3828
- elem.addEventListener("mouseleave", hideIconTooltip);
3829
- })(tooltipText, wtEl);
3830
-
3831
- if (isAccessible) {
3832
- (function (slug) {
3833
- wtEl.addEventListener("click", function (e) {
3834
- e.preventDefault();
3835
- if (ctx.switchProject) ctx.switchProject(slug);
3836
- });
3837
- })(wt.slug);
3838
- } else {
3839
- wtEl.addEventListener("click", function (e) {
3840
- e.preventDefault();
3841
- });
3842
- }
3843
-
3844
- if (isAccessible) {
3845
- (function (slug, name, elem) {
3846
- elem.addEventListener("contextmenu", function (e) {
3847
- e.preventDefault();
3848
- e.stopPropagation();
3849
- showIconCtxMenu(elem, slug, name);
3850
- });
3851
- })(wt.slug, wt.name, wtEl);
3852
- } else {
3853
- wtEl.addEventListener("contextmenu", function (e) {
3854
- e.preventDefault();
3855
- e.stopPropagation();
3856
- });
3857
- }
3858
-
3859
- // Pending permission shake for worktree items
3860
- if (wt.pendingPermissions > 0 && !isWtActive) {
3861
- wtEl.classList.add("has-pending-perm");
3862
- }
3863
-
3864
- itemsContainer.appendChild(wtEl);
3865
- })(worktrees[wi]);
3866
- }
3867
-
3868
- // Force expand if any worktree has pending permissions
3869
- var hasWtPendingPerm = false;
3870
- for (var wpi2 = 0; wpi2 < worktrees.length; wpi2++) {
3871
- if (worktrees[wpi2].pendingPermissions > 0) { hasWtPendingPerm = true; break; }
3872
- }
3873
- if (hasWtPendingPerm) folder.classList.remove("collapsed");
3874
-
3875
- // "+" button to add new worktree
3876
- var addBtn = document.createElement("button");
3877
- addBtn.className = "icon-strip-group-add";
3878
- addBtn.textContent = "+";
3879
- (function (parentSlug, parentName, btn) {
3880
- btn.addEventListener("click", function (e) {
3881
- e.preventDefault();
3882
- e.stopPropagation();
3883
- showWorktreeModal(parentSlug, parentName);
3884
- });
3885
- btn.addEventListener("mouseenter", function () { showIconTooltip(btn, "New worktree"); });
3886
- btn.addEventListener("mouseleave", hideIconTooltip);
3887
- })(p.slug, p.name, addBtn);
3888
- itemsContainer.appendChild(addBtn);
3889
-
3890
- folder.appendChild(itemsContainer);
3891
- container.appendChild(folder);
3892
- }
3893
-
3894
- // Update home icon active state
3895
- var homeIcon = document.querySelector(".icon-strip-home");
3896
- if (homeIcon) {
3897
- if ((!currentSlug || projects.length === 0) && !currentDmUserId) {
3898
- homeIcon.classList.add("active");
3899
- } else {
3900
- homeIcon.classList.remove("active");
3901
- }
3902
- }
3903
-
3904
- renderProjectList(projects, currentSlug);
3905
-
3906
- // Render Lucide icons added dynamically (e.g. worktree git-branch icon)
3907
- try { lucide.createIcons({ nodes: [container] }); } catch (e) {}
3908
- }
3909
-
3910
- function renderProjectList(projects, currentSlug) {
3911
- var list = document.getElementById("project-list");
3912
- if (!list) return;
3913
- list.innerHTML = "";
3914
-
3915
- var grouped = groupProjects(projects);
3916
-
3917
- for (var i = 0; i < grouped.parents.length; i++) {
3918
- var p = grouped.parents[i];
3919
- var worktrees = grouped.wtByParent[p.slug] || [];
3920
-
3921
- if (worktrees.length === 0) {
3922
- // Regular project
3923
- list.appendChild(createMobileProjectItem(p, currentSlug, false));
3924
- continue;
3925
- }
3926
-
3927
- // Folder for parent + worktrees
3928
- var folderDiv = document.createElement("div");
3929
- folderDiv.className = "mobile-project-folder";
3930
- if (wtCollapsed[p.slug]) folderDiv.classList.add("collapsed");
3931
-
3932
- var headerEl = createMobileProjectItem(p, currentSlug, false);
3933
- var chevron = document.createElement("span");
3934
- chevron.className = "mobile-folder-chevron";
3935
- chevron.innerHTML = "&#9660;";
3936
- (function (parentSlug, fDiv) {
3937
- chevron.addEventListener("click", function (e) {
3938
- e.preventDefault();
3939
- e.stopPropagation();
3940
- var nowCollapsed = fDiv.classList.toggle("collapsed");
3941
- setWtCollapsed(parentSlug, nowCollapsed);
3942
- });
3943
- })(p.slug, folderDiv);
3944
- headerEl.appendChild(chevron);
3945
- folderDiv.appendChild(headerEl);
3946
-
3947
- var wtList = document.createElement("div");
3948
- wtList.className = "mobile-folder-items";
3949
- for (var wi = 0; wi < worktrees.length; wi++) {
3950
- var isAccessible = worktrees[wi].worktreeAccessible !== false;
3951
- var wtItem = createMobileProjectItem(worktrees[wi], currentSlug, true);
3952
- if (!isAccessible) wtItem.classList.add("wt-disabled");
3953
- if (!isAccessible) {
3954
- wtItem.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); });
3955
- }
3956
- wtList.appendChild(wtItem);
3957
- }
3958
- folderDiv.appendChild(wtList);
3959
- list.appendChild(folderDiv);
3960
- }
3961
- }
3962
-
3963
- function createMobileProjectItem(p, currentSlug, isWorktree) {
3964
- var el = document.createElement("button");
3965
- el.className = "mobile-project-item" + (p.slug === currentSlug ? " active" : "") + (isWorktree ? " wt-item" : "");
3966
-
3967
- var abbrev = document.createElement("span");
3968
- abbrev.className = "mobile-project-abbrev";
3969
- if (p.icon) {
3970
- abbrev.textContent = p.icon;
3971
- parseEmojis(abbrev);
3972
- } else {
3973
- abbrev.textContent = getProjectAbbrev(p.name);
3974
- }
3975
- el.appendChild(abbrev);
3976
-
3977
- var name = document.createElement("span");
3978
- name.className = "mobile-project-name";
3979
- name.textContent = p.name;
3980
- el.appendChild(name);
3981
-
3982
- if (p.isProcessing) {
3983
- var dot = document.createElement("span");
3984
- dot.className = "mobile-project-processing";
3985
- el.appendChild(dot);
3986
- }
3987
-
3988
- el.addEventListener("click", function () {
3989
- if (ctx.switchProject) ctx.switchProject(p.slug);
3990
- closeSidebar();
3991
- });
3992
-
3993
- return el;
3994
- }
3995
-
3996
- export function getEmojiCategories() { return EMOJI_CATEGORIES; }
3997
-
3998
- // --- User strip (DM targets) ---
3999
- var cachedAllUsers = [];
4000
- var cachedOnlineUserIds = [];
4001
- var cachedDmFavorites = [];
4002
- var cachedDmConversations = [];
4003
- var cachedDmUnread = {};
4004
- var cachedMyUserId = null;
4005
- var currentDmUserId = null;
4006
- var dmPickerOpen = false;
4007
-
4008
- var cachedDmRemovedUsers = {};
4009
- var cachedMates = [];
4010
-
4011
- export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites, dmConversations, dmUnread, dmRemovedUsers, matesList) {
4012
- cachedMates = matesList || cachedMates || [];
4013
- cachedAllUsers = allUsers || [];
4014
- cachedOnlineUserIds = onlineUserIds || [];
4015
- cachedDmFavorites = dmFavorites || [];
4016
- cachedDmConversations = dmConversations || [];
4017
- cachedDmUnread = dmUnread || {};
4018
- cachedDmRemovedUsers = dmRemovedUsers || {};
4019
- cachedMyUserId = myUserId;
4020
- var container = document.getElementById("icon-strip-users");
4021
- if (!container) return;
4022
-
4023
- // All other users
4024
- var allOthers = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
4025
-
4026
- // Hide section if no other users and no mates
4027
- if (allOthers.length === 0 && cachedMates.length === 0) {
4028
- container.innerHTML = "";
4029
- container.classList.add("hidden");
4030
- return;
4031
- }
4032
-
4033
- // Filter to show only: favorites + users with unread + users with DM conversations
4034
- // But exclude users explicitly removed from favorites
4035
- var others = allOthers.filter(function (u) {
4036
- if (cachedDmRemovedUsers[u.id]) return false;
4037
- if (cachedDmFavorites.indexOf(u.id) !== -1) return true;
4038
- if (cachedDmUnread[u.id] && cachedDmUnread[u.id] > 0) return true;
4039
- if (cachedDmConversations.indexOf(u.id) !== -1) return true;
4040
- return false;
4041
- });
4042
-
4043
- container.classList.remove("hidden");
4044
- container.innerHTML = "";
4045
-
4046
- for (var i = 0; i < others.length; i++) {
4047
- (function (u) {
4048
- var el = document.createElement("div");
4049
- el.className = "icon-strip-user";
4050
- el.dataset.userId = u.id;
4051
- if (u.id === currentDmUserId) el.classList.add("active");
4052
- if (onlineUserIds.indexOf(u.id) !== -1) el.classList.add("online");
4053
-
4054
- var pill = document.createElement("span");
4055
- pill.className = "icon-strip-pill";
4056
- el.appendChild(pill);
4057
-
4058
- var avatar = document.createElement("img");
4059
- avatar.className = "icon-strip-user-avatar";
4060
- avatar.src = userAvatarUrl(u, 34);
4061
- avatar.alt = u.displayName;
4062
- el.appendChild(avatar);
4063
-
4064
- var onlineDot = document.createElement("span");
4065
- onlineDot.className = "icon-strip-user-online";
4066
- el.appendChild(onlineDot);
4067
-
4068
- var badge = document.createElement("span");
4069
- badge.className = "icon-strip-user-badge";
4070
- badge.dataset.userId = u.id;
4071
- el.appendChild(badge);
4072
-
4073
- // Tooltip
4074
- el.addEventListener("mouseenter", function () { showIconTooltip(el, u.displayName); });
4075
- el.addEventListener("mouseleave", hideIconTooltip);
4076
-
4077
- // Click: open DM
4078
- el.addEventListener("click", function () {
4079
- if (ctx.openDm) ctx.openDm(u.id);
4080
- });
4081
-
4082
- // Right-click: show context menu
4083
- el.addEventListener("contextmenu", function (e) {
4084
- e.preventDefault();
4085
- e.stopPropagation();
4086
- showUserCtxMenu(el, u);
4087
- });
4088
-
4089
- container.appendChild(el);
4090
- })(others[i]);
4091
- }
4092
-
4093
- // Build mate project status lookup from project list
4094
- var mateProjectStatus = {};
4095
- if (ctx && ctx.projectList) {
4096
- var allProjects = ctx.projectList;
4097
- for (var pi = 0; pi < allProjects.length; pi++) {
4098
- if (allProjects[pi].isMate) {
4099
- mateProjectStatus[allProjects[pi].slug] = allProjects[pi];
4100
- }
4101
- }
4102
- }
4103
-
4104
- // Render mates (only favorites, built-in first, then user-created)
4105
- var favoriteMates = cachedMates.filter(function (m) {
4106
- if (cachedDmRemovedUsers[m.id]) return false;
4107
- if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
4108
- if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
4109
- return false;
4110
- });
4111
- var sortedMates = favoriteMates.sort(function (a, b) {
4112
- var aBuiltin = a.builtinKey ? 1 : 0;
4113
- var bBuiltin = b.builtinKey ? 1 : 0;
4114
- if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
4115
- return (a.createdAt || 0) - (b.createdAt || 0);
4116
- });
4117
- for (var mi = 0; mi < sortedMates.length; mi++) {
4118
- (function (mate) {
4119
- var mp = mate.profile || {};
4120
- var mateSlug = "mate-" + mate.id;
4121
- var mateProj = mateProjectStatus[mateSlug] || {};
4122
- var isActive = mate.id === currentDmUserId;
4123
- var el = document.createElement("div");
4124
- el.className = "icon-strip-user icon-strip-mate";
4125
- el.dataset.userId = mate.id;
4126
- el.dataset.mateSlug = mateSlug;
4127
- if (isActive) el.classList.add("active");
4128
-
4129
- // Pending permission shake
4130
- if (mateProj.pendingPermissions > 0 && !isActive) {
4131
- el.classList.add("has-pending-perm");
4132
- }
4133
-
4134
- var pill = document.createElement("span");
4135
- pill.className = "icon-strip-pill";
4136
- el.appendChild(pill);
4137
-
4138
- var avatar = document.createElement("img");
4139
- avatar.className = "icon-strip-user-avatar" + (mate.primary ? " icon-strip-primary-mate" : "");
4140
- avatar.src = mateAvatarUrl(mate, 34);
4141
- avatar.alt = mp.displayName || mate.name || "Mate";
4142
- var mateColor = (mp.avatarColor) || mate.avatarColor || "#7c3aed";
4143
- avatar.style.background = mateColor + "30";
4144
- el.appendChild(avatar);
4145
-
4146
- // Processing status dot (IO blink)
4147
- var statusDot = document.createElement("span");
4148
- statusDot.className = "icon-strip-status";
4149
- if (mateProj.isProcessing) statusDot.classList.add("processing");
4150
- el.appendChild(statusDot);
4151
-
4152
- // Mate badge (bot icon)
4153
- var mateBadge = document.createElement("span");
4154
- mateBadge.className = "icon-strip-user-mate-badge";
4155
- mateBadge.innerHTML = iconHtml("bot");
4156
- el.appendChild(mateBadge);
4157
-
4158
- var badge = document.createElement("span");
4159
- badge.className = "icon-strip-user-badge";
4160
- badge.dataset.userId = mate.id;
4161
- el.appendChild(badge);
4162
-
4163
- // Restore unread badge if cached
4164
- var unreadCount = cachedDmUnread[mate.id] || 0;
4165
- if (unreadCount > 0 && !isActive) {
4166
- badge.textContent = unreadCount > 99 ? "99+" : String(unreadCount);
4167
- badge.classList.add("has-unread");
4168
- }
4169
-
4170
- // Tooltip
4171
- var displayName = mp.displayName || mate.name || "New Mate";
4172
- el.addEventListener("mouseenter", function () {
4173
- var html = '<div style="font-weight:600">' + escapeHtml(displayName);
4174
- if (mate.primary) {
4175
- 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>';
4176
- }
4177
- html += '</div>';
4178
- if (mate.bio) {
4179
- html += '<div style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-top:2px">' + escapeHtml(mate.bio) + '</div>';
4180
- }
4181
- showIconTooltipHtml(el, html);
4182
- });
4183
- el.addEventListener("mouseleave", hideIconTooltip);
4184
-
4185
- // Click: open DM with mate
4186
- el.addEventListener("click", function () {
4187
- if (ctx.openDm) ctx.openDm(mate.id);
4188
- });
4189
-
4190
- // Right-click: context menu for mate
4191
- el.addEventListener("contextmenu", function (e) {
4192
- e.preventDefault();
4193
- e.stopPropagation();
4194
- showMateCtxMenu(el, mate);
4195
- });
4196
-
4197
- container.appendChild(el);
4198
- })(sortedMates[mi]);
4199
- }
4200
-
4201
- // Show container if we have mates even with no other users
4202
- if (cachedMates.length > 0) {
4203
- container.classList.remove("hidden");
4204
- }
4205
-
4206
- // Add user (+) button
4207
- var addBtn = document.createElement("button");
4208
- addBtn.className = "icon-strip-invite";
4209
- addBtn.innerHTML = iconHtml("user-plus");
4210
- addBtn.addEventListener("click", function (e) {
4211
- e.stopPropagation();
4212
- toggleDmUserPicker(addBtn);
4213
- });
4214
- addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add user or create mate"); });
4215
- addBtn.addEventListener("mouseleave", hideIconTooltip);
4216
- container.appendChild(addBtn);
4217
- refreshIcons();
4218
- }
4219
-
4220
- function toggleDmUserPicker(anchorEl) {
4221
- if (dmPickerOpen) {
4222
- closeDmUserPicker();
4223
- return;
4224
- }
4225
- dmPickerOpen = true;
4226
-
4227
- var picker = document.createElement("div");
4228
- picker.className = "dm-user-picker";
4229
- picker.id = "dm-user-picker";
4230
-
4231
- // Search input
4232
- var searchInput = document.createElement("input");
4233
- searchInput.className = "dm-user-picker-search";
4234
- searchInput.type = "text";
4235
- searchInput.placeholder = "Search mates and users...";
4236
- picker.appendChild(searchInput);
4237
-
4238
- // User list element (appended later, after USERS label)
4239
- var listEl = document.createElement("div");
4240
- listEl.className = "dm-user-picker-list";
4241
-
4242
- // Position the picker above the + button
4243
- document.body.appendChild(picker);
4244
- var rect = anchorEl.getBoundingClientRect();
4245
- picker.style.left = (rect.right + 8) + "px";
4246
- picker.style.bottom = (window.innerHeight - rect.bottom) + "px";
4247
-
4248
- function renderPickerList(filter) {
4249
- listEl.innerHTML = "";
4250
- var allOthers = cachedAllUsers.filter(function (u) { return u.id !== cachedMyUserId; });
4251
- // Exclude already-favorited users
4252
- var available = allOthers.filter(function (u) {
4253
- return cachedDmFavorites.indexOf(u.id) === -1;
4254
- });
4255
- if (filter) {
4256
- var lf = filter.toLowerCase();
4257
- available = available.filter(function (u) {
4258
- return (u.displayName && u.displayName.toLowerCase().indexOf(lf) !== -1) ||
4259
- (u.username && u.username.toLowerCase().indexOf(lf) !== -1);
4260
- });
4261
- }
4262
- if (available.length === 0) {
4263
- var emptyEl = document.createElement("div");
4264
- emptyEl.className = "dm-user-picker-empty";
4265
- emptyEl.textContent = filter ? "No users found" : "No more users to add";
4266
- listEl.appendChild(emptyEl);
4267
- return;
4268
- }
4269
- for (var i = 0; i < available.length; i++) {
4270
- (function (u) {
4271
- var item = document.createElement("div");
4272
- item.className = "dm-user-picker-item";
4273
-
4274
- var av = document.createElement("img");
4275
- av.className = "dm-user-picker-avatar";
4276
- av.src = userAvatarUrl(u, 28);
4277
- av.alt = u.displayName;
4278
- item.appendChild(av);
4279
-
4280
- var name = document.createElement("span");
4281
- name.className = "dm-user-picker-name";
4282
- name.textContent = u.displayName;
4283
- item.appendChild(name);
4284
-
4285
- item.addEventListener("click", function () {
4286
- if (ctx.sendWs) {
4287
- ctx.sendWs({ type: "dm_add_favorite", targetUserId: u.id });
4288
- }
4289
- closeDmUserPicker();
4290
- });
4291
-
4292
- listEl.appendChild(item);
4293
- })(available[i]);
4294
- }
4295
- }
4296
-
4297
- // --- MATES section ---
4298
- var matesSectionLabel = document.createElement("div");
4299
- matesSectionLabel.className = "dm-user-picker-section";
4300
- matesSectionLabel.textContent = "Mates";
4301
- picker.appendChild(matesSectionLabel);
4302
-
4303
- var matesListEl = document.createElement("div");
4304
- matesListEl.className = "dm-user-picker-list dm-mates-list";
4305
- picker.appendChild(matesListEl);
4306
-
4307
- // Update scroll gradient hint
4308
- function updateMatesScrollHint() {
4309
- var isOverflow = matesListEl.scrollHeight > matesListEl.clientHeight + 2;
4310
- if (!isOverflow) {
4311
- matesListEl.classList.add("no-overflow");
4312
- matesListEl.classList.remove("scrolled-bottom");
4313
- return;
4314
- }
4315
- matesListEl.classList.remove("no-overflow");
4316
- var atBottom = matesListEl.scrollTop + matesListEl.clientHeight >= matesListEl.scrollHeight - 4;
4317
- if (atBottom) {
4318
- matesListEl.classList.add("scrolled-bottom");
4319
- } else {
4320
- matesListEl.classList.remove("scrolled-bottom");
4321
- }
4322
- }
4323
- matesListEl.addEventListener("scroll", updateMatesScrollHint);
4324
-
4325
- function renderMatesList(filter) {
4326
- matesListEl.innerHTML = "";
4327
- var allMates = cachedMates || [];
4328
- if (filter) {
4329
- var lf = filter.toLowerCase();
4330
- allMates = allMates.filter(function (m) {
4331
- var name = (m.profile && m.profile.displayName) || m.name || "";
4332
- return name.toLowerCase().indexOf(lf) !== -1;
4333
- });
4334
- }
4335
- // Build unified list: installed builtins, deleted builtins, user-created
4336
- var availBuiltins = (ctx.availableBuiltins && ctx.availableBuiltins()) || [];
4337
- var entries = [];
4338
- // 1. Installed builtin mates
4339
- for (var si = 0; si < allMates.length; si++) {
4340
- if (allMates[si].builtinKey) entries.push({ type: "mate", data: allMates[si] });
4341
- }
4342
- // 2. Deleted builtins (only when not filtering)
4343
- if (!filter) {
4344
- for (var di = 0; di < availBuiltins.length; di++) {
4345
- entries.push({ type: "deleted", data: availBuiltins[di] });
4346
- }
4347
- }
4348
- // 3. User-created mates
4349
- var userMates = allMates.filter(function (m) { return !m.builtinKey; });
4350
- userMates.sort(function (a, b) { return (a.createdAt || 0) - (b.createdAt || 0); });
4351
- for (var ui = 0; ui < userMates.length; ui++) {
4352
- entries.push({ type: "mate", data: userMates[ui] });
4353
- }
4354
-
4355
- for (var i = 0; i < entries.length; i++) {
4356
- var entry = entries[i];
4357
- if (entry.type === "deleted") {
4358
- // Deleted builtin: show with "+ Add" button
4359
- (function (b) {
4360
- var bItem = document.createElement("div");
4361
- bItem.className = "dm-user-picker-item dm-user-picker-builtin-item";
4362
- bItem.style.opacity = "0.7";
4363
- var bAv = document.createElement("img");
4364
- bAv.className = "dm-user-picker-avatar";
4365
- bAv.src = mateAvatarUrl({ avatarCustom: b.avatarCustom, avatarStyle: b.avatarStyle || "bottts", avatarSeed: b.displayName, id: b.key }, 28);
4366
- bAv.alt = b.displayName;
4367
- bItem.appendChild(bAv);
4368
- var bNameWrap = document.createElement("div");
4369
- bNameWrap.style.cssText = "flex:1;min-width:0;";
4370
- var bName = document.createElement("span");
4371
- bName.className = "dm-user-picker-name";
4372
- bName.textContent = b.displayName;
4373
- bNameWrap.appendChild(bName);
4374
- var bBio = document.createElement("div");
4375
- bBio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
4376
- bBio.textContent = b.bio || b.displayName;
4377
- bNameWrap.appendChild(bBio);
4378
- bItem.appendChild(bNameWrap);
4379
- var bAddBtn = document.createElement("button");
4380
- 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;";
4381
- bAddBtn.textContent = "+ Add";
4382
- bAddBtn.title = "Re-add " + b.displayName;
4383
- bAddBtn.addEventListener("click", function (e) {
4384
- e.stopPropagation();
4385
- if (ctx.sendWs) ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
4386
- closeDmUserPicker();
4387
- });
4388
- bItem.appendChild(bAddBtn);
4389
- bItem.addEventListener("click", function () {
4390
- if (ctx.sendWs) ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
4391
- closeDmUserPicker();
4392
- });
4393
- matesListEl.appendChild(bItem);
4394
- })(entry.data);
4395
- } else {
4396
- // Normal mate
4397
- (function (m) {
4398
- var mp = m.profile || {};
4399
- var isFav = cachedDmFavorites.indexOf(m.id) !== -1;
4400
- var item = document.createElement("div");
4401
- item.className = "dm-user-picker-item";
4402
- if (isFav) item.classList.add("dm-picker-fav");
4403
- var av = document.createElement("img");
4404
- av.className = "dm-user-picker-avatar";
4405
- av.src = mateAvatarUrl(m, 28);
4406
- av.alt = mp.displayName || m.name || "Mate";
4407
- item.appendChild(av);
4408
- var nameWrap = document.createElement("div");
4409
- nameWrap.style.cssText = "flex:1;min-width:0;";
4410
- var name = document.createElement("span");
4411
- name.className = "dm-user-picker-name";
4412
- name.textContent = mp.displayName || m.name || "Mate";
4413
- nameWrap.appendChild(name);
4414
- if (m.bio) {
4415
- var bio = document.createElement("div");
4416
- bio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
4417
- bio.textContent = m.bio;
4418
- nameWrap.appendChild(bio);
4419
- }
4420
- item.appendChild(nameWrap);
4421
- // Delete button with inline confirm
4422
- var delBtn = document.createElement("button");
4423
- delBtn.className = "dm-picker-del-btn";
4424
- delBtn.innerHTML = m.builtinKey ? iconHtml("minus-circle") : iconHtml("trash-2");
4425
- delBtn.title = m.builtinKey ? "Remove mate" : "Delete mate";
4426
- delBtn.addEventListener("click", function (e) {
4427
- e.stopPropagation();
4428
- var origHtml = item.innerHTML;
4429
- item.innerHTML = "";
4430
- item.style.justifyContent = "center";
4431
- item.style.gap = "6px";
4432
- var confirmMsg = document.createElement("span");
4433
- confirmMsg.style.cssText = "font-size:12px;color:var(--text-dimmer);";
4434
- confirmMsg.textContent = m.builtinKey ? "Remove? You can add back anytime." : "Delete permanently?";
4435
- item.appendChild(confirmMsg);
4436
- var yesBtn = document.createElement("button");
4437
- yesBtn.style.cssText = "border:none;background:var(--danger,#e74c3c);color:#fff;padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
4438
- yesBtn.textContent = m.builtinKey ? "Remove" : "Delete";
4439
- yesBtn.addEventListener("click", function (e2) {
4440
- e2.stopPropagation();
4441
- if (ctx.sendWs) ctx.sendWs({ type: "mate_delete", mateId: m.id });
4442
- closeDmUserPicker();
4443
- });
4444
- item.appendChild(yesBtn);
4445
- var noBtn = document.createElement("button");
4446
- noBtn.style.cssText = "border:1px solid var(--border);background:none;color:var(--text);padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
4447
- noBtn.textContent = "Cancel";
4448
- noBtn.addEventListener("click", function (e2) {
4449
- e2.stopPropagation();
4450
- item.innerHTML = origHtml;
4451
- item.style.justifyContent = "";
4452
- item.style.gap = "";
4453
- refreshIcons();
4454
- });
4455
- item.appendChild(noBtn);
4456
- });
4457
- item.appendChild(delBtn);
4458
- item.addEventListener("click", function () {
4459
- if (ctx.openDm) ctx.openDm(m.id);
4460
- if (!isFav && ctx.sendWs) ctx.sendWs({ type: "dm_add_favorite", targetUserId: m.id });
4461
- closeDmUserPicker();
4462
- });
4463
- matesListEl.appendChild(item);
4464
- })(entry.data);
4465
- }
4466
- }
4467
-
4468
- if (entries.length === 0 && filter) {
4469
- var emptyEl = document.createElement("div");
4470
- emptyEl.className = "dm-user-picker-empty";
4471
- emptyEl.textContent = "No mates found";
4472
- matesListEl.appendChild(emptyEl);
4473
- }
4474
- refreshIcons();
4475
- requestAnimationFrame(updateMatesScrollHint);
4476
- }
4477
-
4478
- // Create Mate option
4479
- var createMateEl = document.createElement("div");
4480
- createMateEl.className = "dm-user-picker-create-mate";
4481
- var hasCustomMates = (cachedMates || []).some(function (m) { return !m.builtinKey; });
4482
- var createMateLabel = hasCustomMates ? "Create a Mate" : "Create a Mate for what you're doing";
4483
- createMateEl.innerHTML = iconHtml("bot") + " <span>" + createMateLabel + "</span>";
4484
- createMateEl.addEventListener("click", function () {
4485
- closeDmUserPicker();
4486
- if (ctx.openMateWizard) ctx.openMateWizard();
4487
- });
4488
- picker.appendChild(createMateEl);
4489
-
4490
- // Divider
4491
- var divider = document.createElement("div");
4492
- divider.style.borderTop = "1px solid var(--border, #333)";
4493
- divider.style.margin = "4px 0";
4494
- picker.appendChild(divider);
4495
-
4496
- // Section label for users
4497
- var sectionLabel = document.createElement("div");
4498
- sectionLabel.className = "dm-user-picker-section";
4499
- sectionLabel.textContent = "Users";
4500
- picker.appendChild(sectionLabel);
4501
- picker.appendChild(listEl);
4502
-
4503
- renderMatesList("");
4504
- renderPickerList("");
4505
- searchInput.addEventListener("input", function () {
4506
- var val = searchInput.value;
4507
- renderMatesList(val);
4508
- renderPickerList(val);
4509
- });
4510
-
4511
- // Focus search
4512
- setTimeout(function () { searchInput.focus(); }, 50);
4513
-
4514
- // Close on click outside
4515
- function onDocClick(e) {
4516
- if (!picker.contains(e.target) && e.target !== anchorEl && !anchorEl.contains(e.target)) {
4517
- closeDmUserPicker();
4518
- document.removeEventListener("click", onDocClick, true);
4519
- }
4520
- }
4521
- setTimeout(function () {
4522
- document.addEventListener("click", onDocClick, true);
4523
- }, 10);
4524
- picker._docClickHandler = onDocClick;
4525
- }
4526
-
4527
- export function closeDmUserPicker() {
4528
- dmPickerOpen = false;
4529
- var picker = document.getElementById("dm-user-picker");
4530
- if (picker) {
4531
- if (picker._docClickHandler) {
4532
- document.removeEventListener("click", picker._docClickHandler, true);
4533
- }
4534
- picker.remove();
4535
- }
4536
- }
4537
-
4538
- export function setCurrentDmUser(userId) {
4539
- currentDmUserId = userId;
4540
- // Update active state on user icons immediately
4541
- var container = document.getElementById("icon-strip-users");
4542
- if (!container) return;
4543
- var items = container.querySelectorAll(".icon-strip-user");
4544
- for (var i = 0; i < items.length; i++) {
4545
- if (items[i].dataset.userId === userId) {
4546
- items[i].classList.add("active");
4547
- } else {
4548
- items[i].classList.remove("active");
4549
- }
4550
- }
4551
- }
4552
-
4553
- export function updateDmBadge(userId, count) {
4554
- var badge = document.querySelector('.icon-strip-user-badge[data-user-id="' + userId + '"]');
4555
- if (!badge) return;
4556
- if (count > 0) {
4557
- badge.textContent = count > 99 ? "99+" : String(count);
4558
- badge.classList.add("has-unread");
4559
- } else {
4560
- badge.textContent = "";
4561
- badge.classList.remove("has-unread");
4562
- }
4563
- }
4564
-
4565
- export function updateSessionBadge(sessionId, count) {
4566
- var badge = document.querySelector('.session-unread-badge[data-session-id="' + sessionId + '"]');
4567
- if (!badge) return;
4568
- if (count > 0) {
4569
- badge.textContent = count > 99 ? "99+" : String(count);
4570
- badge.classList.add("has-unread");
4571
- } else {
4572
- badge.textContent = "";
4573
- badge.classList.remove("has-unread");
4574
- }
4575
- }
4576
-
4577
- export function updateProjectBadge(slug, count) {
4578
- var icon = document.querySelector('.icon-strip-item[data-slug="' + slug + '"]');
4579
- if (!icon) return;
4580
- var badge = icon.querySelector(".icon-strip-project-badge");
4581
- if (!badge) return;
4582
- if (count > 0) {
4583
- badge.textContent = count > 99 ? "99+" : String(count);
4584
- badge.classList.add("has-unread");
4585
- } else {
4586
- badge.textContent = "";
4587
- badge.classList.remove("has-unread");
4588
- }
4589
- }
4590
-
4591
- export function initIconStrip(_ctx) {
4592
- var addBtn = document.getElementById("icon-strip-add");
4593
- if (addBtn) {
4594
- addBtn.addEventListener("click", function () {
4595
- if (_ctx.openAddProjectModal) {
4596
- _ctx.openAddProjectModal();
4597
- } else {
4598
- var modal = _ctx.$("add-project-modal");
4599
- if (modal) modal.classList.remove("hidden");
4600
- }
4601
- });
4602
- addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add project"); });
4603
- addBtn.addEventListener("mouseleave", hideIconTooltip);
4604
- }
280
+ for (var i = 0; i < count; i++) {
281
+ var dot = document.createElement("div");
282
+ dot.className = "dust-particle";
283
+ var size = 3 + Math.random() * 5;
284
+ var angle = Math.random() * Math.PI * 2;
285
+ var dist = 30 + Math.random() * 60;
286
+ var dx = Math.cos(angle) * dist;
287
+ var dy = Math.sin(angle) * dist - 20; // bias upward
288
+ var duration = 600 + Math.random() * 500;
4605
289
 
4606
- var exploreBtn = document.getElementById("icon-strip-explore");
4607
- if (exploreBtn) {
4608
- exploreBtn.addEventListener("click", function () {
4609
- // Toggle file browser
4610
- var fileBrowserBtn = _ctx.$("file-browser-btn");
4611
- if (fileBrowserBtn) fileBrowserBtn.click();
4612
- });
4613
- exploreBtn.addEventListener("mouseenter", function () { showIconTooltip(exploreBtn, "File browser"); });
4614
- exploreBtn.addEventListener("mouseleave", hideIconTooltip);
4615
- }
290
+ dot.style.width = size + "px";
291
+ dot.style.height = size + "px";
292
+ dot.style.left = cx + "px";
293
+ dot.style.top = cy + "px";
294
+ dot.style.background = colors[Math.floor(Math.random() * colors.length)];
295
+ dot.style.setProperty("--dust-x", dx + "px");
296
+ dot.style.setProperty("--dust-y", dy + "px");
297
+ dot.style.setProperty("--dust-duration", duration + "ms");
4616
298
 
4617
- // Tooltip + click for home icon
4618
- var homeIcon = document.querySelector(".icon-strip-home");
4619
- if (homeIcon) {
4620
- homeIcon.addEventListener("mouseenter", function () { showIconTooltip(homeIcon, "Clay"); });
4621
- homeIcon.addEventListener("mouseleave", hideIconTooltip);
4622
- homeIcon.addEventListener("click", function (e) {
4623
- e.preventDefault();
4624
- if (_ctx.showHomeHub) _ctx.showHomeHub();
4625
- });
4626
- homeIcon.style.cursor = "pointer";
299
+ container.appendChild(dot);
4627
300
  }
4628
301
 
4629
- // Chevron dropdown on project name
4630
- var dropdownBtn = document.getElementById("title-bar-project-dropdown");
4631
- if (dropdownBtn) {
4632
- dropdownBtn.addEventListener("click", function (e) {
4633
- e.stopPropagation();
4634
- // Find current project info from cached list
4635
- var current = null;
4636
- for (var i = 0; i < cachedProjectList.length; i++) {
4637
- if (cachedProjectList[i].slug === cachedCurrentSlug) {
4638
- current = cachedProjectList[i];
4639
- break;
4640
- }
4641
- }
4642
- if (!current) return;
4643
-
4644
- // Toggle open state
4645
- if (projectCtxMenu) {
4646
- closeProjectCtxMenu();
4647
- dropdownBtn.classList.remove("open");
4648
- return;
4649
- }
4650
- dropdownBtn.classList.add("open");
4651
- showProjectCtxMenu(dropdownBtn, current.slug, current.name, current.icon, "below");
4652
- // Remove open class when menu closes
4653
- var observer = new MutationObserver(function () {
4654
- if (!projectCtxMenu) {
4655
- dropdownBtn.classList.remove("open");
4656
- observer.disconnect();
4657
- }
4658
- });
4659
- observer.observe(document.body, { childList: true });
4660
- });
4661
- }
302
+ setTimeout(function () { container.remove(); }, 1200);
4662
303
  }