clay-server 2.34.0-beta.8 → 2.34.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.
@@ -8,7 +8,7 @@ import { openSearch as openSessionSearch } from './session-search.js';
8
8
  import { store } from './store.js';
9
9
  import { getWs } from './ws-ref.js';
10
10
  import { getSessionListEl } from './dom-refs.js';
11
- import { dismissOverlayPanels, closeSidebar, updatePageTitle } from './sidebar.js';
11
+ import { dismissOverlayPanels, closeSidebar, updatePageTitle, spawnDustParticles } from './sidebar.js';
12
12
  import { showConfirm } from './app-misc.js';
13
13
  import { getUpcomingSchedules } from './scheduler.js';
14
14
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
@@ -32,6 +32,16 @@ var countdownContainer = null;
32
32
  // --- Session context menu ---
33
33
  var sessionCtxMenu = null;
34
34
  var sessionCtxSessionId = null;
35
+ var draggedSessionId = null;
36
+ var draggedSessionBookmarked = false;
37
+ var openResumePickerModal = function () {};
38
+ var headerSearchOpen = false;
39
+ var armedDeleteSessionId = null;
40
+ var armedDeleteTimer = null;
41
+
42
+ export function openResumePicker() {
43
+ openResumePickerModal();
44
+ }
35
45
 
36
46
  function sendSessionBookmark(sessionId, bookmarked) {
37
47
  if (getWs() && store.get('connected')) {
@@ -40,76 +50,415 @@ function sendSessionBookmark(sessionId, bookmarked) {
40
50
  }
41
51
 
42
52
  function compareSessionListItems(a, b) {
43
- var aBookmarked = !!a.bookmarked;
44
- var bBookmarked = !!b.bookmarked;
53
+ var aData = a && a.type === "session" ? a.data : a;
54
+ var bData = b && b.type === "session" ? b.data : b;
55
+ var aBookmarked = !!(aData && aData.bookmarked);
56
+ var bBookmarked = !!(bData && bData.bookmarked);
45
57
  if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
58
+ if (aBookmarked && bBookmarked) {
59
+ var ao = aData && typeof aData.favoriteOrder === "number" ? aData.favoriteOrder : Number.MAX_SAFE_INTEGER;
60
+ var bo = bData && typeof bData.favoriteOrder === "number" ? bData.favoriteOrder : Number.MAX_SAFE_INTEGER;
61
+ if (ao !== bo) return ao - bo;
62
+ }
46
63
  return (b.lastActivity || 0) - (a.lastActivity || 0);
47
64
  }
48
65
 
49
- export function initSidebarSessions() {
66
+ function clearSessionDragIndicators() {
67
+ var listEl = getSessionListEl();
68
+ if (!listEl) return;
69
+ var active = listEl.querySelectorAll(".session-favorites-divider.drag-hover, .session-regular-drop.drag-hover, .session-item.dragging");
70
+ for (var i = 0; i < active.length; i++) {
71
+ active[i].classList.remove("drag-hover", "dragging");
72
+ }
73
+ }
50
74
 
51
- document.addEventListener("click", function () { closeSessionCtxMenu(); });
75
+ function setupSessionDragHandlers(el, session) {
76
+ el.setAttribute("draggable", "true");
52
77
 
53
- // --- Session search ---
54
- var searchBtn = document.getElementById("search-session-btn");
55
- var searchBox = document.getElementById("session-search");
56
- var searchInput = document.getElementById("session-search-input");
57
- var searchClear = document.getElementById("session-search-clear");
78
+ el.addEventListener("dragstart", function (e) {
79
+ draggedSessionId = session.id;
80
+ draggedSessionBookmarked = !!session.bookmarked;
81
+ e.dataTransfer.effectAllowed = "move";
82
+ e.dataTransfer.setData("text/plain", String(session.id));
83
+
84
+ var ghost = document.createElement("div");
85
+ ghost.textContent = session.title || "New Session";
86
+ ghost.style.cssText = "position:fixed;left:-200px;top:-200px;max-width:220px;padding:8px 12px;border-radius:10px;" +
87
+ "background:var(--sidebar-active);color:var(--text);font-size:13px;font-weight:600;pointer-events:none;z-index:-1;";
88
+ document.body.appendChild(ghost);
89
+ e.dataTransfer.setDragImage(ghost, 18, 18);
90
+ setTimeout(function () { ghost.remove(); }, 0);
91
+
92
+ setTimeout(function () { el.classList.add("dragging"); }, 0);
93
+ });
94
+
95
+ el.addEventListener("dragend", function () {
96
+ clearSessionDragIndicators();
97
+ draggedSessionId = null;
98
+ draggedSessionBookmarked = false;
99
+ });
58
100
 
59
- function openSearch() {
60
- searchBox.classList.remove("hidden");
61
- searchBtn.classList.add("active");
62
- searchInput.value = "";
63
- searchQuery = "";
64
- setTimeout(function () { searchInput.focus(); }, 50);
101
+ if (session.bookmarked) {
102
+ el.addEventListener("dragover", function (e) {
103
+ if (!draggedSessionId || draggedSessionId === session.id) return;
104
+ e.preventDefault();
105
+ e.dataTransfer.dropEffect = "move";
106
+ var rect = el.getBoundingClientRect();
107
+ var insertBefore = e.clientY < rect.top + rect.height / 2;
108
+ el.classList.remove("drag-over-above", "drag-over-below");
109
+ el.classList.add(insertBefore ? "drag-over-above" : "drag-over-below");
110
+ });
111
+
112
+ el.addEventListener("dragleave", function () {
113
+ el.classList.remove("drag-over-above", "drag-over-below");
114
+ });
115
+
116
+ el.addEventListener("drop", function (e) {
117
+ if (!draggedSessionId || draggedSessionId === session.id) return;
118
+ e.preventDefault();
119
+ var rect = el.getBoundingClientRect();
120
+ var insertBefore = e.clientY < rect.top + rect.height / 2;
121
+ el.classList.remove("drag-over-above", "drag-over-below");
122
+ if (draggedSessionBookmarked) {
123
+ if (getWs() && store.get('connected')) {
124
+ getWs().send(JSON.stringify({
125
+ type: "reorder_session_bookmarks",
126
+ sourceId: draggedSessionId,
127
+ targetId: session.id,
128
+ insertBefore: insertBefore,
129
+ }));
130
+ }
131
+ } else {
132
+ sendSessionBookmark(draggedSessionId, true);
133
+ }
134
+ });
65
135
  }
136
+ }
66
137
 
67
- function closeSearch() {
68
- searchBox.classList.add("hidden");
69
- searchBtn.classList.remove("active");
70
- searchInput.value = "";
71
- searchQuery = "";
72
- searchMatchIds = null;
73
- if (searchDebounce) { clearTimeout(searchDebounce); searchDebounce = null; }
74
- renderSessionList(null);
138
+ function setupBookmarkDropTarget(el, bookmarked) {
139
+ el.addEventListener("dragover", function (e) {
140
+ if (!draggedSessionId) return;
141
+ e.preventDefault();
142
+ e.dataTransfer.dropEffect = "move";
143
+ el.classList.add("drag-hover");
144
+ });
145
+
146
+ el.addEventListener("dragleave", function () {
147
+ el.classList.remove("drag-hover");
148
+ });
149
+
150
+ el.addEventListener("drop", function (e) {
151
+ if (!draggedSessionId) return;
152
+ e.preventDefault();
153
+ el.classList.remove("drag-hover");
154
+ if (draggedSessionBookmarked !== !!bookmarked) {
155
+ sendSessionBookmark(draggedSessionId, !!bookmarked);
156
+ }
157
+ clearSessionDragIndicators();
158
+ draggedSessionId = null;
159
+ draggedSessionBookmarked = false;
160
+ });
161
+ }
162
+
163
+ function spawnSessionDeleteParticles(sessionId) {
164
+ if (!spawnDustParticles) return;
165
+ setTimeout(function () {
166
+ var el = getSessionListEl().querySelector('[data-session-id="' + sessionId + '"]');
167
+ if (!el) return;
168
+ var rect = el.getBoundingClientRect();
169
+ spawnDustParticles(rect.left + rect.width / 2, rect.top + rect.height / 2);
170
+ }, 0);
171
+ }
172
+
173
+ function confirmDeleteSession(session) {
174
+ showConfirm('Delete "' + (session.title || "New Session") + '"? This session and its history will be permanently removed.', function () {
175
+ var ws = getWs();
176
+ if (ws && store.get('connected')) {
177
+ ws.send(JSON.stringify({ type: "delete_session", id: session.id }));
178
+ spawnSessionDeleteParticles(session.id);
179
+ }
180
+ });
181
+ }
182
+
183
+ function clearArmedSessionDelete() {
184
+ if (armedDeleteTimer) {
185
+ clearTimeout(armedDeleteTimer);
186
+ armedDeleteTimer = null;
75
187
  }
188
+ if (armedDeleteSessionId !== null) {
189
+ var prevBtn = getSessionListEl() ? getSessionListEl().querySelector('.session-close-btn[data-session-id="' + armedDeleteSessionId + '"]') : null;
190
+ if (prevBtn) {
191
+ prevBtn.classList.remove("armed");
192
+ prevBtn.innerHTML = iconHtml("x");
193
+ prevBtn.title = "Delete session";
194
+ prevBtn.setAttribute("aria-label", "Delete session");
195
+ refreshIcons();
196
+ }
197
+ }
198
+ armedDeleteSessionId = null;
199
+ }
76
200
 
77
- searchBtn.addEventListener("click", function () {
78
- if (searchBox.classList.contains("hidden")) {
79
- openSearch();
80
- } else {
81
- closeSearch();
201
+ function armSessionDelete(closeBtn, session) {
202
+ clearArmedSessionDelete();
203
+ armedDeleteSessionId = session.id;
204
+ closeBtn.classList.add("armed");
205
+ closeBtn.innerHTML = iconHtml("check");
206
+ closeBtn.title = "Click again to delete";
207
+ closeBtn.setAttribute("aria-label", "Click again to delete");
208
+ refreshIcons();
209
+ armedDeleteTimer = setTimeout(function () {
210
+ clearArmedSessionDelete();
211
+ }, 1800);
212
+ }
213
+
214
+ function deleteSessionImmediately(session) {
215
+ var ws = getWs();
216
+ if (ws && store.get('connected')) {
217
+ ws.send(JSON.stringify({ type: "delete_session", id: session.id }));
218
+ spawnSessionDeleteParticles(session.id);
219
+ }
220
+ }
221
+
222
+ function collectItemSessionIds(item) {
223
+ if (!item) return [];
224
+ if (item.type === "session" && item.data && typeof item.data.id === "number") {
225
+ if (!isSessionVisibleBySearch(item.data.id)) return [];
226
+ return [item.data.id];
227
+ }
228
+ if (item.type === "loop" && Array.isArray(item.children)) {
229
+ var ids = [];
230
+ for (var i = 0; i < item.children.length; i++) {
231
+ if (typeof item.children[i].id === "number" && isSessionVisibleBySearch(item.children[i].id)) {
232
+ ids.push(item.children[i].id);
233
+ }
234
+ }
235
+ return ids;
236
+ }
237
+ return [];
238
+ }
239
+
240
+ function confirmDeleteSessionGroup(groupLabel, sessionIds) {
241
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) return;
242
+ var count = sessionIds.length;
243
+ var noun = count === 1 ? "session" : "sessions";
244
+ showConfirm('Clear "' + groupLabel + '"? ' + count + " " + noun + ' will be permanently removed.', function () {
245
+ var ws = getWs();
246
+ if (ws && store.get('connected')) {
247
+ ws.send(JSON.stringify({ type: "bulk_delete_sessions", sessionIds: sessionIds }));
82
248
  }
83
249
  });
250
+ }
84
251
 
85
- if (searchClear) {
86
- searchClear.addEventListener("click", function () {
87
- closeSearch();
252
+ function createSessionGroupHeader(group, sessionIds) {
253
+ var header = document.createElement("div");
254
+ header.className = "session-group-header";
255
+
256
+ var label = document.createElement("span");
257
+ label.className = "session-group-header-label";
258
+ label.textContent = group;
259
+ header.appendChild(label);
260
+
261
+ if ((!store.get('permissions') || store.get('permissions').sessionDelete !== false) && Array.isArray(sessionIds) && sessionIds.length > 0) {
262
+ var clearBtn = document.createElement("button");
263
+ clearBtn.className = "session-group-clear-btn";
264
+ clearBtn.type = "button";
265
+ clearBtn.textContent = "Clear";
266
+ clearBtn.addEventListener("click", function (e) {
267
+ e.preventDefault();
268
+ e.stopPropagation();
269
+ confirmDeleteSessionGroup(group, sessionIds);
88
270
  });
271
+ header.appendChild(clearBtn);
89
272
  }
90
273
 
91
- searchInput.addEventListener("input", function () {
92
- searchQuery = searchInput.value.trim();
93
- if (searchDebounce) clearTimeout(searchDebounce);
94
- if (!searchQuery) {
95
- searchMatchIds = null;
96
- renderSessionList(null);
274
+ return header;
275
+ }
276
+
277
+ function appendSessionCloseButton(el, session) {
278
+ if (store.get('permissions') && store.get('permissions').sessionDelete === false) return;
279
+
280
+ var closeBtn = document.createElement("button");
281
+ closeBtn.className = "session-close-btn";
282
+ closeBtn.dataset.sessionId = session.id;
283
+ closeBtn.type = "button";
284
+ closeBtn.title = "Delete session";
285
+ closeBtn.setAttribute("aria-label", "Delete session");
286
+ closeBtn.innerHTML = iconHtml("x");
287
+ closeBtn.addEventListener("click", function (e) {
288
+ e.preventDefault();
289
+ e.stopPropagation();
290
+ if (armedDeleteSessionId === session.id) {
291
+ clearArmedSessionDelete();
292
+ deleteSessionImmediately(session);
97
293
  return;
98
294
  }
99
- searchDebounce = setTimeout(function () {
100
- if (getWs() && store.get('connected')) {
101
- getWs().send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
102
- }
103
- }, 200);
295
+ armSessionDelete(closeBtn, session);
104
296
  });
297
+ el.appendChild(closeBtn);
298
+ }
105
299
 
106
- searchInput.addEventListener("keydown", function (e) {
107
- if (e.key === "Escape") {
108
- e.preventDefault();
109
- closeSearch();
300
+ function renderSessionTopActions() {
301
+ var wrap = document.createElement("div");
302
+ wrap.className = "session-top-actions";
303
+
304
+ var newBtn = document.createElement("button");
305
+ newBtn.className = "session-top-action";
306
+ newBtn.type = "button";
307
+ newBtn.innerHTML = iconHtml("plus") + '<span>New Session</span>';
308
+ newBtn.addEventListener("click", function () {
309
+ if (getWs() && store.get('connected')) {
310
+ getWs().send(JSON.stringify({ type: "new_session" }));
311
+ }
312
+ });
313
+ wrap.appendChild(newBtn);
314
+
315
+ var importBtn = document.createElement("button");
316
+ importBtn.className = "session-top-action";
317
+ importBtn.type = "button";
318
+ importBtn.innerHTML = iconHtml("import") + '<span>Import CLI</span>';
319
+ importBtn.addEventListener("click", function () {
320
+ openResumePickerModal();
321
+ });
322
+ wrap.appendChild(importBtn);
323
+
324
+ return wrap;
325
+ }
326
+
327
+ function runSessionSearch(query) {
328
+ var normalizedQuery = query || "";
329
+ var trimmedQuery = normalizedQuery.trim();
330
+ searchQuery = normalizedQuery;
331
+ if (searchDebounce) {
332
+ clearTimeout(searchDebounce);
333
+ searchDebounce = null;
334
+ }
335
+ if (!trimmedQuery) {
336
+ searchMatchIds = null;
337
+ renderSessionList(null);
338
+ return;
339
+ }
340
+ searchDebounce = setTimeout(function () {
341
+ if (getWs() && store.get('connected')) {
342
+ getWs().send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
110
343
  }
344
+ }, 200);
345
+ }
346
+
347
+ function syncHeaderSearchUi() {
348
+ var searchInline = document.getElementById("session-header-search-inline");
349
+ var searchInput = document.getElementById("session-header-search-input");
350
+ var searchClear = document.getElementById("session-header-search-clear");
351
+ var searchBtn = document.getElementById("session-header-search-btn");
352
+ var filterCount = document.getElementById("session-filter-count");
353
+ var isOpen = headerSearchOpen || !!searchQuery;
354
+ if (!searchInline || !searchInput || !searchClear || !searchBtn || !filterCount) return;
355
+ searchInline.classList.toggle("hidden", !isOpen);
356
+ searchBtn.classList.toggle("active", isOpen);
357
+ if (searchInput.value !== searchQuery) {
358
+ searchInput.value = searchQuery;
359
+ }
360
+ searchClear.classList.toggle("hidden", !searchQuery);
361
+ if (!searchQuery || searchMatchIds === null) {
362
+ filterCount.classList.add("hidden");
363
+ filterCount.textContent = "";
364
+ } else {
365
+ filterCount.classList.remove("hidden");
366
+ filterCount.textContent = String(searchMatchIds.size);
367
+ }
368
+ }
369
+
370
+ function openHeaderSearch() {
371
+ headerSearchOpen = true;
372
+ syncHeaderSearchUi();
373
+ var searchInput = document.getElementById("session-header-search-input");
374
+ if (searchInput) {
375
+ requestAnimationFrame(function () {
376
+ searchInput.focus();
377
+ searchInput.select();
378
+ });
379
+ }
380
+ }
381
+
382
+ function closeHeaderSearch() {
383
+ headerSearchOpen = false;
384
+ syncHeaderSearchUi();
385
+ }
386
+
387
+ function clearSessionSearch(shouldBlur, input, shouldClose) {
388
+ if (searchDebounce) {
389
+ clearTimeout(searchDebounce);
390
+ searchDebounce = null;
391
+ }
392
+ searchQuery = "";
393
+ searchMatchIds = null;
394
+ if (shouldClose) {
395
+ headerSearchOpen = false;
396
+ }
397
+ syncHeaderSearchUi();
398
+ renderSessionList(null);
399
+ if (shouldBlur && input) {
400
+ input.blur();
401
+ }
402
+ }
403
+
404
+ export function initSidebarSessions() {
405
+
406
+ document.addEventListener("click", function () {
407
+ closeSessionCtxMenu();
408
+ clearArmedSessionDelete();
111
409
  });
112
410
 
411
+ var searchBtn = document.getElementById("session-header-search-btn");
412
+ var searchInput = document.getElementById("session-header-search-input");
413
+ var searchClear = document.getElementById("session-header-search-clear");
414
+ var searchInline = document.getElementById("session-header-search-inline");
415
+
416
+ if (searchBtn && searchInput && searchClear && searchInline) {
417
+ searchBtn.addEventListener("click", function () {
418
+ if (!headerSearchOpen && !searchQuery) {
419
+ openHeaderSearch();
420
+ return;
421
+ }
422
+ if (!searchQuery) {
423
+ closeHeaderSearch();
424
+ return;
425
+ }
426
+ searchInput.focus();
427
+ searchInput.select();
428
+ });
429
+
430
+ searchInput.addEventListener("input", function () {
431
+ runSessionSearch(searchInput.value);
432
+ syncHeaderSearchUi();
433
+ });
434
+
435
+ searchInput.addEventListener("keydown", function (e) {
436
+ if (e.key === "Escape") {
437
+ e.preventDefault();
438
+ if (searchInput.value.trim()) {
439
+ clearSessionSearch(false, searchInput, false);
440
+ return;
441
+ }
442
+ clearSessionSearch(true, searchInput, true);
443
+ }
444
+ });
445
+
446
+ searchInput.addEventListener("blur", function () {
447
+ setTimeout(function () {
448
+ if (!searchQuery && document.activeElement !== searchBtn && document.activeElement !== searchClear) {
449
+ closeHeaderSearch();
450
+ }
451
+ }, 0);
452
+ });
453
+
454
+ searchClear.addEventListener("click", function () {
455
+ clearSessionSearch(false, searchInput, false);
456
+ searchInput.focus();
457
+ });
458
+
459
+ syncHeaderSearchUi();
460
+ }
461
+
113
462
  // --- Resume session picker ---
114
463
  var resumeModal = document.getElementById("resume-modal");
115
464
  var resumeCancel = document.getElementById("resume-cancel");
@@ -127,13 +476,12 @@ export function initSidebarSessions() {
127
476
  getWs().send(JSON.stringify({ type: "list_cli_sessions" }));
128
477
  }
129
478
  }
479
+ openResumePickerModal = openResumeModal;
130
480
 
131
481
  function closeResumeModal() {
132
482
  resumeModal.classList.add("hidden");
133
483
  }
134
484
 
135
- var resumeBtn = document.getElementById("resume-session-btn");
136
- if (resumeBtn) resumeBtn.addEventListener("click", openResumeModal);
137
485
  resumeCancel.addEventListener("click", closeResumeModal);
138
486
  resumeModal.querySelector(".confirm-backdrop").addEventListener("click", closeResumeModal);
139
487
 
@@ -182,7 +530,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
182
530
 
183
531
  var bookmarkItem = document.createElement("button");
184
532
  bookmarkItem.className = "session-ctx-item";
185
- bookmarkItem.innerHTML = iconHtml(sessionData && sessionData.bookmarked ? "star-off" : "star") + " <span>" + (sessionData && sessionData.bookmarked ? "Remove Bookmark" : "Bookmark") + "</span>";
533
+ bookmarkItem.innerHTML = iconHtml(sessionData && sessionData.bookmarked ? "arrow-down" : "arrow-up") + " <span>" + (sessionData && sessionData.bookmarked ? "Remove from Favorites" : "Add to Favorites") + "</span>";
186
534
  bookmarkItem.addEventListener("click", function (e) {
187
535
  e.stopPropagation();
188
536
  closeSessionCtxMenu();
@@ -225,12 +573,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
225
573
  deleteItem.addEventListener("click", function (e) {
226
574
  e.stopPropagation();
227
575
  closeSessionCtxMenu();
228
- showConfirm('Delete "' + (title || "New Session") + '"? This session and its history will be permanently removed.', function () {
229
- var ws = getWs();
230
- if (ws && store.get('connected')) {
231
- ws.send(JSON.stringify({ type: "delete_session", id: sessionId }));
232
- }
233
- });
576
+ confirmDeleteSession({ id: sessionId, title: title });
234
577
  });
235
578
  menu.appendChild(deleteItem);
236
579
  }
@@ -409,13 +752,17 @@ export function highlightMatch(text, query) {
409
752
  return escapeHtml(before) + '<mark class="session-highlight">' + escapeHtml(match) + '</mark>' + escapeHtml(after);
410
753
  }
411
754
 
755
+ function isSessionVisibleBySearch(sessionId) {
756
+ if (searchMatchIds === null) return true;
757
+ return searchMatchIds.has(sessionId);
758
+ }
759
+
412
760
  // --- Loop child / run / group rendering ---
413
761
 
414
762
  function renderLoopChild(s) {
415
763
  var el = document.createElement("div");
416
764
  var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
417
- var dimmed = searchMatchIds !== null && !isMatch;
418
- el.className = "session-loop-child" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
765
+ el.className = "session-loop-child" + (s.active ? " active" : "") + (isMatch ? " search-match" : "");
419
766
  el.dataset.sessionId = s.id;
420
767
 
421
768
  var textSpan = document.createElement("span");
@@ -433,6 +780,7 @@ function renderLoopChild(s) {
433
780
  }
434
781
  textSpan.innerHTML = textHtml;
435
782
  el.appendChild(textSpan);
783
+ appendSessionCloseButton(el, s);
436
784
 
437
785
  el.addEventListener("click", (function (id) {
438
786
  return function () {
@@ -448,14 +796,27 @@ function renderLoopChild(s) {
448
796
  }
449
797
 
450
798
  function renderLoopGroup(loopId, children, groupKey) {
799
+ var visibleChildren = children;
800
+ if (searchMatchIds !== null) {
801
+ visibleChildren = [];
802
+ for (var vi = 0; vi < children.length; vi++) {
803
+ if (isSessionVisibleBySearch(children[vi].id)) {
804
+ visibleChildren.push(children[vi]);
805
+ }
806
+ }
807
+ if (visibleChildren.length === 0) {
808
+ return null;
809
+ }
810
+ }
811
+
451
812
  var gk = groupKey || loopId;
452
813
 
453
814
  // Sub-group children by startedAt (each run)
454
815
  var runMap = {};
455
- for (var i = 0; i < children.length; i++) {
456
- var runKey = String(children[i].loop && children[i].loop.startedAt || 0);
816
+ for (var i = 0; i < visibleChildren.length; i++) {
817
+ var runKey = String(visibleChildren[i].loop && visibleChildren[i].loop.startedAt || 0);
457
818
  if (!runMap[runKey]) runMap[runKey] = [];
458
- runMap[runKey].push(children[i]);
819
+ runMap[runKey].push(visibleChildren[i]);
459
820
  }
460
821
  var runKeys = Object.keys(runMap);
461
822
 
@@ -477,21 +838,21 @@ function renderLoopGroup(loopId, children, groupKey) {
477
838
  var expanded = expandedLoopGroups.has(gk);
478
839
  var hasActive = false;
479
840
  var anyProcessing = false;
480
- var latestSession = children[0];
481
- for (var ci = 0; ci < children.length; ci++) {
482
- if (children[ci].active) hasActive = true;
483
- if (children[ci].isProcessing) anyProcessing = true;
484
- if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
485
- latestSession = children[ci];
841
+ var latestSession = visibleChildren[0];
842
+ for (var ci = 0; ci < visibleChildren.length; ci++) {
843
+ if (visibleChildren[ci].active) hasActive = true;
844
+ if (visibleChildren[ci].isProcessing) anyProcessing = true;
845
+ if ((visibleChildren[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
846
+ latestSession = visibleChildren[ci];
486
847
  }
487
848
  }
488
849
 
489
- var loopName = (children[0].loop && children[0].loop.name) || "Loop";
490
- var isRalph = children[0].loop && children[0].loop.source === "ralph";
491
- var isDebate = children[0].loop && children[0].loop.source === "debate";
850
+ var loopName = (visibleChildren[0].loop && visibleChildren[0].loop.name) || "Loop";
851
+ var isRalph = visibleChildren[0].loop && visibleChildren[0].loop.source === "ralph";
852
+ var isDebate = visibleChildren[0].loop && visibleChildren[0].loop.source === "debate";
492
853
  var isCrafting = false;
493
- for (var j = 0; j < children.length; j++) {
494
- if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
854
+ for (var j = 0; j < visibleChildren.length; j++) {
855
+ if (visibleChildren[j].loop && visibleChildren[j].loop.role === "crafting") isCrafting = true;
495
856
  }
496
857
 
497
858
  var runCount = runKeys.length;
@@ -536,7 +897,7 @@ function renderLoopGroup(loopId, children, groupKey) {
536
897
  if (isCrafting && children.length === 1) {
537
898
  textHtml += '<span class="session-loop-badge crafting">Crafting</span>';
538
899
  } else {
539
- var countLabel = runCount === 1 ? children.length : runCount + (runCount === 1 ? " run" : " runs");
900
+ var countLabel = runCount === 1 ? visibleChildren.length : runCount + (runCount === 1 ? " run" : " runs");
540
901
  var countClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
541
902
  textHtml += '<span class="session-loop-count' + countClass + '">' + countLabel + '</span>';
542
903
  }
@@ -553,7 +914,7 @@ function renderLoopGroup(loopId, children, groupKey) {
553
914
  e.stopPropagation();
554
915
  showLoopCtxMenu(btn, lid, name, count);
555
916
  };
556
- })(loopId, loopName, children.length, moreBtn));
917
+ })(loopId, loopName, visibleChildren.length, moreBtn));
557
918
  el.appendChild(moreBtn);
558
919
 
559
920
  // Click row (not chevron/more) -> switch to latest session
@@ -673,27 +1034,9 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
673
1034
  function renderSessionItem(s) {
674
1035
  var el = document.createElement("div");
675
1036
  var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
676
- var dimmed = searchMatchIds !== null && !isMatch;
677
- el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
1037
+ el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "");
678
1038
  el.dataset.sessionId = s.id;
679
1039
 
680
- var bookmarkBtn = document.createElement("button");
681
- bookmarkBtn.className = "session-bookmark-btn" + (s.bookmarked ? " bookmarked inline" : " hover");
682
- bookmarkBtn.type = "button";
683
- bookmarkBtn.title = s.bookmarked ? "Remove bookmark" : "Bookmark";
684
- bookmarkBtn.setAttribute("aria-label", s.bookmarked ? "Remove bookmark" : "Bookmark");
685
- bookmarkBtn.innerHTML = iconHtml("star");
686
- bookmarkBtn.addEventListener("click", (function (id, bookmarked) {
687
- return function (e) {
688
- e.preventDefault();
689
- e.stopPropagation();
690
- sendSessionBookmark(id, !bookmarked);
691
- };
692
- })(s.id, !!s.bookmarked));
693
- if (s.bookmarked) {
694
- el.appendChild(bookmarkBtn);
695
- }
696
-
697
1040
  var textSpan = document.createElement("span");
698
1041
  textSpan.className = "session-item-text";
699
1042
  var textHtml = "";
@@ -728,10 +1071,7 @@ function renderSessionItem(s) {
728
1071
  unreadBadge.classList.add("has-unread");
729
1072
  }
730
1073
  el.appendChild(unreadBadge);
731
-
732
- if (!s.bookmarked) {
733
- el.appendChild(bookmarkBtn);
734
- }
1074
+ appendSessionCloseButton(el, s);
735
1075
 
736
1076
  el.addEventListener("click", (function (id) {
737
1077
  return function () {
@@ -749,6 +1089,7 @@ function renderSessionItem(s) {
749
1089
 
750
1090
  // Presence avatars (multi-user)
751
1091
  renderPresenceAvatars(el, String(s.id));
1092
+ setupSessionDragHandlers(el, s);
752
1093
 
753
1094
  return el;
754
1095
  }
@@ -808,6 +1149,9 @@ export function renderSessionList(sessions) {
808
1149
  var regularItems = [];
809
1150
  for (var n = 0; n < items.length; n++) {
810
1151
  var item = items[n];
1152
+ if (item.type === "session" && item.data && !isSessionVisibleBySearch(item.data.id)) {
1153
+ continue;
1154
+ }
811
1155
  if (item.type === "session" && item.data && item.data.bookmarked) {
812
1156
  bookmarkedItems.push(item);
813
1157
  } else {
@@ -815,36 +1159,62 @@ export function renderSessionList(sessions) {
815
1159
  }
816
1160
  }
817
1161
 
818
- if (bookmarkedItems.length > 0) {
819
- var bookmarkedHeader = document.createElement("div");
820
- bookmarkedHeader.className = "session-group-header";
821
- bookmarkedHeader.textContent = "Bookmarked";
822
- getSessionListEl().appendChild(bookmarkedHeader);
823
-
824
- for (var bi = 0; bi < bookmarkedItems.length; bi++) {
825
- getSessionListEl().appendChild(renderSessionItem(bookmarkedItems[bi].data));
826
- }
1162
+ var favoritesContainer = document.createElement("div");
1163
+ favoritesContainer.className = "session-favorites-section";
1164
+ setupBookmarkDropTarget(favoritesContainer, true);
1165
+ if (bookmarkedItems.length === 0) {
1166
+ var emptyHint = document.createElement("div");
1167
+ emptyHint.className = "session-favorites-empty";
1168
+ emptyHint.textContent = "Drag and drop sessions here to add favorites.";
1169
+ favoritesContainer.appendChild(emptyHint);
1170
+ }
1171
+ for (var bi = 0; bi < bookmarkedItems.length; bi++) {
1172
+ favoritesContainer.appendChild(renderSessionItem(bookmarkedItems[bi].data));
827
1173
  }
828
1174
 
1175
+ var divider = document.createElement("div");
1176
+ divider.className = "session-favorites-divider";
1177
+
1178
+ var regularContainer = document.createElement("div");
1179
+ regularContainer.className = "session-regular-drop";
1180
+ setupBookmarkDropTarget(regularContainer, false);
1181
+ var stickyTop = document.createElement("div");
1182
+ stickyTop.className = "session-list-sticky-top";
1183
+ stickyTop.appendChild(favoritesContainer);
1184
+ stickyTop.appendChild(divider);
1185
+ stickyTop.appendChild(renderSessionTopActions());
1186
+ getSessionListEl().appendChild(stickyTop);
1187
+
829
1188
  var currentGroup = "";
1189
+ var currentGroupIds = [];
830
1190
  for (var ri = 0; ri < regularItems.length; ri++) {
831
1191
  var item = regularItems[ri];
832
1192
  var group = getDateGroup(item.lastActivity || 0);
833
1193
  if (group !== currentGroup) {
834
1194
  currentGroup = group;
835
- var header = document.createElement("div");
836
- header.className = "session-group-header";
837
- header.textContent = group;
838
- getSessionListEl().appendChild(header);
1195
+ currentGroupIds = [];
1196
+ for (var gi = ri; gi < regularItems.length; gi++) {
1197
+ if (getDateGroup(regularItems[gi].lastActivity || 0) !== group) break;
1198
+ var groupIds = collectItemSessionIds(regularItems[gi]);
1199
+ for (var gj = 0; gj < groupIds.length; gj++) currentGroupIds.push(groupIds[gj]);
1200
+ }
1201
+ if (group !== "Today") {
1202
+ regularContainer.appendChild(createSessionGroupHeader(group, currentGroupIds));
1203
+ }
839
1204
  }
840
1205
  if (item.type === "loop") {
841
- getSessionListEl().appendChild(renderLoopGroup(item.loopId, item.children, item.groupKey));
1206
+ var loopEl = renderLoopGroup(item.loopId, item.children, item.groupKey);
1207
+ if (loopEl) {
1208
+ regularContainer.appendChild(loopEl);
1209
+ }
842
1210
  } else {
843
- getSessionListEl().appendChild(renderSessionItem(item.data));
1211
+ regularContainer.appendChild(renderSessionItem(item.data));
844
1212
  }
845
1213
  }
1214
+ getSessionListEl().appendChild(regularContainer);
846
1215
  refreshIcons();
847
1216
  if (updatePageTitle) updatePageTitle();
1217
+ syncHeaderSearchUi();
848
1218
  }
849
1219
 
850
1220
  // --- Search results ---
@@ -955,7 +1325,14 @@ function updateCountdowns() {
955
1325
  if (!countdownContainer) {
956
1326
  countdownContainer = document.createElement("div");
957
1327
  countdownContainer.className = "session-countdown-group";
958
- getSessionListEl().insertBefore(countdownContainer, getSessionListEl().firstChild);
1328
+ var stickyTop = getSessionListEl().querySelector(".session-list-sticky-top");
1329
+ if (stickyTop && stickyTop.nextSibling) {
1330
+ getSessionListEl().insertBefore(countdownContainer, stickyTop.nextSibling);
1331
+ } else if (stickyTop) {
1332
+ getSessionListEl().appendChild(countdownContainer);
1333
+ } else {
1334
+ getSessionListEl().insertBefore(countdownContainer, getSessionListEl().firstChild);
1335
+ }
959
1336
  }
960
1337
 
961
1338
  var html = "";