clay-server 2.7.2 → 2.8.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 (58) hide show
  1. package/bin/cli.js +2 -1
  2. package/lib/config.js +7 -4
  3. package/lib/project.js +343 -15
  4. package/lib/public/app.js +1043 -135
  5. package/lib/public/apple-touch-icon-dark.png +0 -0
  6. package/lib/public/apple-touch-icon.png +0 -0
  7. package/lib/public/clay-logo.png +0 -0
  8. package/lib/public/css/base.css +10 -0
  9. package/lib/public/css/filebrowser.css +1 -0
  10. package/lib/public/css/home-hub.css +455 -0
  11. package/lib/public/css/icon-strip.css +6 -5
  12. package/lib/public/css/loop.css +141 -23
  13. package/lib/public/css/messages.css +2 -0
  14. package/lib/public/css/mobile-nav.css +38 -12
  15. package/lib/public/css/overlays.css +205 -169
  16. package/lib/public/css/playbook.css +264 -0
  17. package/lib/public/css/profile.css +268 -0
  18. package/lib/public/css/scheduler-modal.css +1429 -0
  19. package/lib/public/css/scheduler.css +1305 -0
  20. package/lib/public/css/sidebar.css +305 -11
  21. package/lib/public/css/sticky-notes.css +23 -19
  22. package/lib/public/css/stt.css +155 -0
  23. package/lib/public/css/title-bar.css +14 -6
  24. package/lib/public/favicon-banded-32.png +0 -0
  25. package/lib/public/favicon-banded.png +0 -0
  26. package/lib/public/icon-192-dark.png +0 -0
  27. package/lib/public/icon-192.png +0 -0
  28. package/lib/public/icon-512-dark.png +0 -0
  29. package/lib/public/icon-512.png +0 -0
  30. package/lib/public/icon-banded-76.png +0 -0
  31. package/lib/public/icon-banded-96.png +0 -0
  32. package/lib/public/index.html +335 -42
  33. package/lib/public/modules/ascii-logo.js +389 -0
  34. package/lib/public/modules/filebrowser.js +2 -1
  35. package/lib/public/modules/markdown.js +118 -0
  36. package/lib/public/modules/notifications.js +50 -63
  37. package/lib/public/modules/playbook.js +578 -0
  38. package/lib/public/modules/profile.js +357 -0
  39. package/lib/public/modules/project-settings.js +4 -9
  40. package/lib/public/modules/scheduler.js +2826 -0
  41. package/lib/public/modules/server-settings.js +1 -1
  42. package/lib/public/modules/sidebar.js +378 -31
  43. package/lib/public/modules/sticky-notes.js +2 -0
  44. package/lib/public/modules/stt.js +272 -0
  45. package/lib/public/modules/terminal.js +32 -0
  46. package/lib/public/modules/theme.js +3 -10
  47. package/lib/public/modules/tools.js +2 -1
  48. package/lib/public/style.css +6 -0
  49. package/lib/public/sw.js +82 -3
  50. package/lib/public/wordmark-banded-20.png +0 -0
  51. package/lib/public/wordmark-banded-32.png +0 -0
  52. package/lib/public/wordmark-banded-64.png +0 -0
  53. package/lib/public/wordmark-banded-80.png +0 -0
  54. package/lib/scheduler.js +402 -0
  55. package/lib/sdk-bridge.js +3 -2
  56. package/lib/server.js +124 -3
  57. package/lib/sessions.js +35 -2
  58. package/package.json +1 -1
@@ -316,7 +316,7 @@ function populateSettings() {
316
316
  if (wsPathEl) wsPathEl.textContent = ctx.wsPath || "/ws";
317
317
 
318
318
  // Skip permissions
319
- var spBanner = document.getElementById("skip-perms-banner");
319
+ var spBanner = document.getElementById("skip-perms-pill");
320
320
  if (skipPermsEl) {
321
321
  var isSkip = spBanner && !spBanner.classList.contains("hidden");
322
322
  skipPermsEl.textContent = isSkip ? "Enabled" : "Disabled";
@@ -2,6 +2,7 @@ import { escapeHtml, copyToClipboard } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { openProjectSettings } from './project-settings.js';
4
4
  import { triggerShare } from './qrcode.js';
5
+ import { parseEmojis } from './markdown.js';
5
6
 
6
7
  var ctx;
7
8
 
@@ -10,11 +11,16 @@ var searchQuery = "";
10
11
  var searchMatchIds = null; // null = no search, Set of matched session IDs
11
12
  var searchDebounce = null;
12
13
  var cachedSessions = [];
14
+ var expandedLoopGroups = new Set();
13
15
 
14
16
  // --- Cached project data for mobile sheet ---
15
17
  var cachedProjectList = [];
16
18
  var cachedCurrentSlug = null;
17
19
 
20
+ // --- Countdown timer for upcoming schedules ---
21
+ var countdownTimer = null;
22
+ var countdownContainer = null;
23
+
18
24
  // --- Session context menu ---
19
25
  var sessionCtxMenu = null;
20
26
  var sessionCtxSessionId = null;
@@ -115,6 +121,94 @@ function startInlineRename(sessionId, currentTitle) {
115
121
  input.addEventListener("click", function (e) { e.stopPropagation(); });
116
122
  }
117
123
 
124
+ function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
125
+ closeSessionCtxMenu();
126
+
127
+ var menu = document.createElement("div");
128
+ menu.className = "session-ctx-menu";
129
+
130
+ var renameItem = document.createElement("button");
131
+ renameItem.className = "session-ctx-item";
132
+ renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
133
+ renameItem.addEventListener("click", function (e) {
134
+ e.stopPropagation();
135
+ closeSessionCtxMenu();
136
+ startLoopInlineRename(loopId, loopName);
137
+ });
138
+ menu.appendChild(renameItem);
139
+
140
+ var deleteItem = document.createElement("button");
141
+ deleteItem.className = "session-ctx-item session-ctx-delete";
142
+ deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
143
+ deleteItem.addEventListener("click", function (e) {
144
+ e.stopPropagation();
145
+ closeSessionCtxMenu();
146
+ var msg = 'Delete "' + (loopName || "Ralph Loop") + '"';
147
+ if (childCount > 1) msg += " and its " + childCount + " sessions";
148
+ msg += "? This cannot be undone.";
149
+ ctx.showConfirm(msg, function () {
150
+ if (ctx.ws && ctx.connected) {
151
+ ctx.ws.send(JSON.stringify({ type: "delete_loop_group", loopId: loopId }));
152
+ }
153
+ });
154
+ });
155
+ menu.appendChild(deleteItem);
156
+
157
+ document.body.appendChild(menu);
158
+ sessionCtxMenu = menu;
159
+ refreshIcons();
160
+
161
+ requestAnimationFrame(function () {
162
+ var btnRect = anchorBtn.getBoundingClientRect();
163
+ menu.style.position = "fixed";
164
+ menu.style.top = (btnRect.bottom + 2) + "px";
165
+ menu.style.right = (window.innerWidth - btnRect.right) + "px";
166
+ menu.style.left = "auto";
167
+ var menuRect = menu.getBoundingClientRect();
168
+ if (menuRect.bottom > window.innerHeight - 8) {
169
+ menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
170
+ }
171
+ });
172
+ }
173
+
174
+ function startLoopInlineRename(loopId, currentName) {
175
+ var el = ctx.sessionListEl.querySelector('.session-loop-group[data-loop-id="' + loopId + '"]');
176
+ if (!el) return;
177
+ var textSpan = el.querySelector(".session-item-text");
178
+ if (!textSpan) return;
179
+
180
+ var input = document.createElement("input");
181
+ input.type = "text";
182
+ input.className = "session-rename-input";
183
+ input.value = currentName || "Ralph Loop";
184
+
185
+ var originalHtml = textSpan.innerHTML;
186
+ textSpan.innerHTML = "";
187
+ textSpan.appendChild(input);
188
+ input.focus();
189
+ input.select();
190
+
191
+ function commitRename() {
192
+ var newName = input.value.trim();
193
+ if (newName && newName !== currentName && ctx.ws && ctx.connected) {
194
+ ctx.ws.send(JSON.stringify({ type: "loop_registry_rename", id: loopId, name: newName }));
195
+ }
196
+ textSpan.innerHTML = originalHtml;
197
+ if (newName && newName !== currentName) {
198
+ // Update text inline immediately
199
+ var nameNode = textSpan.querySelector(".session-loop-name");
200
+ if (nameNode) nameNode.textContent = newName;
201
+ }
202
+ }
203
+
204
+ input.addEventListener("keydown", function (e) {
205
+ if (e.key === "Enter") { e.preventDefault(); commitRename(); }
206
+ if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
207
+ });
208
+ input.addEventListener("blur", commitRename);
209
+ input.addEventListener("click", function (e) { e.stopPropagation(); });
210
+ }
211
+
118
212
  function getDateGroup(ts) {
119
213
  var now = new Date();
120
214
  var d = new Date(ts);
@@ -139,6 +233,155 @@ function highlightMatch(text, query) {
139
233
  return escapeHtml(before) + '<mark class="session-highlight">' + escapeHtml(match) + '</mark>' + escapeHtml(after);
140
234
  }
141
235
 
236
+ function renderLoopChild(s) {
237
+ var el = document.createElement("div");
238
+ var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
239
+ var dimmed = searchMatchIds !== null && !isMatch;
240
+ el.className = "session-loop-child" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
241
+ el.dataset.sessionId = s.id;
242
+
243
+ var textSpan = document.createElement("span");
244
+ textSpan.className = "session-item-text";
245
+ var textHtml = "";
246
+ if (s.isProcessing) {
247
+ textHtml += '<span class="session-processing"></span>';
248
+ }
249
+ if (s.loop) {
250
+ var isRalphChild = s.loop.source === "ralph";
251
+ var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
252
+ var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
253
+ var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
254
+ textHtml += '<span class="session-loop-role-badge' + roleCls + '">' + roleName + iterSuffix + '</span>';
255
+ }
256
+ textSpan.innerHTML = textHtml;
257
+ el.appendChild(textSpan);
258
+
259
+ el.addEventListener("click", (function (id) {
260
+ return function () {
261
+ if (ctx.ws && ctx.connected) {
262
+ ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
263
+ closeSidebar();
264
+ }
265
+ };
266
+ })(s.id));
267
+
268
+ return el;
269
+ }
270
+
271
+ function renderLoopGroup(loopId, children, groupKey) {
272
+ var gk = groupKey || loopId;
273
+ // Sort children by iteration then role (coder before judge)
274
+ children.sort(function (a, b) {
275
+ var ai = (a.loop && a.loop.iteration) || 0;
276
+ var bi = (b.loop && b.loop.iteration) || 0;
277
+ if (ai !== bi) return ai - bi;
278
+ // coder before judge within same iteration
279
+ var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
280
+ var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
281
+ return ar - br;
282
+ });
283
+
284
+ var expanded = expandedLoopGroups.has(gk);
285
+ var hasActive = false;
286
+ var anyProcessing = false;
287
+ var latestSession = children[0];
288
+ for (var i = 0; i < children.length; i++) {
289
+ if (children[i].active) hasActive = true;
290
+ if (children[i].isProcessing) anyProcessing = true;
291
+ if ((children[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
292
+ latestSession = children[i];
293
+ }
294
+ }
295
+
296
+ var loopName = (children[0].loop && children[0].loop.name) || "Ralph Loop";
297
+ var isRalph = children[0].loop && children[0].loop.source === "ralph";
298
+ var isCrafting = false;
299
+ var maxIter = 0;
300
+ for (var j = 0; j < children.length; j++) {
301
+ var iter = (children[j].loop && children[j].loop.iteration) || 0;
302
+ if (iter > maxIter) maxIter = iter;
303
+ if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
304
+ }
305
+
306
+ var wrapper = document.createElement("div");
307
+ wrapper.className = "session-loop-wrapper";
308
+
309
+ // Group header row
310
+ var el = document.createElement("div");
311
+ el.className = "session-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
312
+ el.dataset.loopId = loopId;
313
+
314
+ var chevron = document.createElement("button");
315
+ chevron.className = "session-loop-chevron";
316
+ chevron.innerHTML = iconHtml("chevron-right");
317
+ chevron.addEventListener("click", (function (lid) {
318
+ return function (e) {
319
+ e.stopPropagation();
320
+ if (expandedLoopGroups.has(lid)) {
321
+ expandedLoopGroups.delete(lid);
322
+ } else {
323
+ expandedLoopGroups.add(lid);
324
+ }
325
+ renderSessionList(null);
326
+ };
327
+ })(gk));
328
+ el.appendChild(chevron);
329
+
330
+ var textSpan = document.createElement("span");
331
+ textSpan.className = "session-item-text";
332
+ var textHtml = "";
333
+ if (anyProcessing) {
334
+ textHtml += '<span class="session-processing"></span>';
335
+ }
336
+ var groupIcon = isRalph ? "repeat" : "calendar-clock";
337
+ textHtml += '<span class="session-loop-icon' + (isRalph ? "" : " scheduled") + '">' + iconHtml(groupIcon) + '</span>';
338
+ textHtml += '<span class="session-loop-name">' + escapeHtml(loopName) + '</span>';
339
+ if (isCrafting && children.length === 1) {
340
+ textHtml += '<span class="session-loop-badge crafting">Crafting</span>';
341
+ } else {
342
+ textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + children.length + '</span>';
343
+ }
344
+ textSpan.innerHTML = textHtml;
345
+ el.appendChild(textSpan);
346
+
347
+ // More button (ellipsis)
348
+ var moreBtn = document.createElement("button");
349
+ moreBtn.className = "session-more-btn";
350
+ moreBtn.innerHTML = iconHtml("ellipsis");
351
+ moreBtn.title = "More options";
352
+ moreBtn.addEventListener("click", (function (lid, name, count, btn) {
353
+ return function (e) {
354
+ e.stopPropagation();
355
+ showLoopCtxMenu(btn, lid, name, count);
356
+ };
357
+ })(loopId, loopName, children.length, moreBtn));
358
+ el.appendChild(moreBtn);
359
+
360
+ // Click row (not chevron/more) → switch to latest session
361
+ el.addEventListener("click", (function (id) {
362
+ return function () {
363
+ if (ctx.ws && ctx.connected) {
364
+ ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
365
+ closeSidebar();
366
+ }
367
+ };
368
+ })(latestSession.id));
369
+
370
+ wrapper.appendChild(el);
371
+
372
+ // Expanded children
373
+ if (expanded) {
374
+ var childContainer = document.createElement("div");
375
+ childContainer.className = "session-loop-children";
376
+ for (var k = 0; k < children.length; k++) {
377
+ childContainer.appendChild(renderLoopChild(children[k]));
378
+ }
379
+ wrapper.appendChild(childContainer);
380
+ }
381
+
382
+ return wrapper;
383
+ }
384
+
142
385
  function renderSessionItem(s) {
143
386
  var el = document.createElement("div");
144
387
  var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
@@ -185,15 +428,51 @@ export function renderSessionList(sessions) {
185
428
 
186
429
  ctx.sessionListEl.innerHTML = "";
187
430
 
188
- // Sort by lastActivity descending (most recent first)
189
- var sorted = cachedSessions.slice().sort(function (a, b) {
431
+ // Partition: loop sessions vs normal sessions
432
+ // Group by loopId + startedAt so different runs of the same task are separate groups
433
+ var loopGroups = {}; // groupKey -> [sessions]
434
+ var normalSessions = [];
435
+ for (var i = 0; i < cachedSessions.length; i++) {
436
+ var s = cachedSessions[i];
437
+ if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph") {
438
+ // Task crafting sessions live in the scheduler calendar, not the main list
439
+ continue;
440
+ } else if (s.loop && s.loop.loopId) {
441
+ var groupKey = s.loop.loopId + ":" + (s.loop.startedAt || 0);
442
+ if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
443
+ loopGroups[groupKey].push(s);
444
+ } else {
445
+ normalSessions.push(s);
446
+ }
447
+ }
448
+
449
+ // Build virtual items: normal sessions + one entry per loop group (using latest child's lastActivity)
450
+ var items = [];
451
+ for (var j = 0; j < normalSessions.length; j++) {
452
+ items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
453
+ }
454
+ var groupKeys = Object.keys(loopGroups);
455
+ for (var k = 0; k < groupKeys.length; k++) {
456
+ var gk = groupKeys[k];
457
+ var children = loopGroups[gk];
458
+ var realLoopId = children[0].loop.loopId;
459
+ var maxActivity = 0;
460
+ for (var m = 0; m < children.length; m++) {
461
+ var act = children[m].lastActivity || 0;
462
+ if (act > maxActivity) maxActivity = act;
463
+ }
464
+ items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
465
+ }
466
+
467
+ // Sort by lastActivity descending
468
+ items.sort(function (a, b) {
190
469
  return (b.lastActivity || 0) - (a.lastActivity || 0);
191
470
  });
192
471
 
193
472
  var currentGroup = "";
194
- for (var i = 0; i < sorted.length; i++) {
195
- var s = sorted[i];
196
- var group = getDateGroup(s.lastActivity || 0);
473
+ for (var n = 0; n < items.length; n++) {
474
+ var item = items[n];
475
+ var group = getDateGroup(item.lastActivity || 0);
197
476
  if (group !== currentGroup) {
198
477
  currentGroup = group;
199
478
  var header = document.createElement("div");
@@ -201,7 +480,11 @@ export function renderSessionList(sessions) {
201
480
  header.textContent = group;
202
481
  ctx.sessionListEl.appendChild(header);
203
482
  }
204
- ctx.sessionListEl.appendChild(renderSessionItem(s));
483
+ if (item.type === "loop") {
484
+ ctx.sessionListEl.appendChild(renderLoopGroup(item.loopId, item.children, item.groupKey));
485
+ } else {
486
+ ctx.sessionListEl.appendChild(renderSessionItem(item.data));
487
+ }
205
488
  }
206
489
  refreshIcons();
207
490
  updatePageTitle();
@@ -356,6 +639,18 @@ function renderSheetProjects(listEl) {
356
639
  }
357
640
 
358
641
  function renderSheetSessions(listEl) {
642
+ // New session button at top
643
+ var newBtn = document.createElement("button");
644
+ newBtn.className = "mobile-session-new";
645
+ newBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
646
+ newBtn.addEventListener("click", function () {
647
+ if (ctx.ws && ctx.connected) {
648
+ ctx.ws.send(JSON.stringify({ type: "new_session" }));
649
+ }
650
+ closeMobileSheet();
651
+ });
652
+ listEl.appendChild(newBtn);
653
+
359
654
  var sorted = cachedSessions.slice().sort(function (a, b) {
360
655
  return (b.lastActivity || 0) - (a.lastActivity || 0);
361
656
  });
@@ -435,6 +730,14 @@ export function initSidebar(_ctx) {
435
730
  }
436
731
  });
437
732
 
733
+ // --- New Ralph Loop button ---
734
+ var newRalphBtn = ctx.$("new-ralph-btn");
735
+ if (newRalphBtn) {
736
+ newRalphBtn.addEventListener("click", function () {
737
+ if (ctx.openRalphWizard) ctx.openRalphWizard();
738
+ });
739
+ }
740
+
438
741
  // --- Session search ---
439
742
  var searchBtn = ctx.$("search-session-btn");
440
743
  var searchBox = ctx.$("session-search");
@@ -577,7 +880,7 @@ export function initSidebar(_ctx) {
577
880
  // --- Mobile tab bar ---
578
881
  var mobileTabBar = document.getElementById("mobile-tab-bar");
579
882
  var mobileTabs = mobileTabBar ? mobileTabBar.querySelectorAll(".mobile-tab") : [];
580
- var mobileNewBtn = document.getElementById("mobile-new-session-btn");
883
+ var mobileHomeBtn = document.getElementById("mobile-home-btn");
581
884
 
582
885
  function setMobileTabActive(tabName) {
583
886
  for (var i = 0; i < mobileTabs.length; i++) {
@@ -615,13 +918,11 @@ export function initSidebar(_ctx) {
615
918
  })(mobileTabs[t]);
616
919
  }
617
920
 
618
- if (mobileNewBtn) {
619
- mobileNewBtn.addEventListener("click", function () {
620
- if (ctx.ws && ctx.connected) {
621
- ctx.ws.send(JSON.stringify({ type: "new_session" }));
622
- closeSidebar();
623
- setMobileTabActive("");
624
- }
921
+ if (mobileHomeBtn) {
922
+ mobileHomeBtn.addEventListener("click", function () {
923
+ closeSidebar();
924
+ setMobileTabActive("");
925
+ if (ctx.showHomeHub) ctx.showHomeHub();
625
926
  });
626
927
  }
627
928
 
@@ -710,6 +1011,54 @@ export function initSidebar(_ctx) {
710
1011
 
711
1012
  // Initial sync even if no resize handle
712
1013
  syncUserIslandWidth();
1014
+
1015
+ // --- Schedule countdown timer ---
1016
+ startCountdownTimer();
1017
+ }
1018
+
1019
+ function startCountdownTimer() {
1020
+ if (countdownTimer) clearInterval(countdownTimer);
1021
+ countdownTimer = setInterval(updateCountdowns, 1000);
1022
+ }
1023
+
1024
+ function updateCountdowns() {
1025
+ if (!ctx || !ctx.getUpcomingSchedules || !ctx.sessionListEl) return;
1026
+ var upcoming = ctx.getUpcomingSchedules(3 * 60 * 1000); // 3 minutes
1027
+
1028
+ // Remove stale container
1029
+ if (countdownContainer && !ctx.sessionListEl.contains(countdownContainer)) {
1030
+ countdownContainer = null;
1031
+ }
1032
+
1033
+ if (upcoming.length === 0) {
1034
+ if (countdownContainer) {
1035
+ countdownContainer.remove();
1036
+ countdownContainer = null;
1037
+ }
1038
+ return;
1039
+ }
1040
+
1041
+ if (!countdownContainer) {
1042
+ countdownContainer = document.createElement("div");
1043
+ countdownContainer.className = "session-countdown-group";
1044
+ ctx.sessionListEl.insertBefore(countdownContainer, ctx.sessionListEl.firstChild);
1045
+ }
1046
+
1047
+ var html = "";
1048
+ var now = Date.now();
1049
+ for (var i = 0; i < upcoming.length; i++) {
1050
+ var u = upcoming[i];
1051
+ var remaining = Math.max(0, Math.ceil((u.nextRunAt - now) / 1000));
1052
+ var min = Math.floor(remaining / 60);
1053
+ var sec = remaining % 60;
1054
+ var timeStr = min + ":" + (sec < 10 ? "0" : "") + sec;
1055
+ var colorStyle = u.color ? " style=\"border-left-color:" + u.color + "\"" : "";
1056
+ html += '<div class="session-countdown-item"' + colorStyle + '>';
1057
+ html += '<span class="session-countdown-name">' + escapeHtml(u.name) + '</span>';
1058
+ html += '<span class="session-countdown-badge">' + timeStr + '</span>';
1059
+ html += '</div>';
1060
+ }
1061
+ countdownContainer.innerHTML = html;
713
1062
  }
714
1063
 
715
1064
  // --- CLI session picker ---
@@ -1249,11 +1598,10 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
1249
1598
  deleteItem.addEventListener("click", function (e) {
1250
1599
  e.stopPropagation();
1251
1600
  closeProjectCtxMenu();
1252
- ctx.showConfirm('Remove project "' + name + '"?', function () {
1253
- if (ctx.ws && ctx.connected) {
1254
- ctx.ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
1255
- }
1256
- });
1601
+ // Check for tasks/schedules first before removing
1602
+ if (ctx.ws && ctx.connected) {
1603
+ ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name }));
1604
+ }
1257
1605
  });
1258
1606
  menu.appendChild(deleteItem);
1259
1607
 
@@ -1366,9 +1714,7 @@ function showEmojiPicker(slug, anchorEl) {
1366
1714
  grid.appendChild(btn);
1367
1715
  })(emojis[i]);
1368
1716
  }
1369
- if (typeof twemoji !== "undefined") {
1370
- twemoji.parse(grid, { folder: "svg", ext: ".svg" });
1371
- }
1717
+ parseEmojis(grid);
1372
1718
  scrollArea.scrollTop = 0;
1373
1719
  }
1374
1720
 
@@ -1383,9 +1729,7 @@ function showEmojiPicker(slug, anchorEl) {
1383
1729
  buildGrid(EMOJI_CATEGORIES[0].emojis);
1384
1730
 
1385
1731
  // Parse tabs with twemoji
1386
- if (typeof twemoji !== "undefined") {
1387
- twemoji.parse(tabBar, { folder: "svg", ext: ".svg" });
1388
- }
1732
+ parseEmojis(tabBar);
1389
1733
 
1390
1734
  document.body.appendChild(picker);
1391
1735
  emojiPickerEl = picker;
@@ -1489,8 +1833,8 @@ function showTrashZone() {
1489
1833
  // Spawn dust particles at trash position
1490
1834
  var rect = trash.getBoundingClientRect();
1491
1835
  spawnDustParticles(rect.left + rect.width / 2, rect.top + rect.height / 2);
1492
- // Remove project
1493
- ctx.ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
1836
+ // Check for tasks before removing
1837
+ ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug }));
1494
1838
  }
1495
1839
  });
1496
1840
  }
@@ -1649,9 +1993,7 @@ export function renderIconStrip(projects, currentSlug) {
1649
1993
  var emojiSpan = document.createElement("span");
1650
1994
  emojiSpan.className = "project-emoji";
1651
1995
  emojiSpan.textContent = p.icon;
1652
- if (typeof twemoji !== "undefined") {
1653
- twemoji.parse(emojiSpan, { folder: "svg", ext: ".svg" });
1654
- }
1996
+ parseEmojis(emojiSpan);
1655
1997
  el.appendChild(emojiSpan);
1656
1998
  } else {
1657
1999
  el.appendChild(document.createTextNode(getProjectAbbrev(p.name)));
@@ -1771,11 +2113,16 @@ export function initIconStrip(_ctx) {
1771
2113
  exploreBtn.addEventListener("mouseleave", hideIconTooltip);
1772
2114
  }
1773
2115
 
1774
- // Tooltip for home icon
2116
+ // Tooltip + click for home icon
1775
2117
  var homeIcon = document.querySelector(".icon-strip-home");
1776
2118
  if (homeIcon) {
1777
2119
  homeIcon.addEventListener("mouseenter", function () { showIconTooltip(homeIcon, "Clay"); });
1778
2120
  homeIcon.addEventListener("mouseleave", hideIconTooltip);
2121
+ homeIcon.addEventListener("click", function (e) {
2122
+ e.preventDefault();
2123
+ if (_ctx.showHomeHub) _ctx.showHomeHub();
2124
+ });
2125
+ homeIcon.style.cursor = "pointer";
1779
2126
  }
1780
2127
 
1781
2128
  // Chevron dropdown on project name
@@ -1,4 +1,5 @@
1
1
  import { refreshIcons, iconHtml } from './icons.js';
2
+ import { parseEmojis } from './markdown.js';
2
3
 
3
4
  var ctx;
4
5
  var notes = new Map(); // id -> { data, el }
@@ -1123,6 +1124,7 @@ function renderArchiveCards() {
1123
1124
  var bodyLines = (noteData.data.text || "").split("\n").slice(1).join("\n").trim();
1124
1125
  if (bodyLines) {
1125
1126
  body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
1127
+ parseEmojis(body);
1126
1128
  }
1127
1129
  card.appendChild(body);
1128
1130