clay-server 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/bin/claude-relay.js +6 -0
  4. package/bin/cli.js +2602 -0
  5. package/lib/cli-sessions.js +265 -0
  6. package/lib/config.js +338 -0
  7. package/lib/daemon.js +802 -0
  8. package/lib/ipc.js +124 -0
  9. package/lib/notes.js +121 -0
  10. package/lib/pages.js +1308 -0
  11. package/lib/project.js +3172 -0
  12. package/lib/public/app.js +4795 -0
  13. package/lib/public/apple-touch-icon-dark.png +0 -0
  14. package/lib/public/apple-touch-icon.png +0 -0
  15. package/lib/public/clay-logo.png +0 -0
  16. package/lib/public/css/admin.css +576 -0
  17. package/lib/public/css/base.css +284 -0
  18. package/lib/public/css/diff.css +139 -0
  19. package/lib/public/css/filebrowser.css +1482 -0
  20. package/lib/public/css/highlight.css +144 -0
  21. package/lib/public/css/home-hub.css +455 -0
  22. package/lib/public/css/icon-strip.css +614 -0
  23. package/lib/public/css/input.css +654 -0
  24. package/lib/public/css/loop.css +898 -0
  25. package/lib/public/css/menus.css +823 -0
  26. package/lib/public/css/messages.css +1448 -0
  27. package/lib/public/css/mobile-nav.css +384 -0
  28. package/lib/public/css/overlays.css +893 -0
  29. package/lib/public/css/playbook.css +264 -0
  30. package/lib/public/css/profile.css +268 -0
  31. package/lib/public/css/rewind.css +528 -0
  32. package/lib/public/css/scheduler-modal.css +1429 -0
  33. package/lib/public/css/scheduler.css +1306 -0
  34. package/lib/public/css/server-settings.css +811 -0
  35. package/lib/public/css/sidebar.css +1189 -0
  36. package/lib/public/css/skills.css +789 -0
  37. package/lib/public/css/sticky-notes.css +848 -0
  38. package/lib/public/css/stt.css +155 -0
  39. package/lib/public/css/title-bar.css +517 -0
  40. package/lib/public/favicon-banded-32.png +0 -0
  41. package/lib/public/favicon-banded.png +0 -0
  42. package/lib/public/favicon-dark.svg +1 -0
  43. package/lib/public/favicon.svg +1 -0
  44. package/lib/public/icon-192-dark.png +0 -0
  45. package/lib/public/icon-192.png +0 -0
  46. package/lib/public/icon-512-dark.png +0 -0
  47. package/lib/public/icon-512.png +0 -0
  48. package/lib/public/icon-banded-76.png +0 -0
  49. package/lib/public/icon-banded-96.png +0 -0
  50. package/lib/public/icon-mono.svg +1 -0
  51. package/lib/public/index.html +1437 -0
  52. package/lib/public/manifest.json +27 -0
  53. package/lib/public/modules/admin.js +631 -0
  54. package/lib/public/modules/ascii-logo.js +442 -0
  55. package/lib/public/modules/diff.js +398 -0
  56. package/lib/public/modules/events.js +21 -0
  57. package/lib/public/modules/filebrowser.js +1535 -0
  58. package/lib/public/modules/fileicons.js +172 -0
  59. package/lib/public/modules/icons.js +54 -0
  60. package/lib/public/modules/input.js +661 -0
  61. package/lib/public/modules/markdown.js +378 -0
  62. package/lib/public/modules/notifications.js +548 -0
  63. package/lib/public/modules/playbook.js +578 -0
  64. package/lib/public/modules/profile.js +378 -0
  65. package/lib/public/modules/project-settings.js +901 -0
  66. package/lib/public/modules/qrcode.js +67 -0
  67. package/lib/public/modules/rewind.js +345 -0
  68. package/lib/public/modules/scheduler.js +2833 -0
  69. package/lib/public/modules/server-settings.js +928 -0
  70. package/lib/public/modules/sidebar.js +2264 -0
  71. package/lib/public/modules/skills.js +794 -0
  72. package/lib/public/modules/state.js +3 -0
  73. package/lib/public/modules/sticky-notes.js +1253 -0
  74. package/lib/public/modules/stt.js +272 -0
  75. package/lib/public/modules/terminal.js +736 -0
  76. package/lib/public/modules/theme.js +720 -0
  77. package/lib/public/modules/tools.js +1622 -0
  78. package/lib/public/modules/utils.js +56 -0
  79. package/lib/public/style.css +24 -0
  80. package/lib/public/sw.js +154 -0
  81. package/lib/public/wordmark-banded-20.png +0 -0
  82. package/lib/public/wordmark-banded-32.png +0 -0
  83. package/lib/public/wordmark-banded-64.png +0 -0
  84. package/lib/public/wordmark-banded-80.png +0 -0
  85. package/lib/push.js +130 -0
  86. package/lib/scheduler.js +402 -0
  87. package/lib/sdk-bridge.js +1035 -0
  88. package/lib/server.js +2055 -0
  89. package/lib/sessions.js +552 -0
  90. package/lib/smtp.js +221 -0
  91. package/lib/terminal-manager.js +187 -0
  92. package/lib/terminal.js +24 -0
  93. package/lib/themes/ayu-light.json +9 -0
  94. package/lib/themes/catppuccin-latte.json +9 -0
  95. package/lib/themes/catppuccin-mocha.json +9 -0
  96. package/lib/themes/clay-light.json +10 -0
  97. package/lib/themes/clay.json +10 -0
  98. package/lib/themes/dracula.json +9 -0
  99. package/lib/themes/everforest-light.json +9 -0
  100. package/lib/themes/everforest.json +9 -0
  101. package/lib/themes/github-light.json +9 -0
  102. package/lib/themes/gruvbox-dark.json +9 -0
  103. package/lib/themes/gruvbox-light.json +9 -0
  104. package/lib/themes/monokai.json +9 -0
  105. package/lib/themes/nord-light.json +9 -0
  106. package/lib/themes/nord.json +9 -0
  107. package/lib/themes/one-dark.json +9 -0
  108. package/lib/themes/one-light.json +9 -0
  109. package/lib/themes/rose-pine-dawn.json +9 -0
  110. package/lib/themes/rose-pine.json +9 -0
  111. package/lib/themes/solarized-dark.json +9 -0
  112. package/lib/themes/solarized-light.json +9 -0
  113. package/lib/themes/tokyo-night-light.json +9 -0
  114. package/lib/themes/tokyo-night.json +9 -0
  115. package/lib/updater.js +97 -0
  116. package/lib/users.js +459 -0
  117. package/lib/utils.js +64 -0
  118. package/package.json +56 -0
@@ -0,0 +1,2264 @@
1
+ import { escapeHtml, copyToClipboard } from './utils.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+ import { openProjectSettings } from './project-settings.js';
4
+ import { triggerShare } from './qrcode.js';
5
+ import { parseEmojis } from './markdown.js';
6
+
7
+ var ctx;
8
+
9
+ // --- Session search ---
10
+ var searchQuery = "";
11
+ var searchMatchIds = null; // null = no search, Set of matched session IDs
12
+ var searchDebounce = null;
13
+ var cachedSessions = [];
14
+ var expandedLoopGroups = new Set();
15
+
16
+ // --- Cached project data for mobile sheet ---
17
+ var cachedProjectList = [];
18
+ var cachedCurrentSlug = null;
19
+
20
+ // --- Session presence (multi-user: who is viewing which session) ---
21
+ var sessionPresence = {}; // { sessionId: [{ id, displayName, avatarStyle, avatarSeed }] }
22
+
23
+ // --- Countdown timer for upcoming schedules ---
24
+ var countdownTimer = null;
25
+ var countdownContainer = null;
26
+
27
+ // --- Session context menu ---
28
+ var sessionCtxMenu = null;
29
+ var sessionCtxSessionId = null;
30
+
31
+ function closeSessionCtxMenu() {
32
+ if (sessionCtxMenu) {
33
+ sessionCtxMenu.remove();
34
+ sessionCtxMenu = null;
35
+ sessionCtxSessionId = null;
36
+ }
37
+ }
38
+
39
+ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
40
+ closeSessionCtxMenu();
41
+ sessionCtxSessionId = sessionId;
42
+
43
+ var menu = document.createElement("div");
44
+ menu.className = "session-ctx-menu";
45
+
46
+ var renameItem = document.createElement("button");
47
+ renameItem.className = "session-ctx-item";
48
+ renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
49
+ renameItem.addEventListener("click", function (e) {
50
+ e.stopPropagation();
51
+ closeSessionCtxMenu();
52
+ startInlineRename(sessionId, title);
53
+ });
54
+ menu.appendChild(renameItem);
55
+
56
+ // Session visibility toggle (only the session owner can change)
57
+ if (ctx.multiUser && sessionData && sessionData.ownerId && sessionData.ownerId === ctx.myUserId) {
58
+ var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
59
+ var isPrivate = currentVis === "private";
60
+ var visItem = document.createElement("button");
61
+ visItem.className = "session-ctx-item";
62
+ visItem.innerHTML = iconHtml(isPrivate ? "eye" : "eye-off") + " <span>" + (isPrivate ? "Make Shared" : "Make Private") + "</span>";
63
+ visItem.addEventListener("click", function (e) {
64
+ e.stopPropagation();
65
+ closeSessionCtxMenu();
66
+ var newVis = isPrivate ? "shared" : "private";
67
+ if (ctx.ws && ctx.connected) {
68
+ ctx.ws.send(JSON.stringify({ type: "set_session_visibility", sessionId: sessionId, visibility: newVis }));
69
+ }
70
+ });
71
+ menu.appendChild(visItem);
72
+ }
73
+
74
+ var deleteItem = document.createElement("button");
75
+ deleteItem.className = "session-ctx-item session-ctx-delete";
76
+ deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
77
+ deleteItem.addEventListener("click", function (e) {
78
+ e.stopPropagation();
79
+ closeSessionCtxMenu();
80
+ ctx.showConfirm('Delete "' + (title || "New Session") + '"? This session and its history will be permanently removed.', function () {
81
+ var ws = ctx.ws;
82
+ if (ws && ctx.connected) {
83
+ ws.send(JSON.stringify({ type: "delete_session", id: sessionId }));
84
+ }
85
+ });
86
+ });
87
+ menu.appendChild(deleteItem);
88
+
89
+ document.body.appendChild(menu);
90
+ sessionCtxMenu = menu;
91
+ refreshIcons();
92
+
93
+ // Position: fixed relative to the anchor button
94
+ requestAnimationFrame(function () {
95
+ var btnRect = anchorBtn.getBoundingClientRect();
96
+ menu.style.position = "fixed";
97
+ menu.style.top = (btnRect.bottom + 2) + "px";
98
+ menu.style.right = (window.innerWidth - btnRect.right) + "px";
99
+ menu.style.left = "auto";
100
+ // If menu overflows below viewport, flip up
101
+ var menuRect = menu.getBoundingClientRect();
102
+ if (menuRect.bottom > window.innerHeight - 8) {
103
+ menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
104
+ }
105
+ });
106
+ }
107
+
108
+ function startInlineRename(sessionId, currentTitle) {
109
+ var el = ctx.sessionListEl.querySelector('.session-item[data-session-id="' + sessionId + '"]');
110
+ if (!el) return;
111
+ var textSpan = el.querySelector(".session-item-text");
112
+ if (!textSpan) return;
113
+
114
+ var input = document.createElement("input");
115
+ input.type = "text";
116
+ input.className = "session-rename-input";
117
+ input.value = currentTitle || "New Session";
118
+
119
+ var originalHtml = textSpan.innerHTML;
120
+ textSpan.innerHTML = "";
121
+ textSpan.appendChild(input);
122
+ input.focus();
123
+ input.select();
124
+
125
+ function commitRename() {
126
+ var newTitle = input.value.trim();
127
+ if (newTitle && newTitle !== currentTitle && ctx.ws && ctx.connected) {
128
+ ctx.ws.send(JSON.stringify({ type: "rename_session", id: sessionId, title: newTitle }));
129
+ }
130
+ // Restore text (server will send updated session_list)
131
+ textSpan.innerHTML = originalHtml;
132
+ if (newTitle && newTitle !== currentTitle) {
133
+ textSpan.textContent = newTitle;
134
+ }
135
+ }
136
+
137
+ input.addEventListener("keydown", function (e) {
138
+ if (e.key === "Enter") { e.preventDefault(); commitRename(); }
139
+ if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
140
+ });
141
+ input.addEventListener("blur", commitRename);
142
+ input.addEventListener("click", function (e) { e.stopPropagation(); });
143
+ }
144
+
145
+ function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
146
+ closeSessionCtxMenu();
147
+
148
+ var menu = document.createElement("div");
149
+ menu.className = "session-ctx-menu";
150
+
151
+ var renameItem = document.createElement("button");
152
+ renameItem.className = "session-ctx-item";
153
+ renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
154
+ renameItem.addEventListener("click", function (e) {
155
+ e.stopPropagation();
156
+ closeSessionCtxMenu();
157
+ startLoopInlineRename(loopId, loopName);
158
+ });
159
+ menu.appendChild(renameItem);
160
+
161
+ var deleteItem = document.createElement("button");
162
+ deleteItem.className = "session-ctx-item session-ctx-delete";
163
+ deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
164
+ deleteItem.addEventListener("click", function (e) {
165
+ e.stopPropagation();
166
+ closeSessionCtxMenu();
167
+ var msg = 'Delete "' + (loopName || "Ralph Loop") + '"';
168
+ if (childCount > 1) msg += " and its " + childCount + " sessions";
169
+ msg += "? This cannot be undone.";
170
+ ctx.showConfirm(msg, function () {
171
+ if (ctx.ws && ctx.connected) {
172
+ ctx.ws.send(JSON.stringify({ type: "delete_loop_group", loopId: loopId }));
173
+ }
174
+ });
175
+ });
176
+ menu.appendChild(deleteItem);
177
+
178
+ document.body.appendChild(menu);
179
+ sessionCtxMenu = menu;
180
+ refreshIcons();
181
+
182
+ requestAnimationFrame(function () {
183
+ var btnRect = anchorBtn.getBoundingClientRect();
184
+ menu.style.position = "fixed";
185
+ menu.style.top = (btnRect.bottom + 2) + "px";
186
+ menu.style.right = (window.innerWidth - btnRect.right) + "px";
187
+ menu.style.left = "auto";
188
+ var menuRect = menu.getBoundingClientRect();
189
+ if (menuRect.bottom > window.innerHeight - 8) {
190
+ menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
191
+ }
192
+ });
193
+ }
194
+
195
+ function startLoopInlineRename(loopId, currentName) {
196
+ var el = ctx.sessionListEl.querySelector('.session-loop-group[data-loop-id="' + loopId + '"]');
197
+ if (!el) return;
198
+ var textSpan = el.querySelector(".session-item-text");
199
+ if (!textSpan) return;
200
+
201
+ var input = document.createElement("input");
202
+ input.type = "text";
203
+ input.className = "session-rename-input";
204
+ input.value = currentName || "Ralph Loop";
205
+
206
+ var originalHtml = textSpan.innerHTML;
207
+ textSpan.innerHTML = "";
208
+ textSpan.appendChild(input);
209
+ input.focus();
210
+ input.select();
211
+
212
+ function commitRename() {
213
+ var newName = input.value.trim();
214
+ if (newName && newName !== currentName && ctx.ws && ctx.connected) {
215
+ ctx.ws.send(JSON.stringify({ type: "loop_registry_rename", id: loopId, name: newName }));
216
+ }
217
+ textSpan.innerHTML = originalHtml;
218
+ if (newName && newName !== currentName) {
219
+ // Update text inline immediately
220
+ var nameNode = textSpan.querySelector(".session-loop-name");
221
+ if (nameNode) nameNode.textContent = newName;
222
+ }
223
+ }
224
+
225
+ input.addEventListener("keydown", function (e) {
226
+ if (e.key === "Enter") { e.preventDefault(); commitRename(); }
227
+ if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
228
+ });
229
+ input.addEventListener("blur", commitRename);
230
+ input.addEventListener("click", function (e) { e.stopPropagation(); });
231
+ }
232
+
233
+ function getDateGroup(ts) {
234
+ var now = new Date();
235
+ var d = new Date(ts);
236
+ var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
237
+ var yesterday = new Date(today.getTime() - 86400000);
238
+ var weekAgo = new Date(today.getTime() - 7 * 86400000);
239
+ if (d >= today) return "Today";
240
+ if (d >= yesterday) return "Yesterday";
241
+ if (d >= weekAgo) return "This Week";
242
+ return "Older";
243
+ }
244
+
245
+ function highlightMatch(text, query) {
246
+ if (!query) return escapeHtml(text);
247
+ var lower = text.toLowerCase();
248
+ var qLower = query.toLowerCase();
249
+ var idx = lower.indexOf(qLower);
250
+ if (idx === -1) return escapeHtml(text);
251
+ var before = text.substring(0, idx);
252
+ var match = text.substring(idx, idx + query.length);
253
+ var after = text.substring(idx + query.length);
254
+ return escapeHtml(before) + '<mark class="session-highlight">' + escapeHtml(match) + '</mark>' + escapeHtml(after);
255
+ }
256
+
257
+ function renderLoopChild(s) {
258
+ var el = document.createElement("div");
259
+ var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
260
+ var dimmed = searchMatchIds !== null && !isMatch;
261
+ el.className = "session-loop-child" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
262
+ el.dataset.sessionId = s.id;
263
+
264
+ var textSpan = document.createElement("span");
265
+ textSpan.className = "session-item-text";
266
+ var textHtml = "";
267
+ if (s.isProcessing) {
268
+ textHtml += '<span class="session-processing"></span>';
269
+ }
270
+ if (s.loop) {
271
+ var isRalphChild = s.loop.source === "ralph";
272
+ var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
273
+ var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
274
+ var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
275
+ textHtml += '<span class="session-loop-role-badge' + roleCls + '">' + roleName + iterSuffix + '</span>';
276
+ }
277
+ textSpan.innerHTML = textHtml;
278
+ el.appendChild(textSpan);
279
+
280
+ el.addEventListener("click", (function (id) {
281
+ return function () {
282
+ if (ctx.ws && ctx.connected) {
283
+ ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
284
+ closeSidebar();
285
+ }
286
+ };
287
+ })(s.id));
288
+
289
+ return el;
290
+ }
291
+
292
+ function renderLoopGroup(loopId, children, groupKey) {
293
+ var gk = groupKey || loopId;
294
+ // Sort children by iteration then role (coder before judge)
295
+ children.sort(function (a, b) {
296
+ var ai = (a.loop && a.loop.iteration) || 0;
297
+ var bi = (b.loop && b.loop.iteration) || 0;
298
+ if (ai !== bi) return ai - bi;
299
+ // coder before judge within same iteration
300
+ var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
301
+ var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
302
+ return ar - br;
303
+ });
304
+
305
+ var expanded = expandedLoopGroups.has(gk);
306
+ var hasActive = false;
307
+ var anyProcessing = false;
308
+ var latestSession = children[0];
309
+ for (var i = 0; i < children.length; i++) {
310
+ if (children[i].active) hasActive = true;
311
+ if (children[i].isProcessing) anyProcessing = true;
312
+ if ((children[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
313
+ latestSession = children[i];
314
+ }
315
+ }
316
+
317
+ var loopName = (children[0].loop && children[0].loop.name) || "Ralph Loop";
318
+ var isRalph = children[0].loop && children[0].loop.source === "ralph";
319
+ var isCrafting = false;
320
+ var maxIter = 0;
321
+ for (var j = 0; j < children.length; j++) {
322
+ var iter = (children[j].loop && children[j].loop.iteration) || 0;
323
+ if (iter > maxIter) maxIter = iter;
324
+ if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
325
+ }
326
+
327
+ var wrapper = document.createElement("div");
328
+ wrapper.className = "session-loop-wrapper";
329
+
330
+ // Group header row
331
+ var el = document.createElement("div");
332
+ el.className = "session-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
333
+ el.dataset.loopId = loopId;
334
+
335
+ var chevron = document.createElement("button");
336
+ chevron.className = "session-loop-chevron";
337
+ chevron.innerHTML = iconHtml("chevron-right");
338
+ chevron.addEventListener("click", (function (lid) {
339
+ return function (e) {
340
+ e.stopPropagation();
341
+ if (expandedLoopGroups.has(lid)) {
342
+ expandedLoopGroups.delete(lid);
343
+ } else {
344
+ expandedLoopGroups.add(lid);
345
+ }
346
+ renderSessionList(null);
347
+ };
348
+ })(gk));
349
+ el.appendChild(chevron);
350
+
351
+ var textSpan = document.createElement("span");
352
+ textSpan.className = "session-item-text";
353
+ var textHtml = "";
354
+ if (anyProcessing) {
355
+ textHtml += '<span class="session-processing"></span>';
356
+ }
357
+ var groupIcon = isRalph ? "repeat" : "calendar-clock";
358
+ textHtml += '<span class="session-loop-icon' + (isRalph ? "" : " scheduled") + '">' + iconHtml(groupIcon) + '</span>';
359
+ textHtml += '<span class="session-loop-name">' + escapeHtml(loopName) + '</span>';
360
+ if (isCrafting && children.length === 1) {
361
+ textHtml += '<span class="session-loop-badge crafting">Crafting</span>';
362
+ } else {
363
+ textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + children.length + '</span>';
364
+ }
365
+ textSpan.innerHTML = textHtml;
366
+ el.appendChild(textSpan);
367
+
368
+ // More button (ellipsis)
369
+ var moreBtn = document.createElement("button");
370
+ moreBtn.className = "session-more-btn";
371
+ moreBtn.innerHTML = iconHtml("ellipsis");
372
+ moreBtn.title = "More options";
373
+ moreBtn.addEventListener("click", (function (lid, name, count, btn) {
374
+ return function (e) {
375
+ e.stopPropagation();
376
+ showLoopCtxMenu(btn, lid, name, count);
377
+ };
378
+ })(loopId, loopName, children.length, moreBtn));
379
+ el.appendChild(moreBtn);
380
+
381
+ // Click row (not chevron/more) → switch to latest session
382
+ el.addEventListener("click", (function (id) {
383
+ return function () {
384
+ if (ctx.ws && ctx.connected) {
385
+ ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
386
+ closeSidebar();
387
+ }
388
+ };
389
+ })(latestSession.id));
390
+
391
+ wrapper.appendChild(el);
392
+
393
+ // Expanded children
394
+ if (expanded) {
395
+ var childContainer = document.createElement("div");
396
+ childContainer.className = "session-loop-children";
397
+ for (var k = 0; k < children.length; k++) {
398
+ childContainer.appendChild(renderLoopChild(children[k]));
399
+ }
400
+ wrapper.appendChild(childContainer);
401
+ }
402
+
403
+ return wrapper;
404
+ }
405
+
406
+ function renderSessionItem(s) {
407
+ var el = document.createElement("div");
408
+ var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
409
+ var dimmed = searchMatchIds !== null && !isMatch;
410
+ el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
411
+ el.dataset.sessionId = s.id;
412
+
413
+ var textSpan = document.createElement("span");
414
+ textSpan.className = "session-item-text";
415
+ var textHtml = "";
416
+ if (s.isProcessing) {
417
+ textHtml += '<span class="session-processing"></span>';
418
+ }
419
+ if (ctx.multiUser && s.sessionVisibility === "private") {
420
+ textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
421
+ }
422
+ textHtml += highlightMatch(s.title || "New Session", searchQuery);
423
+ textSpan.innerHTML = textHtml;
424
+ el.appendChild(textSpan);
425
+
426
+ var moreBtn = document.createElement("button");
427
+ moreBtn.className = "session-more-btn";
428
+ moreBtn.innerHTML = iconHtml("ellipsis");
429
+ moreBtn.title = "More options";
430
+ moreBtn.addEventListener("click", (function(id, title, cliSid, btn, sData) {
431
+ return function(e) {
432
+ e.stopPropagation();
433
+ showSessionCtxMenu(btn, id, title, cliSid, sData);
434
+ };
435
+ })(s.id, s.title, s.cliSessionId, moreBtn, s));
436
+ el.appendChild(moreBtn);
437
+
438
+ el.addEventListener("click", (function (id) {
439
+ return function () {
440
+ if (ctx.ws && ctx.connected) {
441
+ ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
442
+ closeSidebar();
443
+ }
444
+ };
445
+ })(s.id));
446
+
447
+ // Presence avatars (multi-user)
448
+ renderPresenceAvatars(el, String(s.id));
449
+
450
+ return el;
451
+ }
452
+
453
+ export function renderSessionList(sessions) {
454
+ if (sessions) cachedSessions = sessions;
455
+
456
+ ctx.sessionListEl.innerHTML = "";
457
+
458
+ // Partition: loop sessions vs normal sessions
459
+ // Group by loopId + startedAt so different runs of the same task are separate groups
460
+ var loopGroups = {}; // groupKey -> [sessions]
461
+ var normalSessions = [];
462
+ for (var i = 0; i < cachedSessions.length; i++) {
463
+ var s = cachedSessions[i];
464
+ if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph") {
465
+ // Task crafting sessions live in the scheduler calendar, not the main list
466
+ continue;
467
+ } else if (s.loop && s.loop.loopId) {
468
+ var groupKey = s.loop.loopId + ":" + (s.loop.startedAt || 0);
469
+ if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
470
+ loopGroups[groupKey].push(s);
471
+ } else {
472
+ normalSessions.push(s);
473
+ }
474
+ }
475
+
476
+ // Build virtual items: normal sessions + one entry per loop group (using latest child's lastActivity)
477
+ var items = [];
478
+ for (var j = 0; j < normalSessions.length; j++) {
479
+ items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
480
+ }
481
+ var groupKeys = Object.keys(loopGroups);
482
+ for (var k = 0; k < groupKeys.length; k++) {
483
+ var gk = groupKeys[k];
484
+ var children = loopGroups[gk];
485
+ var realLoopId = children[0].loop.loopId;
486
+ var maxActivity = 0;
487
+ for (var m = 0; m < children.length; m++) {
488
+ var act = children[m].lastActivity || 0;
489
+ if (act > maxActivity) maxActivity = act;
490
+ }
491
+ items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
492
+ }
493
+
494
+ // Sort by lastActivity descending
495
+ items.sort(function (a, b) {
496
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
497
+ });
498
+
499
+ var currentGroup = "";
500
+ for (var n = 0; n < items.length; n++) {
501
+ var item = items[n];
502
+ var group = getDateGroup(item.lastActivity || 0);
503
+ if (group !== currentGroup) {
504
+ currentGroup = group;
505
+ var header = document.createElement("div");
506
+ header.className = "session-group-header";
507
+ header.textContent = group;
508
+ ctx.sessionListEl.appendChild(header);
509
+ }
510
+ if (item.type === "loop") {
511
+ ctx.sessionListEl.appendChild(renderLoopGroup(item.loopId, item.children, item.groupKey));
512
+ } else {
513
+ ctx.sessionListEl.appendChild(renderSessionItem(item.data));
514
+ }
515
+ }
516
+ refreshIcons();
517
+ updatePageTitle();
518
+ }
519
+
520
+ export function handleSearchResults(msg) {
521
+ if (msg.query !== searchQuery) return; // stale response
522
+ var ids = new Set();
523
+ for (var i = 0; i < msg.results.length; i++) {
524
+ ids.add(msg.results[i].id);
525
+ }
526
+ searchMatchIds = ids;
527
+ renderSessionList(null);
528
+
529
+ // Build timeline for current session if it matches
530
+ var activeEl = ctx.sessionListEl.querySelector(".session-item.active");
531
+ if (activeEl) {
532
+ var activeId = parseInt(activeEl.dataset.sessionId, 10);
533
+ if (ids.has(activeId)) {
534
+ buildSearchTimeline(searchQuery);
535
+ } else {
536
+ removeSearchTimeline();
537
+ }
538
+ }
539
+ }
540
+
541
+ export function updateSessionPresence(presence) {
542
+ sessionPresence = presence;
543
+ // Update presence avatars on existing session items without full re-render
544
+ var items = ctx.sessionListEl.querySelectorAll("[data-session-id]");
545
+ for (var i = 0; i < items.length; i++) {
546
+ renderPresenceAvatars(items[i], items[i].dataset.sessionId);
547
+ }
548
+ }
549
+
550
+ function presenceAvatarUrl(style, seed) {
551
+ var s = encodeURIComponent(seed || "anonymous");
552
+ return "https://api.dicebear.com/9.x/" + (style || "thumbs") + "/svg?seed=" + s + "&size=24";
553
+ }
554
+
555
+ function renderPresenceAvatars(el, sessionId) {
556
+ // Remove existing presence container
557
+ var existing = el.querySelector(".session-presence");
558
+ if (existing) existing.remove();
559
+
560
+ var users = sessionPresence[sessionId];
561
+ if (!users || users.length === 0) return;
562
+
563
+ var container = document.createElement("span");
564
+ container.className = "session-presence";
565
+
566
+ var max = 3;
567
+ var shown = users.length > max ? max : users.length;
568
+ for (var i = 0; i < shown; i++) {
569
+ var u = users[i];
570
+ var img = document.createElement("img");
571
+ img.className = "session-presence-avatar";
572
+ img.src = presenceAvatarUrl(u.avatarStyle, u.avatarSeed);
573
+ img.alt = u.displayName;
574
+ img.dataset.tip = u.displayName + (u.username ? " (@" + u.username + ")" : "");
575
+ if (i > 0) img.style.marginLeft = "-6px";
576
+ container.appendChild(img);
577
+ }
578
+ if (users.length > max) {
579
+ var more = document.createElement("span");
580
+ more.className = "session-presence-more";
581
+ more.textContent = "+" + (users.length - max);
582
+ container.appendChild(more);
583
+ }
584
+
585
+ // Insert before the more-btn
586
+ var moreBtn = el.querySelector(".session-more-btn");
587
+ if (moreBtn) {
588
+ el.insertBefore(container, moreBtn);
589
+ } else {
590
+ el.appendChild(container);
591
+ }
592
+ }
593
+
594
+ export function updatePageTitle() {
595
+ var sessionTitle = "";
596
+ var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
597
+ if (activeItem) sessionTitle = activeItem.textContent;
598
+ if (ctx.headerTitleEl) {
599
+ ctx.headerTitleEl.textContent = sessionTitle || ctx.projectName || "Clay";
600
+ }
601
+ var tbProjectName = ctx.$("title-bar-project-name");
602
+ if (tbProjectName && ctx.projectName) {
603
+ tbProjectName.textContent = ctx.projectName;
604
+ } else if (tbProjectName && !tbProjectName.textContent) {
605
+ // Fallback: derive name from URL slug when projectName not yet available
606
+ var _m = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
607
+ if (_m) tbProjectName.textContent = _m[1];
608
+ }
609
+ if (ctx.projectName && sessionTitle) {
610
+ document.title = sessionTitle + " - " + ctx.projectName;
611
+ } else if (ctx.projectName) {
612
+ document.title = ctx.projectName + " - Clay";
613
+ } else {
614
+ document.title = "Clay";
615
+ }
616
+ }
617
+
618
+ export function openSidebar() {
619
+ ctx.sidebar.classList.add("open");
620
+ ctx.sidebarOverlay.classList.add("visible");
621
+ }
622
+
623
+ export function closeSidebar() {
624
+ ctx.sidebar.classList.remove("open");
625
+ ctx.sidebarOverlay.classList.remove("visible");
626
+ }
627
+
628
+ // --- Mobile sheet (fullscreen overlay for Projects / Sessions) ---
629
+
630
+ function openMobileSheet(type) {
631
+ var sheet = document.getElementById("mobile-sheet");
632
+ if (!sheet) return;
633
+
634
+ var titleEl = sheet.querySelector(".mobile-sheet-title");
635
+ var listEl = sheet.querySelector(".mobile-sheet-list");
636
+ if (!titleEl || !listEl) return;
637
+
638
+ // Return file tree to sidebar before clearing (prevents destroying it)
639
+ if (sheet.classList.contains("sheet-files")) {
640
+ var prevFileTree = document.getElementById("file-tree");
641
+ var prevPanel = document.getElementById("sidebar-panel-files");
642
+ if (prevFileTree && prevPanel) prevPanel.appendChild(prevFileTree);
643
+ }
644
+
645
+ listEl.innerHTML = "";
646
+ sheet.classList.remove("sheet-files");
647
+
648
+ if (type === "projects") {
649
+ titleEl.textContent = "Projects";
650
+ renderSheetProjects(listEl);
651
+ } else if (type === "sessions") {
652
+ titleEl.textContent = "Sessions";
653
+ renderSheetSessions(listEl);
654
+ } else if (type === "files") {
655
+ titleEl.textContent = "Files";
656
+ sheet.classList.add("sheet-files");
657
+ var fileTree = document.getElementById("file-tree");
658
+ if (fileTree) {
659
+ listEl.appendChild(fileTree);
660
+ fileTree.classList.remove("hidden");
661
+ }
662
+ if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
663
+ }
664
+
665
+ sheet.classList.remove("hidden", "closing");
666
+ refreshIcons();
667
+ }
668
+
669
+ function closeMobileSheet() {
670
+ var sheet = document.getElementById("mobile-sheet");
671
+ if (!sheet || sheet.classList.contains("hidden")) return;
672
+
673
+ // Return file tree to sidebar if it was moved
674
+ if (sheet.classList.contains("sheet-files")) {
675
+ var fileTree = document.getElementById("file-tree");
676
+ var sidebarFilesPanel = document.getElementById("sidebar-panel-files");
677
+ if (fileTree && sidebarFilesPanel) {
678
+ sidebarFilesPanel.appendChild(fileTree);
679
+ }
680
+ }
681
+
682
+ sheet.classList.add("closing");
683
+ setTimeout(function () {
684
+ sheet.classList.add("hidden");
685
+ sheet.classList.remove("closing", "sheet-files");
686
+ }, 230);
687
+ }
688
+
689
+ function renderSheetProjects(listEl) {
690
+ for (var i = 0; i < cachedProjectList.length; i++) {
691
+ (function (p) {
692
+ var el = document.createElement("button");
693
+ el.className = "mobile-project-item" + (p.slug === cachedCurrentSlug ? " active" : "");
694
+
695
+ var abbrev = document.createElement("span");
696
+ abbrev.className = "mobile-project-abbrev";
697
+ abbrev.textContent = getProjectAbbrev(p.name);
698
+ el.appendChild(abbrev);
699
+
700
+ var name = document.createElement("span");
701
+ name.className = "mobile-project-name";
702
+ name.textContent = p.name;
703
+ el.appendChild(name);
704
+
705
+ if (p.isProcessing) {
706
+ var dot = document.createElement("span");
707
+ dot.className = "mobile-project-processing";
708
+ el.appendChild(dot);
709
+ }
710
+
711
+ el.addEventListener("click", function () {
712
+ if (ctx.switchProject) ctx.switchProject(p.slug);
713
+ closeMobileSheet();
714
+ });
715
+
716
+ listEl.appendChild(el);
717
+ })(cachedProjectList[i]);
718
+ }
719
+ }
720
+
721
+ function renderSheetSessions(listEl) {
722
+ // New session button at top
723
+ var newBtn = document.createElement("button");
724
+ newBtn.className = "mobile-session-new";
725
+ newBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
726
+ newBtn.addEventListener("click", function () {
727
+ if (ctx.ws && ctx.connected) {
728
+ ctx.ws.send(JSON.stringify({ type: "new_session" }));
729
+ }
730
+ closeMobileSheet();
731
+ });
732
+ listEl.appendChild(newBtn);
733
+
734
+ var sorted = cachedSessions.slice().sort(function (a, b) {
735
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
736
+ });
737
+
738
+ var currentGroup = "";
739
+ for (var i = 0; i < sorted.length; i++) {
740
+ var s = sorted[i];
741
+ var group = getDateGroup(s.lastActivity || 0);
742
+ if (group !== currentGroup) {
743
+ currentGroup = group;
744
+ var header = document.createElement("div");
745
+ header.className = "mobile-sheet-group";
746
+ header.textContent = group;
747
+ listEl.appendChild(header);
748
+ }
749
+
750
+ var el = document.createElement("button");
751
+ el.className = "mobile-session-item" + (s.active ? " active" : "");
752
+
753
+ var titleSpan = document.createElement("span");
754
+ titleSpan.className = "mobile-session-title";
755
+ titleSpan.textContent = s.title || "New Session";
756
+ el.appendChild(titleSpan);
757
+
758
+ if (s.isProcessing) {
759
+ var dot = document.createElement("span");
760
+ dot.className = "mobile-session-processing";
761
+ el.appendChild(dot);
762
+ }
763
+
764
+ (function (id) {
765
+ el.addEventListener("click", function () {
766
+ if (ctx.ws && ctx.connected) {
767
+ ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
768
+ }
769
+ closeMobileSheet();
770
+ });
771
+ })(s.id);
772
+
773
+ listEl.appendChild(el);
774
+ }
775
+ }
776
+
777
+ export function initSidebar(_ctx) {
778
+ ctx = _ctx;
779
+
780
+ document.addEventListener("click", function () { closeSessionCtxMenu(); });
781
+
782
+ ctx.hamburgerBtn.addEventListener("click", function () {
783
+ ctx.sidebar.classList.contains("open") ? closeSidebar() : openSidebar();
784
+ });
785
+
786
+ ctx.sidebarOverlay.addEventListener("click", closeSidebar);
787
+
788
+ // --- Desktop sidebar collapse/expand ---
789
+ function toggleSidebarCollapse() {
790
+ var layout = ctx.$("layout");
791
+ var collapsed = layout.classList.toggle("sidebar-collapsed");
792
+ try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
793
+ setTimeout(function () { syncUserIslandWidth(); syncResizeHandle(); }, 210);
794
+ }
795
+
796
+ if (ctx.sidebarToggleBtn) ctx.sidebarToggleBtn.addEventListener("click", toggleSidebarCollapse);
797
+ if (ctx.sidebarExpandBtn) ctx.sidebarExpandBtn.addEventListener("click", toggleSidebarCollapse);
798
+
799
+ // Restore collapsed state from localStorage
800
+ try {
801
+ if (localStorage.getItem("sidebar-collapsed") === "1") {
802
+ ctx.$("layout").classList.add("sidebar-collapsed");
803
+ }
804
+ } catch (e) {}
805
+
806
+ ctx.newSessionBtn.addEventListener("click", function () {
807
+ if (ctx.ws && ctx.connected) {
808
+ ctx.ws.send(JSON.stringify({ type: "new_session" }));
809
+ closeSidebar();
810
+ }
811
+ });
812
+
813
+ // --- New Ralph Loop button ---
814
+ var newRalphBtn = ctx.$("new-ralph-btn");
815
+ if (newRalphBtn) {
816
+ newRalphBtn.addEventListener("click", function () {
817
+ if (ctx.openRalphWizard) ctx.openRalphWizard();
818
+ });
819
+ }
820
+
821
+ // --- Session search ---
822
+ var searchBtn = ctx.$("search-session-btn");
823
+ var searchBox = ctx.$("session-search");
824
+ var searchInput = ctx.$("session-search-input");
825
+ var searchClear = ctx.$("session-search-clear");
826
+
827
+ function openSearch() {
828
+ searchBox.classList.remove("hidden");
829
+ searchBtn.classList.add("active");
830
+ searchInput.value = "";
831
+ searchQuery = "";
832
+ setTimeout(function () { searchInput.focus(); }, 50);
833
+ }
834
+
835
+ function closeSearch() {
836
+ searchBox.classList.add("hidden");
837
+ searchBtn.classList.remove("active");
838
+ searchInput.value = "";
839
+ searchQuery = "";
840
+ searchMatchIds = null;
841
+ if (searchDebounce) { clearTimeout(searchDebounce); searchDebounce = null; }
842
+ removeSearchTimeline();
843
+ renderSessionList(null);
844
+ }
845
+
846
+ searchBtn.addEventListener("click", function () {
847
+ if (searchBox.classList.contains("hidden")) {
848
+ openSearch();
849
+ } else {
850
+ closeSearch();
851
+ }
852
+ });
853
+
854
+ if (searchClear) {
855
+ searchClear.addEventListener("click", function () {
856
+ closeSearch();
857
+ });
858
+ }
859
+
860
+ searchInput.addEventListener("input", function () {
861
+ searchQuery = searchInput.value.trim();
862
+ if (searchDebounce) clearTimeout(searchDebounce);
863
+ if (!searchQuery) {
864
+ searchMatchIds = null;
865
+ removeSearchTimeline();
866
+ renderSessionList(null);
867
+ return;
868
+ }
869
+ searchDebounce = setTimeout(function () {
870
+ if (ctx.ws && ctx.connected) {
871
+ ctx.ws.send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
872
+ }
873
+ }, 200);
874
+ });
875
+
876
+ searchInput.addEventListener("keydown", function (e) {
877
+ if (e.key === "Escape") {
878
+ e.preventDefault();
879
+ closeSearch();
880
+ }
881
+ });
882
+
883
+ // --- Resume session picker ---
884
+ var resumeModal = ctx.$("resume-modal");
885
+ var resumeCancel = ctx.$("resume-cancel");
886
+ var pickerLoading = ctx.$("resume-picker-loading");
887
+ var pickerEmpty = ctx.$("resume-picker-empty");
888
+ var pickerList = ctx.$("resume-picker-list");
889
+
890
+ function openResumeModal() {
891
+ resumeModal.classList.remove("hidden");
892
+ pickerLoading.classList.remove("hidden");
893
+ pickerEmpty.classList.add("hidden");
894
+ pickerList.classList.add("hidden");
895
+ pickerList.innerHTML = "";
896
+ if (ctx.ws && ctx.connected) {
897
+ ctx.ws.send(JSON.stringify({ type: "list_cli_sessions" }));
898
+ }
899
+ }
900
+
901
+ function closeResumeModal() {
902
+ resumeModal.classList.add("hidden");
903
+ }
904
+
905
+ ctx.resumeSessionBtn.addEventListener("click", openResumeModal);
906
+ resumeCancel.addEventListener("click", closeResumeModal);
907
+ resumeModal.querySelector(".confirm-backdrop").addEventListener("click", closeResumeModal);
908
+
909
+ // --- Panel switch (sessions / files / projects) ---
910
+ var fileBrowserBtn = ctx.$("file-browser-btn");
911
+ var projectsPanel = ctx.$("sidebar-panel-projects");
912
+ var sessionsPanel = ctx.$("sidebar-panel-sessions");
913
+ var filesPanel = ctx.$("sidebar-panel-files");
914
+ var sessionsHeaderContent = ctx.$("sessions-header-content");
915
+ var filesHeaderContent = ctx.$("files-header-content");
916
+ var filePanelClose = ctx.$("file-panel-close");
917
+
918
+ function hideAllPanels() {
919
+ if (projectsPanel) projectsPanel.classList.add("hidden");
920
+ if (sessionsPanel) sessionsPanel.classList.add("hidden");
921
+ if (filesPanel) filesPanel.classList.add("hidden");
922
+ if (sessionsHeaderContent) sessionsHeaderContent.classList.add("hidden");
923
+ if (filesHeaderContent) filesHeaderContent.classList.add("hidden");
924
+ }
925
+
926
+ function showProjectsPanel() {
927
+ hideAllPanels();
928
+ if (projectsPanel) projectsPanel.classList.remove("hidden");
929
+ }
930
+
931
+ function showSessionsPanel() {
932
+ hideAllPanels();
933
+ if (sessionsPanel) sessionsPanel.classList.remove("hidden");
934
+ if (sessionsHeaderContent) sessionsHeaderContent.classList.remove("hidden");
935
+ }
936
+
937
+ function showFilesPanel() {
938
+ hideAllPanels();
939
+ if (filesPanel) filesPanel.classList.remove("hidden");
940
+ if (filesHeaderContent) filesHeaderContent.classList.remove("hidden");
941
+ if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
942
+ }
943
+
944
+ if (fileBrowserBtn) {
945
+ fileBrowserBtn.addEventListener("click", showFilesPanel);
946
+ }
947
+ if (filePanelClose) {
948
+ filePanelClose.addEventListener("click", showSessionsPanel);
949
+ }
950
+
951
+ // --- Mobile sheet close handlers ---
952
+ var mobileSheet = document.getElementById("mobile-sheet");
953
+ if (mobileSheet) {
954
+ var sheetBackdrop = mobileSheet.querySelector(".mobile-sheet-backdrop");
955
+ var sheetCloseBtn = mobileSheet.querySelector(".mobile-sheet-close");
956
+ if (sheetBackdrop) sheetBackdrop.addEventListener("click", closeMobileSheet);
957
+ if (sheetCloseBtn) sheetCloseBtn.addEventListener("click", closeMobileSheet);
958
+ }
959
+
960
+ // --- Mobile tab bar ---
961
+ var mobileTabBar = document.getElementById("mobile-tab-bar");
962
+ var mobileTabs = mobileTabBar ? mobileTabBar.querySelectorAll(".mobile-tab") : [];
963
+ var mobileHomeBtn = document.getElementById("mobile-home-btn");
964
+
965
+ function setMobileTabActive(tabName) {
966
+ for (var i = 0; i < mobileTabs.length; i++) {
967
+ if (mobileTabs[i].dataset.tab === tabName) {
968
+ mobileTabs[i].classList.add("active");
969
+ } else {
970
+ mobileTabs[i].classList.remove("active");
971
+ }
972
+ }
973
+ }
974
+
975
+ for (var t = 0; t < mobileTabs.length; t++) {
976
+ (function (tab) {
977
+ tab.addEventListener("click", function () {
978
+ var name = tab.dataset.tab;
979
+
980
+ if (name === "terminal") {
981
+ closeSidebar();
982
+ setMobileTabActive("");
983
+ if (ctx.openTerminal) ctx.openTerminal();
984
+ return;
985
+ }
986
+
987
+ if (name === "projects") {
988
+ openMobileSheet("projects");
989
+ setMobileTabActive("projects");
990
+ } else if (name === "sessions") {
991
+ openMobileSheet("sessions");
992
+ setMobileTabActive("sessions");
993
+ } else if (name === "files") {
994
+ openMobileSheet("files");
995
+ setMobileTabActive("files");
996
+ }
997
+ });
998
+ })(mobileTabs[t]);
999
+ }
1000
+
1001
+ if (mobileHomeBtn) {
1002
+ mobileHomeBtn.addEventListener("click", function () {
1003
+ closeSidebar();
1004
+ setMobileTabActive("");
1005
+ if (ctx.showHomeHub) ctx.showHomeHub();
1006
+ });
1007
+ }
1008
+
1009
+ // --- User island width sync ---
1010
+ var userIsland = document.getElementById("user-island");
1011
+ var sidebarColumn = document.getElementById("sidebar-column");
1012
+
1013
+ function syncUserIslandWidth() {
1014
+ if (!userIsland || !sidebarColumn) return;
1015
+ var rect = sidebarColumn.getBoundingClientRect();
1016
+ userIsland.style.width = (rect.right - 8 - 8) + "px";
1017
+ }
1018
+
1019
+ // --- Sidebar resize handle ---
1020
+ var resizeHandle = document.getElementById("sidebar-resize-handle");
1021
+
1022
+ function syncResizeHandle() {
1023
+ if (!resizeHandle || !sidebarColumn) return;
1024
+ var rect = sidebarColumn.getBoundingClientRect();
1025
+ var parentRect = sidebarColumn.parentElement.getBoundingClientRect();
1026
+ resizeHandle.style.left = (rect.right - parentRect.left) + "px";
1027
+ }
1028
+
1029
+ if (resizeHandle && sidebarColumn) {
1030
+ var dragging = false;
1031
+
1032
+ function onResizeMove(e) {
1033
+ if (!dragging) return;
1034
+ e.preventDefault();
1035
+ var clientX = e.touches ? e.touches[0].clientX : e.clientX;
1036
+ var iconStrip = document.getElementById("icon-strip");
1037
+ var stripWidth = iconStrip ? iconStrip.offsetWidth : 72;
1038
+ var newWidth = clientX - stripWidth;
1039
+ if (newWidth < 192) newWidth = 192;
1040
+ if (newWidth > 320) newWidth = 320;
1041
+ sidebarColumn.style.width = newWidth + "px";
1042
+ syncResizeHandle();
1043
+ syncUserIslandWidth();
1044
+ }
1045
+
1046
+ function onResizeEnd() {
1047
+ if (!dragging) return;
1048
+ dragging = false;
1049
+ resizeHandle.classList.remove("dragging");
1050
+ document.body.style.cursor = "";
1051
+ document.body.style.userSelect = "";
1052
+ document.removeEventListener("mousemove", onResizeMove);
1053
+ document.removeEventListener("mouseup", onResizeEnd);
1054
+ document.removeEventListener("touchmove", onResizeMove);
1055
+ document.removeEventListener("touchend", onResizeEnd);
1056
+ try { localStorage.setItem("sidebar-width", sidebarColumn.style.width); } catch (e) {}
1057
+ }
1058
+
1059
+ function onResizeStart(e) {
1060
+ e.preventDefault();
1061
+ dragging = true;
1062
+ resizeHandle.classList.add("dragging");
1063
+ document.body.style.cursor = "col-resize";
1064
+ document.body.style.userSelect = "none";
1065
+ document.addEventListener("mousemove", onResizeMove);
1066
+ document.addEventListener("mouseup", onResizeEnd);
1067
+ document.addEventListener("touchmove", onResizeMove, { passive: false });
1068
+ document.addEventListener("touchend", onResizeEnd);
1069
+ }
1070
+
1071
+ resizeHandle.addEventListener("mousedown", onResizeStart);
1072
+ resizeHandle.addEventListener("touchstart", onResizeStart, { passive: false });
1073
+
1074
+ // Restore saved width (skip transition so user-island syncs immediately)
1075
+ try {
1076
+ var savedWidth = localStorage.getItem("sidebar-width");
1077
+ if (savedWidth) {
1078
+ var px = parseInt(savedWidth, 10);
1079
+ if (px >= 192 && px <= 320) {
1080
+ sidebarColumn.style.transition = "none";
1081
+ sidebarColumn.style.width = px + "px";
1082
+ sidebarColumn.offsetWidth; // force reflow
1083
+ sidebarColumn.style.transition = "";
1084
+ }
1085
+ }
1086
+ } catch (e) {}
1087
+
1088
+ syncResizeHandle();
1089
+ syncUserIslandWidth();
1090
+ }
1091
+
1092
+ // Initial sync even if no resize handle
1093
+ syncUserIslandWidth();
1094
+
1095
+ // --- Schedule countdown timer ---
1096
+ startCountdownTimer();
1097
+ }
1098
+
1099
+ function startCountdownTimer() {
1100
+ if (countdownTimer) clearInterval(countdownTimer);
1101
+ countdownTimer = setInterval(updateCountdowns, 1000);
1102
+ }
1103
+
1104
+ function updateCountdowns() {
1105
+ if (!ctx || !ctx.getUpcomingSchedules || !ctx.sessionListEl) return;
1106
+ var upcoming = ctx.getUpcomingSchedules(3 * 60 * 1000); // 3 minutes
1107
+
1108
+ // Remove stale container
1109
+ if (countdownContainer && !ctx.sessionListEl.contains(countdownContainer)) {
1110
+ countdownContainer = null;
1111
+ }
1112
+
1113
+ if (upcoming.length === 0) {
1114
+ if (countdownContainer) {
1115
+ countdownContainer.remove();
1116
+ countdownContainer = null;
1117
+ }
1118
+ return;
1119
+ }
1120
+
1121
+ if (!countdownContainer) {
1122
+ countdownContainer = document.createElement("div");
1123
+ countdownContainer.className = "session-countdown-group";
1124
+ ctx.sessionListEl.insertBefore(countdownContainer, ctx.sessionListEl.firstChild);
1125
+ }
1126
+
1127
+ var html = "";
1128
+ var now = Date.now();
1129
+ for (var i = 0; i < upcoming.length; i++) {
1130
+ var u = upcoming[i];
1131
+ var remaining = Math.max(0, Math.ceil((u.nextRunAt - now) / 1000));
1132
+ var min = Math.floor(remaining / 60);
1133
+ var sec = remaining % 60;
1134
+ var timeStr = min + ":" + (sec < 10 ? "0" : "") + sec;
1135
+ var colorStyle = u.color ? " style=\"border-left-color:" + u.color + "\"" : "";
1136
+ html += '<div class="session-countdown-item"' + colorStyle + '>';
1137
+ html += '<span class="session-countdown-name">' + escapeHtml(u.name) + '</span>';
1138
+ html += '<span class="session-countdown-badge">' + timeStr + '</span>';
1139
+ html += '</div>';
1140
+ }
1141
+ countdownContainer.innerHTML = html;
1142
+ }
1143
+
1144
+ // --- CLI session picker ---
1145
+ function relativeTime(isoString) {
1146
+ if (!isoString) return "";
1147
+ var ms = Date.now() - new Date(isoString).getTime();
1148
+ var sec = Math.floor(ms / 1000);
1149
+ if (sec < 60) return "just now";
1150
+ var min = Math.floor(sec / 60);
1151
+ if (min < 60) return min + "m ago";
1152
+ var hr = Math.floor(min / 60);
1153
+ if (hr < 24) return hr + "h ago";
1154
+ var days = Math.floor(hr / 24);
1155
+ if (days < 30) return days + "d ago";
1156
+ return new Date(isoString).toLocaleDateString();
1157
+ }
1158
+
1159
+ export function populateCliSessionList(sessions) {
1160
+ var pickerLoading = ctx.$("resume-picker-loading");
1161
+ var pickerEmpty = ctx.$("resume-picker-empty");
1162
+ var pickerList = ctx.$("resume-picker-list");
1163
+ if (!pickerLoading || !pickerList) return;
1164
+
1165
+ pickerLoading.classList.add("hidden");
1166
+
1167
+ if (!sessions || sessions.length === 0) {
1168
+ pickerEmpty.classList.remove("hidden");
1169
+ pickerList.classList.add("hidden");
1170
+ return;
1171
+ }
1172
+
1173
+ pickerEmpty.classList.add("hidden");
1174
+ pickerList.classList.remove("hidden");
1175
+ pickerList.innerHTML = "";
1176
+
1177
+ for (var i = 0; i < sessions.length; i++) {
1178
+ var s = sessions[i];
1179
+ var item = document.createElement("div");
1180
+ item.className = "cli-session-item";
1181
+
1182
+ var title = document.createElement("div");
1183
+ title.className = "cli-session-title";
1184
+ title.textContent = s.firstPrompt || "Untitled session";
1185
+ item.appendChild(title);
1186
+
1187
+ var meta = document.createElement("div");
1188
+ meta.className = "cli-session-meta";
1189
+ if (s.lastActivity) {
1190
+ var time = document.createElement("span");
1191
+ time.textContent = relativeTime(s.lastActivity);
1192
+ meta.appendChild(time);
1193
+ }
1194
+ if (s.model) {
1195
+ var model = document.createElement("span");
1196
+ model.className = "badge";
1197
+ model.textContent = s.model;
1198
+ meta.appendChild(model);
1199
+ }
1200
+ if (s.gitBranch) {
1201
+ var branch = document.createElement("span");
1202
+ branch.className = "badge";
1203
+ branch.textContent = s.gitBranch;
1204
+ meta.appendChild(branch);
1205
+ }
1206
+ item.appendChild(meta);
1207
+
1208
+ (function (sessionId) {
1209
+ item.addEventListener("click", function () {
1210
+ if (ctx.ws && ctx.connected) {
1211
+ ctx.ws.send(JSON.stringify({ type: "resume_session", cliSessionId: sessionId }));
1212
+ }
1213
+ var modal = ctx.$("resume-modal");
1214
+ if (modal) modal.classList.add("hidden");
1215
+ closeSidebar();
1216
+ });
1217
+ })(s.sessionId);
1218
+
1219
+ pickerList.appendChild(item);
1220
+ }
1221
+ }
1222
+
1223
+ // --- Search hit timeline (right-side markers) ---
1224
+ var searchTimelineScrollHandler = null;
1225
+ var activeSearchQuery = ""; // query active in the timeline
1226
+
1227
+ export function getActiveSearchQuery() {
1228
+ return searchQuery;
1229
+ }
1230
+
1231
+ export function buildSearchTimeline(query) {
1232
+ removeSearchTimeline();
1233
+ if (!query) return;
1234
+ activeSearchQuery = query;
1235
+
1236
+ var q = query.toLowerCase();
1237
+ var messagesEl = ctx.messagesEl;
1238
+
1239
+ // Collect all message elements that contain the query
1240
+ var allMsgs = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
1241
+ var hits = [];
1242
+ for (var i = 0; i < allMsgs.length; i++) {
1243
+ var msgEl = allMsgs[i];
1244
+ var textEl = msgEl.querySelector(".bubble") || msgEl.querySelector(".md-content");
1245
+ if (!textEl) continue;
1246
+ var text = textEl.textContent || "";
1247
+ if (text.toLowerCase().indexOf(q) === -1) continue;
1248
+
1249
+ // Extract a snippet around the match
1250
+ var idx = text.toLowerCase().indexOf(q);
1251
+ var start = Math.max(0, idx - 10);
1252
+ var end = Math.min(text.length, idx + query.length + 10);
1253
+ var snippet = (start > 0 ? "\u2026" : "") + text.substring(start, end) + (end < text.length ? "\u2026" : "");
1254
+ hits.push({ el: msgEl, snippet: snippet });
1255
+ }
1256
+
1257
+ if (hits.length === 0) return;
1258
+
1259
+ var timeline = document.createElement("div");
1260
+ timeline.className = "search-timeline";
1261
+ timeline.id = "search-timeline";
1262
+
1263
+ var track = document.createElement("div");
1264
+ track.className = "rewind-timeline-track";
1265
+
1266
+ var viewport = document.createElement("div");
1267
+ viewport.className = "rewind-timeline-viewport";
1268
+ track.appendChild(viewport);
1269
+
1270
+ for (var i = 0; i < hits.length; i++) {
1271
+ var hit = hits[i];
1272
+ var pct = hits.length === 1 ? 50 : 6 + (i / (hits.length - 1)) * 88;
1273
+
1274
+ var snippetText = hit.snippet;
1275
+ if (snippetText.length > 24) snippetText = snippetText.substring(0, 24) + "\u2026";
1276
+
1277
+ var marker = document.createElement("div");
1278
+ marker.className = "rewind-timeline-marker search-hit-marker";
1279
+ marker.innerHTML = iconHtml("search") + '<span class="marker-text">' + escapeHtml(snippetText) + '</span>';
1280
+ marker.style.top = pct + "%";
1281
+ marker.dataset.offsetTop = hit.el.offsetTop;
1282
+
1283
+ (function(targetEl, markerEl) {
1284
+ markerEl.addEventListener("click", function() {
1285
+ targetEl.scrollIntoView({ behavior: "smooth", block: "center" });
1286
+ targetEl.classList.remove("search-blink");
1287
+ void targetEl.offsetWidth; // force reflow
1288
+ targetEl.classList.add("search-blink");
1289
+ });
1290
+ })(hit.el, marker);
1291
+
1292
+ track.appendChild(marker);
1293
+ }
1294
+
1295
+ timeline.appendChild(track);
1296
+
1297
+ // Position to align with messages area
1298
+ var appEl = ctx.$("app");
1299
+ var titleBarEl = document.querySelector(".title-bar-content");
1300
+ var inputAreaEl = ctx.$("input-area");
1301
+ var appRect = appEl.getBoundingClientRect();
1302
+ var titleBarRect = titleBarEl ? titleBarEl.getBoundingClientRect() : { bottom: appRect.top };
1303
+ var inputRect = inputAreaEl.getBoundingClientRect();
1304
+
1305
+ timeline.style.top = (titleBarRect.bottom - appRect.top + 4) + "px";
1306
+ timeline.style.bottom = (appRect.bottom - inputRect.top + 4) + "px";
1307
+
1308
+ appEl.appendChild(timeline);
1309
+ refreshIcons();
1310
+
1311
+ searchTimelineScrollHandler = function() { updateSearchTimelineViewport(track, viewport); };
1312
+ messagesEl.addEventListener("scroll", searchTimelineScrollHandler);
1313
+ updateSearchTimelineViewport(track, viewport);
1314
+ }
1315
+
1316
+ function updateSearchTimelineViewport(track, viewport) {
1317
+ if (!track) return;
1318
+ var messagesEl = ctx.messagesEl;
1319
+ var scrollH = messagesEl.scrollHeight;
1320
+ var viewH = messagesEl.clientHeight;
1321
+ if (scrollH <= viewH) {
1322
+ viewport.style.top = "0";
1323
+ viewport.style.height = "100%";
1324
+ } else {
1325
+ var viewTop = messagesEl.scrollTop / scrollH;
1326
+ var viewBot = (messagesEl.scrollTop + viewH) / scrollH;
1327
+ viewport.style.top = (viewTop * 100) + "%";
1328
+ viewport.style.height = ((viewBot - viewTop) * 100) + "%";
1329
+ }
1330
+
1331
+ var markers = track.querySelectorAll(".search-hit-marker");
1332
+ var vTop = messagesEl.scrollTop;
1333
+ var vBot = vTop + viewH;
1334
+
1335
+ for (var i = 0; i < markers.length; i++) {
1336
+ var msgTop = parseInt(markers[i].dataset.offsetTop, 10);
1337
+ if (msgTop >= vTop && msgTop <= vBot) {
1338
+ markers[i].classList.add("in-view");
1339
+ } else {
1340
+ markers[i].classList.remove("in-view");
1341
+ }
1342
+ }
1343
+ }
1344
+
1345
+ export function removeSearchTimeline() {
1346
+ var existing = document.getElementById("search-timeline");
1347
+ if (existing) existing.remove();
1348
+ if (searchTimelineScrollHandler && ctx.messagesEl) {
1349
+ ctx.messagesEl.removeEventListener("scroll", searchTimelineScrollHandler);
1350
+ searchTimelineScrollHandler = null;
1351
+ }
1352
+ activeSearchQuery = "";
1353
+ }
1354
+
1355
+ // --- Icon Strip (Discord-style project icons) ---
1356
+ var iconStripTooltip = null;
1357
+
1358
+ function getProjectAbbrev(name) {
1359
+ if (!name) return "?";
1360
+ // Take first letter of each word, max 2 chars
1361
+ var words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
1362
+ if (words.length >= 2) {
1363
+ return (words[0][0] + words[1][0]).toUpperCase();
1364
+ }
1365
+ return name.substring(0, 2).toUpperCase();
1366
+ }
1367
+
1368
+ function showIconTooltip(el, text) {
1369
+ hideIconTooltip();
1370
+ var tip = document.createElement("div");
1371
+ tip.className = "icon-strip-tooltip";
1372
+ tip.textContent = text;
1373
+ document.body.appendChild(tip);
1374
+ iconStripTooltip = tip;
1375
+
1376
+ requestAnimationFrame(function () {
1377
+ var rect = el.getBoundingClientRect();
1378
+ tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
1379
+ tip.classList.add("visible");
1380
+ });
1381
+ }
1382
+
1383
+ function hideIconTooltip() {
1384
+ if (iconStripTooltip) {
1385
+ iconStripTooltip.remove();
1386
+ iconStripTooltip = null;
1387
+ }
1388
+ }
1389
+
1390
+ // --- Project context menu ---
1391
+ var projectCtxMenu = null;
1392
+
1393
+ var EMOJI_CATEGORIES = [
1394
+ { id: "frequent", icon: "🕐", label: "Frequent", emojis: [
1395
+ "😀","😎","🤓","🧠","💡","🔥","⚡","🚀",
1396
+ "🎯","🎮","🎨","🎵","📦","📁","📝","💻",
1397
+ "🖥️","⌨️","🔧","🛠️","⚙️","🧪","🔬","🧬",
1398
+ "🌍","🌱","🌊","🌸","🍀","🌈","☀️","🌙",
1399
+ "🐱","🐶","🐼","🦊","🦋","🐝","🐙","🦄",
1400
+ "🍕","🍔","☕","🍩","🍎","🍇","🧁","🍣",
1401
+ "❤️","💜","💙","💚","💛","🧡","🤍","🖤",
1402
+ "⭐","✨","💎","🏆","👑","🎪","🎭","🃏",
1403
+ ]},
1404
+ { id: "smileys", icon: "😀", label: "Smileys & People", emojis: [
1405
+ "😀","😃","😄","😁","😆","😅","🤣","😂",
1406
+ "🙂","😊","😇","🥰","😍","🤩","😘","😗",
1407
+ "😚","😙","🥲","😋","😛","😜","🤪","😝",
1408
+ "🤑","🤗","🤭","🫢","🤫","🤔","🫡","🤐",
1409
+ "🤨","😐","😑","😶","🫥","😏","😒","🙄",
1410
+ "😬","🤥","😌","😔","😪","🤤","😴","😷",
1411
+ "🤒","🤕","🤢","🤮","🥴","😵","🤯","🥳",
1412
+ "🥸","😎","🤓","🧐","😕","🫤","😟","🙁",
1413
+ "😮","😯","😲","😳","🥺","🥹","😦","😧",
1414
+ "😨","😰","😥","😢","😭","😱","😖","😣",
1415
+ "😞","😓","😩","😫","🥱","😤","😡","😠",
1416
+ "🤬","😈","👿","💀","☠️","💩","🤡","👹",
1417
+ "👺","👻","👽","👾","🤖","😺","😸","😹",
1418
+ "😻","😼","😽","🙀","😿","😾","🙈","🙉",
1419
+ "🙊","👋","🤚","🖐️","✋","🖖","🫱","🫲",
1420
+ "🫳","🫴","👌","🤌","🤏","✌️","🤞","🫰",
1421
+ "🤟","🤘","🤙","👈","👉","👆","🖕","👇",
1422
+ "☝️","🫵","👍","👎","✊","👊","🤛","🤜",
1423
+ "👏","🙌","🫶","👐","🤲","🤝","🙏","💪",
1424
+ ]},
1425
+ { id: "animals", icon: "🐻", label: "Animals & Nature", emojis: [
1426
+ "🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼",
1427
+ "🐻‍❄️","🐨","🐯","🦁","🐮","🐷","🐽","🐸",
1428
+ "🐵","🙈","🙉","🙊","🐒","🐔","🐧","🐦",
1429
+ "🐤","🐣","🐥","🦆","🦅","🦉","🦇","🐺",
1430
+ "🐗","🐴","🦄","🐝","🪱","🐛","🦋","🐌",
1431
+ "🐞","🐜","🪰","🪲","🪳","🦟","🦗","🕷️",
1432
+ "🦂","🐢","🐍","🦎","🦖","🦕","🐙","🦑",
1433
+ "🦐","🦞","🦀","🪸","🐡","🐠","🐟","🐬",
1434
+ "🐳","🐋","🦈","🐊","🐅","🐆","🦓","🫏",
1435
+ "🦍","🦧","🦣","🐘","🦛","🦏","🐪","🐫",
1436
+ "🦒","🦘","🦬","🐃","🐂","🐄","🐎","🐖",
1437
+ "🐏","🐑","🦙","🐐","🦌","🫎","🐕","🐩",
1438
+ "🦮","🐕‍🦺","🐈","🐈‍⬛","🪶","🐓","🦃","🦤",
1439
+ "🦚","🦜","🦢","🪿","🦩","🕊️","🐇","🦝",
1440
+ "🦨","🦡","🦫","🦦","🦥","🐁","🐀","🐿️",
1441
+ "🦔","🌵","🎄","🌲","🌳","🌴","🪵","🌱",
1442
+ "🌿","☘️","🍀","🎍","🪴","🎋","🍃","🍂",
1443
+ "🍁","🪺","🪹","🍄","🌾","💐","🌷","🌹",
1444
+ "🥀","🪻","🌺","🌸","🌼","🌻","🌞","🌝",
1445
+ "🌛","🌜","🌚","🌕","🌖","🌗","🌘","🌑",
1446
+ "🌒","🌓","🌔","🌙","🌎","🌍","🌏","🪐",
1447
+ "💫","⭐","🌟","✨","⚡","☄️","💥","🔥",
1448
+ "🌪️","🌈","☀️","🌤️","⛅","🌥️","☁️","🌦️",
1449
+ "🌧️","⛈️","🌩️","❄️","☃️","⛄","🌬️","💨",
1450
+ "💧","💦","🫧","☔","☂️","🌊","🌫️",
1451
+ ]},
1452
+ { id: "food", icon: "🍔", label: "Food & Drink", emojis: [
1453
+ "🍇","🍈","🍉","🍊","🍋","🍌","🍍","🥭",
1454
+ "🍎","🍏","🍐","🍑","🍒","🍓","🫐","🥝",
1455
+ "🍅","🫒","🥥","🥑","🍆","🥔","🥕","🌽",
1456
+ "🌶️","🫑","🥒","🥬","🥦","🧄","🧅","🥜",
1457
+ "🫘","🌰","🫚","🫛","🍞","🥐","🥖","🫓",
1458
+ "🥨","🥯","🥞","🧇","🧀","🍖","🍗","🥩",
1459
+ "🥓","🍔","🍟","🍕","🌭","🥪","🌮","🌯",
1460
+ "🫔","🥙","🧆","🥚","🍳","🥘","🍲","🫕",
1461
+ "🥣","🥗","🍿","🧈","🧂","🥫","🍱","🍘",
1462
+ "🍙","🍚","🍛","🍜","🍝","🍠","🍢","🍣",
1463
+ "🍤","🍥","🥮","🍡","🥟","🥠","🥡","🦀",
1464
+ "🦞","🦐","🦑","🦪","🍦","🍧","🍨","🍩",
1465
+ "🍪","🎂","🍰","🧁","🥧","🍫","🍬","🍭",
1466
+ "🍮","🍯","🍼","🥛","☕","🫖","🍵","🍶",
1467
+ "🍾","🍷","🍸","🍹","🍺","🍻","🥂","🥃",
1468
+ "🫗","🥤","🧋","🧃","🧉","🧊",
1469
+ ]},
1470
+ { id: "activity", icon: "⚽", label: "Activity", emojis: [
1471
+ "⚽","🏀","🏈","⚾","🥎","🎾","🏐","🏉",
1472
+ "🥏","🎱","🪀","🏓","🏸","🏒","🏑","🥍",
1473
+ "🏏","🪃","🥅","⛳","🪁","🛝","🏹","🎣",
1474
+ "🤿","🥊","🥋","🎽","🛹","🛼","🛷","⛸️",
1475
+ "🥌","🎿","⛷️","🏂","🪂","🏋️","🤸","🤺",
1476
+ "⛹️","🤾","🏌️","🏇","🧘","🏄","🏊","🤽",
1477
+ "🚣","🧗","🚵","🚴","🎪","🤹","🎭","🎨",
1478
+ "🎬","🎤","🎧","🎼","🎹","🥁","🪘","🎷",
1479
+ "🎺","🪗","🎸","🪕","🎻","🪈","🎲","♟️",
1480
+ "🎯","🎳","🎮","🕹️","🧩","🪩",
1481
+ ]},
1482
+ { id: "travel", icon: "🚗", label: "Travel & Places", emojis: [
1483
+ "🚗","🚕","🚙","🚌","🚎","🏎️","🚓","🚑",
1484
+ "🚒","🚐","🛻","🚚","🚛","🚜","🛵","🏍️",
1485
+ "🛺","🚲","🛴","🛹","🚏","🛣️","🛤️","⛽",
1486
+ "🛞","🚨","🚥","🚦","🛑","🚧","⚓","🛟",
1487
+ "⛵","🛶","🚤","🛳️","⛴️","🛥️","🚢","✈️",
1488
+ "🛩️","🛫","🛬","🪂","💺","🚁","🚟","🚠",
1489
+ "🚡","🛰️","🚀","🛸","🏠","🏡","🏘️","🏚️",
1490
+ "🏗️","🏭","🏢","🏬","🏣","🏤","🏥","🏦",
1491
+ "🏨","🏪","🏫","🏩","💒","🏛️","⛪","🕌",
1492
+ "🛕","🕍","⛩️","🕋","⛲","⛺","🌁","🌃",
1493
+ "🏙️","🌄","🌅","🌆","🌇","🌉","🗼","🗽",
1494
+ "🗻","🏕️","🎠","🎡","🎢","🏖️","🏝️","🏜️",
1495
+ "🌋","⛰️","🗺️","🧭","🏔️",
1496
+ ]},
1497
+ { id: "objects", icon: "💡", label: "Objects", emojis: [
1498
+ "⌚","📱","📲","💻","⌨️","🖥️","🖨️","🖱️",
1499
+ "🖲️","🕹️","🗜️","💽","💾","💿","📀","📼",
1500
+ "📷","📸","📹","🎥","📽️","🎞️","📞","☎️",
1501
+ "📟","📠","📺","📻","🎙️","🎚️","🎛️","🧭",
1502
+ "⏱️","⏲️","⏰","🕰️","⌛","⏳","📡","🔋",
1503
+ "🪫","🔌","💡","🔦","🕯️","🪔","🧯","🛢️",
1504
+ "🛍️","💰","💴","💵","💶","💷","🪙","💸",
1505
+ "💳","🧾","💹","✉️","📧","📨","📩","📤",
1506
+ "📥","📦","📫","📬","📭","📮","🗳️","✏️",
1507
+ "✒️","🖋️","🖊️","🖌️","🖍️","📝","💼","📁",
1508
+ "📂","🗂️","📅","📆","🗒️","🗓️","📇","📈",
1509
+ "📉","📊","📋","📌","📍","📎","🖇️","📏",
1510
+ "📐","✂️","🗃️","🗄️","🗑️","🔒","🔓","🔏",
1511
+ "🔐","🔑","🗝️","🔨","🪓","⛏️","⚒️","🛠️",
1512
+ "🗡️","⚔️","💣","🪃","🏹","🛡️","🪚","🔧",
1513
+ "🪛","🔩","⚙️","🗜️","⚖️","🦯","🔗","⛓️",
1514
+ "🪝","🧰","🧲","🪜","⚗️","🧪","🧫","🧬",
1515
+ "🔬","🔭","📡","💉","🩸","💊","🩹","🩼",
1516
+ "🩺","🩻","🚪","🛗","🪞","🪟","🛏️","🛋️",
1517
+ "🪑","🚽","🪠","🚿","🛁","🪤","🪒","🧴",
1518
+ "🧷","🧹","🧺","🧻","🪣","🧼","🫧","🪥",
1519
+ "🧽","🧯","🛒","🚬","⚰️","🪦","⚱️","🧿",
1520
+ "🪬","🗿","🪧","🪪",
1521
+ ]},
1522
+ { id: "symbols", icon: "❤️", label: "Symbols", emojis: [
1523
+ "❤️","🧡","💛","💚","💙","💜","🖤","🤍",
1524
+ "🤎","💔","❤️‍🔥","❤️‍🩹","❣️","💕","💞","💓",
1525
+ "💗","💖","💘","💝","💟","☮️","✝️","☪️",
1526
+ "🕉️","☸️","🪯","✡️","🔯","🕎","☯️","☦️",
1527
+ "🛐","⛎","♈","♉","♊","♋","♌","♍",
1528
+ "♎","♏","♐","♑","♒","♓","🆔","⚛️",
1529
+ "🉑","☢️","☣️","📴","📳","🈶","🈚","🈸",
1530
+ "🈺","🈷️","✴️","🆚","💮","🉐","㊙️","㊗️",
1531
+ "🈴","🈵","🈹","🈲","🅰️","🅱️","🆎","🆑",
1532
+ "🅾️","🆘","❌","⭕","🛑","⛔","📛","🚫",
1533
+ "💯","💢","♨️","🚷","🚯","🚳","🚱","🔞",
1534
+ "📵","🚭","❗","❕","❓","❔","‼️","⁉️",
1535
+ "🔅","🔆","〽️","⚠️","🚸","🔱","⚜️","🔰",
1536
+ "♻️","✅","🈯","💹","❇️","✳️","❎","🌐",
1537
+ "💠","Ⓜ️","🌀","💤","🏧","🚾","♿","🅿️",
1538
+ "🛗","🈳","🈂️","🛂","🛃","🛄","🛅","🚹",
1539
+ "🚺","🚼","⚧️","🚻","🚮","🎦","📶","🈁",
1540
+ "🔣","ℹ️","🔤","🔡","🔠","🆖","🆗","🆙",
1541
+ "🆒","🆕","🆓","0️⃣","1️⃣","2️⃣","3️⃣","4️⃣",
1542
+ "5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🔟","🔢","#️⃣",
1543
+ "*️⃣","⏏️","▶️","⏸️","⏯️","⏹️","⏺️","⏭️",
1544
+ "⏮️","⏩","⏪","⏫","⏬","◀️","🔼","🔽",
1545
+ "➡️","⬅️","⬆️","⬇️","↗️","↘️","↙️","↖️",
1546
+ "↕️","↔️","↩️","↪️","⤴️","⤵️","🔀","🔁",
1547
+ "🔂","🔄","🔃","🎵","🎶","✖️","➕","➖",
1548
+ "➗","🟰","♾️","💲","💱","™️","©️","®️",
1549
+ "〰️","➰","➿","🔚","🔙","🔛","🔝","🔜",
1550
+ "✔️","☑️","🔘","🔴","🟠","🟡","🟢","🔵",
1551
+ "🟣","⚫","⚪","🟤","🔺","🔻","🔸","🔹",
1552
+ "🔶","🔷","🔳","🔲","▪️","▫️","◾","◽",
1553
+ "◼️","◻️","🟥","🟧","🟨","🟩","🟦","🟪",
1554
+ "⬛","⬜","🟫","🔈","🔇","🔉","🔊","🔔",
1555
+ "🔕","📣","📢","👁️‍🗨️","💬","💭","🗯️","♠️",
1556
+ "♣️","♥️","♦️","🃏","🎴","🀄","🕐","🕑",
1557
+ "🕒","🕓","🕔","🕕","🕖","🕗","🕘","🕙","🕚","🕛",
1558
+ ]},
1559
+ { id: "flags", icon: "🏁", label: "Flags", emojis: [
1560
+ "🏁","🚩","🎌","🏴","🏳️","🏳️‍🌈","🏳️‍⚧️","🏴‍☠️",
1561
+ "🇦🇨","🇦🇩","🇦🇪","🇦🇫","🇦🇬","🇦🇮","🇦🇱","🇦🇲",
1562
+ "🇦🇴","🇦🇶","🇦🇷","🇦🇸","🇦🇹","🇦🇺","🇦🇼","🇦🇽",
1563
+ "🇦🇿","🇧🇦","🇧🇧","🇧🇩","🇧🇪","🇧🇫","🇧🇬","🇧🇭",
1564
+ "🇧🇮","🇧🇯","🇧🇱","🇧🇲","🇧🇳","🇧🇴","🇧🇶","🇧🇷",
1565
+ "🇧🇸","🇧🇹","🇧🇻","🇧🇼","🇧🇾","🇧🇿","🇨🇦","🇨🇨",
1566
+ "🇨🇩","🇨🇫","🇨🇬","🇨🇭","🇨🇮","🇨🇰","🇨🇱","🇨🇲",
1567
+ "🇨🇳","🇨🇴","🇨🇵","🇨🇷","🇨🇺","🇨🇻","🇨🇼","🇨🇽",
1568
+ "🇨🇾","🇨🇿","🇩🇪","🇩🇬","🇩🇯","🇩🇰","🇩🇲","🇩🇴",
1569
+ "🇩🇿","🇪🇦","🇪🇨","🇪🇪","🇪🇬","🇪🇭","🇪🇷","🇪🇸",
1570
+ "🇪🇹","🇪🇺","🇫🇮","🇫🇯","🇫🇰","🇫🇲","🇫🇴","🇫🇷",
1571
+ "🇬🇦","🇬🇧","🇬🇩","🇬🇪","🇬🇫","🇬🇬","🇬🇭","🇬🇮",
1572
+ "🇬🇱","🇬🇲","🇬🇳","🇬🇵","🇬🇶","🇬🇷","🇬🇸","🇬🇹",
1573
+ "🇬🇺","🇬🇼","🇬🇾","🇭🇰","🇭🇲","🇭🇳","🇭🇷","🇭🇹",
1574
+ "🇭🇺","🇮🇨","🇮🇩","🇮🇪","🇮🇱","🇮🇲","🇮🇳","🇮🇴",
1575
+ "🇮🇶","🇮🇷","🇮🇸","🇮🇹","🇯🇪","🇯🇲","🇯🇴","🇯🇵",
1576
+ "🇰🇪","🇰🇬","🇰🇭","🇰🇮","🇰🇲","🇰🇳","🇰🇵","🇰🇷",
1577
+ "🇰🇼","🇰🇾","🇰🇿","🇱🇦","🇱🇧","🇱🇨","🇱🇮","🇱🇰",
1578
+ "🇱🇷","🇱🇸","🇱🇹","🇱🇺","🇱🇻","🇱🇾","🇲🇦","🇲🇨",
1579
+ "🇲🇩","🇲🇪","🇲🇫","🇲🇬","🇲🇭","🇲🇰","🇲🇱","🇲🇲",
1580
+ "🇲🇳","🇲🇴","🇲🇵","🇲🇶","🇲🇷","🇲🇸","🇲🇹","🇲🇺",
1581
+ "🇲🇻","🇲🇼","🇲🇽","🇲🇾","🇲🇿","🇳🇦","🇳🇨","🇳🇪",
1582
+ "🇳🇫","🇳🇬","🇳🇮","🇳🇱","🇳🇴","🇳🇵","🇳🇷","🇳🇺",
1583
+ "🇳🇿","🇴🇲","🇵🇦","🇵🇪","🇵🇫","🇵🇬","🇵🇭","🇵🇰",
1584
+ "🇵🇱","🇵🇲","🇵🇳","🇵🇷","🇵🇸","🇵🇹","🇵🇼","🇵🇾",
1585
+ "🇶🇦","🇷🇪","🇷🇴","🇷🇸","🇷🇺","🇷🇼","🇸🇦","🇸🇧",
1586
+ "🇸🇨","🇸🇩","🇸🇪","🇸🇬","🇸🇭","🇸🇮","🇸🇯","🇸🇰",
1587
+ "🇸🇱","🇸🇲","🇸🇳","🇸🇴","🇸🇷","🇸🇸","🇸🇹","🇸🇻",
1588
+ "🇸🇽","🇸🇾","🇸🇿","🇹🇦","🇹🇨","🇹🇩","🇹🇫","🇹🇬",
1589
+ "🇹🇭","🇹🇯","🇹🇰","🇹🇱","🇹🇲","🇹🇳","🇹🇴","🇹🇷",
1590
+ "🇹🇹","🇹🇻","🇹🇼","🇹🇿","🇺🇦","🇺🇬","🇺🇲","🇺🇳",
1591
+ "🇺🇸","🇺🇾","🇺🇿","🇻🇦","🇻🇨","🇻🇪","🇻🇬","🇻🇮",
1592
+ "🇻🇳","🇻🇺","🇼🇫","🇼🇸","🇽🇰","🇾🇪","🇾🇹","🇿🇦",
1593
+ "🇿🇲","🇿🇼",
1594
+ ]},
1595
+ ];
1596
+
1597
+ function closeProjectCtxMenu() {
1598
+ if (projectCtxMenu) {
1599
+ projectCtxMenu.remove();
1600
+ projectCtxMenu = null;
1601
+ }
1602
+ }
1603
+
1604
+ function showIconCtxMenu(anchorEl, slug) {
1605
+ closeProjectCtxMenu();
1606
+ closeEmojiPicker();
1607
+
1608
+ var menu = document.createElement("div");
1609
+ menu.className = "project-ctx-menu";
1610
+
1611
+ var iconItem = document.createElement("button");
1612
+ iconItem.className = "project-ctx-item";
1613
+ iconItem.innerHTML = iconHtml("smile") + " <span>Set Icon</span>";
1614
+ iconItem.addEventListener("click", function (e) {
1615
+ e.stopPropagation();
1616
+ closeProjectCtxMenu();
1617
+ showEmojiPicker(slug, anchorEl);
1618
+ });
1619
+ menu.appendChild(iconItem);
1620
+
1621
+ document.body.appendChild(menu);
1622
+ projectCtxMenu = menu;
1623
+ refreshIcons();
1624
+
1625
+ requestAnimationFrame(function () {
1626
+ var rect = anchorEl.getBoundingClientRect();
1627
+ menu.style.position = "fixed";
1628
+ menu.style.left = (rect.right + 6) + "px";
1629
+ menu.style.top = rect.top + "px";
1630
+ var menuRect = menu.getBoundingClientRect();
1631
+ if (menuRect.right > window.innerWidth - 8) {
1632
+ menu.style.left = (rect.left - menuRect.width - 6) + "px";
1633
+ }
1634
+ if (menuRect.bottom > window.innerHeight - 8) {
1635
+ menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
1636
+ }
1637
+ });
1638
+ }
1639
+
1640
+ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
1641
+ closeProjectCtxMenu();
1642
+ closeEmojiPicker();
1643
+
1644
+ var menu = document.createElement("div");
1645
+ menu.className = "project-ctx-menu";
1646
+
1647
+ // --- Project Settings ---
1648
+ var settingsItem = document.createElement("button");
1649
+ settingsItem.className = "project-ctx-item";
1650
+ settingsItem.innerHTML = iconHtml("settings") + " <span>Project Settings</span>";
1651
+ settingsItem.addEventListener("click", function (e) {
1652
+ e.stopPropagation();
1653
+ closeProjectCtxMenu();
1654
+ openProjectSettings(slug, { slug: slug, name: name, icon: icon });
1655
+ });
1656
+ menu.appendChild(settingsItem);
1657
+
1658
+ // --- Share ---
1659
+ var shareItem = document.createElement("button");
1660
+ shareItem.className = "project-ctx-item";
1661
+ shareItem.innerHTML = iconHtml("share") + " <span>Share</span>";
1662
+ shareItem.addEventListener("click", function (e) {
1663
+ e.stopPropagation();
1664
+ closeProjectCtxMenu();
1665
+ triggerShare();
1666
+ });
1667
+ menu.appendChild(shareItem);
1668
+
1669
+ // --- Separator ---
1670
+ var sep = document.createElement("div");
1671
+ sep.className = "project-ctx-separator";
1672
+ menu.appendChild(sep);
1673
+
1674
+ // --- Delete ---
1675
+ var deleteItem = document.createElement("button");
1676
+ deleteItem.className = "project-ctx-item project-ctx-delete";
1677
+ deleteItem.innerHTML = iconHtml("trash-2") + " <span>Remove Project</span>";
1678
+ deleteItem.addEventListener("click", function (e) {
1679
+ e.stopPropagation();
1680
+ closeProjectCtxMenu();
1681
+ // Check for tasks/schedules first before removing
1682
+ if (ctx.ws && ctx.connected) {
1683
+ ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name }));
1684
+ }
1685
+ });
1686
+ menu.appendChild(deleteItem);
1687
+
1688
+ document.body.appendChild(menu);
1689
+ projectCtxMenu = menu;
1690
+ refreshIcons();
1691
+
1692
+ // Position
1693
+ requestAnimationFrame(function () {
1694
+ var rect = anchorEl.getBoundingClientRect();
1695
+ menu.style.position = "fixed";
1696
+ if (position === "below") {
1697
+ // Chevron dropdown: directly below the anchor
1698
+ menu.style.left = rect.left + "px";
1699
+ menu.style.top = (rect.bottom + 4) + "px";
1700
+ } else {
1701
+ // Icon strip right-click: to the right of the anchor
1702
+ menu.style.left = (rect.right + 6) + "px";
1703
+ menu.style.top = rect.top + "px";
1704
+ }
1705
+ var menuRect = menu.getBoundingClientRect();
1706
+ if (menuRect.right > window.innerWidth - 8) {
1707
+ menu.style.left = (rect.left - menuRect.width - 6) + "px";
1708
+ }
1709
+ if (menuRect.bottom > window.innerHeight - 8) {
1710
+ menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
1711
+ }
1712
+ });
1713
+ }
1714
+
1715
+ // --- Emoji picker ---
1716
+ var emojiPickerEl = null;
1717
+
1718
+ function closeEmojiPicker() {
1719
+ if (emojiPickerEl) {
1720
+ emojiPickerEl.remove();
1721
+ emojiPickerEl = null;
1722
+ }
1723
+ }
1724
+
1725
+ function showEmojiPicker(slug, anchorEl) {
1726
+ closeEmojiPicker();
1727
+
1728
+ var picker = document.createElement("div");
1729
+ picker.className = "emoji-picker";
1730
+ picker.addEventListener("click", function (e) { e.stopPropagation(); });
1731
+
1732
+ // --- Header ---
1733
+ var header = document.createElement("div");
1734
+ header.className = "emoji-picker-header";
1735
+ header.textContent = "Choose Icon";
1736
+
1737
+ var removeBtn = document.createElement("button");
1738
+ removeBtn.className = "emoji-picker-remove";
1739
+ removeBtn.textContent = "Remove";
1740
+ removeBtn.addEventListener("click", function (e) {
1741
+ e.stopPropagation();
1742
+ closeEmojiPicker();
1743
+ if (ctx.ws && ctx.connected) {
1744
+ ctx.ws.send(JSON.stringify({ type: "set_project_icon", slug: slug, icon: null }));
1745
+ }
1746
+ });
1747
+ header.appendChild(removeBtn);
1748
+ picker.appendChild(header);
1749
+
1750
+ // --- Category tabs ---
1751
+ var tabBar = document.createElement("div");
1752
+ tabBar.className = "emoji-picker-tabs";
1753
+ var tabBtns = [];
1754
+
1755
+ for (var t = 0; t < EMOJI_CATEGORIES.length; t++) {
1756
+ (function (cat, idx) {
1757
+ var tab = document.createElement("button");
1758
+ tab.className = "emoji-picker-tab" + (idx === 0 ? " active" : "");
1759
+ tab.textContent = cat.icon;
1760
+ tab.title = cat.label;
1761
+ tab.addEventListener("click", function (e) {
1762
+ e.stopPropagation();
1763
+ switchCategory(idx);
1764
+ });
1765
+ tabBar.appendChild(tab);
1766
+ tabBtns.push(tab);
1767
+ })(EMOJI_CATEGORIES[t], t);
1768
+ }
1769
+ parseEmojis(tabBar);
1770
+ picker.appendChild(tabBar);
1771
+
1772
+ // --- Scrollable grid area ---
1773
+ var scrollArea = document.createElement("div");
1774
+ scrollArea.className = "emoji-picker-scroll";
1775
+
1776
+ var grid = document.createElement("div");
1777
+ grid.className = "emoji-picker-grid";
1778
+ scrollArea.appendChild(grid);
1779
+ picker.appendChild(scrollArea);
1780
+
1781
+ function buildGrid(emojis) {
1782
+ grid.innerHTML = "";
1783
+ for (var i = 0; i < emojis.length; i++) {
1784
+ (function (emoji) {
1785
+ var btn = document.createElement("button");
1786
+ btn.className = "emoji-picker-item";
1787
+ btn.textContent = emoji;
1788
+ btn.addEventListener("click", function (e) {
1789
+ e.stopPropagation();
1790
+ closeEmojiPicker();
1791
+ if (ctx.ws && ctx.connected) {
1792
+ ctx.ws.send(JSON.stringify({ type: "set_project_icon", slug: slug, icon: emoji }));
1793
+ }
1794
+ });
1795
+ grid.appendChild(btn);
1796
+ })(emojis[i]);
1797
+ }
1798
+ parseEmojis(grid);
1799
+ scrollArea.scrollTop = 0;
1800
+ }
1801
+
1802
+ function switchCategory(idx) {
1803
+ for (var j = 0; j < tabBtns.length; j++) {
1804
+ tabBtns[j].classList.toggle("active", j === idx);
1805
+ }
1806
+ buildGrid(EMOJI_CATEGORIES[idx].emojis);
1807
+ }
1808
+
1809
+ // Start with first category (Frequent)
1810
+ buildGrid(EMOJI_CATEGORIES[0].emojis);
1811
+
1812
+
1813
+
1814
+ document.body.appendChild(picker);
1815
+ emojiPickerEl = picker;
1816
+
1817
+ // Position
1818
+ requestAnimationFrame(function () {
1819
+ var rect = anchorEl.getBoundingClientRect();
1820
+ picker.style.left = (rect.right + 6) + "px";
1821
+ picker.style.top = rect.top + "px";
1822
+ var pRect = picker.getBoundingClientRect();
1823
+ if (pRect.right > window.innerWidth - 8) {
1824
+ picker.style.left = (rect.left - pRect.width - 6) + "px";
1825
+ }
1826
+ if (pRect.bottom > window.innerHeight - 8) {
1827
+ picker.style.top = (window.innerHeight - pRect.height - 8) + "px";
1828
+ }
1829
+ });
1830
+ }
1831
+
1832
+ // --- Rename prompt ---
1833
+ function showProjectRename(slug, currentName) {
1834
+ var nameEl = document.getElementById("title-bar-project-name");
1835
+ if (!nameEl) return;
1836
+
1837
+ var input = document.createElement("input");
1838
+ input.type = "text";
1839
+ input.className = "project-rename-input";
1840
+ input.value = currentName || "";
1841
+
1842
+ var originalText = nameEl.textContent;
1843
+ nameEl.textContent = "";
1844
+ nameEl.appendChild(input);
1845
+ input.focus();
1846
+ input.select();
1847
+
1848
+ var committed = false;
1849
+
1850
+ function commitRename() {
1851
+ if (committed) return;
1852
+ committed = true;
1853
+ var newName = input.value.trim();
1854
+ if (newName && newName !== currentName && ctx.ws && ctx.connected) {
1855
+ ctx.ws.send(JSON.stringify({ type: "set_project_title", slug: slug, title: newName }));
1856
+ nameEl.textContent = newName;
1857
+ } else {
1858
+ nameEl.textContent = originalText;
1859
+ }
1860
+ }
1861
+
1862
+ input.addEventListener("keydown", function (e) {
1863
+ e.stopPropagation();
1864
+ if (e.key === "Enter") { e.preventDefault(); commitRename(); }
1865
+ if (e.key === "Escape") { e.preventDefault(); committed = true; nameEl.textContent = originalText; }
1866
+ });
1867
+ input.addEventListener("blur", commitRename);
1868
+ input.addEventListener("click", function (e) { e.stopPropagation(); });
1869
+ }
1870
+
1871
+ // Click outside to close
1872
+ document.addEventListener("click", function () {
1873
+ closeProjectCtxMenu();
1874
+ closeEmojiPicker();
1875
+ });
1876
+
1877
+ // --- Drag-and-drop state ---
1878
+ var draggedSlug = null;
1879
+ var draggedEl = null;
1880
+
1881
+ function showTrashZone() {
1882
+ var addBtn = document.getElementById("icon-strip-add");
1883
+ if (!addBtn) return;
1884
+ addBtn.style.display = "none";
1885
+
1886
+ var existing = document.getElementById("icon-strip-trash");
1887
+ if (existing) existing.remove();
1888
+
1889
+ var trash = document.createElement("div");
1890
+ trash.id = "icon-strip-trash";
1891
+ trash.className = "icon-strip-trash";
1892
+ trash.innerHTML = iconHtml("trash-2");
1893
+ addBtn.parentNode.insertBefore(trash, addBtn.nextSibling);
1894
+ refreshIcons();
1895
+
1896
+ // Tooltip
1897
+ trash.addEventListener("mouseenter", function () { showIconTooltip(trash, "Remove project"); });
1898
+ trash.addEventListener("mouseleave", hideIconTooltip);
1899
+
1900
+ trash.addEventListener("dragover", function (e) {
1901
+ e.preventDefault();
1902
+ e.dataTransfer.dropEffect = "move";
1903
+ trash.classList.add("drag-hover");
1904
+ });
1905
+ trash.addEventListener("dragleave", function () {
1906
+ trash.classList.remove("drag-hover");
1907
+ });
1908
+ trash.addEventListener("drop", function (e) {
1909
+ e.preventDefault();
1910
+ trash.classList.remove("drag-hover");
1911
+ var slug = e.dataTransfer.getData("text/plain");
1912
+ if (slug && ctx.ws && ctx.connected) {
1913
+ // Spawn dust particles at trash position
1914
+ var rect = trash.getBoundingClientRect();
1915
+ spawnDustParticles(rect.left + rect.width / 2, rect.top + rect.height / 2);
1916
+ // Check for tasks before removing
1917
+ ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug }));
1918
+ }
1919
+ });
1920
+ }
1921
+
1922
+ function hideTrashZone() {
1923
+ var trash = document.getElementById("icon-strip-trash");
1924
+ if (trash) trash.remove();
1925
+ var addBtn = document.getElementById("icon-strip-add");
1926
+ if (addBtn) addBtn.style.display = "";
1927
+ }
1928
+
1929
+ function spawnDustParticles(cx, cy) {
1930
+ var colors = ["#8B7355", "#A0522D", "#D2B48C", "#C4A882", "#9E9E9E", "#B8860B", "#BC8F8F"];
1931
+ var count = 24;
1932
+ var container = document.createElement("div");
1933
+ container.style.position = "fixed";
1934
+ container.style.top = "0";
1935
+ container.style.left = "0";
1936
+ container.style.width = "0";
1937
+ container.style.height = "0";
1938
+ container.style.pointerEvents = "none";
1939
+ container.style.zIndex = "10000";
1940
+ document.body.appendChild(container);
1941
+
1942
+ for (var i = 0; i < count; i++) {
1943
+ var dot = document.createElement("div");
1944
+ dot.className = "dust-particle";
1945
+ var size = 3 + Math.random() * 5;
1946
+ var angle = Math.random() * Math.PI * 2;
1947
+ var dist = 30 + Math.random() * 60;
1948
+ var dx = Math.cos(angle) * dist;
1949
+ var dy = Math.sin(angle) * dist - 20; // bias upward
1950
+ var duration = 600 + Math.random() * 500;
1951
+
1952
+ dot.style.width = size + "px";
1953
+ dot.style.height = size + "px";
1954
+ dot.style.left = cx + "px";
1955
+ dot.style.top = cy + "px";
1956
+ dot.style.background = colors[Math.floor(Math.random() * colors.length)];
1957
+ dot.style.setProperty("--dust-x", dx + "px");
1958
+ dot.style.setProperty("--dust-y", dy + "px");
1959
+ dot.style.setProperty("--dust-duration", duration + "ms");
1960
+
1961
+ container.appendChild(dot);
1962
+ }
1963
+
1964
+ setTimeout(function () { container.remove(); }, 1200);
1965
+ }
1966
+
1967
+ function clearDragIndicators() {
1968
+ var items = document.querySelectorAll(".icon-strip-item.drag-over-above, .icon-strip-item.drag-over-below");
1969
+ for (var i = 0; i < items.length; i++) {
1970
+ items[i].classList.remove("drag-over-above", "drag-over-below");
1971
+ }
1972
+ }
1973
+
1974
+ function setupDragHandlers(el, slug) {
1975
+ el.setAttribute("draggable", "true");
1976
+
1977
+ el.addEventListener("dragstart", function (e) {
1978
+ draggedSlug = slug;
1979
+ draggedEl = el;
1980
+ e.dataTransfer.effectAllowed = "move";
1981
+ e.dataTransfer.setData("text/plain", slug);
1982
+
1983
+ // Custom drag image — just the 38px rounded icon, no pill/status
1984
+ var ghost = document.createElement("div");
1985
+ ghost.textContent = el.textContent.trim().split("\n")[0]; // abbreviation only
1986
+ ghost.style.cssText = "position:fixed;left:-200px;top:-200px;width:38px;height:38px;border-radius:12px;" +
1987
+ "background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;" +
1988
+ "font-size:15px;font-weight:600;pointer-events:none;z-index:-1;";
1989
+ document.body.appendChild(ghost);
1990
+ e.dataTransfer.setDragImage(ghost, 19, 19);
1991
+ setTimeout(function () { ghost.remove(); }, 0);
1992
+
1993
+ setTimeout(function () { el.classList.add("dragging"); }, 0);
1994
+ hideIconTooltip();
1995
+ showTrashZone();
1996
+ });
1997
+
1998
+ el.addEventListener("dragover", function (e) {
1999
+ e.preventDefault();
2000
+ if (!draggedSlug || draggedSlug === slug) return;
2001
+ e.dataTransfer.dropEffect = "move";
2002
+
2003
+ clearDragIndicators();
2004
+ var rect = el.getBoundingClientRect();
2005
+ var midY = rect.top + rect.height / 2;
2006
+ if (e.clientY < midY) {
2007
+ el.classList.add("drag-over-above");
2008
+ } else {
2009
+ el.classList.add("drag-over-below");
2010
+ }
2011
+ });
2012
+
2013
+ el.addEventListener("dragleave", function () {
2014
+ el.classList.remove("drag-over-above", "drag-over-below");
2015
+ });
2016
+
2017
+ el.addEventListener("drop", function (e) {
2018
+ e.preventDefault();
2019
+ clearDragIndicators();
2020
+ if (!draggedSlug || draggedSlug === slug) return;
2021
+
2022
+ var rect = el.getBoundingClientRect();
2023
+ var midY = rect.top + rect.height / 2;
2024
+ var insertBefore = e.clientY < midY;
2025
+
2026
+ // Build new slug order
2027
+ var container = document.getElementById("icon-strip-projects");
2028
+ var items = container.querySelectorAll(".icon-strip-item");
2029
+ var slugs = [];
2030
+ for (var i = 0; i < items.length; i++) {
2031
+ if (items[i].dataset.slug !== draggedSlug) {
2032
+ slugs.push(items[i].dataset.slug);
2033
+ }
2034
+ }
2035
+ // Insert dragged slug at correct position
2036
+ var targetIdx = slugs.indexOf(slug);
2037
+ if (!insertBefore) targetIdx++;
2038
+ slugs.splice(targetIdx, 0, draggedSlug);
2039
+
2040
+ // Send reorder to server
2041
+ if (ctx.ws && ctx.connected) {
2042
+ ctx.ws.send(JSON.stringify({ type: "reorder_projects", slugs: slugs }));
2043
+ }
2044
+ });
2045
+
2046
+ el.addEventListener("dragend", function () {
2047
+ el.classList.remove("dragging");
2048
+ clearDragIndicators();
2049
+ draggedSlug = null;
2050
+ draggedEl = null;
2051
+ hideTrashZone();
2052
+ });
2053
+ }
2054
+
2055
+ export function renderSidebarPresence(onlineUsers) {
2056
+ var container = document.getElementById("sidebar-presence");
2057
+ if (!container) return;
2058
+ container.innerHTML = "";
2059
+ if (!onlineUsers || onlineUsers.length < 2) return;
2060
+ var maxShow = 4;
2061
+ for (var i = 0; i < Math.min(onlineUsers.length, maxShow); i++) {
2062
+ var ou = onlineUsers[i];
2063
+ var img = document.createElement("img");
2064
+ img.className = "sidebar-presence-avatar";
2065
+ img.src = presenceAvatarUrl(ou.avatarStyle, ou.avatarSeed);
2066
+ img.alt = ou.displayName;
2067
+ img.dataset.tip = ou.displayName + " (@" + ou.username + ")";
2068
+ container.appendChild(img);
2069
+ }
2070
+ if (onlineUsers.length > maxShow) {
2071
+ var more = document.createElement("span");
2072
+ more.className = "sidebar-presence-more";
2073
+ more.textContent = "+" + (onlineUsers.length - maxShow);
2074
+ container.appendChild(more);
2075
+ }
2076
+ }
2077
+
2078
+ export function renderIconStrip(projects, currentSlug) {
2079
+ // Cache for mobile sheet
2080
+ cachedProjectList = projects;
2081
+ cachedCurrentSlug = currentSlug;
2082
+
2083
+ var container = document.getElementById("icon-strip-projects");
2084
+ if (!container) return;
2085
+ container.innerHTML = "";
2086
+
2087
+ for (var i = 0; i < projects.length; i++) {
2088
+ var p = projects[i];
2089
+ var el = document.createElement("a");
2090
+ el.className = "icon-strip-item" + (p.slug === currentSlug ? " active" : "");
2091
+ el.href = "/p/" + p.slug + "/";
2092
+ el.dataset.slug = p.slug;
2093
+
2094
+ // Icon: twemoji or abbreviation
2095
+ if (p.icon) {
2096
+ var emojiSpan = document.createElement("span");
2097
+ emojiSpan.className = "project-emoji";
2098
+ emojiSpan.textContent = p.icon;
2099
+ parseEmojis(emojiSpan);
2100
+ el.appendChild(emojiSpan);
2101
+ } else {
2102
+ el.appendChild(document.createTextNode(getProjectAbbrev(p.name)));
2103
+ }
2104
+
2105
+ var pill = document.createElement("span");
2106
+ pill.className = "icon-strip-pill";
2107
+ el.appendChild(pill);
2108
+
2109
+ // Socket status indicator dot (bottom-right)
2110
+ var statusDot = document.createElement("span");
2111
+ statusDot.className = "icon-strip-status";
2112
+ if (p.isProcessing) statusDot.classList.add("processing");
2113
+ el.appendChild(statusDot);
2114
+
2115
+ // Tooltip on hover
2116
+ (function (name, elem) {
2117
+ elem.addEventListener("mouseenter", function () { showIconTooltip(elem, name); });
2118
+ elem.addEventListener("mouseleave", hideIconTooltip);
2119
+ })(p.name, el);
2120
+
2121
+ // Click handler — switch to project (no reload)
2122
+ (function (slug) {
2123
+ el.addEventListener("click", function (e) {
2124
+ e.preventDefault();
2125
+ if (ctx.switchProject) ctx.switchProject(slug);
2126
+ });
2127
+ })(p.slug);
2128
+
2129
+ // Right-click context menu (icon only)
2130
+ (function (slug, elem) {
2131
+ elem.addEventListener("contextmenu", function (e) {
2132
+ e.preventDefault();
2133
+ e.stopPropagation();
2134
+ showIconCtxMenu(elem, slug);
2135
+ });
2136
+ })(p.slug, el);
2137
+
2138
+ // Drag-and-drop reordering
2139
+ setupDragHandlers(el, p.slug);
2140
+
2141
+ container.appendChild(el);
2142
+ }
2143
+
2144
+ // Update home icon active state
2145
+ var homeIcon = document.querySelector(".icon-strip-home");
2146
+ if (homeIcon) {
2147
+ if (!currentSlug || projects.length === 0) {
2148
+ homeIcon.classList.add("active");
2149
+ } else {
2150
+ homeIcon.classList.remove("active");
2151
+ }
2152
+ }
2153
+
2154
+ // Also update mobile project list
2155
+ renderProjectList(projects, currentSlug);
2156
+ }
2157
+
2158
+ function renderProjectList(projects, currentSlug) {
2159
+ var list = document.getElementById("project-list");
2160
+ if (!list) return;
2161
+ list.innerHTML = "";
2162
+
2163
+ for (var i = 0; i < projects.length; i++) {
2164
+ (function (p) {
2165
+ var el = document.createElement("button");
2166
+ el.className = "mobile-project-item" + (p.slug === currentSlug ? " active" : "");
2167
+
2168
+ var abbrev = document.createElement("span");
2169
+ abbrev.className = "mobile-project-abbrev";
2170
+ abbrev.textContent = getProjectAbbrev(p.name);
2171
+ el.appendChild(abbrev);
2172
+
2173
+ var name = document.createElement("span");
2174
+ name.className = "mobile-project-name";
2175
+ name.textContent = p.name;
2176
+ el.appendChild(name);
2177
+
2178
+ if (p.isProcessing) {
2179
+ var dot = document.createElement("span");
2180
+ dot.className = "mobile-project-processing";
2181
+ el.appendChild(dot);
2182
+ }
2183
+
2184
+ el.addEventListener("click", function () {
2185
+ if (ctx.switchProject) ctx.switchProject(p.slug);
2186
+ closeSidebar();
2187
+ });
2188
+
2189
+ list.appendChild(el);
2190
+ })(projects[i]);
2191
+ }
2192
+ }
2193
+
2194
+ export function getEmojiCategories() { return EMOJI_CATEGORIES; }
2195
+
2196
+ export function initIconStrip(_ctx) {
2197
+ var addBtn = document.getElementById("icon-strip-add");
2198
+ if (addBtn) {
2199
+ addBtn.addEventListener("click", function () {
2200
+ // Reuse existing add-project modal
2201
+ var modal = _ctx.$("add-project-modal");
2202
+ if (modal) modal.classList.remove("hidden");
2203
+ });
2204
+ addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add project"); });
2205
+ addBtn.addEventListener("mouseleave", hideIconTooltip);
2206
+ }
2207
+
2208
+ var exploreBtn = document.getElementById("icon-strip-explore");
2209
+ if (exploreBtn) {
2210
+ exploreBtn.addEventListener("click", function () {
2211
+ // Toggle file browser
2212
+ var fileBrowserBtn = _ctx.$("file-browser-btn");
2213
+ if (fileBrowserBtn) fileBrowserBtn.click();
2214
+ });
2215
+ exploreBtn.addEventListener("mouseenter", function () { showIconTooltip(exploreBtn, "File browser"); });
2216
+ exploreBtn.addEventListener("mouseleave", hideIconTooltip);
2217
+ }
2218
+
2219
+ // Tooltip + click for home icon
2220
+ var homeIcon = document.querySelector(".icon-strip-home");
2221
+ if (homeIcon) {
2222
+ homeIcon.addEventListener("mouseenter", function () { showIconTooltip(homeIcon, "Clay"); });
2223
+ homeIcon.addEventListener("mouseleave", hideIconTooltip);
2224
+ homeIcon.addEventListener("click", function (e) {
2225
+ e.preventDefault();
2226
+ if (_ctx.showHomeHub) _ctx.showHomeHub();
2227
+ });
2228
+ homeIcon.style.cursor = "pointer";
2229
+ }
2230
+
2231
+ // Chevron dropdown on project name
2232
+ var dropdownBtn = document.getElementById("title-bar-project-dropdown");
2233
+ if (dropdownBtn) {
2234
+ dropdownBtn.addEventListener("click", function (e) {
2235
+ e.stopPropagation();
2236
+ // Find current project info from cached list
2237
+ var current = null;
2238
+ for (var i = 0; i < cachedProjectList.length; i++) {
2239
+ if (cachedProjectList[i].slug === cachedCurrentSlug) {
2240
+ current = cachedProjectList[i];
2241
+ break;
2242
+ }
2243
+ }
2244
+ if (!current) return;
2245
+
2246
+ // Toggle open state
2247
+ if (projectCtxMenu) {
2248
+ closeProjectCtxMenu();
2249
+ dropdownBtn.classList.remove("open");
2250
+ return;
2251
+ }
2252
+ dropdownBtn.classList.add("open");
2253
+ showProjectCtxMenu(dropdownBtn, current.slug, current.name, current.icon, "below");
2254
+ // Remove open class when menu closes
2255
+ var observer = new MutationObserver(function () {
2256
+ if (!projectCtxMenu) {
2257
+ dropdownBtn.classList.remove("open");
2258
+ observer.disconnect();
2259
+ }
2260
+ });
2261
+ observer.observe(document.body, { childList: true });
2262
+ });
2263
+ }
2264
+ }