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
package/lib/public/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
2
2
  import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
3
- import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal } from './modules/markdown.js';
3
+ import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
4
4
  import { initSidebar, renderSessionList, handleSearchResults, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, initIconStrip, getEmojiCategories } from './modules/sidebar.js';
5
5
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
6
6
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
@@ -14,6 +14,11 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
14
14
  import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
15
15
  import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved } from './modules/project-settings.js';
16
16
  import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
17
+ import { initScheduler, resetScheduler, handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, openSchedulerToTab, isSchedulerOpen, closeScheduler, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles, getUpcomingSchedules } from './modules/scheduler.js';
18
+ import { initAsciiLogo, startLogoAnimation, stopLogoAnimation } from './modules/ascii-logo.js';
19
+ import { initPlaybook, openPlaybook, getPlaybooks, getPlaybookForTip, isCompleted as isPlaybookCompleted } from './modules/playbook.js';
20
+ import { initSTT } from './modules/stt.js';
21
+ import { initProfile } from './modules/profile.js';
17
22
 
18
23
  // --- Base path for multi-project routing ---
19
24
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -43,6 +48,532 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
43
48
  var imagePreviewBar = $("image-preview-bar");
44
49
  var connectOverlay = $("connect-overlay");
45
50
 
51
+ // --- Home Hub ---
52
+ var homeHub = $("home-hub");
53
+ var homeHubVisible = false;
54
+ var hubSchedules = [];
55
+
56
+ var hubTips = [
57
+ "Sticky notes let you pin important info that persists across sessions.",
58
+ "You can run terminal commands directly from the terminal tab — no need to switch windows.",
59
+ "Rename your sessions to keep conversations organized and easy to find later.",
60
+ "The file browser lets you explore and open any file in your project.",
61
+ "Paste images from your clipboard into the chat to include them in your message.",
62
+ "Use /commands (slash commands) for quick access to common actions.",
63
+ "You can resize the sidebar by dragging its edge.",
64
+ "Click the session info button in the header to see token usage and costs.",
65
+ "You can switch between projects without losing your conversation history.",
66
+ "The status dot on project icons shows whether Claude is currently processing.",
67
+ "Right-click on a project icon for quick actions like rename or delete.",
68
+ "Push notifications can alert you when Claude finishes a long task.",
69
+ "You can search through your conversation history within a session.",
70
+ "Session history is preserved — come back anytime to continue where you left off.",
71
+ "Use the rewind feature to go back to an earlier point in your conversation.",
72
+ "You can open multiple terminal tabs for parallel command execution.",
73
+ "Clay works offline as a PWA — install it from your browser for quick access.",
74
+ "Schedule recurring tasks with cron expressions to automate your workflow.",
75
+ "Use Ralph Loops to run autonomous coding sessions while you're away.",
76
+ "Right-click a project icon to set a custom emoji — make each project instantly recognizable.",
77
+ "Multiple people can connect to the same project at once — great for pair programming.",
78
+ "Drag and drop project icons to reorder them in the sidebar.",
79
+ "Drag a project icon to the trash to delete it.",
80
+ "Honey never spoils. 🍯",
81
+ "The Earth is round. 🌍",
82
+ "Computers use electricity. 🔌",
83
+ "Christmas is in summer in some countries. 🎄",
84
+ ];
85
+ // Fisher-Yates shuffle
86
+ for (var _si = hubTips.length - 1; _si > 0; _si--) {
87
+ var _sj = Math.floor(Math.random() * (_si + 1));
88
+ var _tmp = hubTips[_si];
89
+ hubTips[_si] = hubTips[_sj];
90
+ hubTips[_sj] = _tmp;
91
+ }
92
+ var hubTipIndex = 0;
93
+ var hubTipTimer = null;
94
+
95
+ var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
96
+ var MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
97
+ var WEEKDAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
98
+
99
+ // --- Weather (hidden detail) ---
100
+ var weatherEmoji = null; // null = not yet fetched, "" = failed
101
+ var weatherCondition = ""; // e.g. "Light rain, Auckland"
102
+ var weatherFetchedAt = 0;
103
+ var WEATHER_CACHE_MS = 60 * 60 * 1000; // 1 hour
104
+ // WMO weather code → emoji + description
105
+ var WMO_MAP = {
106
+ 0: ["☀️", "Clear sky"], 1: ["🌤", "Mainly clear"], 2: ["⛅", "Partly cloudy"], 3: ["☁️", "Overcast"],
107
+ 45: ["🌫", "Fog"], 48: ["🌫", "Depositing rime fog"],
108
+ 51: ["🌦", "Light drizzle"], 53: ["🌦", "Moderate drizzle"], 55: ["🌧", "Dense drizzle"],
109
+ 56: ["🌧", "Light freezing drizzle"], 57: ["🌧", "Dense freezing drizzle"],
110
+ 61: ["🌧", "Slight rain"], 63: ["🌧", "Moderate rain"], 65: ["🌧", "Heavy rain"],
111
+ 66: ["🌧", "Light freezing rain"], 67: ["🌧", "Heavy freezing rain"],
112
+ 71: ["🌨", "Slight snow"], 73: ["🌨", "Moderate snow"], 75: ["❄️", "Heavy snow"],
113
+ 77: ["🌨", "Snow grains"],
114
+ 80: ["🌦", "Slight rain showers"], 81: ["🌧", "Moderate rain showers"], 82: ["🌧", "Violent rain showers"],
115
+ 85: ["🌨", "Slight snow showers"], 86: ["❄️", "Heavy snow showers"],
116
+ 95: ["⛈", "Thunderstorm"], 96: ["⛈", "Thunderstorm with slight hail"], 99: ["⛈", "Thunderstorm with heavy hail"],
117
+ };
118
+
119
+ function fetchWeather() {
120
+ // Use cache if we have a successful result within the last hour
121
+ if (weatherEmoji && weatherFetchedAt && (Date.now() - weatherFetchedAt < WEATHER_CACHE_MS)) return;
122
+ // Try localStorage cache
123
+ if (!weatherEmoji) {
124
+ try {
125
+ var cached = JSON.parse(localStorage.getItem("clay-weather") || "null");
126
+ if (cached && cached.emoji && (Date.now() - cached.ts < WEATHER_CACHE_MS)) {
127
+ weatherEmoji = cached.emoji;
128
+ weatherCondition = cached.condition || "";
129
+ weatherFetchedAt = cached.ts;
130
+ if (homeHubVisible) updateGreetingWeather();
131
+ return;
132
+ }
133
+ } catch (e) {}
134
+ }
135
+ if (weatherFetchedAt && (Date.now() - weatherFetchedAt < 30000)) return; // don't retry within 30s
136
+ weatherFetchedAt = Date.now();
137
+ // Step 1: IP geolocation → lat/lon + city
138
+ fetch("https://ipapi.co/json/", { signal: AbortSignal.timeout(4000) })
139
+ .then(function (res) { return res.ok ? res.json() : Promise.reject(); })
140
+ .then(function (geo) {
141
+ var lat = geo.latitude;
142
+ var lon = geo.longitude;
143
+ var city = geo.city || geo.region || "";
144
+ var country = geo.country_name || "";
145
+ var locationStr = city + (country ? ", " + country : "");
146
+ // Step 2: Open-Meteo → current weather
147
+ var meteoUrl = "https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current=weather_code&timezone=auto";
148
+ return fetch(meteoUrl, { signal: AbortSignal.timeout(4000) })
149
+ .then(function (res) { return res.ok ? res.json() : Promise.reject(); })
150
+ .then(function (data) {
151
+ var code = data && data.current && data.current.weather_code;
152
+ if (code === undefined || code === null) return;
153
+ var mapped = WMO_MAP[code] || WMO_MAP[0];
154
+ weatherEmoji = mapped[0];
155
+ weatherCondition = mapped[1] + (locationStr ? " in " + locationStr : "");
156
+ weatherFetchedAt = Date.now();
157
+ try {
158
+ localStorage.setItem("clay-weather", JSON.stringify({
159
+ emoji: weatherEmoji, condition: weatherCondition, ts: weatherFetchedAt
160
+ }));
161
+ } catch (e) {}
162
+ if (homeHubVisible) updateGreetingWeather();
163
+ });
164
+ })
165
+ .catch(function () {
166
+ if (!weatherEmoji) weatherEmoji = "";
167
+ });
168
+ }
169
+
170
+ var SLOT_EMOJIS = ["☀️", "🌤", "⛅", "☁️", "🌧", "🌦", "⛈", "🌨", "❄️", "🌫", "🌙", "✨"];
171
+ var weatherSlotPlayed = false;
172
+
173
+ function updateGreetingWeather() {
174
+ var greetEl = $("hub-greeting-text");
175
+ if (!greetEl) return;
176
+ // If we have real weather and haven't played the slot yet, do the reel
177
+ if (weatherEmoji && !weatherSlotPlayed && homeHubVisible) {
178
+ weatherSlotPlayed = true;
179
+ playWeatherSlot(greetEl);
180
+ return;
181
+ }
182
+ // Normal update (no animation)
183
+ greetEl.textContent = getGreeting();
184
+ parseEmojis(greetEl);
185
+ applyWeatherTooltip(greetEl);
186
+ }
187
+
188
+ function applyWeatherTooltip(greetEl) {
189
+ if (!weatherCondition) return;
190
+ var emojis = greetEl.querySelectorAll("img.emoji");
191
+ var lastEmoji = emojis.length > 0 ? emojis[emojis.length - 1] : null;
192
+ if (lastEmoji) {
193
+ lastEmoji.title = weatherCondition;
194
+ lastEmoji.style.cursor = "default";
195
+ }
196
+ }
197
+
198
+ function playWeatherSlot(greetEl) {
199
+ var h = new Date().getHours();
200
+ var prefix;
201
+ if (h < 6) prefix = "Good night";
202
+ else if (h < 12) prefix = "Good morning";
203
+ else if (h < 18) prefix = "Good afternoon";
204
+ else prefix = "Good evening";
205
+
206
+ // Build schedule: fast ticks → slow ticks → land (~3s total)
207
+ var intervals = [50, 50, 50, 60, 70, 80, 100, 120, 150, 190, 240, 300, 370, 450, 530, 640];
208
+ var totalSteps = intervals.length;
209
+ var step = 0;
210
+ var startIdx = Math.floor(Math.random() * SLOT_EMOJIS.length);
211
+
212
+ function tick() {
213
+ if (step < totalSteps) {
214
+ var idx = (startIdx + step) % SLOT_EMOJIS.length;
215
+ greetEl.textContent = prefix + " " + SLOT_EMOJIS[idx];
216
+ parseEmojis(greetEl);
217
+ step++;
218
+ setTimeout(tick, intervals[step - 1]);
219
+ } else {
220
+ // Final: land on actual weather
221
+ greetEl.textContent = prefix + " " + weatherEmoji;
222
+ parseEmojis(greetEl);
223
+ applyWeatherTooltip(greetEl);
224
+ }
225
+ }
226
+ tick();
227
+ }
228
+
229
+ function getGreeting() {
230
+ var h = new Date().getHours();
231
+ var emoji = weatherEmoji || "";
232
+ // Fallback to time-based emoji if weather not available
233
+ if (!emoji) {
234
+ if (h < 6) emoji = "✨";
235
+ else if (h < 12) emoji = "☀️";
236
+ else if (h < 18) emoji = "🌤";
237
+ else emoji = "🌙";
238
+ }
239
+ var prefix;
240
+ if (h < 6) prefix = "Good night";
241
+ else if (h < 12) prefix = "Good morning";
242
+ else if (h < 18) prefix = "Good afternoon";
243
+ else prefix = "Good evening";
244
+ return prefix + " " + emoji;
245
+ }
246
+
247
+ function getFormattedDate() {
248
+ var now = new Date();
249
+ return WEEKDAY_NAMES[now.getDay()] + ", " + MONTH_NAMES[now.getMonth()] + " " + now.getDate() + ", " + now.getFullYear();
250
+ }
251
+
252
+ function formatScheduleTime(ts) {
253
+ var d = new Date(ts);
254
+ var now = new Date();
255
+ var todayStr = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0") + "-" + String(now.getDate()).padStart(2, "0");
256
+ var schedStr = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
257
+ var h = d.getHours();
258
+ var m = String(d.getMinutes()).padStart(2, "0");
259
+ var ampm = h >= 12 ? "PM" : "AM";
260
+ var h12 = h % 12 || 12;
261
+ var timeStr = h12 + ":" + m + " " + ampm;
262
+ if (schedStr === todayStr) return timeStr;
263
+ // Tomorrow check
264
+ var tomorrow = new Date(now);
265
+ tomorrow.setDate(tomorrow.getDate() + 1);
266
+ var tomStr = tomorrow.getFullYear() + "-" + String(tomorrow.getMonth() + 1).padStart(2, "0") + "-" + String(tomorrow.getDate()).padStart(2, "0");
267
+ if (schedStr === tomStr) return "Tomorrow";
268
+ return DAY_NAMES[d.getDay()] + " " + timeStr;
269
+ }
270
+
271
+ function renderHomeHub(projects) {
272
+ // Greeting + weather tooltip
273
+ updateGreetingWeather();
274
+
275
+ // Date
276
+ var dateEl = $("hub-greeting-date");
277
+ if (dateEl) dateEl.textContent = getFormattedDate();
278
+
279
+ // --- Upcoming tasks ---
280
+ var upcomingList = $("hub-upcoming-list");
281
+ var upcomingCount = $("hub-upcoming-count");
282
+ if (upcomingList) {
283
+ var now = Date.now();
284
+ var upcoming = hubSchedules.filter(function (s) {
285
+ return s.enabled && s.nextRunAt && s.nextRunAt > now;
286
+ }).sort(function (a, b) {
287
+ return a.nextRunAt - b.nextRunAt;
288
+ });
289
+ // Show up to next 48 hours
290
+ var cutoff = now + 48 * 60 * 60 * 1000;
291
+ var filtered = upcoming.filter(function (s) { return s.nextRunAt <= cutoff; });
292
+
293
+ if (upcomingCount) {
294
+ upcomingCount.textContent = filtered.length > 0 ? filtered.length : "";
295
+ }
296
+
297
+ upcomingList.innerHTML = "";
298
+ if (filtered.length === 0) {
299
+ // Empty state with CTA
300
+ var emptyDiv = document.createElement("div");
301
+ emptyDiv.className = "hub-upcoming-empty";
302
+ emptyDiv.innerHTML = '<div class="hub-upcoming-empty-icon">📋</div>' +
303
+ '<div class="hub-upcoming-empty-text">No upcoming tasks</div>' +
304
+ '<button class="hub-upcoming-cta" id="hub-upcoming-cta">' +
305
+ '<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>' +
306
+ 'Create a schedule</button>';
307
+ upcomingList.appendChild(emptyDiv);
308
+ var ctaBtn = emptyDiv.querySelector("#hub-upcoming-cta");
309
+ if (ctaBtn) {
310
+ ctaBtn.addEventListener("click", function () {
311
+ hideHomeHub();
312
+ openSchedulerToTab("calendar");
313
+ });
314
+ }
315
+ } else {
316
+ var maxShow = 5;
317
+ var shown = filtered.slice(0, maxShow);
318
+ for (var i = 0; i < shown.length; i++) {
319
+ (function (sched) {
320
+ var item = document.createElement("div");
321
+ item.className = "hub-upcoming-item";
322
+ var dotColor = sched.color || "";
323
+ item.innerHTML = '<span class="hub-upcoming-dot"' + (dotColor ? ' style="background:' + dotColor + '"' : '') + '></span>' +
324
+ '<span class="hub-upcoming-time">' + formatScheduleTime(sched.nextRunAt) + '</span>' +
325
+ '<span class="hub-upcoming-name">' + escapeHtml(sched.name || "Untitled") + '</span>' +
326
+ '<span class="hub-upcoming-project">' + escapeHtml(sched.projectTitle || "") + '</span>';
327
+ item.addEventListener("click", function () {
328
+ if (sched.projectSlug) {
329
+ switchProject(sched.projectSlug);
330
+ setTimeout(function () {
331
+ openSchedulerToTab("library");
332
+ }, 300);
333
+ }
334
+ });
335
+ upcomingList.appendChild(item);
336
+ })(shown[i]);
337
+ }
338
+ if (filtered.length > maxShow) {
339
+ var moreEl = document.createElement("div");
340
+ moreEl.className = "hub-upcoming-more";
341
+ moreEl.textContent = "+" + (filtered.length - maxShow) + " more";
342
+ upcomingList.appendChild(moreEl);
343
+ }
344
+ }
345
+ }
346
+
347
+ // --- Projects summary ---
348
+ var projectsList = $("hub-projects-list");
349
+ if (projectsList && projects) {
350
+ projectsList.innerHTML = "";
351
+ for (var p = 0; p < projects.length; p++) {
352
+ (function (proj) {
353
+ var item = document.createElement("div");
354
+ item.className = "hub-project-item";
355
+ var dotClass = "hub-project-dot" + (proj.isProcessing ? " processing" : "");
356
+ var iconHtml = proj.icon ? '<span class="hub-project-icon">' + proj.icon + '</span>' : '';
357
+ var sessionsLabel = typeof proj.sessions === "number" ? proj.sessions : "";
358
+ item.innerHTML = '<span class="' + dotClass + '"></span>' +
359
+ iconHtml +
360
+ '<span class="hub-project-name">' + escapeHtml(proj.title || proj.project || proj.slug) + '</span>' +
361
+ (sessionsLabel !== "" ? '<span class="hub-project-sessions">' + sessionsLabel + '</span>' : '');
362
+ item.addEventListener("click", function () {
363
+ switchProject(proj.slug);
364
+ });
365
+ projectsList.appendChild(item);
366
+ })(projects[p]);
367
+ }
368
+ // Render emoji icons
369
+ parseEmojis(projectsList);
370
+ }
371
+
372
+ // --- Week strip ---
373
+ var weekStrip = $("hub-week-strip");
374
+ if (weekStrip) {
375
+ weekStrip.innerHTML = "";
376
+ var today = new Date();
377
+ var todayDate = today.getDate();
378
+ var todayMonth = today.getMonth();
379
+ var todayYear = today.getFullYear();
380
+ // Find Monday of current week
381
+ var dayOfWeek = today.getDay();
382
+ var mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
383
+ var monday = new Date(today);
384
+ monday.setDate(today.getDate() + mondayOffset);
385
+
386
+ // Build set of dates that have events
387
+ var eventDates = {};
388
+ for (var si = 0; si < hubSchedules.length; si++) {
389
+ var sched = hubSchedules[si];
390
+ if (!sched.enabled) continue;
391
+ if (sched.nextRunAt) {
392
+ var sd = new Date(sched.nextRunAt);
393
+ var key = sd.getFullYear() + "-" + sd.getMonth() + "-" + sd.getDate();
394
+ eventDates[key] = (eventDates[key] || 0) + 1;
395
+ }
396
+ if (sched.date) {
397
+ var parts = sched.date.split("-");
398
+ var dateKey = parseInt(parts[0], 10) + "-" + (parseInt(parts[1], 10) - 1) + "-" + parseInt(parts[2], 10);
399
+ eventDates[dateKey] = (eventDates[dateKey] || 0) + 1;
400
+ }
401
+ }
402
+
403
+ for (var d = 0; d < 7; d++) {
404
+ var dayDate = new Date(monday);
405
+ dayDate.setDate(monday.getDate() + d);
406
+ var isToday = dayDate.getDate() === todayDate && dayDate.getMonth() === todayMonth && dayDate.getFullYear() === todayYear;
407
+ var dateKey = dayDate.getFullYear() + "-" + dayDate.getMonth() + "-" + dayDate.getDate();
408
+ var eventCount = eventDates[dateKey] || 0;
409
+
410
+ var cell = document.createElement("div");
411
+ cell.className = "hub-week-day" + (isToday ? " today" : "");
412
+ var dotsHtml = '<div class="hub-week-dots">';
413
+ var dotCount = Math.min(eventCount, 3);
414
+ for (var di = 0; di < dotCount; di++) {
415
+ dotsHtml += '<span class="hub-week-dot"></span>';
416
+ }
417
+ dotsHtml += '</div>';
418
+ cell.innerHTML = '<span class="hub-week-label">' + DAY_NAMES[(dayDate.getDay())] + '</span>' +
419
+ '<span class="hub-week-num">' + dayDate.getDate() + '</span>' +
420
+ dotsHtml;
421
+ weekStrip.appendChild(cell);
422
+ }
423
+ }
424
+
425
+ // --- Playbooks ---
426
+ var pbGrid = $("hub-playbooks-grid");
427
+ var pbSection = $("hub-playbooks");
428
+ if (pbGrid) {
429
+ var pbs = getPlaybooks();
430
+ if (pbs.length === 0) {
431
+ if (pbSection) pbSection.style.display = "none";
432
+ } else {
433
+ if (pbSection) pbSection.style.display = "";
434
+ pbGrid.innerHTML = "";
435
+ for (var pi = 0; pi < pbs.length; pi++) {
436
+ (function (pb) {
437
+ var card = document.createElement("div");
438
+ card.className = "hub-playbook-card" + (pb.completed ? " completed" : "");
439
+ card.innerHTML = '<span class="hub-playbook-card-icon">' + pb.icon + '</span>' +
440
+ '<div class="hub-playbook-card-body">' +
441
+ '<div class="hub-playbook-card-title">' + escapeHtml(pb.title) + '</div>' +
442
+ '<div class="hub-playbook-card-desc">' + escapeHtml(pb.description) + '</div>' +
443
+ '</div>' +
444
+ (pb.completed ? '<span class="hub-playbook-card-check">✓</span>' : '');
445
+ card.addEventListener("click", function () {
446
+ openPlaybook(pb.id, function () {
447
+ // Re-render hub after playbook closes to update completion state
448
+ renderHomeHub(cachedProjects);
449
+ });
450
+ });
451
+ pbGrid.appendChild(card);
452
+ })(pbs[pi]);
453
+ }
454
+ parseEmojis(pbGrid);
455
+ }
456
+ }
457
+
458
+
459
+ // --- Tip ---
460
+ var currentTip = hubTips[hubTipIndex % hubTips.length];
461
+ var tipEl = $("hub-tip-text");
462
+ if (tipEl) tipEl.textContent = currentTip;
463
+
464
+ // "Try it" button if tip has a linked playbook
465
+ var existingTry = homeHub.querySelector(".hub-tip-try");
466
+ if (existingTry) existingTry.remove();
467
+ var linkedPb = getPlaybookForTip(currentTip);
468
+ if (linkedPb && tipEl) {
469
+ var tryBtn = document.createElement("button");
470
+ tryBtn.className = "hub-tip-try";
471
+ tryBtn.textContent = "Try it →";
472
+ tryBtn.addEventListener("click", function () {
473
+ openPlaybook(linkedPb, function () {
474
+ renderHomeHub(cachedProjects);
475
+ });
476
+ });
477
+ tipEl.appendChild(tryBtn);
478
+ }
479
+
480
+ // Tip prev/next buttons
481
+ var prevBtn = $("hub-tip-prev");
482
+ if (prevBtn && !prevBtn._hubWired) {
483
+ prevBtn._hubWired = true;
484
+ prevBtn.addEventListener("click", function () {
485
+ hubTipIndex = (hubTipIndex - 1 + hubTips.length) % hubTips.length;
486
+ renderHomeHub(cachedProjects);
487
+ startTipRotation();
488
+ });
489
+ }
490
+ var nextBtn = $("hub-tip-next");
491
+ if (nextBtn && !nextBtn._hubWired) {
492
+ nextBtn._hubWired = true;
493
+ nextBtn.addEventListener("click", function () {
494
+ hubTipIndex = (hubTipIndex + 1) % hubTips.length;
495
+ renderHomeHub(cachedProjects);
496
+ startTipRotation();
497
+ });
498
+ }
499
+
500
+ // Render twemoji for all emoji in the hub
501
+ parseEmojis(homeHub);
502
+ }
503
+
504
+ function handleHubSchedules(msg) {
505
+ if (msg.schedules) {
506
+ hubSchedules = msg.schedules;
507
+ if (homeHubVisible) renderHomeHub(cachedProjects);
508
+ }
509
+ }
510
+
511
+ function startTipRotation() {
512
+ stopTipRotation();
513
+ hubTipTimer = setInterval(function () {
514
+ hubTipIndex = (hubTipIndex + 1) % hubTips.length;
515
+ renderHomeHub(cachedProjects);
516
+ }, 15000);
517
+ }
518
+
519
+ function stopTipRotation() {
520
+ if (hubTipTimer) {
521
+ clearInterval(hubTipTimer);
522
+ hubTipTimer = null;
523
+ }
524
+ }
525
+
526
+ var hubCloseBtn = document.getElementById("home-hub-close");
527
+
528
+ function showHomeHub() {
529
+ homeHubVisible = true;
530
+ homeHub.classList.remove("hidden");
531
+ // Show close button only if there's a project to return to
532
+ if (hubCloseBtn) {
533
+ if (currentSlug) hubCloseBtn.classList.remove("hidden");
534
+ else hubCloseBtn.classList.add("hidden");
535
+ }
536
+ // Fetch weather silently (once)
537
+ fetchWeather();
538
+ // Request cross-project schedules
539
+ if (ws && ws.readyState === 1) {
540
+ ws.send(JSON.stringify({ type: "hub_schedules_list" }));
541
+ }
542
+ renderHomeHub(cachedProjects);
543
+ startTipRotation();
544
+ history.pushState(null, "", "/");
545
+ // Update icon strip active state
546
+ var homeIcon = document.querySelector(".icon-strip-home");
547
+ if (homeIcon) homeIcon.classList.add("active");
548
+ var activeProj = document.querySelector("#icon-strip-projects .icon-strip-item.active");
549
+ if (activeProj) activeProj.classList.remove("active");
550
+ // Mobile home button active
551
+ var mobileHome = document.getElementById("mobile-home-btn");
552
+ if (mobileHome) mobileHome.classList.add("active");
553
+ }
554
+
555
+ if (hubCloseBtn) {
556
+ hubCloseBtn.addEventListener("click", function () {
557
+ hideHomeHub();
558
+ if (currentSlug) {
559
+ history.pushState(null, "", "/p/" + currentSlug + "/");
560
+ // Restore icon strip active state
561
+ var homeIcon = document.querySelector(".icon-strip-home");
562
+ if (homeIcon) homeIcon.classList.remove("active");
563
+ renderProjectList();
564
+ }
565
+ });
566
+ }
567
+
568
+ function hideHomeHub() {
569
+ if (!homeHubVisible) return;
570
+ homeHubVisible = false;
571
+ homeHub.classList.add("hidden");
572
+ stopTipRotation();
573
+ var mobileHome = document.getElementById("mobile-home-btn");
574
+ if (mobileHome) mobileHome.classList.remove("active");
575
+ }
576
+
46
577
  // --- Project List ---
47
578
  var projectListSection = $("project-list-section");
48
579
  var projectListEl = $("project-list");
@@ -86,9 +617,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
86
617
  var pIcon = cachedProjects[pi].icon || null;
87
618
  if (pIcon) {
88
619
  tbIcon.textContent = pIcon;
89
- if (typeof twemoji !== "undefined") {
90
- twemoji.parse(tbIcon, { folder: "svg", ext: ".svg" });
91
- }
620
+ parseEmojis(tbIcon);
92
621
  tbIcon.classList.add("has-icon");
93
622
  try { localStorage.setItem("clay-project-icon-" + (currentSlug || "default"), pIcon); } catch (e) {}
94
623
  } else {
@@ -118,6 +647,10 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
118
647
 
119
648
  document.addEventListener("keydown", function (e) {
120
649
  if (e.key === "Escape") {
650
+ if (homeHubVisible && currentSlug) {
651
+ hubCloseBtn.click();
652
+ return;
653
+ }
121
654
  closeImageModal();
122
655
  }
123
656
  });
@@ -179,7 +712,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
179
712
  var ralphPhase = "idle"; // idle | wizard | crafting | approval | executing | done
180
713
  var ralphCraftingSessionId = null;
181
714
  var wizardStep = 1;
182
- var wizardData = { name: "", task: "", maxIterations: 25 };
715
+ var wizardData = { name: "", task: "", maxIterations: 3, cron: null };
183
716
  var ralphFilesReady = { promptReady: false, judgeReady: false, bothReady: false };
184
717
  var ralphPreviewContent = { prompt: "", judge: "" };
185
718
  var slashCommands = [];
@@ -203,9 +736,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
203
736
  var _tbi = $("title-bar-project-icon");
204
737
  if (_tbi) {
205
738
  _tbi.textContent = _cachedProjectIcon;
206
- if (typeof twemoji !== "undefined") {
207
- twemoji.parse(_tbi, { folder: "svg", ext: ".svg" });
208
- }
739
+ parseEmojis(_tbi);
209
740
  _tbi.classList.add("has-icon");
210
741
  }
211
742
  }
@@ -402,66 +933,104 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
402
933
  onFilesTabOpen: function () { loadRootDirectory(); },
403
934
  switchProject: function (slug) { switchProject(slug); },
404
935
  openTerminal: function () { openTerminal(); },
936
+ showHomeHub: function () { showHomeHub(); },
937
+ openRalphWizard: function () { openRalphWizard(); },
938
+ getUpcomingSchedules: getUpcomingSchedules,
405
939
  };
406
940
  initSidebar(sidebarCtx);
407
941
  initIconStrip(sidebarCtx);
408
942
 
409
- // --- Connect overlay (logo + wordmark only) ---
410
- function startVerbCycle() {}
411
- function stopVerbCycle() {}
943
+ // --- Connect overlay (animated ASCII logo) ---
944
+ var asciiLogoCanvas = $("ascii-logo-canvas");
945
+ initAsciiLogo(asciiLogoCanvas);
946
+ startLogoAnimation();
947
+ function startVerbCycle() { startLogoAnimation(); }
948
+ function stopVerbCycle() { stopLogoAnimation(); }
412
949
 
413
- // Reset favicon cache when theme changes (variant may switch light ↔ dark)
950
+ // Reset favicon cache when theme changes
414
951
  onThemeChange(function () {
415
- faviconSvgLight = null;
416
- faviconSvgDark = null;
417
952
  faviconOrigHref = null;
418
953
  });
419
954
 
420
955
  function startPixelAnim() {}
421
956
  function stopPixelAnim() {}
422
957
 
423
- // --- Dynamic favicon ---
958
+ // --- Dynamic favicon (canvas-based banded C with color flow animation) ---
424
959
  var faviconLink = document.querySelector('link[rel="icon"]');
425
- var faviconSvgLight = null;
426
- var faviconSvgDark = null;
427
960
  var faviconOrigHref = null;
961
+ var faviconCanvas = document.createElement("canvas");
962
+ faviconCanvas.width = 32;
963
+ faviconCanvas.height = 32;
964
+ var faviconCtx = faviconCanvas.getContext("2d");
965
+ var faviconImg = null;
966
+ var faviconImgReady = false;
967
+
968
+ // Banded colors from the Clay CLI logo gradient
969
+ var BAND_COLORS = [
970
+ [0, 235, 160],
971
+ [0, 200, 220],
972
+ [30, 100, 255],
973
+ [88, 50, 255],
974
+ [200, 60, 180],
975
+ [255, 90, 50],
976
+ ];
428
977
 
429
- // Background fill colors in each favicon variant (terracotta / dark-brown)
430
- var LIGHT_BG_FILLS = ["#E3D0CC", "#C0A9A4", "#D6B6B0", "#DAC7C4", "#D4C0BD", "#CBB8B2"];
431
- var DARK_BG_FILLS = ["#3A3535", "#252121", "#2E2929", "#332E2E", "#312C2C", "#292525"];
432
-
433
- function getFaviconSvg() {
434
- var theme = getCurrentTheme();
435
- var isLight = theme.variant === "light";
436
- var src = isLight ? "favicon.svg" : "favicon-dark.svg";
437
- var cached = isLight ? faviconSvgLight : faviconSvgDark;
438
- if (cached) return cached;
439
- var xhr = new XMLHttpRequest();
440
- xhr.open("GET", basePath + src, false);
441
- xhr.send();
442
- if (xhr.status !== 200) return null;
443
- if (isLight) { faviconSvgLight = xhr.responseText; return faviconSvgLight; }
444
- faviconSvgDark = xhr.responseText;
445
- return faviconSvgDark;
446
- }
978
+ // Load the banded favicon image for masking
979
+ (function () {
980
+ faviconImg = new Image();
981
+ faviconImg.onload = function () { faviconImgReady = true; };
982
+ faviconImg.src = basePath + "favicon-banded.png";
983
+ })();
447
984
 
448
985
  function updateFavicon(bgColor) {
449
986
  if (!faviconLink) return;
450
987
  if (!bgColor) {
451
- // Restore original
452
988
  if (faviconOrigHref) { faviconLink.href = faviconOrigHref; faviconOrigHref = null; }
453
989
  return;
454
990
  }
455
- var raw = getFaviconSvg();
456
- if (!raw) return;
457
991
  if (!faviconOrigHref) faviconOrigHref = faviconLink.href;
458
- var theme = getCurrentTheme();
459
- var fills = theme.variant === "light" ? LIGHT_BG_FILLS : DARK_BG_FILLS;
460
- var svg = raw;
461
- for (var i = 0; i < fills.length; i++) {
462
- svg = svg.split(fills[i]).join(bgColor);
992
+ // Simple solid-color favicon for non-animated states
993
+ faviconCtx.clearRect(0, 0, 32, 32);
994
+ faviconCtx.fillStyle = bgColor;
995
+ faviconCtx.beginPath();
996
+ faviconCtx.arc(16, 16, 14, 0, Math.PI * 2);
997
+ faviconCtx.fill();
998
+ faviconCtx.fillStyle = "#fff";
999
+ faviconCtx.font = "bold 22px Nunito, sans-serif";
1000
+ faviconCtx.textAlign = "center";
1001
+ faviconCtx.textBaseline = "middle";
1002
+ faviconCtx.fillText("C", 16, 17);
1003
+ faviconLink.href = faviconCanvas.toDataURL("image/png");
1004
+ }
1005
+
1006
+ // Animated favicon: banded colors flow top-to-bottom
1007
+ var faviconAnimTimer = null;
1008
+ var faviconAnimFrame = 0;
1009
+
1010
+ function drawFaviconAnimFrame() {
1011
+ if (!faviconImgReady) return;
1012
+ var S = 32;
1013
+ var bands = BAND_COLORS.length;
1014
+ var totalFrames = bands * 2;
1015
+ var offset = faviconAnimFrame % totalFrames;
1016
+
1017
+ // Draw flowing color bands as background
1018
+ faviconCtx.clearRect(0, 0, S, S);
1019
+ var bandH = Math.ceil(S / bands);
1020
+ for (var i = 0; i < bands + totalFrames; i++) {
1021
+ var ci = ((i + offset) % bands + bands) % bands;
1022
+ var c = BAND_COLORS[ci];
1023
+ faviconCtx.fillStyle = "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")";
1024
+ faviconCtx.fillRect(0, (i - offset) * bandH, S, bandH);
463
1025
  }
464
- faviconLink.href = "data:image/svg+xml," + encodeURIComponent(svg);
1026
+
1027
+ // Use the banded C image as a mask — draw it on top with destination-in
1028
+ faviconCtx.globalCompositeOperation = "destination-in";
1029
+ faviconCtx.drawImage(faviconImg, 0, 0, S, S);
1030
+ faviconCtx.globalCompositeOperation = "source-over";
1031
+
1032
+ faviconLink.href = faviconCanvas.toDataURL("image/png");
1033
+ faviconAnimFrame++;
465
1034
  }
466
1035
 
467
1036
  // --- Status & Activity ---
@@ -523,24 +1092,31 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
523
1092
  crossProjectBlinkTimer = setTimeout(doBlink, 50);
524
1093
  }
525
1094
 
526
- // --- Urgent favicon blink (permission / ask user) ---
1095
+ // --- Urgent favicon animation (banded color flow + title blink) ---
527
1096
  var urgentBlinkTimer = null;
1097
+ var urgentTitleTimer = null;
528
1098
  var savedTitle = null;
529
1099
  function startUrgentBlink() {
530
1100
  if (urgentBlinkTimer) return;
531
1101
  savedTitle = document.title;
532
- var tick = 0;
533
- urgentBlinkTimer = setInterval(function () {
534
- var on = tick % 2 === 0;
535
- updateFavicon(on ? getComputedVar("--error") : null);
536
- document.title = on ? "\u26A0 Input needed" : savedTitle;
537
- tick++;
538
- }, 180);
1102
+ if (!faviconOrigHref && faviconLink) faviconOrigHref = faviconLink.href;
1103
+ faviconAnimFrame = 0;
1104
+ // Color flow animation at ~12fps
1105
+ urgentBlinkTimer = setInterval(drawFaviconAnimFrame, 83);
1106
+ // Title blink separately
1107
+ var titleTick = 0;
1108
+ urgentTitleTimer = setInterval(function () {
1109
+ document.title = titleTick % 2 === 0 ? "\u26A0 Input needed" : savedTitle;
1110
+ titleTick++;
1111
+ }, 500);
539
1112
  }
540
1113
  function stopUrgentBlink() {
541
1114
  if (!urgentBlinkTimer) return;
542
1115
  clearInterval(urgentBlinkTimer);
1116
+ clearInterval(urgentTitleTimer);
543
1117
  urgentBlinkTimer = null;
1118
+ urgentTitleTimer = null;
1119
+ faviconAnimFrame = 0;
544
1120
  updateFavicon(null);
545
1121
  if (savedTitle) document.title = savedTitle;
546
1122
  savedTitle = null;
@@ -565,6 +1141,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
565
1141
  connected = false;
566
1142
  sendBtn.disabled = true;
567
1143
  connectOverlay.classList.remove("hidden");
1144
+ startVerbCycle();
568
1145
  }
569
1146
  }
570
1147
 
@@ -1271,6 +1848,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1271
1848
  bubble.appendChild(textEl);
1272
1849
  }
1273
1850
 
1851
+ parseEmojis(bubble);
1274
1852
  div.appendChild(bubble);
1275
1853
 
1276
1854
  // Action bar below bubble (icons visible on hover)
@@ -1397,6 +1975,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1397
1975
  if (highlightTimer) clearTimeout(highlightTimer);
1398
1976
  highlightTimer = setTimeout(function () {
1399
1977
  highlightCodeBlocks(contentEl);
1978
+ parseEmojis(contentEl);
1400
1979
  }, 150);
1401
1980
 
1402
1981
  scrollToBottom();
@@ -1417,6 +1996,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1417
1996
  if (contentEl) {
1418
1997
  contentEl.innerHTML = renderMarkdown(currentFullText);
1419
1998
  highlightCodeBlocks(contentEl);
1999
+ parseEmojis(contentEl);
1420
2000
  }
1421
2001
  }
1422
2002
  }
@@ -1428,6 +2008,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1428
2008
  if (contentEl) {
1429
2009
  highlightCodeBlocks(contentEl);
1430
2010
  renderMermaidBlocks(contentEl);
2011
+ parseEmojis(contentEl);
1431
2012
  }
1432
2013
  if (currentFullText) {
1433
2014
  addCopyHandler(currentMsgEl, currentFullText);
@@ -1726,7 +2307,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1726
2307
  removeSearchTimeline();
1727
2308
  setActivity(null);
1728
2309
  setStatus("connected");
1729
- enableMainInput();
2310
+ if (!loopActive) enableMainInput();
1730
2311
  resetUsage();
1731
2312
  resetContext();
1732
2313
  // Clear header indicators
@@ -1741,9 +2322,16 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1741
2322
 
1742
2323
  // --- Project switching (no full reload) ---
1743
2324
  function switchProject(slug) {
1744
- if (!slug || slug === currentSlug) return;
2325
+ if (!slug) return;
2326
+ if (homeHubVisible) {
2327
+ hideHomeHub();
2328
+ if (slug === currentSlug) return;
2329
+ }
2330
+ if (slug === currentSlug) return;
1745
2331
  resetFileBrowser();
1746
2332
  closeArchive();
2333
+ if (isSchedulerOpen()) closeScheduler();
2334
+ resetScheduler(slug);
1747
2335
  currentSlug = slug;
1748
2336
  basePath = "/p/" + slug + "/";
1749
2337
  wsPath = "/p/" + slug + "/ws";
@@ -1758,6 +2346,8 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1758
2346
  if (newSlug && newSlug !== currentSlug) {
1759
2347
  resetFileBrowser();
1760
2348
  closeArchive();
2349
+ if (isSchedulerOpen()) closeScheduler();
2350
+ resetScheduler(newSlug);
1761
2351
  currentSlug = newSlug;
1762
2352
  basePath = "/p/" + newSlug + "/";
1763
2353
  wsPath = "/p/" + newSlug + "/ws";
@@ -1947,22 +2537,22 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1947
2537
  if (msg.lanHost) window.__lanHost = msg.lanHost;
1948
2538
  if (msg.dangerouslySkipPermissions) {
1949
2539
  skipPermsEnabled = true;
1950
- var spBanner = $("skip-perms-banner");
2540
+ var spBanner = $("skip-perms-pill");
1951
2541
  if (spBanner) spBanner.classList.remove("hidden");
1952
2542
  }
1953
2543
  updateProjectList(msg);
1954
2544
  break;
1955
2545
 
1956
2546
  case "update_available":
1957
- var updateBanner = $("update-banner");
2547
+ var updatePillWrap = $("update-pill-wrap");
1958
2548
  var updateVersion = $("update-version");
1959
- if (updateBanner && updateVersion && msg.version) {
2549
+ if (updatePillWrap && updateVersion && msg.version) {
1960
2550
  updateVersion.textContent = "v" + msg.version;
1961
- updateBanner.classList.remove("hidden");
2551
+ updatePillWrap.classList.remove("hidden");
1962
2552
  // Reset button state (may be stuck on "Updating..." after restart)
1963
2553
  var updResetBtn = $("update-now");
1964
2554
  if (updResetBtn) {
1965
- updResetBtn.textContent = "Update now";
2555
+ updResetBtn.innerHTML = '<i data-lucide="download"></i> Update now';
1966
2556
  updResetBtn.disabled = false;
1967
2557
  }
1968
2558
  refreshIcons();
@@ -1984,8 +2574,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1984
2574
  case "update_started":
1985
2575
  var updNowBtn = $("update-now");
1986
2576
  if (updNowBtn) {
1987
- updNowBtn.textContent = "Updating...";
2577
+ updNowBtn.innerHTML = '<i data-lucide="loader"></i> Updating...';
1988
2578
  updNowBtn.disabled = true;
2579
+ refreshIcons();
2580
+ var spinIcon = updNowBtn.querySelector(".lucide");
2581
+ if (spinIcon) spinIcon.classList.add("icon-spin-inline");
1989
2582
  }
1990
2583
  // Block the entire screen with the connect overlay
1991
2584
  connectOverlay.classList.remove("hidden");
@@ -2063,6 +2656,38 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2063
2656
  handleSkillUninstalled(msg);
2064
2657
  break;
2065
2658
 
2659
+ case "loop_registry_updated":
2660
+ handleLoopRegistryUpdated(msg);
2661
+ break;
2662
+
2663
+ case "schedule_run_started":
2664
+ handleScheduleRunStarted(msg);
2665
+ break;
2666
+
2667
+ case "schedule_run_finished":
2668
+ handleScheduleRunFinished(msg);
2669
+ break;
2670
+
2671
+ case "loop_scheduled":
2672
+ handleLoopScheduled(msg);
2673
+ break;
2674
+
2675
+ case "schedule_move_result":
2676
+ if (msg.ok) {
2677
+ showToast("Task moved", "success");
2678
+ } else {
2679
+ showToast(msg.error || "Failed to move task", "error");
2680
+ }
2681
+ break;
2682
+
2683
+ case "remove_project_check_result":
2684
+ handleRemoveProjectCheckResult(msg);
2685
+ break;
2686
+
2687
+ case "hub_schedules":
2688
+ handleHubSchedules(msg);
2689
+ break;
2690
+
2066
2691
  case "input_sync":
2067
2692
  handleInputSync(msg.text);
2068
2693
  break;
@@ -2084,6 +2709,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2084
2709
  break;
2085
2710
 
2086
2711
  case "session_switched":
2712
+ hideHomeHub();
2087
2713
  // Save draft from outgoing session
2088
2714
  if (activeSessionId && inputEl.value) {
2089
2715
  sessionDrafts[activeSessionId] = inputEl.value;
@@ -2321,7 +2947,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2321
2947
  finalizeAssistantBlock();
2322
2948
  processing = false;
2323
2949
  setStatus("connected");
2324
- enableMainInput();
2950
+ if (!loopActive) enableMainInput();
2325
2951
  resetToolState();
2326
2952
  stopUrgentBlink();
2327
2953
  if (document.hidden) {
@@ -2559,6 +3185,8 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2559
3185
  if (loopIteration > 0) {
2560
3186
  updateLoopBanner(loopIteration, loopMaxIterations, "running");
2561
3187
  }
3188
+ inputEl.disabled = true;
3189
+ inputEl.placeholder = "Ralph Loop is running...";
2562
3190
  }
2563
3191
  break;
2564
3192
 
@@ -2570,17 +3198,25 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2570
3198
  showLoopBanner(true);
2571
3199
  updateLoopButton();
2572
3200
  addSystemMessage("Ralph Loop started (max " + msg.maxIterations + " iterations)", false);
3201
+ inputEl.disabled = true;
3202
+ inputEl.placeholder = "Ralph Loop is running...";
2573
3203
  break;
2574
3204
 
2575
3205
  case "loop_iteration":
2576
3206
  loopIteration = msg.iteration;
3207
+ loopMaxIterations = msg.maxIterations;
2577
3208
  updateLoopBanner(msg.iteration, msg.maxIterations, "running");
3209
+ updateLoopButton();
2578
3210
  addSystemMessage("Ralph Loop iteration #" + msg.iteration + " started", false);
3211
+ inputEl.disabled = true;
3212
+ inputEl.placeholder = "Ralph Loop is running...";
2579
3213
  break;
2580
3214
 
2581
3215
  case "loop_judging":
2582
3216
  updateLoopBanner(loopIteration, loopMaxIterations, "judging");
2583
3217
  addSystemMessage("Judging iteration #" + msg.iteration + "...", false);
3218
+ inputEl.disabled = true;
3219
+ inputEl.placeholder = "Ralph Loop is judging...";
2584
3220
  break;
2585
3221
 
2586
3222
  case "loop_verdict":
@@ -2596,6 +3232,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2596
3232
  ralphPhase = "done";
2597
3233
  showLoopBanner(false);
2598
3234
  updateLoopButton();
3235
+ enableMainInput();
2599
3236
  var finishMsg = msg.reason === "pass"
2600
3237
  ? "Ralph Loop completed successfully after " + msg.iterations + " iteration(s)."
2601
3238
  : msg.reason === "max_iterations"
@@ -2623,6 +3260,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2623
3260
  ralphCraftingSessionId = msg.sessionId || activeSessionId;
2624
3261
  updateLoopButton();
2625
3262
  updateRalphBars();
3263
+ if (msg.source !== "ralph") {
3264
+ // Task sessions open in the scheduler calendar window
3265
+ enterCraftingMode(msg.sessionId, msg.taskId);
3266
+ }
3267
+ // Ralph crafting sessions show in session list as part of the loop group
2626
3268
  break;
2627
3269
 
2628
3270
  case "ralph_files_status":
@@ -2633,15 +3275,28 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2633
3275
  };
2634
3276
  if (msg.bothReady && (ralphPhase === "crafting" || ralphPhase === "approval")) {
2635
3277
  ralphPhase = "approval";
2636
- showRalphApprovalBar(true);
3278
+ if (isSchedulerOpen()) {
3279
+ // Task crafting in scheduler: switch from crafting chat to detail view showing files
3280
+ exitCraftingMode(msg.taskId);
3281
+ } else {
3282
+ showRalphApprovalBar(true);
3283
+ }
2637
3284
  }
2638
3285
  updateRalphApprovalStatus();
2639
3286
  break;
2640
3287
 
3288
+ case "loop_registry_files_content":
3289
+ handleLoopRegistryFiles(msg);
3290
+ break;
3291
+
2641
3292
  case "ralph_files_content":
2642
3293
  ralphPreviewContent = { prompt: msg.prompt || "", judge: msg.judge || "" };
2643
3294
  openRalphPreviewModal();
2644
3295
  break;
3296
+
3297
+ case "loop_registry_error":
3298
+ addSystemMessage("Error: " + msg.text, true);
3299
+ break;
2645
3300
  }
2646
3301
  }
2647
3302
 
@@ -2790,6 +3445,17 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2790
3445
  setSendBtnMode: setSendBtnMode,
2791
3446
  });
2792
3447
 
3448
+ // --- STT module (voice input via Web Speech API) ---
3449
+ initSTT({
3450
+ inputEl: inputEl,
3451
+ addSystemMessage: addSystemMessage,
3452
+ });
3453
+
3454
+ // --- User profile (Discord-style popover on user island) ---
3455
+ initProfile({
3456
+ basePath: basePath,
3457
+ });
3458
+
2793
3459
  // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
2794
3460
  initNotifications({
2795
3461
  $: $,
@@ -2852,6 +3518,9 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2852
3518
  fileViewerEl: $("file-viewer"),
2853
3519
  });
2854
3520
 
3521
+ // --- Playbook Engine ---
3522
+ initPlaybook();
3523
+
2855
3524
  // --- Sticky Notes ---
2856
3525
  initStickyNotes({
2857
3526
  get ws() { return ws; },
@@ -2862,6 +3531,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2862
3531
  var stickyNotesSidebarBtn = $("sticky-notes-sidebar-btn");
2863
3532
  if (stickyNotesSidebarBtn) {
2864
3533
  stickyNotesSidebarBtn.addEventListener("click", function () {
3534
+ if (isSchedulerOpen()) closeScheduler();
2865
3535
  if (isArchiveOpen()) {
2866
3536
  closeArchive();
2867
3537
  } else {
@@ -2870,17 +3540,17 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2870
3540
  });
2871
3541
  }
2872
3542
 
2873
- // Close archive when switching to other sidebar panels
3543
+ // Close archive / scheduler panel when switching to other sidebar panels
2874
3544
  var fileBrowserBtn = $("file-browser-btn");
2875
3545
  var terminalSidebarBtn = $("terminal-sidebar-btn");
2876
- if (fileBrowserBtn) fileBrowserBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); });
2877
- if (terminalSidebarBtn) terminalSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); });
3546
+ if (fileBrowserBtn) fileBrowserBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
3547
+ if (terminalSidebarBtn) terminalSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
2878
3548
 
2879
3549
  // --- Ralph Loop UI ---
2880
3550
  function updateLoopInputVisibility(loop) {
2881
3551
  var inputArea = document.getElementById("input-area");
2882
3552
  if (!inputArea) return;
2883
- if (loop && loop.active) {
3553
+ if (loop && loop.active && loop.role !== "crafting") {
2884
3554
  inputArea.style.display = "none";
2885
3555
  } else {
2886
3556
  inputArea.style.display = "";
@@ -2888,39 +3558,72 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2888
3558
  }
2889
3559
 
2890
3560
  function updateLoopButton() {
2891
- var existing = document.getElementById("loop-start-btn");
2892
- if (!existing) {
2893
- var btn = document.createElement("button");
2894
- btn.id = "loop-start-btn";
2895
- btn.innerHTML = '<i data-lucide="repeat"></i> <span>Ralph Loop</span><span class="loop-experimental"><i data-lucide="flask-conical"></i> Experimental</span>';
2896
- btn.title = "Start a new Ralph Loop";
2897
- btn.addEventListener("click", function() {
2898
- var busy = loopActive || ralphPhase === "executing";
2899
- if (busy) {
3561
+ var section = document.getElementById("ralph-loop-section");
3562
+ if (!section) return;
3563
+
3564
+ var busy = loopActive || ralphPhase === "executing";
3565
+ var phase = busy ? "executing" : ralphPhase;
3566
+
3567
+ var statusHtml = "";
3568
+ var statusClass = "";
3569
+ var clickAction = "wizard"; // default
3570
+
3571
+ if (phase === "crafting") {
3572
+ statusHtml = '<span class="ralph-section-status crafting">' + iconHtml("loader", "icon-spin") + ' Crafting\u2026</span>';
3573
+ clickAction = "none";
3574
+ } else if (phase === "approval") {
3575
+ statusHtml = '<span class="ralph-section-status ready">Ready</span>';
3576
+ statusClass = "ralph-section-ready";
3577
+ clickAction = "none";
3578
+ } else if (phase === "executing") {
3579
+ var iterText = loopIteration > 0 ? "Running \u00b7 iteration " + loopIteration + "/" + loopMaxIterations : "Starting\u2026";
3580
+ statusHtml = '<span class="ralph-section-status running">' + iconHtml("loader", "icon-spin") + ' ' + iterText + '</span>';
3581
+ statusClass = "ralph-section-running";
3582
+ clickAction = "popover";
3583
+ } else if (phase === "done") {
3584
+ statusHtml = '<span class="ralph-section-status done">\u2713 Done</span>';
3585
+ statusHtml += '<a href="#" class="ralph-section-tasks-link">View in Scheduled Tasks</a>';
3586
+ statusClass = "ralph-section-done";
3587
+ clickAction = "wizard";
3588
+ } else {
3589
+ // idle
3590
+ statusHtml = '<span class="ralph-section-hint">Start a new loop</span>';
3591
+ }
3592
+
3593
+ section.className = "ralph-loop-section" + (statusClass ? " " + statusClass : "");
3594
+ section.innerHTML =
3595
+ '<div class="ralph-section-inner">' +
3596
+ '<div class="ralph-section-header">' +
3597
+ '<span class="ralph-section-icon">' + iconHtml("repeat") + '</span>' +
3598
+ '<span class="ralph-section-label">Ralph Loop</span>' +
3599
+ '<span class="loop-experimental"><i data-lucide="flask-conical"></i> experimental</span>' +
3600
+ '</div>' +
3601
+ '<div class="ralph-section-body">' + statusHtml + '</div>' +
3602
+ '</div>';
3603
+
3604
+ refreshIcons();
3605
+
3606
+ // Click handler on header
3607
+ var header = section.querySelector(".ralph-section-header");
3608
+ if (header) {
3609
+ header.style.cursor = clickAction === "none" ? "default" : "pointer";
3610
+ header.addEventListener("click", function() {
3611
+ if (clickAction === "popover") {
2900
3612
  toggleLoopPopover();
2901
- } else {
3613
+ } else if (clickAction === "wizard") {
2902
3614
  openRalphWizard();
2903
3615
  }
2904
3616
  });
2905
- var sessionActions = document.getElementById("session-actions");
2906
- if (sessionActions) sessionActions.appendChild(btn);
2907
- if (typeof lucide !== "undefined") lucide.createIcons();
2908
- existing = btn;
2909
3617
  }
2910
- var busy = loopActive || ralphPhase === "executing";
2911
- var hint = existing.querySelector(".loop-busy-hint");
2912
- if (busy) {
2913
- existing.style.opacity = "";
2914
- existing.style.pointerEvents = "";
2915
- if (!hint) {
2916
- hint = document.createElement("span");
2917
- hint.className = "loop-busy-hint";
2918
- hint.innerHTML = iconHtml("loader", "icon-spin");
2919
- existing.appendChild(hint);
2920
- refreshIcons();
2921
- }
2922
- } else {
2923
- if (hint) hint.remove();
3618
+
3619
+ // "View in Scheduled Tasks" link
3620
+ var tasksLink = section.querySelector(".ralph-section-tasks-link");
3621
+ if (tasksLink) {
3622
+ tasksLink.addEventListener("click", function(e) {
3623
+ e.preventDefault();
3624
+ e.stopPropagation();
3625
+ openSchedulerToTab("library");
3626
+ });
2924
3627
  }
2925
3628
  }
2926
3629
 
@@ -3053,13 +3756,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3053
3756
  }
3054
3757
 
3055
3758
  function openRalphWizard() {
3056
- wizardData = { name: "", task: "", maxIterations: 25 };
3759
+ wizardData = { name: "", task: "", maxIterations: 3 };
3057
3760
  ralphSkillInstalling = false;
3058
3761
  var el = document.getElementById("ralph-wizard");
3059
3762
  if (!el) return;
3060
3763
 
3061
- var nameEl = document.getElementById("ralph-name");
3062
- if (nameEl) nameEl.value = "";
3063
3764
  var taskEl = document.getElementById("ralph-task");
3064
3765
  if (taskEl) taskEl.value = "";
3065
3766
  var iterEl = document.getElementById("ralph-max-iterations");
@@ -3103,29 +3804,68 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3103
3804
  var nextBtn = document.getElementById("ralph-wizard-next");
3104
3805
  if (backBtn) backBtn.style.visibility = wizardStep === 1 ? "hidden" : "visible";
3105
3806
  if (skipBtn) skipBtn.style.display = "none";
3106
- if (nextBtn) nextBtn.textContent = wizardStep === 3 ? "Launch" : wizardStep === 1 ? "Get Started" : "Next";
3107
-
3108
- // Build review on step 3
3109
- if (wizardStep === 3) {
3110
- collectWizardData();
3111
- var summary = document.getElementById("ralph-review-summary");
3112
- if (summary) {
3113
- summary.innerHTML =
3114
- '<div class="ralph-review-label">Name</div>' +
3115
- '<div class="ralph-review-value">' + escapeHtml(wizardData.name || "(empty)") + '</div>' +
3116
- '<div class="ralph-review-label">Task</div>' +
3117
- '<div class="ralph-review-value">' + escapeHtml(wizardData.task || "(empty)") + '</div>';
3118
- }
3119
- }
3807
+ if (nextBtn) nextBtn.textContent = wizardStep === 2 ? "Launch" : "Get Started";
3120
3808
  }
3121
3809
 
3122
3810
  function collectWizardData() {
3123
- var nameEl = document.getElementById("ralph-name");
3124
3811
  var taskEl = document.getElementById("ralph-task");
3125
3812
  var iterEl = document.getElementById("ralph-max-iterations");
3126
- wizardData.name = nameEl ? nameEl.value.replace(/[^a-zA-Z0-9_-]/g, "").trim() : "";
3813
+ wizardData.name = "";
3127
3814
  wizardData.task = taskEl ? taskEl.value.trim() : "";
3128
- wizardData.maxIterations = iterEl ? parseInt(iterEl.value, 10) || 25 : 25;
3815
+ wizardData.maxIterations = iterEl ? parseInt(iterEl.value, 10) || 3 : 3;
3816
+ wizardData.cron = null;
3817
+ }
3818
+
3819
+ function buildWizardCron() {
3820
+ var repeatEl = document.getElementById("ralph-repeat");
3821
+ if (!repeatEl) return null;
3822
+ var preset = repeatEl.value;
3823
+ if (preset === "none") return null;
3824
+
3825
+ var timeEl = document.getElementById("ralph-time");
3826
+ var timeVal = timeEl ? timeEl.value : "09:00";
3827
+ var timeParts = timeVal.split(":");
3828
+ var hour = parseInt(timeParts[0], 10) || 9;
3829
+ var minute = parseInt(timeParts[1], 10) || 0;
3830
+
3831
+ if (preset === "daily") return minute + " " + hour + " * * *";
3832
+ if (preset === "weekdays") return minute + " " + hour + " * * 1-5";
3833
+ if (preset === "weekly") return minute + " " + hour + " * * " + new Date().getDay();
3834
+ if (preset === "monthly") return minute + " " + hour + " " + new Date().getDate() + " * *";
3835
+
3836
+ if (preset === "custom") {
3837
+ var unitEl = document.getElementById("ralph-repeat-unit");
3838
+ var unit = unitEl ? unitEl.value : "day";
3839
+ if (unit === "day") return minute + " " + hour + " * * *";
3840
+ if (unit === "month") return minute + " " + hour + " " + new Date().getDate() + " * *";
3841
+ // week: collect selected days
3842
+ var dowBtns = document.querySelectorAll("#ralph-custom-repeat .sched-dow-btn.active");
3843
+ var days = [];
3844
+ for (var i = 0; i < dowBtns.length; i++) {
3845
+ days.push(dowBtns[i].dataset.dow);
3846
+ }
3847
+ if (days.length === 0) days.push(String(new Date().getDay()));
3848
+ return minute + " " + hour + " * * " + days.join(",");
3849
+ }
3850
+ return null;
3851
+ }
3852
+
3853
+ function cronToHumanText(cron) {
3854
+ if (!cron) return "";
3855
+ var parts = cron.trim().split(/\s+/);
3856
+ if (parts.length !== 5) return cron;
3857
+ var m = parts[0], h = parts[1], dom = parts[2], dow = parts[4];
3858
+ var pad = function(n) { return (parseInt(n,10) < 10 ? "0" : "") + parseInt(n,10); };
3859
+ var t = pad(h) + ":" + pad(m);
3860
+ var dayNames = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
3861
+ if (dow === "*" && dom === "*") return "Every day at " + t;
3862
+ if (dow === "1-5" && dom === "*") return "Weekdays at " + t;
3863
+ if (dom !== "*" && dow === "*") return "Monthly on day " + dom + " at " + t;
3864
+ if (dow !== "*" && dom === "*") {
3865
+ var ds = dow.split(",").map(function(d) { return dayNames[parseInt(d,10)] || d; });
3866
+ return "Every " + ds.join(", ") + " at " + t;
3867
+ }
3868
+ return cron;
3129
3869
  }
3130
3870
 
3131
3871
  function wizardNext() {
@@ -3167,18 +3907,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3167
3907
  }
3168
3908
 
3169
3909
  if (wizardStep === 2) {
3170
- var nameEl = document.getElementById("ralph-name");
3171
3910
  var taskEl = document.getElementById("ralph-task");
3172
- if (!wizardData.name) {
3173
- if (nameEl) { nameEl.focus(); nameEl.style.borderColor = "#e74c3c"; setTimeout(function() { nameEl.style.borderColor = ""; }, 2000); }
3174
- return;
3175
- }
3176
3911
  if (!wizardData.task) {
3177
3912
  if (taskEl) { taskEl.focus(); taskEl.style.borderColor = "#e74c3c"; setTimeout(function() { taskEl.style.borderColor = ""; }, 2000); }
3178
3913
  return;
3179
3914
  }
3180
- }
3181
- if (wizardStep === 3) {
3182
3915
  wizardSubmit();
3183
3916
  return;
3184
3917
  }
@@ -3195,7 +3928,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3195
3928
  }
3196
3929
 
3197
3930
  function wizardSkip() {
3198
- if (wizardStep < 3) {
3931
+ if (wizardStep < 2) {
3199
3932
  wizardStep++;
3200
3933
  updateWizardStep();
3201
3934
  }
@@ -3222,11 +3955,49 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3222
3955
  if (wizardSkipBtn) wizardSkipBtn.addEventListener("click", wizardSkip);
3223
3956
  if (wizardNextBtn) wizardNextBtn.addEventListener("click", wizardNext);
3224
3957
 
3225
- // Enforce alphanumeric + hyphens + underscores on name input
3226
- var wizardNameEl = document.getElementById("ralph-name");
3227
- if (wizardNameEl) {
3228
- wizardNameEl.addEventListener("input", function() {
3229
- this.value = this.value.replace(/[^a-zA-Z0-9_-]/g, "");
3958
+ // --- Repeat picker handlers ---
3959
+ var repeatSelect = document.getElementById("ralph-repeat");
3960
+ var repeatTimeRow = document.getElementById("ralph-time-row");
3961
+ var repeatCustom = document.getElementById("ralph-custom-repeat");
3962
+ var repeatUnitSelect = document.getElementById("ralph-repeat-unit");
3963
+ var repeatDowRow = document.getElementById("ralph-custom-dow-row");
3964
+ var cronPreview = document.getElementById("ralph-cron-preview");
3965
+
3966
+ function updateRepeatUI() {
3967
+ if (!repeatSelect) return;
3968
+ var val = repeatSelect.value;
3969
+ var isScheduled = val !== "none";
3970
+ if (repeatTimeRow) repeatTimeRow.style.display = isScheduled ? "" : "none";
3971
+ if (repeatCustom) repeatCustom.style.display = val === "custom" ? "" : "none";
3972
+ if (cronPreview) cronPreview.style.display = isScheduled ? "" : "none";
3973
+ if (isScheduled) {
3974
+ var cron = buildWizardCron();
3975
+ var humanEl = document.getElementById("ralph-cron-human");
3976
+ var cronEl = document.getElementById("ralph-cron-expr");
3977
+ if (humanEl) humanEl.textContent = cronToHumanText(cron);
3978
+ if (cronEl) cronEl.textContent = cron || "";
3979
+ }
3980
+ }
3981
+
3982
+ if (repeatSelect) {
3983
+ repeatSelect.addEventListener("change", updateRepeatUI);
3984
+ }
3985
+ if (repeatUnitSelect) {
3986
+ repeatUnitSelect.addEventListener("change", function () {
3987
+ if (repeatDowRow) repeatDowRow.style.display = this.value === "week" ? "" : "none";
3988
+ updateRepeatUI();
3989
+ });
3990
+ }
3991
+
3992
+ var timeInput = document.getElementById("ralph-time");
3993
+ if (timeInput) timeInput.addEventListener("change", updateRepeatUI);
3994
+
3995
+ // DOW buttons in custom repeat
3996
+ var customDowBtns = document.querySelectorAll("#ralph-custom-repeat .sched-dow-btn");
3997
+ for (var di = 0; di < customDowBtns.length; di++) {
3998
+ customDowBtns[di].addEventListener("click", function () {
3999
+ this.classList.toggle("active");
4000
+ updateRepeatUI();
3230
4001
  });
3231
4002
  }
3232
4003
 
@@ -3284,7 +4055,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3284
4055
  '<span class="ralph-sticky-label">Ralph</span>' +
3285
4056
  '<span class="ralph-sticky-status" id="ralph-sticky-status">Ready</span>' +
3286
4057
  '<button class="ralph-sticky-action ralph-sticky-preview" title="Preview files">' + iconHtml("eye") + '</button>' +
3287
- '<button class="ralph-sticky-action ralph-sticky-start" title="Start loop">' + iconHtml("play") + '</button>' +
4058
+ '<button class="ralph-sticky-action ralph-sticky-start" title="' + (wizardData.cron ? 'Schedule' : 'Start loop') + '">' + iconHtml(wizardData.cron ? "calendar-clock" : "play") + '</button>' +
3288
4059
  '<button class="ralph-sticky-action ralph-sticky-dismiss" title="Cancel and discard">' + iconHtml("x") + '</button>' +
3289
4060
  '</div>' +
3290
4061
  '</div>';
@@ -3371,7 +4142,24 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3371
4142
  var modal = document.getElementById("ralph-preview-modal");
3372
4143
  if (!modal) return;
3373
4144
  modal.classList.remove("hidden");
4145
+
4146
+ // Set name from wizard data
4147
+ var nameEl = document.getElementById("ralph-preview-name");
4148
+ if (nameEl) {
4149
+ var name = (wizardData && wizardData.name) || "Ralph Loop";
4150
+ nameEl.textContent = name;
4151
+ }
4152
+
4153
+ // Update run button label based on cron
4154
+ var runBtn = document.getElementById("ralph-preview-run");
4155
+ if (runBtn) {
4156
+ var hasCron = wizardData && wizardData.cron;
4157
+ runBtn.innerHTML = iconHtml(hasCron ? "calendar-clock" : "play") + " " + (hasCron ? "Schedule" : "Run now");
4158
+ runBtn.disabled = !(ralphFilesReady && ralphFilesReady.bothReady);
4159
+ }
4160
+
3374
4161
  showRalphPreviewTab("prompt");
4162
+ refreshIcons();
3375
4163
  }
3376
4164
 
3377
4165
  function closeRalphPreviewModal() {
@@ -3380,7 +4168,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3380
4168
  }
3381
4169
 
3382
4170
  function showRalphPreviewTab(tab) {
3383
- var tabs = document.querySelectorAll(".ralph-tab");
4171
+ var tabs = document.querySelectorAll("#ralph-preview-modal .ralph-tab");
3384
4172
  for (var i = 0; i < tabs.length; i++) {
3385
4173
  if (tabs[i].getAttribute("data-tab") === tab) {
3386
4174
  tabs[i].classList.add("active");
@@ -3392,19 +4180,44 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3392
4180
  if (!body) return;
3393
4181
  var content = tab === "prompt" ? ralphPreviewContent.prompt : ralphPreviewContent.judge;
3394
4182
  if (typeof marked !== "undefined" && marked.parse) {
3395
- body.innerHTML = DOMPurify.sanitize(marked.parse(content));
4183
+ body.innerHTML = '<div class="md-content">' + DOMPurify.sanitize(marked.parse(content)) + '</div>';
3396
4184
  } else {
3397
4185
  body.textContent = content;
3398
4186
  }
3399
4187
  }
3400
4188
 
3401
4189
  // Preview modal listeners
3402
- var previewCloseBtn = document.getElementById("ralph-preview-close");
3403
- if (previewCloseBtn) previewCloseBtn.addEventListener("click", closeRalphPreviewModal);
3404
-
3405
4190
  var previewBackdrop = document.querySelector("#ralph-preview-modal .confirm-backdrop");
3406
4191
  if (previewBackdrop) previewBackdrop.addEventListener("click", closeRalphPreviewModal);
3407
4192
 
4193
+ // Run now button in preview modal
4194
+ var previewRunBtn = document.getElementById("ralph-preview-run");
4195
+ if (previewRunBtn) {
4196
+ previewRunBtn.addEventListener("click", function (e) {
4197
+ e.stopPropagation();
4198
+ closeRalphPreviewModal();
4199
+ // Trigger the same flow as the sticky start button
4200
+ var stickyStart = document.querySelector(".ralph-sticky-start");
4201
+ if (stickyStart) {
4202
+ stickyStart.click();
4203
+ }
4204
+ });
4205
+ }
4206
+
4207
+ // Delete/cancel button in preview modal
4208
+ var previewDeleteBtn = document.getElementById("ralph-preview-delete");
4209
+ if (previewDeleteBtn) {
4210
+ previewDeleteBtn.addEventListener("click", function (e) {
4211
+ e.stopPropagation();
4212
+ closeRalphPreviewModal();
4213
+ // Trigger the same flow as the sticky dismiss button
4214
+ var stickyDismiss = document.querySelector(".ralph-sticky-dismiss");
4215
+ if (stickyDismiss) {
4216
+ stickyDismiss.click();
4217
+ }
4218
+ });
4219
+ }
4220
+
3408
4221
  var previewTabs = document.querySelectorAll(".ralph-tab");
3409
4222
  for (var ti = 0; ti < previewTabs.length; ti++) {
3410
4223
  previewTabs[ti].addEventListener("click", function() {
@@ -3421,12 +4234,103 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3421
4234
  sendTerminalCommand: function (cmd) { sendTerminalCommand(cmd); },
3422
4235
  });
3423
4236
 
4237
+ // --- Scheduler ---
4238
+ initScheduler({
4239
+ get ws() { return ws; },
4240
+ get connected() { return connected; },
4241
+ get activeSessionId() { return activeSessionId; },
4242
+ basePath: basePath,
4243
+ currentSlug: currentSlug,
4244
+ openRalphWizard: function () { openRalphWizard(); },
4245
+ getProjects: function () { return cachedProjects; },
4246
+ });
4247
+
3424
4248
  // --- Remove project ---
4249
+ var pendingRemoveSlug = null;
4250
+ var pendingRemoveName = null;
4251
+
3425
4252
  function confirmRemoveProject(slug, name) {
3426
- showConfirm("Remove project \"" + name + "\"?", function () {
4253
+ // First check if the project has tasks/schedules
4254
+ pendingRemoveSlug = slug;
4255
+ pendingRemoveName = name;
4256
+ if (ws && ws.readyState === 1) {
4257
+ ws.send(JSON.stringify({ type: "remove_project_check", slug: slug }));
4258
+ }
4259
+ }
4260
+
4261
+ function handleRemoveProjectCheckResult(msg) {
4262
+ var slug = msg.slug || pendingRemoveSlug;
4263
+ var name = msg.name || pendingRemoveName || slug;
4264
+ if (!slug) return;
4265
+
4266
+ if (msg.count > 0) {
4267
+ // Project has tasks — show dialog with options
4268
+ showRemoveProjectTaskDialog(slug, name, msg.count);
4269
+ } else {
4270
+ // No tasks — simple confirm
4271
+ showConfirm('Remove project "' + name + '"?', function () {
4272
+ if (ws && ws.readyState === 1) {
4273
+ ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
4274
+ }
4275
+ });
4276
+ }
4277
+ pendingRemoveSlug = null;
4278
+ pendingRemoveName = null;
4279
+ }
4280
+
4281
+ function showRemoveProjectTaskDialog(slug, name, taskCount) {
4282
+ // Build list of other projects to move tasks to
4283
+ var otherProjects = cachedProjects.filter(function (p) { return p.slug !== slug; });
4284
+
4285
+ var modal = document.createElement("div");
4286
+ modal.className = "remove-project-task-modal";
4287
+ modal.innerHTML =
4288
+ '<div class="remove-project-task-backdrop"></div>' +
4289
+ '<div class="remove-project-task-dialog">' +
4290
+ '<div class="remove-project-task-title">Remove project "' + (name || slug) + '"</div>' +
4291
+ '<div class="remove-project-task-text">This project has <strong>' + taskCount + '</strong> task' + (taskCount > 1 ? 's' : '') + '/schedule' + (taskCount > 1 ? 's' : '') + '.</div>' +
4292
+ '<div class="remove-project-task-options">' +
4293
+ (otherProjects.length > 0
4294
+ ? '<div class="remove-project-task-label">Move tasks to:</div>' +
4295
+ '<select class="remove-project-task-select" id="rpt-move-target">' +
4296
+ otherProjects.map(function (p) {
4297
+ return '<option value="' + p.slug + '">' + (p.title || p.project || p.slug) + '</option>';
4298
+ }).join("") +
4299
+ '</select>' +
4300
+ '<button class="remove-project-task-btn move" id="rpt-move-btn">Move &amp; Remove</button>'
4301
+ : '') +
4302
+ '<button class="remove-project-task-btn delete" id="rpt-delete-btn">Delete all &amp; Remove</button>' +
4303
+ '<button class="remove-project-task-btn cancel" id="rpt-cancel-btn">Cancel</button>' +
4304
+ '</div>' +
4305
+ '</div>';
4306
+
4307
+ document.body.appendChild(modal);
4308
+
4309
+ var backdrop = modal.querySelector(".remove-project-task-backdrop");
4310
+ var moveBtn = modal.querySelector("#rpt-move-btn");
4311
+ var deleteBtn = modal.querySelector("#rpt-delete-btn");
4312
+ var cancelBtn = modal.querySelector("#rpt-cancel-btn");
4313
+ var selectEl = modal.querySelector("#rpt-move-target");
4314
+
4315
+ function close() { modal.remove(); }
4316
+ backdrop.addEventListener("click", close);
4317
+ cancelBtn.addEventListener("click", close);
4318
+
4319
+ if (moveBtn) {
4320
+ moveBtn.addEventListener("click", function () {
4321
+ var targetSlug = selectEl ? selectEl.value : null;
4322
+ if (ws && ws.readyState === 1 && targetSlug) {
4323
+ ws.send(JSON.stringify({ type: "remove_project", slug: slug, moveTasksTo: targetSlug }));
4324
+ }
4325
+ close();
4326
+ });
4327
+ }
4328
+
4329
+ deleteBtn.addEventListener("click", function () {
3427
4330
  if (ws && ws.readyState === 1) {
3428
4331
  ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
3429
4332
  }
4333
+ close();
3430
4334
  });
3431
4335
  }
3432
4336
 
@@ -3649,4 +4553,8 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3649
4553
  // --- Init ---
3650
4554
  lucide.createIcons();
3651
4555
  connect();
3652
- inputEl.focus();
4556
+ if (!currentSlug) {
4557
+ showHomeHub();
4558
+ } else {
4559
+ inputEl.focus();
4560
+ }