clay-server 2.27.0-beta.9 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-http.js +4 -2
  10. package/lib/project-loop.js +110 -48
  11. package/lib/project-mate-interaction.js +4 -0
  12. package/lib/project-notifications.js +210 -0
  13. package/lib/project-sessions.js +5 -2
  14. package/lib/project-user-message.js +2 -1
  15. package/lib/project.js +26 -2
  16. package/lib/public/app.js +1193 -8517
  17. package/lib/public/css/command-palette.css +14 -0
  18. package/lib/public/css/loop.css +301 -0
  19. package/lib/public/css/notifications-center.css +190 -0
  20. package/lib/public/css/rewind.css +6 -0
  21. package/lib/public/index.html +89 -35
  22. package/lib/public/modules/app-connection.js +160 -0
  23. package/lib/public/modules/app-cursors.js +473 -0
  24. package/lib/public/modules/app-debate-ui.js +389 -0
  25. package/lib/public/modules/app-dm.js +627 -0
  26. package/lib/public/modules/app-favicon.js +212 -0
  27. package/lib/public/modules/app-header.js +229 -0
  28. package/lib/public/modules/app-home-hub.js +600 -0
  29. package/lib/public/modules/app-loop-ui.js +589 -0
  30. package/lib/public/modules/app-loop-wizard.js +439 -0
  31. package/lib/public/modules/app-messages.js +1560 -0
  32. package/lib/public/modules/app-misc.js +299 -0
  33. package/lib/public/modules/app-notifications.js +372 -0
  34. package/lib/public/modules/app-panels.js +888 -0
  35. package/lib/public/modules/app-projects.js +798 -0
  36. package/lib/public/modules/app-rate-limit.js +451 -0
  37. package/lib/public/modules/app-rendering.js +597 -0
  38. package/lib/public/modules/app-skills-install.js +234 -0
  39. package/lib/public/modules/command-palette.js +27 -4
  40. package/lib/public/modules/input.js +31 -20
  41. package/lib/public/modules/scheduler-config.js +1532 -0
  42. package/lib/public/modules/scheduler-history.js +79 -0
  43. package/lib/public/modules/scheduler.js +33 -1554
  44. package/lib/public/modules/session-search.js +13 -1
  45. package/lib/public/modules/sidebar-mates.js +812 -0
  46. package/lib/public/modules/sidebar-mobile.js +1269 -0
  47. package/lib/public/modules/sidebar-projects.js +1449 -0
  48. package/lib/public/modules/sidebar-sessions.js +986 -0
  49. package/lib/public/modules/sidebar.js +232 -4591
  50. package/lib/public/modules/store.js +27 -0
  51. package/lib/public/modules/ws-ref.js +7 -0
  52. package/lib/public/style.css +1 -0
  53. package/lib/sdk-bridge.js +96 -717
  54. package/lib/sdk-message-processor.js +587 -0
  55. package/lib/sdk-message-queue.js +42 -0
  56. package/lib/sdk-skill-discovery.js +131 -0
  57. package/lib/server-admin.js +712 -0
  58. package/lib/server-auth.js +737 -0
  59. package/lib/server-dm.js +221 -0
  60. package/lib/server-mates.js +281 -0
  61. package/lib/server-palette.js +110 -0
  62. package/lib/server-settings.js +479 -0
  63. package/lib/server-skills.js +280 -0
  64. package/lib/server.js +246 -2755
  65. package/lib/sessions.js +11 -4
  66. package/lib/users-auth.js +146 -0
  67. package/lib/users-permissions.js +118 -0
  68. package/lib/users-preferences.js +210 -0
  69. package/lib/users.js +48 -398
  70. package/lib/ws-schema.js +498 -0
  71. package/package.json +1 -1
@@ -0,0 +1,986 @@
1
+ // sidebar-sessions.js - Session list, search, presence, countdown, CLI picker
2
+ // Extracted from sidebar.js (PR-35)
3
+
4
+ import { avatarUrl, userAvatarUrl } from './avatar.js';
5
+ import { escapeHtml } from './utils.js';
6
+ import { iconHtml, refreshIcons } from './icons.js';
7
+ import { openSearch as openSessionSearch } from './session-search.js';
8
+
9
+ var _ctx = null;
10
+
11
+ // --- Session state ---
12
+ var cachedSessions = [];
13
+ var searchQuery = "";
14
+ var searchMatchIds = null; // null = no search, Set of matched session IDs
15
+ var searchDebounce = null;
16
+ var expandedLoopGroups = new Set();
17
+ var expandedLoopRuns = new Set();
18
+
19
+ // --- Session presence (multi-user: who is viewing which session) ---
20
+ var sessionPresence = {}; // { sessionId: [{ id, displayName, avatarStyle, avatarSeed }] }
21
+
22
+ // --- Countdown timer for upcoming schedules ---
23
+ var countdownTimer = null;
24
+ var countdownContainer = null;
25
+
26
+ // --- Session context menu ---
27
+ var sessionCtxMenu = null;
28
+ var sessionCtxSessionId = null;
29
+
30
+ export function initSidebarSessions(ctx) {
31
+ _ctx = ctx;
32
+
33
+ document.addEventListener("click", function () { closeSessionCtxMenu(); });
34
+
35
+ // --- Session search ---
36
+ var searchBtn = _ctx.$("search-session-btn");
37
+ var searchBox = _ctx.$("session-search");
38
+ var searchInput = _ctx.$("session-search-input");
39
+ var searchClear = _ctx.$("session-search-clear");
40
+
41
+ function openSearch() {
42
+ searchBox.classList.remove("hidden");
43
+ searchBtn.classList.add("active");
44
+ searchInput.value = "";
45
+ searchQuery = "";
46
+ setTimeout(function () { searchInput.focus(); }, 50);
47
+ }
48
+
49
+ function closeSearch() {
50
+ searchBox.classList.add("hidden");
51
+ searchBtn.classList.remove("active");
52
+ searchInput.value = "";
53
+ searchQuery = "";
54
+ searchMatchIds = null;
55
+ if (searchDebounce) { clearTimeout(searchDebounce); searchDebounce = null; }
56
+ renderSessionList(null);
57
+ }
58
+
59
+ searchBtn.addEventListener("click", function () {
60
+ if (searchBox.classList.contains("hidden")) {
61
+ openSearch();
62
+ } else {
63
+ closeSearch();
64
+ }
65
+ });
66
+
67
+ if (searchClear) {
68
+ searchClear.addEventListener("click", function () {
69
+ closeSearch();
70
+ });
71
+ }
72
+
73
+ searchInput.addEventListener("input", function () {
74
+ searchQuery = searchInput.value.trim();
75
+ if (searchDebounce) clearTimeout(searchDebounce);
76
+ if (!searchQuery) {
77
+ searchMatchIds = null;
78
+ renderSessionList(null);
79
+ return;
80
+ }
81
+ searchDebounce = setTimeout(function () {
82
+ if (_ctx.ws && _ctx.connected) {
83
+ _ctx.ws.send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
84
+ }
85
+ }, 200);
86
+ });
87
+
88
+ searchInput.addEventListener("keydown", function (e) {
89
+ if (e.key === "Escape") {
90
+ e.preventDefault();
91
+ closeSearch();
92
+ }
93
+ });
94
+
95
+ // --- Resume session picker ---
96
+ var resumeModal = _ctx.$("resume-modal");
97
+ var resumeCancel = _ctx.$("resume-cancel");
98
+ var pickerLoading = _ctx.$("resume-picker-loading");
99
+ var pickerEmpty = _ctx.$("resume-picker-empty");
100
+ var pickerList = _ctx.$("resume-picker-list");
101
+
102
+ function openResumeModal() {
103
+ resumeModal.classList.remove("hidden");
104
+ pickerLoading.classList.remove("hidden");
105
+ pickerEmpty.classList.add("hidden");
106
+ pickerList.classList.add("hidden");
107
+ pickerList.innerHTML = "";
108
+ if (_ctx.ws && _ctx.connected) {
109
+ _ctx.ws.send(JSON.stringify({ type: "list_cli_sessions" }));
110
+ }
111
+ }
112
+
113
+ function closeResumeModal() {
114
+ resumeModal.classList.add("hidden");
115
+ }
116
+
117
+ _ctx.resumeSessionBtn.addEventListener("click", openResumeModal);
118
+ resumeCancel.addEventListener("click", closeResumeModal);
119
+ resumeModal.querySelector(".confirm-backdrop").addEventListener("click", closeResumeModal);
120
+
121
+ // --- Schedule countdown timer ---
122
+ startCountdownTimer();
123
+ }
124
+
125
+ // --- Getters for cross-module access ---
126
+
127
+ export function getCachedSessions() {
128
+ return cachedSessions;
129
+ }
130
+
131
+ export function getSearchQuery() {
132
+ return searchQuery;
133
+ }
134
+
135
+ export function getSearchMatchIds() {
136
+ return searchMatchIds;
137
+ }
138
+
139
+ export function getExpandedLoopGroups() {
140
+ return expandedLoopGroups;
141
+ }
142
+
143
+ export function getExpandedLoopRuns() {
144
+ return expandedLoopRuns;
145
+ }
146
+
147
+ // --- Context menu ---
148
+
149
+ function closeSessionCtxMenu() {
150
+ if (sessionCtxMenu) {
151
+ sessionCtxMenu.remove();
152
+ sessionCtxMenu = null;
153
+ sessionCtxSessionId = null;
154
+ }
155
+ }
156
+
157
+ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
158
+ closeSessionCtxMenu();
159
+ sessionCtxSessionId = sessionId;
160
+
161
+ var menu = document.createElement("div");
162
+ menu.className = "session-ctx-menu";
163
+
164
+ var renameItem = document.createElement("button");
165
+ renameItem.className = "session-ctx-item";
166
+ renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
167
+ renameItem.addEventListener("click", function (e) {
168
+ e.stopPropagation();
169
+ closeSessionCtxMenu();
170
+ startInlineRename(sessionId, title);
171
+ });
172
+ menu.appendChild(renameItem);
173
+
174
+ // Session visibility toggle (only the session owner can change)
175
+ if (_ctx.multiUser && sessionData && sessionData.ownerId && sessionData.ownerId === _ctx.myUserId) {
176
+ var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
177
+ var isPrivate = currentVis === "private";
178
+ var visItem = document.createElement("button");
179
+ visItem.className = "session-ctx-item";
180
+ visItem.innerHTML = iconHtml(isPrivate ? "eye" : "eye-off") + " <span>" + (isPrivate ? "Make Shared" : "Make Private") + "</span>";
181
+ visItem.addEventListener("click", function (e) {
182
+ e.stopPropagation();
183
+ closeSessionCtxMenu();
184
+ var newVis = isPrivate ? "shared" : "private";
185
+ if (_ctx.ws && _ctx.connected) {
186
+ _ctx.ws.send(JSON.stringify({ type: "set_session_visibility", sessionId: sessionId, visibility: newVis }));
187
+ }
188
+ });
189
+ menu.appendChild(visItem);
190
+ }
191
+
192
+ if (!_ctx.permissions || _ctx.permissions.sessionDelete !== false) {
193
+ var deleteItem = document.createElement("button");
194
+ deleteItem.className = "session-ctx-item session-ctx-delete";
195
+ deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
196
+ deleteItem.addEventListener("click", function (e) {
197
+ e.stopPropagation();
198
+ closeSessionCtxMenu();
199
+ _ctx.showConfirm('Delete "' + (title || "New Session") + '"? This session and its history will be permanently removed.', function () {
200
+ var ws = _ctx.ws;
201
+ if (ws && _ctx.connected) {
202
+ ws.send(JSON.stringify({ type: "delete_session", id: sessionId }));
203
+ }
204
+ });
205
+ });
206
+ menu.appendChild(deleteItem);
207
+ }
208
+
209
+ document.body.appendChild(menu);
210
+ sessionCtxMenu = menu;
211
+ refreshIcons();
212
+
213
+ // Position: fixed relative to the anchor button
214
+ requestAnimationFrame(function () {
215
+ var btnRect = anchorBtn.getBoundingClientRect();
216
+ menu.style.position = "fixed";
217
+ menu.style.top = (btnRect.bottom + 2) + "px";
218
+ menu.style.right = (window.innerWidth - btnRect.right) + "px";
219
+ menu.style.left = "auto";
220
+ // If menu overflows below viewport, flip up
221
+ var menuRect = menu.getBoundingClientRect();
222
+ if (menuRect.bottom > window.innerHeight - 8) {
223
+ menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
224
+ }
225
+ });
226
+ }
227
+
228
+ function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
229
+ closeSessionCtxMenu();
230
+
231
+ var menu = document.createElement("div");
232
+ menu.className = "session-ctx-menu";
233
+
234
+ var renameItem = document.createElement("button");
235
+ renameItem.className = "session-ctx-item";
236
+ renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
237
+ renameItem.addEventListener("click", function (e) {
238
+ e.stopPropagation();
239
+ closeSessionCtxMenu();
240
+ startLoopInlineRename(loopId, loopName);
241
+ });
242
+ menu.appendChild(renameItem);
243
+
244
+ if (!_ctx.permissions || _ctx.permissions.sessionDelete !== false) {
245
+ var deleteItem = document.createElement("button");
246
+ deleteItem.className = "session-ctx-item session-ctx-delete";
247
+ deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
248
+ deleteItem.addEventListener("click", function (e) {
249
+ e.stopPropagation();
250
+ closeSessionCtxMenu();
251
+ var msg = 'Delete "' + (loopName || "Loop") + '"';
252
+ if (childCount > 1) msg += " and its " + childCount + " sessions";
253
+ msg += "? This cannot be undone.";
254
+ _ctx.showConfirm(msg, function () {
255
+ if (_ctx.ws && _ctx.connected) {
256
+ _ctx.ws.send(JSON.stringify({ type: "delete_loop_group", loopId: loopId }));
257
+ }
258
+ });
259
+ });
260
+ menu.appendChild(deleteItem);
261
+ }
262
+
263
+ document.body.appendChild(menu);
264
+ sessionCtxMenu = menu;
265
+ refreshIcons();
266
+
267
+ requestAnimationFrame(function () {
268
+ var btnRect = anchorBtn.getBoundingClientRect();
269
+ menu.style.position = "fixed";
270
+ menu.style.top = (btnRect.bottom + 2) + "px";
271
+ menu.style.right = (window.innerWidth - btnRect.right) + "px";
272
+ menu.style.left = "auto";
273
+ var menuRect = menu.getBoundingClientRect();
274
+ if (menuRect.bottom > window.innerHeight - 8) {
275
+ menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
276
+ }
277
+ });
278
+ }
279
+
280
+ // --- Inline rename ---
281
+
282
+ function startInlineRename(sessionId, currentTitle) {
283
+ var el = _ctx.sessionListEl.querySelector('.session-item[data-session-id="' + sessionId + '"]');
284
+ if (!el) return;
285
+ var textSpan = el.querySelector(".session-item-text");
286
+ if (!textSpan) return;
287
+
288
+ var input = document.createElement("input");
289
+ input.type = "text";
290
+ input.className = "session-rename-input";
291
+ input.value = currentTitle || "New Session";
292
+
293
+ var originalHtml = textSpan.innerHTML;
294
+ textSpan.innerHTML = "";
295
+ textSpan.appendChild(input);
296
+ input.focus();
297
+ input.select();
298
+
299
+ function commitRename() {
300
+ var newTitle = input.value.trim();
301
+ if (newTitle && newTitle !== currentTitle && _ctx.ws && _ctx.connected) {
302
+ _ctx.ws.send(JSON.stringify({ type: "rename_session", id: sessionId, title: newTitle }));
303
+ }
304
+ // Restore text (server will send updated session_list)
305
+ textSpan.innerHTML = originalHtml;
306
+ if (newTitle && newTitle !== currentTitle) {
307
+ textSpan.textContent = newTitle;
308
+ }
309
+ }
310
+
311
+ input.addEventListener("keydown", function (e) {
312
+ if (e.key === "Enter") { e.preventDefault(); commitRename(); }
313
+ if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
314
+ });
315
+ input.addEventListener("blur", commitRename);
316
+ input.addEventListener("click", function (e) { e.stopPropagation(); });
317
+ }
318
+
319
+ function startLoopInlineRename(loopId, currentName) {
320
+ var el = _ctx.sessionListEl.querySelector('.session-loop-group[data-loop-id="' + loopId + '"]');
321
+ if (!el) return;
322
+ var textSpan = el.querySelector(".session-item-text");
323
+ if (!textSpan) return;
324
+
325
+ var input = document.createElement("input");
326
+ input.type = "text";
327
+ input.className = "session-rename-input";
328
+ input.value = currentName || "Loop";
329
+
330
+ var originalHtml = textSpan.innerHTML;
331
+ textSpan.innerHTML = "";
332
+ textSpan.appendChild(input);
333
+ input.focus();
334
+ input.select();
335
+
336
+ function commitRename() {
337
+ var newName = input.value.trim();
338
+ if (newName && newName !== currentName && _ctx.ws && _ctx.connected) {
339
+ _ctx.ws.send(JSON.stringify({ type: "loop_registry_rename", id: loopId, name: newName }));
340
+ }
341
+ textSpan.innerHTML = originalHtml;
342
+ if (newName && newName !== currentName) {
343
+ // Update text inline immediately
344
+ var nameNode = textSpan.querySelector(".session-loop-name");
345
+ if (nameNode) nameNode.textContent = newName;
346
+ }
347
+ }
348
+
349
+ input.addEventListener("keydown", function (e) {
350
+ if (e.key === "Enter") { e.preventDefault(); commitRename(); }
351
+ if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
352
+ });
353
+ input.addEventListener("blur", commitRename);
354
+ input.addEventListener("click", function (e) { e.stopPropagation(); });
355
+ }
356
+
357
+ // --- Date grouping / highlighting ---
358
+
359
+ export function getDateGroup(ts) {
360
+ var now = new Date();
361
+ var d = new Date(ts);
362
+ var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
363
+ var yesterday = new Date(today.getTime() - 86400000);
364
+ var weekAgo = new Date(today.getTime() - 7 * 86400000);
365
+ if (d >= today) return "Today";
366
+ if (d >= yesterday) return "Yesterday";
367
+ if (d >= weekAgo) return "This Week";
368
+ return "Older";
369
+ }
370
+
371
+ export function highlightMatch(text, query) {
372
+ if (!query) return escapeHtml(text);
373
+ var lower = text.toLowerCase();
374
+ var qLower = query.toLowerCase();
375
+ var idx = lower.indexOf(qLower);
376
+ if (idx === -1) return escapeHtml(text);
377
+ var before = text.substring(0, idx);
378
+ var match = text.substring(idx, idx + query.length);
379
+ var after = text.substring(idx + query.length);
380
+ return escapeHtml(before) + '<mark class="session-highlight">' + escapeHtml(match) + '</mark>' + escapeHtml(after);
381
+ }
382
+
383
+ // --- Loop child / run / group rendering ---
384
+
385
+ function renderLoopChild(s) {
386
+ var el = document.createElement("div");
387
+ var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
388
+ var dimmed = searchMatchIds !== null && !isMatch;
389
+ el.className = "session-loop-child" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
390
+ el.dataset.sessionId = s.id;
391
+
392
+ var textSpan = document.createElement("span");
393
+ textSpan.className = "session-item-text";
394
+ var textHtml = "";
395
+ if (s.isProcessing) {
396
+ textHtml += '<span class="session-processing"></span>';
397
+ }
398
+ if (s.loop) {
399
+ var isRalphChild = s.loop.source === "ralph";
400
+ var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
401
+ var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
402
+ var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
403
+ textHtml += '<span class="session-loop-role-badge' + roleCls + '">' + roleName + iterSuffix + '</span>';
404
+ }
405
+ textSpan.innerHTML = textHtml;
406
+ el.appendChild(textSpan);
407
+
408
+ el.addEventListener("click", (function (id) {
409
+ return function () {
410
+ if (_ctx.ws && _ctx.connected) {
411
+ _ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
412
+ _ctx.dismissOverlayPanels();
413
+ _ctx.closeSidebar();
414
+ }
415
+ };
416
+ })(s.id));
417
+
418
+ return el;
419
+ }
420
+
421
+ function renderLoopGroup(loopId, children, groupKey) {
422
+ var gk = groupKey || loopId;
423
+
424
+ // Sub-group children by startedAt (each run)
425
+ var runMap = {};
426
+ for (var i = 0; i < children.length; i++) {
427
+ var runKey = String(children[i].loop && children[i].loop.startedAt || 0);
428
+ if (!runMap[runKey]) runMap[runKey] = [];
429
+ runMap[runKey].push(children[i]);
430
+ }
431
+ var runKeys = Object.keys(runMap);
432
+
433
+ // Sort each run's children by iteration then role
434
+ for (var ri = 0; ri < runKeys.length; ri++) {
435
+ runMap[runKeys[ri]].sort(function (a, b) {
436
+ var ai = (a.loop && a.loop.iteration) || 0;
437
+ var bi = (b.loop && b.loop.iteration) || 0;
438
+ if (ai !== bi) return ai - bi;
439
+ var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
440
+ var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
441
+ return ar - br;
442
+ });
443
+ }
444
+
445
+ // Sort runs by startedAt descending (newest first)
446
+ runKeys.sort(function (a, b) { return Number(b) - Number(a); });
447
+
448
+ var expanded = expandedLoopGroups.has(gk);
449
+ var hasActive = false;
450
+ var anyProcessing = false;
451
+ var latestSession = children[0];
452
+ for (var ci = 0; ci < children.length; ci++) {
453
+ if (children[ci].active) hasActive = true;
454
+ if (children[ci].isProcessing) anyProcessing = true;
455
+ if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
456
+ latestSession = children[ci];
457
+ }
458
+ }
459
+
460
+ var loopName = (children[0].loop && children[0].loop.name) || "Loop";
461
+ var isRalph = children[0].loop && children[0].loop.source === "ralph";
462
+ var isDebate = children[0].loop && children[0].loop.source === "debate";
463
+ var isCrafting = false;
464
+ for (var j = 0; j < children.length; j++) {
465
+ if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
466
+ }
467
+
468
+ var runCount = runKeys.length;
469
+
470
+ var wrapper = document.createElement("div");
471
+ wrapper.className = "session-loop-wrapper";
472
+
473
+ // Group header row
474
+ var el = document.createElement("div");
475
+ var groupClass = "session-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "");
476
+ if (isDebate) groupClass += " debate";
477
+ else if (!isRalph) groupClass += " scheduled";
478
+ el.className = groupClass;
479
+ el.dataset.loopId = loopId;
480
+
481
+ var chevron = document.createElement("button");
482
+ chevron.className = "session-loop-chevron";
483
+ chevron.innerHTML = iconHtml("chevron-right");
484
+ chevron.addEventListener("click", (function (lid) {
485
+ return function (e) {
486
+ e.stopPropagation();
487
+ if (expandedLoopGroups.has(lid)) {
488
+ expandedLoopGroups.delete(lid);
489
+ } else {
490
+ expandedLoopGroups.add(lid);
491
+ }
492
+ renderSessionList(null);
493
+ };
494
+ })(gk));
495
+ el.appendChild(chevron);
496
+
497
+ var textSpan = document.createElement("span");
498
+ textSpan.className = "session-item-text";
499
+ var textHtml = "";
500
+ if (anyProcessing) {
501
+ textHtml += '<span class="session-processing"></span>';
502
+ }
503
+ var groupIcon = isDebate ? "mic" : (isRalph ? "repeat" : "calendar-clock");
504
+ var iconClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
505
+ textHtml += '<span class="session-loop-icon' + iconClass + '">' + iconHtml(groupIcon) + '</span>';
506
+ textHtml += '<span class="session-loop-name">' + escapeHtml(loopName) + '</span>';
507
+ if (isCrafting && children.length === 1) {
508
+ textHtml += '<span class="session-loop-badge crafting">Crafting</span>';
509
+ } else {
510
+ var countLabel = runCount === 1 ? children.length : runCount + (runCount === 1 ? " run" : " runs");
511
+ var countClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
512
+ textHtml += '<span class="session-loop-count' + countClass + '">' + countLabel + '</span>';
513
+ }
514
+ textSpan.innerHTML = textHtml;
515
+ el.appendChild(textSpan);
516
+
517
+ // More button (ellipsis)
518
+ var moreBtn = document.createElement("button");
519
+ moreBtn.className = "session-more-btn";
520
+ moreBtn.innerHTML = iconHtml("ellipsis");
521
+ moreBtn.title = "More options";
522
+ moreBtn.addEventListener("click", (function (lid, name, count, btn) {
523
+ return function (e) {
524
+ e.stopPropagation();
525
+ showLoopCtxMenu(btn, lid, name, count);
526
+ };
527
+ })(loopId, loopName, children.length, moreBtn));
528
+ el.appendChild(moreBtn);
529
+
530
+ // Click row (not chevron/more) -> switch to latest session
531
+ el.addEventListener("click", (function (id) {
532
+ return function () {
533
+ if (_ctx.ws && _ctx.connected) {
534
+ _ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
535
+ _ctx.dismissOverlayPanels();
536
+ _ctx.closeSidebar();
537
+ }
538
+ };
539
+ })(latestSession.id));
540
+
541
+ wrapper.appendChild(el);
542
+
543
+ // Expanded: show runs as sub-groups
544
+ if (expanded) {
545
+ var childContainer = document.createElement("div");
546
+ childContainer.className = "session-loop-children";
547
+
548
+ if (runCount === 1) {
549
+ // Single run: show sessions directly (no extra nesting)
550
+ var singleRun = runMap[runKeys[0]];
551
+ for (var sk = 0; sk < singleRun.length; sk++) {
552
+ childContainer.appendChild(renderLoopChild(singleRun[sk]));
553
+ }
554
+ } else {
555
+ // Multiple runs: render each run as a collapsible sub-group
556
+ for (var rk = 0; rk < runKeys.length; rk++) {
557
+ childContainer.appendChild(renderLoopRun(gk, runKeys[rk], runMap[runKeys[rk]], isRalph));
558
+ }
559
+ }
560
+
561
+ wrapper.appendChild(childContainer);
562
+ }
563
+
564
+ return wrapper;
565
+ }
566
+
567
+ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
568
+ var runGk = parentGk + ":" + startedAtKey;
569
+ var expanded = expandedLoopRuns.has(runGk);
570
+ var startedAt = Number(startedAtKey);
571
+ var timeLabel = startedAt ? new Date(startedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "Unknown";
572
+
573
+ var hasActive = false;
574
+ var anyProcessing = false;
575
+ var latestSession = sessions[0];
576
+ for (var i = 0; i < sessions.length; i++) {
577
+ if (sessions[i].active) hasActive = true;
578
+ if (sessions[i].isProcessing) anyProcessing = true;
579
+ if ((sessions[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
580
+ latestSession = sessions[i];
581
+ }
582
+ }
583
+
584
+ var wrapper = document.createElement("div");
585
+ wrapper.className = "session-loop-run-wrapper";
586
+
587
+ var el = document.createElement("div");
588
+ el.className = "session-loop-run" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
589
+
590
+ var chevron = document.createElement("button");
591
+ chevron.className = "session-loop-chevron";
592
+ chevron.innerHTML = iconHtml("chevron-right");
593
+ chevron.addEventListener("click", (function (rk) {
594
+ return function (e) {
595
+ e.stopPropagation();
596
+ if (expandedLoopRuns.has(rk)) {
597
+ expandedLoopRuns.delete(rk);
598
+ } else {
599
+ expandedLoopRuns.add(rk);
600
+ }
601
+ renderSessionList(null);
602
+ };
603
+ })(runGk));
604
+ el.appendChild(chevron);
605
+
606
+ var textSpan = document.createElement("span");
607
+ textSpan.className = "session-item-text";
608
+ var textHtml = "";
609
+ if (anyProcessing) {
610
+ textHtml += '<span class="session-processing"></span>';
611
+ }
612
+ textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
613
+ textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
614
+ textSpan.innerHTML = textHtml;
615
+ el.appendChild(textSpan);
616
+
617
+ // Click row -> switch to latest session of this run
618
+ el.addEventListener("click", (function (id) {
619
+ return function () {
620
+ if (_ctx.ws && _ctx.connected) {
621
+ _ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
622
+ _ctx.dismissOverlayPanels();
623
+ _ctx.closeSidebar();
624
+ }
625
+ };
626
+ })(latestSession.id));
627
+
628
+ wrapper.appendChild(el);
629
+
630
+ if (expanded) {
631
+ var childContainer = document.createElement("div");
632
+ childContainer.className = "session-loop-children";
633
+ for (var k = 0; k < sessions.length; k++) {
634
+ childContainer.appendChild(renderLoopChild(sessions[k]));
635
+ }
636
+ wrapper.appendChild(childContainer);
637
+ }
638
+
639
+ return wrapper;
640
+ }
641
+
642
+ // --- Session item rendering ---
643
+
644
+ function renderSessionItem(s) {
645
+ var el = document.createElement("div");
646
+ var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
647
+ var dimmed = searchMatchIds !== null && !isMatch;
648
+ el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
649
+ el.dataset.sessionId = s.id;
650
+
651
+ var textSpan = document.createElement("span");
652
+ textSpan.className = "session-item-text";
653
+ var textHtml = "";
654
+ if (s.isProcessing) {
655
+ textHtml += '<span class="session-processing"></span>';
656
+ }
657
+ if (s.loop && s.loop.source === "debate") {
658
+ textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
659
+ }
660
+ if (_ctx.multiUser && s.sessionVisibility === "private") {
661
+ textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
662
+ }
663
+ textHtml += highlightMatch(s.title || "New Session", searchQuery);
664
+ textSpan.innerHTML = textHtml;
665
+ el.appendChild(textSpan);
666
+
667
+ // Right-click / long-press: context menu
668
+ el.addEventListener("contextmenu", (function(id, title, cliSid, anchor, sData) {
669
+ return function(e) {
670
+ e.preventDefault();
671
+ e.stopPropagation();
672
+ showSessionCtxMenu(anchor, id, title, cliSid, sData);
673
+ };
674
+ })(s.id, s.title, s.cliSessionId, el, s));
675
+
676
+ // Unread badge
677
+ var unreadBadge = document.createElement("span");
678
+ unreadBadge.className = "session-unread-badge";
679
+ unreadBadge.dataset.sessionId = s.id;
680
+ if (s.unread > 0) {
681
+ unreadBadge.textContent = s.unread > 99 ? "99+" : String(s.unread);
682
+ unreadBadge.classList.add("has-unread");
683
+ }
684
+ el.appendChild(unreadBadge);
685
+
686
+ el.addEventListener("click", (function (id) {
687
+ return function () {
688
+ if (_ctx.ws && _ctx.connected) {
689
+ var pendingQuery = searchQuery || "";
690
+ _ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
691
+ _ctx.dismissOverlayPanels();
692
+ _ctx.closeSidebar();
693
+ if (pendingQuery) {
694
+ setTimeout(function () { openSessionSearch(pendingQuery); }, 400);
695
+ }
696
+ }
697
+ };
698
+ })(s.id));
699
+
700
+ // Presence avatars (multi-user)
701
+ renderPresenceAvatars(el, String(s.id));
702
+
703
+ return el;
704
+ }
705
+
706
+ // --- Main session list ---
707
+
708
+ export function renderSessionList(sessions) {
709
+ if (sessions) cachedSessions = sessions;
710
+
711
+ // If mobile chat sheet is open, refresh it
712
+ if (_ctx.refreshMobileChatSheet) _ctx.refreshMobileChatSheet();
713
+
714
+ _ctx.sessionListEl.innerHTML = "";
715
+
716
+ // Partition: loop sessions vs normal sessions
717
+ // Group by loopId + date so all runs of the same task on the same day are merged
718
+ var loopGroups = {}; // groupKey -> [sessions]
719
+ var normalSessions = [];
720
+ for (var i = 0; i < cachedSessions.length; i++) {
721
+ var s = cachedSessions[i];
722
+ if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
723
+ // Task crafting sessions live in the scheduler calendar, not the main list (except debate)
724
+ continue;
725
+ } else if (s.loop && s.loop.loopId) {
726
+ var startedAt = s.loop.startedAt || 0;
727
+ var dateStr = startedAt ? new Date(startedAt).toISOString().slice(0, 10) : "unknown";
728
+ var groupKey = s.loop.loopId + ":" + dateStr;
729
+ if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
730
+ loopGroups[groupKey].push(s);
731
+ } else {
732
+ normalSessions.push(s);
733
+ }
734
+ }
735
+
736
+ // Build virtual items: normal sessions + one entry per loop group (using latest child's lastActivity)
737
+ var items = [];
738
+ for (var j = 0; j < normalSessions.length; j++) {
739
+ items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
740
+ }
741
+ var groupKeys = Object.keys(loopGroups);
742
+ for (var k = 0; k < groupKeys.length; k++) {
743
+ var gk = groupKeys[k];
744
+ var children = loopGroups[gk];
745
+ var realLoopId = children[0].loop.loopId;
746
+ var maxActivity = 0;
747
+ for (var m = 0; m < children.length; m++) {
748
+ var act = children[m].lastActivity || 0;
749
+ if (act > maxActivity) maxActivity = act;
750
+ }
751
+ items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
752
+ }
753
+
754
+ // Sort by lastActivity descending
755
+ items.sort(function (a, b) {
756
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
757
+ });
758
+
759
+ var currentGroup = "";
760
+ for (var n = 0; n < items.length; n++) {
761
+ var item = items[n];
762
+ var group = getDateGroup(item.lastActivity || 0);
763
+ if (group !== currentGroup) {
764
+ currentGroup = group;
765
+ var header = document.createElement("div");
766
+ header.className = "session-group-header";
767
+ header.textContent = group;
768
+ _ctx.sessionListEl.appendChild(header);
769
+ }
770
+ if (item.type === "loop") {
771
+ _ctx.sessionListEl.appendChild(renderLoopGroup(item.loopId, item.children, item.groupKey));
772
+ } else {
773
+ _ctx.sessionListEl.appendChild(renderSessionItem(item.data));
774
+ }
775
+ }
776
+ refreshIcons();
777
+ if (_ctx.updatePageTitle) _ctx.updatePageTitle();
778
+ }
779
+
780
+ // --- Search results ---
781
+
782
+ export function handleSearchResults(msg) {
783
+ if (msg.query !== searchQuery) return; // stale response
784
+ var ids = new Set();
785
+ for (var i = 0; i < msg.results.length; i++) {
786
+ ids.add(msg.results[i].id);
787
+ }
788
+ searchMatchIds = ids;
789
+ renderSessionList(null);
790
+ }
791
+
792
+ // --- Session presence ---
793
+
794
+ export function updateSessionPresence(presence) {
795
+ sessionPresence = presence;
796
+ // Update presence avatars on existing session items without full re-render
797
+ var items = _ctx.sessionListEl.querySelectorAll("[data-session-id]");
798
+ for (var i = 0; i < items.length; i++) {
799
+ renderPresenceAvatars(items[i], items[i].dataset.sessionId);
800
+ }
801
+ }
802
+
803
+ function presenceAvatarUrl(userOrStyle, seed) {
804
+ if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
805
+ return avatarUrl(userOrStyle || "thumbs", seed, 24);
806
+ }
807
+
808
+ function renderPresenceAvatars(el, sessionId) {
809
+ // Remove existing presence container
810
+ var existing = el.querySelector(".session-presence");
811
+ if (existing) existing.remove();
812
+
813
+ var users = sessionPresence[sessionId];
814
+ if (!users || users.length === 0) return;
815
+
816
+ var container = document.createElement("span");
817
+ container.className = "session-presence";
818
+
819
+ var max = 3;
820
+ var shown = users.length > max ? max : users.length;
821
+ for (var i = 0; i < shown; i++) {
822
+ var u = users[i];
823
+ var img = document.createElement("img");
824
+ img.className = "session-presence-avatar";
825
+ img.src = presenceAvatarUrl(u);
826
+ img.alt = u.displayName;
827
+ img.dataset.tip = u.displayName + (u.username ? " (@" + u.username + ")" : "");
828
+ if (i > 0) img.style.marginLeft = "-6px";
829
+ container.appendChild(img);
830
+ }
831
+ if (users.length > max) {
832
+ var more = document.createElement("span");
833
+ more.className = "session-presence-more";
834
+ more.textContent = "+" + (users.length - max);
835
+ container.appendChild(more);
836
+ }
837
+
838
+ // Insert before the more-btn
839
+ var moreBtn = el.querySelector(".session-more-btn");
840
+ if (moreBtn) {
841
+ el.insertBefore(container, moreBtn);
842
+ } else {
843
+ el.appendChild(container);
844
+ }
845
+ }
846
+
847
+ // --- Session badge ---
848
+
849
+ export function updateSessionBadge(sessionId, count) {
850
+ var badge = document.querySelector('.session-unread-badge[data-session-id="' + sessionId + '"]');
851
+ if (!badge) return;
852
+ if (count > 0) {
853
+ badge.textContent = count > 99 ? "99+" : String(count);
854
+ badge.classList.add("has-unread");
855
+ } else {
856
+ badge.textContent = "";
857
+ badge.classList.remove("has-unread");
858
+ }
859
+ }
860
+
861
+ // --- Countdown timer ---
862
+
863
+ function startCountdownTimer() {
864
+ if (countdownTimer) clearInterval(countdownTimer);
865
+ countdownTimer = setInterval(updateCountdowns, 1000);
866
+ }
867
+
868
+ function updateCountdowns() {
869
+ if (!_ctx || !_ctx.getUpcomingSchedules || !_ctx.sessionListEl) return;
870
+ var upcoming = _ctx.getUpcomingSchedules(3 * 60 * 1000); // 3 minutes
871
+
872
+ // Remove stale container
873
+ if (countdownContainer && !_ctx.sessionListEl.contains(countdownContainer)) {
874
+ countdownContainer = null;
875
+ }
876
+
877
+ if (upcoming.length === 0) {
878
+ if (countdownContainer) {
879
+ countdownContainer.remove();
880
+ countdownContainer = null;
881
+ }
882
+ return;
883
+ }
884
+
885
+ if (!countdownContainer) {
886
+ countdownContainer = document.createElement("div");
887
+ countdownContainer.className = "session-countdown-group";
888
+ _ctx.sessionListEl.insertBefore(countdownContainer, _ctx.sessionListEl.firstChild);
889
+ }
890
+
891
+ var html = "";
892
+ var now = Date.now();
893
+ for (var i = 0; i < upcoming.length; i++) {
894
+ var u = upcoming[i];
895
+ var remaining = Math.max(0, Math.ceil((u.nextRunAt - now) / 1000));
896
+ var min = Math.floor(remaining / 60);
897
+ var sec = remaining % 60;
898
+ var timeStr = min + ":" + (sec < 10 ? "0" : "") + sec;
899
+ var colorStyle = u.color ? " style=\"border-left-color:" + u.color + "\"" : "";
900
+ html += '<div class="session-countdown-item"' + colorStyle + '>';
901
+ html += '<span class="session-countdown-name">' + escapeHtml(u.name) + '</span>';
902
+ html += '<span class="session-countdown-badge">' + timeStr + '</span>';
903
+ html += '</div>';
904
+ }
905
+ countdownContainer.innerHTML = html;
906
+ }
907
+
908
+ // --- CLI session picker ---
909
+
910
+ function relativeTime(isoString) {
911
+ if (!isoString) return "";
912
+ var ms = Date.now() - new Date(isoString).getTime();
913
+ var sec = Math.floor(ms / 1000);
914
+ if (sec < 60) return "just now";
915
+ var min = Math.floor(sec / 60);
916
+ if (min < 60) return min + "m ago";
917
+ var hr = Math.floor(min / 60);
918
+ if (hr < 24) return hr + "h ago";
919
+ var days = Math.floor(hr / 24);
920
+ if (days < 30) return days + "d ago";
921
+ return new Date(isoString).toLocaleDateString();
922
+ }
923
+
924
+ export function populateCliSessionList(sessions) {
925
+ var pickerLoading = _ctx.$("resume-picker-loading");
926
+ var pickerEmpty = _ctx.$("resume-picker-empty");
927
+ var pickerList = _ctx.$("resume-picker-list");
928
+ if (!pickerLoading || !pickerList) return;
929
+
930
+ pickerLoading.classList.add("hidden");
931
+
932
+ if (!sessions || sessions.length === 0) {
933
+ pickerEmpty.classList.remove("hidden");
934
+ pickerList.classList.add("hidden");
935
+ return;
936
+ }
937
+
938
+ pickerEmpty.classList.add("hidden");
939
+ pickerList.classList.remove("hidden");
940
+ pickerList.innerHTML = "";
941
+
942
+ for (var i = 0; i < sessions.length; i++) {
943
+ var s = sessions[i];
944
+ var item = document.createElement("div");
945
+ item.className = "cli-session-item";
946
+
947
+ var title = document.createElement("div");
948
+ title.className = "cli-session-title";
949
+ title.textContent = s.firstPrompt || "Untitled session";
950
+ item.appendChild(title);
951
+
952
+ var meta = document.createElement("div");
953
+ meta.className = "cli-session-meta";
954
+ if (s.lastActivity) {
955
+ var time = document.createElement("span");
956
+ time.textContent = relativeTime(s.lastActivity);
957
+ meta.appendChild(time);
958
+ }
959
+ if (s.model) {
960
+ var model = document.createElement("span");
961
+ model.className = "badge";
962
+ model.textContent = s.model;
963
+ meta.appendChild(model);
964
+ }
965
+ if (s.gitBranch) {
966
+ var branch = document.createElement("span");
967
+ branch.className = "badge";
968
+ branch.textContent = s.gitBranch;
969
+ meta.appendChild(branch);
970
+ }
971
+ item.appendChild(meta);
972
+
973
+ (function (sessionId) {
974
+ item.addEventListener("click", function () {
975
+ if (_ctx.ws && _ctx.connected) {
976
+ _ctx.ws.send(JSON.stringify({ type: "resume_session", cliSessionId: sessionId }));
977
+ }
978
+ var modal = _ctx.$("resume-modal");
979
+ if (modal) modal.classList.add("hidden");
980
+ _ctx.closeSidebar();
981
+ });
982
+ })(s.sessionId);
983
+
984
+ pickerList.appendChild(item);
985
+ }
986
+ }