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,600 @@
1
+ // app-home-hub.js - Home hub rendering, weather, tips
2
+ // Extracted from app.js (PR-25)
3
+
4
+ var _ctx = null;
5
+
6
+ var homeHub = null;
7
+ var homeHubVisible = false;
8
+ var hubSchedules = [];
9
+
10
+ var hubTips = [
11
+ "Sticky notes let you pin important info that persists across sessions.",
12
+ "You can run terminal commands directly from the terminal tab — no need to switch windows.",
13
+ "Rename your sessions to keep conversations organized and easy to find later.",
14
+ "The file browser lets you explore and open any file in your project.",
15
+ "Paste images from your clipboard into the chat to include them in your message.",
16
+ "Use /commands (slash commands) for quick access to common actions.",
17
+ "You can resize the sidebar by dragging its edge.",
18
+ "Click the session info button in the header to see token usage and costs.",
19
+ "You can switch between projects without losing your conversation history.",
20
+ "The status dot on project icons shows whether Claude is currently processing.",
21
+ "Right-click on a project icon for quick actions like rename or delete.",
22
+ "Push notifications can alert you when Claude finishes a long task.",
23
+ "You can search through your conversation history within a session.",
24
+ "Session history is preserved — come back anytime to continue where you left off.",
25
+ "Use the rewind feature to go back to an earlier point in your conversation.",
26
+ "You can open multiple terminal tabs for parallel command execution.",
27
+ "Clay works offline as a PWA — install it from your browser for quick access.",
28
+ "Schedule recurring tasks with cron expressions to automate your workflow.",
29
+ "Use Ralph Loops to run autonomous coding sessions while you're away.",
30
+ "Right-click a project icon to set a custom emoji — make each project instantly recognizable.",
31
+ "Multiple people can connect to the same project at once — great for pair programming.",
32
+ "Drag and drop project icons to reorder them in the sidebar.",
33
+ "Drag a project icon to the trash to delete it.",
34
+ "Honey never spoils. 🍯",
35
+ "The Earth is round. 🌍",
36
+ "Computers use electricity. 🔌",
37
+ "Christmas is in summer in some countries. 🎄",
38
+ ];
39
+ // Fisher-Yates shuffle
40
+ for (var _si = hubTips.length - 1; _si > 0; _si--) {
41
+ var _sj = Math.floor(Math.random() * (_si + 1));
42
+ var _tmp = hubTips[_si];
43
+ hubTips[_si] = hubTips[_sj];
44
+ hubTips[_sj] = _tmp;
45
+ }
46
+ var hubTipIndex = 0;
47
+ var hubTipTimer = null;
48
+
49
+ var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
50
+ var MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
51
+ var WEEKDAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
52
+
53
+ // --- Weather (hidden detail) ---
54
+ var weatherEmoji = null; // null = not yet fetched, "" = failed
55
+ var weatherCondition = ""; // e.g. "Light rain, Auckland"
56
+ var weatherFetchedAt = 0;
57
+ var WEATHER_CACHE_MS = 60 * 60 * 1000; // 1 hour
58
+ // WMO weather code -> emoji + description
59
+ var WMO_MAP = {
60
+ 0: ["☀️", "Clear sky"], 1: ["🌤", "Mainly clear"], 2: ["⛅", "Partly cloudy"], 3: ["☁️", "Overcast"],
61
+ 45: ["🌫", "Fog"], 48: ["🌫", "Depositing rime fog"],
62
+ 51: ["🌦", "Light drizzle"], 53: ["🌦", "Moderate drizzle"], 55: ["🌧", "Dense drizzle"],
63
+ 56: ["🌧", "Light freezing drizzle"], 57: ["🌧", "Dense freezing drizzle"],
64
+ 61: ["🌧", "Slight rain"], 63: ["🌧", "Moderate rain"], 65: ["🌧", "Heavy rain"],
65
+ 66: ["🌧", "Light freezing rain"], 67: ["🌧", "Heavy freezing rain"],
66
+ 71: ["🌨", "Slight snow"], 73: ["🌨", "Moderate snow"], 75: ["❄️", "Heavy snow"],
67
+ 77: ["🌨", "Snow grains"],
68
+ 80: ["🌦", "Slight rain showers"], 81: ["🌧", "Moderate rain showers"], 82: ["🌧", "Violent rain showers"],
69
+ 85: ["🌨", "Slight snow showers"], 86: ["❄️", "Heavy snow showers"],
70
+ 95: ["⛈", "Thunderstorm"], 96: ["⛈", "Thunderstorm with slight hail"], 99: ["⛈", "Thunderstorm with heavy hail"],
71
+ };
72
+
73
+ var SLOT_EMOJIS = ["☀️", "🌤", "⛅", "☁️", "🌧", "🌦", "⛈", "🌨", "❄️", "🌫", "🌙", "✨"];
74
+ var weatherSlotPlayed = false;
75
+
76
+ var hubCloseBtn = null;
77
+
78
+ export function initHomeHub(ctx) {
79
+ _ctx = ctx;
80
+ homeHub = document.getElementById("home-hub");
81
+ hubCloseBtn = document.getElementById("home-hub-close");
82
+
83
+ if (hubCloseBtn) {
84
+ hubCloseBtn.addEventListener("click", function () {
85
+ hideHomeHub();
86
+ if (_ctx.currentSlug) {
87
+ if (document.documentElement.classList.contains("pwa-standalone")) {
88
+ history.replaceState(null, "", "/p/" + _ctx.currentSlug + "/");
89
+ } else {
90
+ history.pushState(null, "", "/p/" + _ctx.currentSlug + "/");
91
+ }
92
+ // Restore icon strip active state
93
+ var homeIcon = document.querySelector(".icon-strip-home");
94
+ if (homeIcon) homeIcon.classList.remove("active");
95
+ _ctx.renderProjectList();
96
+ }
97
+ });
98
+ }
99
+ }
100
+
101
+ export function isHomeHubVisible() { return homeHubVisible; }
102
+
103
+ function fetchWeather() {
104
+ // Use cache if we have a successful result within the last hour
105
+ if (weatherEmoji && weatherFetchedAt && (Date.now() - weatherFetchedAt < WEATHER_CACHE_MS)) return;
106
+ // Try localStorage cache
107
+ if (!weatherEmoji) {
108
+ try {
109
+ var cached = JSON.parse(localStorage.getItem("clay-weather") || "null");
110
+ if (cached && cached.emoji && (Date.now() - cached.ts < WEATHER_CACHE_MS)) {
111
+ weatherEmoji = cached.emoji;
112
+ weatherCondition = cached.condition || "";
113
+ weatherFetchedAt = cached.ts;
114
+ if (homeHubVisible) updateGreetingWeather();
115
+ return;
116
+ }
117
+ } catch (e) {}
118
+ }
119
+ if (weatherFetchedAt && (Date.now() - weatherFetchedAt < 30000)) return; // don't retry within 30s
120
+ weatherFetchedAt = Date.now();
121
+ // Step 1: IP geolocation -> lat/lon + city
122
+ fetch("https://ipapi.co/json/", { signal: AbortSignal.timeout(4000) })
123
+ .then(function (res) { return res.ok ? res.json() : Promise.reject(); })
124
+ .then(function (geo) {
125
+ var lat = geo.latitude;
126
+ var lon = geo.longitude;
127
+ var city = geo.city || geo.region || "";
128
+ var country = geo.country_name || "";
129
+ var locationStr = city + (country ? ", " + country : "");
130
+ // Step 2: Open-Meteo -> current weather
131
+ var meteoUrl = "https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current=weather_code&timezone=auto";
132
+ return fetch(meteoUrl, { signal: AbortSignal.timeout(4000) })
133
+ .then(function (res) { return res.ok ? res.json() : Promise.reject(); })
134
+ .then(function (data) {
135
+ var code = data && data.current && data.current.weather_code;
136
+ if (code === undefined || code === null) return;
137
+ var mapped = WMO_MAP[code] || WMO_MAP[0];
138
+ weatherEmoji = mapped[0];
139
+ weatherCondition = mapped[1] + (locationStr ? " in " + locationStr : "");
140
+ weatherFetchedAt = Date.now();
141
+ try {
142
+ localStorage.setItem("clay-weather", JSON.stringify({
143
+ emoji: weatherEmoji, condition: weatherCondition, ts: weatherFetchedAt
144
+ }));
145
+ } catch (e) {}
146
+ if (homeHubVisible) updateGreetingWeather();
147
+ });
148
+ })
149
+ .catch(function () {
150
+ if (!weatherEmoji) weatherEmoji = "";
151
+ });
152
+ }
153
+
154
+ function updateGreetingWeather() {
155
+ var greetEl = _ctx.$("hub-greeting-text");
156
+ if (!greetEl) return;
157
+ // If we have real weather and haven't played the slot yet, do the reel
158
+ if (weatherEmoji && !weatherSlotPlayed && homeHubVisible) {
159
+ weatherSlotPlayed = true;
160
+ playWeatherSlot(greetEl);
161
+ return;
162
+ }
163
+ // Normal update (no animation)
164
+ greetEl.textContent = getGreeting();
165
+
166
+ applyWeatherTooltip(greetEl);
167
+ }
168
+
169
+ function applyWeatherTooltip(greetEl) {
170
+ if (!weatherCondition) return;
171
+ var emojis = greetEl.querySelectorAll("img.emoji");
172
+ var lastEmoji = emojis.length > 0 ? emojis[emojis.length - 1] : null;
173
+ if (lastEmoji) {
174
+ lastEmoji.title = weatherCondition;
175
+ lastEmoji.style.cursor = "default";
176
+ }
177
+ }
178
+
179
+ function playWeatherSlot(greetEl) {
180
+ var h = new Date().getHours();
181
+ var prefix;
182
+ if (h < 6) prefix = "Good night";
183
+ else if (h < 12) prefix = "Good morning";
184
+ else if (h < 18) prefix = "Good afternoon";
185
+ else prefix = "Good evening";
186
+
187
+ // Build schedule: fast ticks -> slow ticks -> land (~3s total)
188
+ var intervals = [50, 50, 50, 60, 70, 80, 100, 120, 150, 190, 240, 300, 370, 450, 530, 640];
189
+ var totalSteps = intervals.length;
190
+ var step = 0;
191
+ var startIdx = Math.floor(Math.random() * SLOT_EMOJIS.length);
192
+
193
+ function tick() {
194
+ if (step < totalSteps) {
195
+ var idx = (startIdx + step) % SLOT_EMOJIS.length;
196
+ greetEl.textContent = prefix + " " + SLOT_EMOJIS[idx];
197
+
198
+ step++;
199
+ setTimeout(tick, intervals[step - 1]);
200
+ } else {
201
+ // Final: land on actual weather
202
+ greetEl.textContent = prefix + " " + weatherEmoji;
203
+
204
+ applyWeatherTooltip(greetEl);
205
+ }
206
+ }
207
+ tick();
208
+ }
209
+
210
+ function getGreeting() {
211
+ var h = new Date().getHours();
212
+ var emoji = weatherEmoji || "";
213
+ // Fallback to time-based emoji if weather not available
214
+ if (!emoji) {
215
+ if (h < 6) emoji = "✨";
216
+ else if (h < 12) emoji = "☀️";
217
+ else if (h < 18) emoji = "🌤";
218
+ else emoji = "🌙";
219
+ }
220
+ var prefix;
221
+ if (h < 6) prefix = "Good night";
222
+ else if (h < 12) prefix = "Good morning";
223
+ else if (h < 18) prefix = "Good afternoon";
224
+ else prefix = "Good evening";
225
+ return prefix + " " + emoji;
226
+ }
227
+
228
+ function getFormattedDate() {
229
+ var now = new Date();
230
+ return WEEKDAY_NAMES[now.getDay()] + ", " + MONTH_NAMES[now.getMonth()] + " " + now.getDate() + ", " + now.getFullYear();
231
+ }
232
+
233
+ function formatScheduleTime(ts) {
234
+ var d = new Date(ts);
235
+ var now = new Date();
236
+ var todayStr = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0") + "-" + String(now.getDate()).padStart(2, "0");
237
+ var schedStr = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
238
+ var h = d.getHours();
239
+ var m = String(d.getMinutes()).padStart(2, "0");
240
+ var ampm = h >= 12 ? "PM" : "AM";
241
+ var h12 = h % 12 || 12;
242
+ var timeStr = h12 + ":" + m + " " + ampm;
243
+ if (schedStr === todayStr) return timeStr;
244
+ // Tomorrow check
245
+ var tomorrow = new Date(now);
246
+ tomorrow.setDate(tomorrow.getDate() + 1);
247
+ var tomStr = tomorrow.getFullYear() + "-" + String(tomorrow.getMonth() + 1).padStart(2, "0") + "-" + String(tomorrow.getDate()).padStart(2, "0");
248
+ if (schedStr === tomStr) return "Tomorrow";
249
+ return DAY_NAMES[d.getDay()] + " " + timeStr;
250
+ }
251
+
252
+ export function renderHomeHub(projects) {
253
+ // Greeting + weather tooltip
254
+ updateGreetingWeather();
255
+
256
+ // Date
257
+ var dateEl = _ctx.$("hub-greeting-date");
258
+ if (dateEl) dateEl.textContent = getFormattedDate();
259
+
260
+ // --- Upcoming tasks ---
261
+ var upcomingList = _ctx.$("hub-upcoming-list");
262
+ var upcomingCount = _ctx.$("hub-upcoming-count");
263
+ if (upcomingList) {
264
+ var now = Date.now();
265
+ var upcoming = hubSchedules.filter(function (s) {
266
+ return s.enabled && s.nextRunAt && s.nextRunAt > now;
267
+ }).sort(function (a, b) {
268
+ return a.nextRunAt - b.nextRunAt;
269
+ });
270
+ // Show up to next 48 hours
271
+ var cutoff = now + 48 * 60 * 60 * 1000;
272
+ var filtered = upcoming.filter(function (s) { return s.nextRunAt <= cutoff; });
273
+
274
+ if (upcomingCount) {
275
+ upcomingCount.textContent = filtered.length > 0 ? filtered.length : "";
276
+ }
277
+
278
+ upcomingList.innerHTML = "";
279
+ if (filtered.length === 0) {
280
+ // Empty state with CTA
281
+ var emptyDiv = document.createElement("div");
282
+ emptyDiv.className = "hub-upcoming-empty";
283
+ emptyDiv.innerHTML = '<div class="hub-upcoming-empty-icon">📋</div>' +
284
+ '<div class="hub-upcoming-empty-text">No upcoming tasks</div>' +
285
+ '<button class="hub-upcoming-cta" id="hub-upcoming-cta">' +
286
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>' +
287
+ 'Create a schedule</button>';
288
+ upcomingList.appendChild(emptyDiv);
289
+ var ctaBtn = emptyDiv.querySelector("#hub-upcoming-cta");
290
+ if (ctaBtn) {
291
+ ctaBtn.addEventListener("click", function () {
292
+ hideHomeHub();
293
+ _ctx.openSchedulerToTab("calendar");
294
+ });
295
+ }
296
+ } else {
297
+ var maxShow = 5;
298
+ var shown = filtered.slice(0, maxShow);
299
+ for (var i = 0; i < shown.length; i++) {
300
+ (function (sched) {
301
+ var item = document.createElement("div");
302
+ item.className = "hub-upcoming-item";
303
+ var dotColor = sched.color || "";
304
+ item.innerHTML = '<span class="hub-upcoming-dot"' + (dotColor ? ' style="background:' + dotColor + '"' : '') + '></span>' +
305
+ '<span class="hub-upcoming-time">' + formatScheduleTime(sched.nextRunAt) + '</span>' +
306
+ '<span class="hub-upcoming-name">' + _ctx.escapeHtml(sched.name || "Untitled") + '</span>' +
307
+ '<span class="hub-upcoming-project">' + _ctx.escapeHtml(sched.projectTitle || "") + '</span>';
308
+ item.addEventListener("click", function () {
309
+ if (sched.projectSlug) {
310
+ _ctx.switchProject(sched.projectSlug);
311
+ setTimeout(function () {
312
+ _ctx.openSchedulerToTab("library");
313
+ }, 300);
314
+ }
315
+ });
316
+ upcomingList.appendChild(item);
317
+ })(shown[i]);
318
+ }
319
+ if (filtered.length > maxShow) {
320
+ var moreEl = document.createElement("div");
321
+ moreEl.className = "hub-upcoming-more";
322
+ moreEl.textContent = "+" + (filtered.length - maxShow) + " more";
323
+ upcomingList.appendChild(moreEl);
324
+ }
325
+ }
326
+ }
327
+
328
+ // --- Projects summary (exclude mate projects) ---
329
+ var projectsList = _ctx.$("hub-projects-list");
330
+ if (projectsList && projects) {
331
+ projectsList.innerHTML = "";
332
+ var hubProjects = projects.filter(function (p) { return !p.isMate; });
333
+ for (var p = 0; p < hubProjects.length; p++) {
334
+ (function (proj) {
335
+ var item = document.createElement("div");
336
+ item.className = "hub-project-item";
337
+ var dotClass = "hub-project-dot" + (proj.isProcessing ? " processing" : "");
338
+ var iconHtml = proj.icon ? '<span class="hub-project-icon">' + proj.icon + '</span>' : '';
339
+ var sessionsLabel = typeof proj.sessions === "number" ? proj.sessions : "";
340
+ item.innerHTML = '<span class="' + dotClass + '"></span>' +
341
+ iconHtml +
342
+ '<span class="hub-project-name">' + _ctx.escapeHtml(proj.title || proj.project || proj.slug) + '</span>' +
343
+ (sessionsLabel !== "" ? '<span class="hub-project-sessions">' + sessionsLabel + '</span>' : '');
344
+ item.addEventListener("click", function () {
345
+ _ctx.switchProject(proj.slug);
346
+ });
347
+ projectsList.appendChild(item);
348
+ })(hubProjects[p]);
349
+ }
350
+ // Render emoji icons
351
+
352
+ }
353
+
354
+ // --- Week strip ---
355
+ var weekStrip = _ctx.$("hub-week-strip");
356
+ if (weekStrip) {
357
+ weekStrip.innerHTML = "";
358
+ var today = new Date();
359
+ var todayDate = today.getDate();
360
+ var todayMonth = today.getMonth();
361
+ var todayYear = today.getFullYear();
362
+ // Find Monday of current week
363
+ var dayOfWeek = today.getDay();
364
+ var mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
365
+ var monday = new Date(today);
366
+ monday.setDate(today.getDate() + mondayOffset);
367
+
368
+ // Build set of dates that have events
369
+ var eventDates = {};
370
+ for (var si = 0; si < hubSchedules.length; si++) {
371
+ var sched = hubSchedules[si];
372
+ if (!sched.enabled) continue;
373
+ if (sched.nextRunAt) {
374
+ var sd = new Date(sched.nextRunAt);
375
+ var key = sd.getFullYear() + "-" + sd.getMonth() + "-" + sd.getDate();
376
+ eventDates[key] = (eventDates[key] || 0) + 1;
377
+ }
378
+ if (sched.date) {
379
+ var parts = sched.date.split("-");
380
+ var dateKey = parseInt(parts[0], 10) + "-" + (parseInt(parts[1], 10) - 1) + "-" + parseInt(parts[2], 10);
381
+ eventDates[dateKey] = (eventDates[dateKey] || 0) + 1;
382
+ }
383
+ }
384
+
385
+ for (var d = 0; d < 7; d++) {
386
+ var dayDate = new Date(monday);
387
+ dayDate.setDate(monday.getDate() + d);
388
+ var isToday = dayDate.getDate() === todayDate && dayDate.getMonth() === todayMonth && dayDate.getFullYear() === todayYear;
389
+ var dateKey = dayDate.getFullYear() + "-" + dayDate.getMonth() + "-" + dayDate.getDate();
390
+ var eventCount = eventDates[dateKey] || 0;
391
+
392
+ var cell = document.createElement("div");
393
+ cell.className = "hub-week-day" + (isToday ? " today" : "");
394
+ var dotsHtml = '<div class="hub-week-dots">';
395
+ var dotCount = Math.min(eventCount, 3);
396
+ for (var di = 0; di < dotCount; di++) {
397
+ dotsHtml += '<span class="hub-week-dot"></span>';
398
+ }
399
+ dotsHtml += '</div>';
400
+ cell.innerHTML = '<span class="hub-week-label">' + DAY_NAMES[(dayDate.getDay())] + '</span>' +
401
+ '<span class="hub-week-num">' + dayDate.getDate() + '</span>' +
402
+ dotsHtml;
403
+ weekStrip.appendChild(cell);
404
+ }
405
+ }
406
+
407
+ // --- Playbooks ---
408
+ var pbGrid = _ctx.$("hub-playbooks-grid");
409
+ var pbSection = _ctx.$("hub-playbooks");
410
+ if (pbGrid) {
411
+ var pbs = _ctx.getPlaybooks();
412
+ if (pbs.length === 0) {
413
+ if (pbSection) pbSection.style.display = "none";
414
+ } else {
415
+ if (pbSection) pbSection.style.display = "";
416
+ pbGrid.innerHTML = "";
417
+ for (var pi = 0; pi < pbs.length; pi++) {
418
+ (function (pb) {
419
+ var card = document.createElement("div");
420
+ card.className = "hub-playbook-card" + (pb.completed ? " completed" : "");
421
+ card.innerHTML = '<span class="hub-playbook-card-icon">' + pb.icon + '</span>' +
422
+ '<div class="hub-playbook-card-body">' +
423
+ '<div class="hub-playbook-card-title">' + _ctx.escapeHtml(pb.title) + '</div>' +
424
+ '<div class="hub-playbook-card-desc">' + _ctx.escapeHtml(pb.description) + '</div>' +
425
+ '</div>' +
426
+ (pb.completed ? '<span class="hub-playbook-card-check">✓</span>' : '');
427
+ card.addEventListener("click", function () {
428
+ _ctx.openPlaybook(pb.id, function () {
429
+ // Re-render hub after playbook closes to update completion state
430
+ renderHomeHub(_ctx.cachedProjects);
431
+ });
432
+ });
433
+ pbGrid.appendChild(card);
434
+ })(pbs[pi]);
435
+ }
436
+
437
+ }
438
+ }
439
+
440
+
441
+ // --- Tip ---
442
+ var currentTip = hubTips[hubTipIndex % hubTips.length];
443
+ var tipEl = _ctx.$("hub-tip-text");
444
+ if (tipEl) tipEl.textContent = currentTip;
445
+
446
+ // "Try it" button if tip has a linked playbook
447
+ var existingTry = homeHub.querySelector(".hub-tip-try");
448
+ if (existingTry) existingTry.remove();
449
+ var linkedPb = _ctx.getPlaybookForTip(currentTip);
450
+ if (linkedPb && tipEl) {
451
+ var tryBtn = document.createElement("button");
452
+ tryBtn.className = "hub-tip-try";
453
+ tryBtn.textContent = "Try it →";
454
+ tryBtn.addEventListener("click", function () {
455
+ _ctx.openPlaybook(linkedPb, function () {
456
+ renderHomeHub(_ctx.cachedProjects);
457
+ });
458
+ });
459
+ tipEl.appendChild(tryBtn);
460
+ }
461
+
462
+ // Tip prev/next buttons
463
+ var prevBtn = _ctx.$("hub-tip-prev");
464
+ if (prevBtn && !prevBtn._hubWired) {
465
+ prevBtn._hubWired = true;
466
+ prevBtn.addEventListener("click", function () {
467
+ hubTipIndex = (hubTipIndex - 1 + hubTips.length) % hubTips.length;
468
+ renderHomeHub(_ctx.cachedProjects);
469
+ startTipRotation();
470
+ });
471
+ }
472
+ var nextBtn = _ctx.$("hub-tip-next");
473
+ if (nextBtn && !nextBtn._hubWired) {
474
+ nextBtn._hubWired = true;
475
+ nextBtn.addEventListener("click", function () {
476
+ hubTipIndex = (hubTipIndex + 1) % hubTips.length;
477
+ renderHomeHub(_ctx.cachedProjects);
478
+ startTipRotation();
479
+ });
480
+ }
481
+
482
+ // Render twemoji for all emoji in the hub
483
+
484
+ }
485
+
486
+ export function handleHubSchedules(msg) {
487
+ if (msg.schedules) {
488
+ hubSchedules = msg.schedules;
489
+ if (homeHubVisible) renderHomeHub(_ctx.cachedProjects);
490
+ }
491
+ }
492
+
493
+ function startTipRotation() {
494
+ stopTipRotation();
495
+ hubTipTimer = setInterval(function () {
496
+ hubTipIndex = (hubTipIndex + 1) % hubTips.length;
497
+ renderHomeHub(_ctx.cachedProjects);
498
+ }, 15000);
499
+ }
500
+
501
+ function stopTipRotation() {
502
+ if (hubTipTimer) {
503
+ clearInterval(hubTipTimer);
504
+ hubTipTimer = null;
505
+ }
506
+ }
507
+
508
+ function renderHomeHubMates() {
509
+ var container = document.getElementById("home-hub-mates");
510
+ if (!container) return;
511
+ container.innerHTML = "";
512
+ if (!_ctx.cachedMatesList || _ctx.cachedMatesList.length === 0) {
513
+ container.classList.add("hidden");
514
+ return;
515
+ }
516
+ container.classList.remove("hidden");
517
+ for (var i = 0; i < _ctx.cachedMatesList.length; i++) {
518
+ (function (mate) {
519
+ var item = document.createElement("div");
520
+ item.className = "home-hub-mate-item" + (mate.primary ? " home-hub-mate-primary" : "");
521
+
522
+ var avatarWrap = document.createElement("div");
523
+ avatarWrap.className = "home-hub-mate-avatar-wrap";
524
+
525
+ var mp = mate.profile || {};
526
+ var mateAvUrl = _ctx.mateAvatarUrl(mate, 48);
527
+ var avatar = document.createElement("img");
528
+ avatar.className = "home-hub-mate-avatar";
529
+ avatar.src = mateAvUrl;
530
+ avatar.alt = mp.displayName || mate.displayName || mate.name || "";
531
+ avatarWrap.appendChild(avatar);
532
+
533
+ var dot = document.createElement("span");
534
+ dot.className = "home-hub-mate-dot";
535
+ avatarWrap.appendChild(dot);
536
+
537
+ item.appendChild(avatarWrap);
538
+
539
+ var nameEl = document.createElement("span");
540
+ nameEl.className = "home-hub-mate-name";
541
+ nameEl.textContent = mp.displayName || mate.displayName || mate.name || "";
542
+ if (mate.primary) {
543
+ var starEl = document.createElement("span");
544
+ starEl.className = "home-hub-mate-primary-star";
545
+ starEl.title = "System Agent: code-managed, auto-updated, sees across all mates";
546
+ starEl.textContent = "\u2605";
547
+ nameEl.appendChild(starEl);
548
+ }
549
+ item.appendChild(nameEl);
550
+
551
+ item.addEventListener("click", function () {
552
+ _ctx.openDm(mate.id);
553
+ });
554
+
555
+ container.appendChild(item);
556
+ })(_ctx.cachedMatesList[i]);
557
+ }
558
+ }
559
+
560
+ export function showHomeHub() {
561
+ if (_ctx.dmMode) _ctx.exitDmMode();
562
+ homeHubVisible = true;
563
+ homeHub.classList.remove("hidden");
564
+ // Show close button only if there's a project to return to
565
+ if (hubCloseBtn) {
566
+ if (_ctx.currentSlug) hubCloseBtn.classList.remove("hidden");
567
+ else hubCloseBtn.classList.add("hidden");
568
+ }
569
+ // Fetch weather silently (once)
570
+ fetchWeather();
571
+ // Request cross-project schedules
572
+ if (_ctx.ws && _ctx.ws.readyState === 1) {
573
+ _ctx.ws.send(JSON.stringify({ type: "hub_schedules_list" }));
574
+ }
575
+ renderHomeHub(_ctx.cachedProjects);
576
+ renderHomeHubMates();
577
+ startTipRotation();
578
+ if (document.documentElement.classList.contains("pwa-standalone")) {
579
+ history.replaceState(null, "", "/");
580
+ } else {
581
+ history.pushState(null, "", "/");
582
+ }
583
+ // Update icon strip active state
584
+ var homeIcon = document.querySelector(".icon-strip-home");
585
+ if (homeIcon) homeIcon.classList.add("active");
586
+ var activeProj = document.querySelector("#icon-strip-projects .icon-strip-item.active");
587
+ if (activeProj) activeProj.classList.remove("active");
588
+ // Mobile home button active
589
+ var mobileHome = document.getElementById("mobile-home-btn");
590
+ if (mobileHome) mobileHome.classList.add("active");
591
+ }
592
+
593
+ export function hideHomeHub() {
594
+ if (!homeHubVisible) return;
595
+ homeHubVisible = false;
596
+ homeHub.classList.add("hidden");
597
+ stopTipRotation();
598
+ var mobileHome = document.getElementById("mobile-home-btn");
599
+ if (mobileHome) mobileHome.classList.remove("active");
600
+ }