clay-server 2.7.2 → 2.8.2

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 (55) hide show
  1. package/bin/cli.js +31 -17
  2. package/lib/config.js +7 -4
  3. package/lib/project.js +343 -15
  4. package/lib/public/app.js +1039 -134
  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 +18 -1
  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 +336 -44
  33. package/lib/public/modules/ascii-logo.js +442 -0
  34. package/lib/public/modules/markdown.js +18 -0
  35. package/lib/public/modules/notifications.js +50 -63
  36. package/lib/public/modules/playbook.js +578 -0
  37. package/lib/public/modules/profile.js +357 -0
  38. package/lib/public/modules/project-settings.js +1 -9
  39. package/lib/public/modules/scheduler.js +2826 -0
  40. package/lib/public/modules/server-settings.js +1 -1
  41. package/lib/public/modules/sidebar.js +376 -32
  42. package/lib/public/modules/stt.js +272 -0
  43. package/lib/public/modules/terminal.js +32 -0
  44. package/lib/public/modules/theme.js +3 -10
  45. package/lib/public/style.css +6 -0
  46. package/lib/public/sw.js +82 -3
  47. package/lib/public/wordmark-banded-20.png +0 -0
  48. package/lib/public/wordmark-banded-32.png +0 -0
  49. package/lib/public/wordmark-banded-64.png +0 -0
  50. package/lib/public/wordmark-banded-80.png +0 -0
  51. package/lib/scheduler.js +402 -0
  52. package/lib/sdk-bridge.js +3 -2
  53. package/lib/server.js +124 -3
  54. package/lib/sessions.js +35 -2
  55. package/package.json +1 -1
package/lib/public/app.js CHANGED
@@ -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
+
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
+
217
+ step++;
218
+ setTimeout(tick, intervals[step - 1]);
219
+ } else {
220
+ // Final: land on actual weather
221
+ greetEl.textContent = prefix + " " + weatherEmoji;
222
+
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
+
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
+
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
+
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
+
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
+
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
+
1274
1852
  div.appendChild(bubble);
1275
1853
 
1276
1854
  // Action bar below bubble (icons visible on hover)
@@ -1726,7 +2304,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1726
2304
  removeSearchTimeline();
1727
2305
  setActivity(null);
1728
2306
  setStatus("connected");
1729
- enableMainInput();
2307
+ if (!loopActive) enableMainInput();
1730
2308
  resetUsage();
1731
2309
  resetContext();
1732
2310
  // Clear header indicators
@@ -1741,9 +2319,16 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1741
2319
 
1742
2320
  // --- Project switching (no full reload) ---
1743
2321
  function switchProject(slug) {
1744
- if (!slug || slug === currentSlug) return;
2322
+ if (!slug) return;
2323
+ if (homeHubVisible) {
2324
+ hideHomeHub();
2325
+ if (slug === currentSlug) return;
2326
+ }
2327
+ if (slug === currentSlug) return;
1745
2328
  resetFileBrowser();
1746
2329
  closeArchive();
2330
+ if (isSchedulerOpen()) closeScheduler();
2331
+ resetScheduler(slug);
1747
2332
  currentSlug = slug;
1748
2333
  basePath = "/p/" + slug + "/";
1749
2334
  wsPath = "/p/" + slug + "/ws";
@@ -1758,6 +2343,8 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1758
2343
  if (newSlug && newSlug !== currentSlug) {
1759
2344
  resetFileBrowser();
1760
2345
  closeArchive();
2346
+ if (isSchedulerOpen()) closeScheduler();
2347
+ resetScheduler(newSlug);
1761
2348
  currentSlug = newSlug;
1762
2349
  basePath = "/p/" + newSlug + "/";
1763
2350
  wsPath = "/p/" + newSlug + "/ws";
@@ -1947,22 +2534,22 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1947
2534
  if (msg.lanHost) window.__lanHost = msg.lanHost;
1948
2535
  if (msg.dangerouslySkipPermissions) {
1949
2536
  skipPermsEnabled = true;
1950
- var spBanner = $("skip-perms-banner");
2537
+ var spBanner = $("skip-perms-pill");
1951
2538
  if (spBanner) spBanner.classList.remove("hidden");
1952
2539
  }
1953
2540
  updateProjectList(msg);
1954
2541
  break;
1955
2542
 
1956
2543
  case "update_available":
1957
- var updateBanner = $("update-banner");
2544
+ var updatePillWrap = $("update-pill-wrap");
1958
2545
  var updateVersion = $("update-version");
1959
- if (updateBanner && updateVersion && msg.version) {
2546
+ if (updatePillWrap && updateVersion && msg.version) {
1960
2547
  updateVersion.textContent = "v" + msg.version;
1961
- updateBanner.classList.remove("hidden");
2548
+ updatePillWrap.classList.remove("hidden");
1962
2549
  // Reset button state (may be stuck on "Updating..." after restart)
1963
2550
  var updResetBtn = $("update-now");
1964
2551
  if (updResetBtn) {
1965
- updResetBtn.textContent = "Update now";
2552
+ updResetBtn.innerHTML = '<i data-lucide="download"></i> Update now';
1966
2553
  updResetBtn.disabled = false;
1967
2554
  }
1968
2555
  refreshIcons();
@@ -1984,8 +2571,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
1984
2571
  case "update_started":
1985
2572
  var updNowBtn = $("update-now");
1986
2573
  if (updNowBtn) {
1987
- updNowBtn.textContent = "Updating...";
2574
+ updNowBtn.innerHTML = '<i data-lucide="loader"></i> Updating...';
1988
2575
  updNowBtn.disabled = true;
2576
+ refreshIcons();
2577
+ var spinIcon = updNowBtn.querySelector(".lucide");
2578
+ if (spinIcon) spinIcon.classList.add("icon-spin-inline");
1989
2579
  }
1990
2580
  // Block the entire screen with the connect overlay
1991
2581
  connectOverlay.classList.remove("hidden");
@@ -2063,6 +2653,38 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2063
2653
  handleSkillUninstalled(msg);
2064
2654
  break;
2065
2655
 
2656
+ case "loop_registry_updated":
2657
+ handleLoopRegistryUpdated(msg);
2658
+ break;
2659
+
2660
+ case "schedule_run_started":
2661
+ handleScheduleRunStarted(msg);
2662
+ break;
2663
+
2664
+ case "schedule_run_finished":
2665
+ handleScheduleRunFinished(msg);
2666
+ break;
2667
+
2668
+ case "loop_scheduled":
2669
+ handleLoopScheduled(msg);
2670
+ break;
2671
+
2672
+ case "schedule_move_result":
2673
+ if (msg.ok) {
2674
+ showToast("Task moved", "success");
2675
+ } else {
2676
+ showToast(msg.error || "Failed to move task", "error");
2677
+ }
2678
+ break;
2679
+
2680
+ case "remove_project_check_result":
2681
+ handleRemoveProjectCheckResult(msg);
2682
+ break;
2683
+
2684
+ case "hub_schedules":
2685
+ handleHubSchedules(msg);
2686
+ break;
2687
+
2066
2688
  case "input_sync":
2067
2689
  handleInputSync(msg.text);
2068
2690
  break;
@@ -2084,6 +2706,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2084
2706
  break;
2085
2707
 
2086
2708
  case "session_switched":
2709
+ hideHomeHub();
2087
2710
  // Save draft from outgoing session
2088
2711
  if (activeSessionId && inputEl.value) {
2089
2712
  sessionDrafts[activeSessionId] = inputEl.value;
@@ -2321,7 +2944,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2321
2944
  finalizeAssistantBlock();
2322
2945
  processing = false;
2323
2946
  setStatus("connected");
2324
- enableMainInput();
2947
+ if (!loopActive) enableMainInput();
2325
2948
  resetToolState();
2326
2949
  stopUrgentBlink();
2327
2950
  if (document.hidden) {
@@ -2559,6 +3182,8 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2559
3182
  if (loopIteration > 0) {
2560
3183
  updateLoopBanner(loopIteration, loopMaxIterations, "running");
2561
3184
  }
3185
+ inputEl.disabled = true;
3186
+ inputEl.placeholder = "Ralph Loop is running...";
2562
3187
  }
2563
3188
  break;
2564
3189
 
@@ -2570,17 +3195,25 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2570
3195
  showLoopBanner(true);
2571
3196
  updateLoopButton();
2572
3197
  addSystemMessage("Ralph Loop started (max " + msg.maxIterations + " iterations)", false);
3198
+ inputEl.disabled = true;
3199
+ inputEl.placeholder = "Ralph Loop is running...";
2573
3200
  break;
2574
3201
 
2575
3202
  case "loop_iteration":
2576
3203
  loopIteration = msg.iteration;
3204
+ loopMaxIterations = msg.maxIterations;
2577
3205
  updateLoopBanner(msg.iteration, msg.maxIterations, "running");
3206
+ updateLoopButton();
2578
3207
  addSystemMessage("Ralph Loop iteration #" + msg.iteration + " started", false);
3208
+ inputEl.disabled = true;
3209
+ inputEl.placeholder = "Ralph Loop is running...";
2579
3210
  break;
2580
3211
 
2581
3212
  case "loop_judging":
2582
3213
  updateLoopBanner(loopIteration, loopMaxIterations, "judging");
2583
3214
  addSystemMessage("Judging iteration #" + msg.iteration + "...", false);
3215
+ inputEl.disabled = true;
3216
+ inputEl.placeholder = "Ralph Loop is judging...";
2584
3217
  break;
2585
3218
 
2586
3219
  case "loop_verdict":
@@ -2596,6 +3229,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2596
3229
  ralphPhase = "done";
2597
3230
  showLoopBanner(false);
2598
3231
  updateLoopButton();
3232
+ enableMainInput();
2599
3233
  var finishMsg = msg.reason === "pass"
2600
3234
  ? "Ralph Loop completed successfully after " + msg.iterations + " iteration(s)."
2601
3235
  : msg.reason === "max_iterations"
@@ -2623,6 +3257,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2623
3257
  ralphCraftingSessionId = msg.sessionId || activeSessionId;
2624
3258
  updateLoopButton();
2625
3259
  updateRalphBars();
3260
+ if (msg.source !== "ralph") {
3261
+ // Task sessions open in the scheduler calendar window
3262
+ enterCraftingMode(msg.sessionId, msg.taskId);
3263
+ }
3264
+ // Ralph crafting sessions show in session list as part of the loop group
2626
3265
  break;
2627
3266
 
2628
3267
  case "ralph_files_status":
@@ -2633,15 +3272,28 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2633
3272
  };
2634
3273
  if (msg.bothReady && (ralphPhase === "crafting" || ralphPhase === "approval")) {
2635
3274
  ralphPhase = "approval";
2636
- showRalphApprovalBar(true);
3275
+ if (isSchedulerOpen()) {
3276
+ // Task crafting in scheduler: switch from crafting chat to detail view showing files
3277
+ exitCraftingMode(msg.taskId);
3278
+ } else {
3279
+ showRalphApprovalBar(true);
3280
+ }
2637
3281
  }
2638
3282
  updateRalphApprovalStatus();
2639
3283
  break;
2640
3284
 
3285
+ case "loop_registry_files_content":
3286
+ handleLoopRegistryFiles(msg);
3287
+ break;
3288
+
2641
3289
  case "ralph_files_content":
2642
3290
  ralphPreviewContent = { prompt: msg.prompt || "", judge: msg.judge || "" };
2643
3291
  openRalphPreviewModal();
2644
3292
  break;
3293
+
3294
+ case "loop_registry_error":
3295
+ addSystemMessage("Error: " + msg.text, true);
3296
+ break;
2645
3297
  }
2646
3298
  }
2647
3299
 
@@ -2790,6 +3442,17 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2790
3442
  setSendBtnMode: setSendBtnMode,
2791
3443
  });
2792
3444
 
3445
+ // --- STT module (voice input via Web Speech API) ---
3446
+ initSTT({
3447
+ inputEl: inputEl,
3448
+ addSystemMessage: addSystemMessage,
3449
+ });
3450
+
3451
+ // --- User profile (Discord-style popover on user island) ---
3452
+ initProfile({
3453
+ basePath: basePath,
3454
+ });
3455
+
2793
3456
  // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
2794
3457
  initNotifications({
2795
3458
  $: $,
@@ -2852,6 +3515,9 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2852
3515
  fileViewerEl: $("file-viewer"),
2853
3516
  });
2854
3517
 
3518
+ // --- Playbook Engine ---
3519
+ initPlaybook();
3520
+
2855
3521
  // --- Sticky Notes ---
2856
3522
  initStickyNotes({
2857
3523
  get ws() { return ws; },
@@ -2862,6 +3528,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2862
3528
  var stickyNotesSidebarBtn = $("sticky-notes-sidebar-btn");
2863
3529
  if (stickyNotesSidebarBtn) {
2864
3530
  stickyNotesSidebarBtn.addEventListener("click", function () {
3531
+ if (isSchedulerOpen()) closeScheduler();
2865
3532
  if (isArchiveOpen()) {
2866
3533
  closeArchive();
2867
3534
  } else {
@@ -2870,17 +3537,17 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2870
3537
  });
2871
3538
  }
2872
3539
 
2873
- // Close archive when switching to other sidebar panels
3540
+ // Close archive / scheduler panel when switching to other sidebar panels
2874
3541
  var fileBrowserBtn = $("file-browser-btn");
2875
3542
  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(); });
3543
+ if (fileBrowserBtn) fileBrowserBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
3544
+ if (terminalSidebarBtn) terminalSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
2878
3545
 
2879
3546
  // --- Ralph Loop UI ---
2880
3547
  function updateLoopInputVisibility(loop) {
2881
3548
  var inputArea = document.getElementById("input-area");
2882
3549
  if (!inputArea) return;
2883
- if (loop && loop.active) {
3550
+ if (loop && loop.active && loop.role !== "crafting") {
2884
3551
  inputArea.style.display = "none";
2885
3552
  } else {
2886
3553
  inputArea.style.display = "";
@@ -2888,39 +3555,72 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
2888
3555
  }
2889
3556
 
2890
3557
  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) {
3558
+ var section = document.getElementById("ralph-loop-section");
3559
+ if (!section) return;
3560
+
3561
+ var busy = loopActive || ralphPhase === "executing";
3562
+ var phase = busy ? "executing" : ralphPhase;
3563
+
3564
+ var statusHtml = "";
3565
+ var statusClass = "";
3566
+ var clickAction = "wizard"; // default
3567
+
3568
+ if (phase === "crafting") {
3569
+ statusHtml = '<span class="ralph-section-status crafting">' + iconHtml("loader", "icon-spin") + ' Crafting\u2026</span>';
3570
+ clickAction = "none";
3571
+ } else if (phase === "approval") {
3572
+ statusHtml = '<span class="ralph-section-status ready">Ready</span>';
3573
+ statusClass = "ralph-section-ready";
3574
+ clickAction = "none";
3575
+ } else if (phase === "executing") {
3576
+ var iterText = loopIteration > 0 ? "Running \u00b7 iteration " + loopIteration + "/" + loopMaxIterations : "Starting\u2026";
3577
+ statusHtml = '<span class="ralph-section-status running">' + iconHtml("loader", "icon-spin") + ' ' + iterText + '</span>';
3578
+ statusClass = "ralph-section-running";
3579
+ clickAction = "popover";
3580
+ } else if (phase === "done") {
3581
+ statusHtml = '<span class="ralph-section-status done">\u2713 Done</span>';
3582
+ statusHtml += '<a href="#" class="ralph-section-tasks-link">View in Scheduled Tasks</a>';
3583
+ statusClass = "ralph-section-done";
3584
+ clickAction = "wizard";
3585
+ } else {
3586
+ // idle
3587
+ statusHtml = '<span class="ralph-section-hint">Start a new loop</span>';
3588
+ }
3589
+
3590
+ section.className = "ralph-loop-section" + (statusClass ? " " + statusClass : "");
3591
+ section.innerHTML =
3592
+ '<div class="ralph-section-inner">' +
3593
+ '<div class="ralph-section-header">' +
3594
+ '<span class="ralph-section-icon">' + iconHtml("repeat") + '</span>' +
3595
+ '<span class="ralph-section-label">Ralph Loop</span>' +
3596
+ '<span class="loop-experimental"><i data-lucide="flask-conical"></i> experimental</span>' +
3597
+ '</div>' +
3598
+ '<div class="ralph-section-body">' + statusHtml + '</div>' +
3599
+ '</div>';
3600
+
3601
+ refreshIcons();
3602
+
3603
+ // Click handler on header
3604
+ var header = section.querySelector(".ralph-section-header");
3605
+ if (header) {
3606
+ header.style.cursor = clickAction === "none" ? "default" : "pointer";
3607
+ header.addEventListener("click", function() {
3608
+ if (clickAction === "popover") {
2900
3609
  toggleLoopPopover();
2901
- } else {
3610
+ } else if (clickAction === "wizard") {
2902
3611
  openRalphWizard();
2903
3612
  }
2904
3613
  });
2905
- var sessionActions = document.getElementById("session-actions");
2906
- if (sessionActions) sessionActions.appendChild(btn);
2907
- if (typeof lucide !== "undefined") lucide.createIcons();
2908
- existing = btn;
2909
3614
  }
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();
3615
+
3616
+ // "View in Scheduled Tasks" link
3617
+ var tasksLink = section.querySelector(".ralph-section-tasks-link");
3618
+ if (tasksLink) {
3619
+ tasksLink.addEventListener("click", function(e) {
3620
+ e.preventDefault();
3621
+ e.stopPropagation();
3622
+ openSchedulerToTab("library");
3623
+ });
2924
3624
  }
2925
3625
  }
2926
3626
 
@@ -3053,13 +3753,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3053
3753
  }
3054
3754
 
3055
3755
  function openRalphWizard() {
3056
- wizardData = { name: "", task: "", maxIterations: 25 };
3756
+ wizardData = { name: "", task: "", maxIterations: 3 };
3057
3757
  ralphSkillInstalling = false;
3058
3758
  var el = document.getElementById("ralph-wizard");
3059
3759
  if (!el) return;
3060
3760
 
3061
- var nameEl = document.getElementById("ralph-name");
3062
- if (nameEl) nameEl.value = "";
3063
3761
  var taskEl = document.getElementById("ralph-task");
3064
3762
  if (taskEl) taskEl.value = "";
3065
3763
  var iterEl = document.getElementById("ralph-max-iterations");
@@ -3103,29 +3801,68 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3103
3801
  var nextBtn = document.getElementById("ralph-wizard-next");
3104
3802
  if (backBtn) backBtn.style.visibility = wizardStep === 1 ? "hidden" : "visible";
3105
3803
  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
- }
3804
+ if (nextBtn) nextBtn.textContent = wizardStep === 2 ? "Launch" : "Get Started";
3120
3805
  }
3121
3806
 
3122
3807
  function collectWizardData() {
3123
- var nameEl = document.getElementById("ralph-name");
3124
3808
  var taskEl = document.getElementById("ralph-task");
3125
3809
  var iterEl = document.getElementById("ralph-max-iterations");
3126
- wizardData.name = nameEl ? nameEl.value.replace(/[^a-zA-Z0-9_-]/g, "").trim() : "";
3810
+ wizardData.name = "";
3127
3811
  wizardData.task = taskEl ? taskEl.value.trim() : "";
3128
- wizardData.maxIterations = iterEl ? parseInt(iterEl.value, 10) || 25 : 25;
3812
+ wizardData.maxIterations = iterEl ? parseInt(iterEl.value, 10) || 3 : 3;
3813
+ wizardData.cron = null;
3814
+ }
3815
+
3816
+ function buildWizardCron() {
3817
+ var repeatEl = document.getElementById("ralph-repeat");
3818
+ if (!repeatEl) return null;
3819
+ var preset = repeatEl.value;
3820
+ if (preset === "none") return null;
3821
+
3822
+ var timeEl = document.getElementById("ralph-time");
3823
+ var timeVal = timeEl ? timeEl.value : "09:00";
3824
+ var timeParts = timeVal.split(":");
3825
+ var hour = parseInt(timeParts[0], 10) || 9;
3826
+ var minute = parseInt(timeParts[1], 10) || 0;
3827
+
3828
+ if (preset === "daily") return minute + " " + hour + " * * *";
3829
+ if (preset === "weekdays") return minute + " " + hour + " * * 1-5";
3830
+ if (preset === "weekly") return minute + " " + hour + " * * " + new Date().getDay();
3831
+ if (preset === "monthly") return minute + " " + hour + " " + new Date().getDate() + " * *";
3832
+
3833
+ if (preset === "custom") {
3834
+ var unitEl = document.getElementById("ralph-repeat-unit");
3835
+ var unit = unitEl ? unitEl.value : "day";
3836
+ if (unit === "day") return minute + " " + hour + " * * *";
3837
+ if (unit === "month") return minute + " " + hour + " " + new Date().getDate() + " * *";
3838
+ // week: collect selected days
3839
+ var dowBtns = document.querySelectorAll("#ralph-custom-repeat .sched-dow-btn.active");
3840
+ var days = [];
3841
+ for (var i = 0; i < dowBtns.length; i++) {
3842
+ days.push(dowBtns[i].dataset.dow);
3843
+ }
3844
+ if (days.length === 0) days.push(String(new Date().getDay()));
3845
+ return minute + " " + hour + " * * " + days.join(",");
3846
+ }
3847
+ return null;
3848
+ }
3849
+
3850
+ function cronToHumanText(cron) {
3851
+ if (!cron) return "";
3852
+ var parts = cron.trim().split(/\s+/);
3853
+ if (parts.length !== 5) return cron;
3854
+ var m = parts[0], h = parts[1], dom = parts[2], dow = parts[4];
3855
+ var pad = function(n) { return (parseInt(n,10) < 10 ? "0" : "") + parseInt(n,10); };
3856
+ var t = pad(h) + ":" + pad(m);
3857
+ var dayNames = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
3858
+ if (dow === "*" && dom === "*") return "Every day at " + t;
3859
+ if (dow === "1-5" && dom === "*") return "Weekdays at " + t;
3860
+ if (dom !== "*" && dow === "*") return "Monthly on day " + dom + " at " + t;
3861
+ if (dow !== "*" && dom === "*") {
3862
+ var ds = dow.split(",").map(function(d) { return dayNames[parseInt(d,10)] || d; });
3863
+ return "Every " + ds.join(", ") + " at " + t;
3864
+ }
3865
+ return cron;
3129
3866
  }
3130
3867
 
3131
3868
  function wizardNext() {
@@ -3167,18 +3904,11 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3167
3904
  }
3168
3905
 
3169
3906
  if (wizardStep === 2) {
3170
- var nameEl = document.getElementById("ralph-name");
3171
3907
  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
3908
  if (!wizardData.task) {
3177
3909
  if (taskEl) { taskEl.focus(); taskEl.style.borderColor = "#e74c3c"; setTimeout(function() { taskEl.style.borderColor = ""; }, 2000); }
3178
3910
  return;
3179
3911
  }
3180
- }
3181
- if (wizardStep === 3) {
3182
3912
  wizardSubmit();
3183
3913
  return;
3184
3914
  }
@@ -3195,7 +3925,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3195
3925
  }
3196
3926
 
3197
3927
  function wizardSkip() {
3198
- if (wizardStep < 3) {
3928
+ if (wizardStep < 2) {
3199
3929
  wizardStep++;
3200
3930
  updateWizardStep();
3201
3931
  }
@@ -3222,11 +3952,49 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3222
3952
  if (wizardSkipBtn) wizardSkipBtn.addEventListener("click", wizardSkip);
3223
3953
  if (wizardNextBtn) wizardNextBtn.addEventListener("click", wizardNext);
3224
3954
 
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, "");
3955
+ // --- Repeat picker handlers ---
3956
+ var repeatSelect = document.getElementById("ralph-repeat");
3957
+ var repeatTimeRow = document.getElementById("ralph-time-row");
3958
+ var repeatCustom = document.getElementById("ralph-custom-repeat");
3959
+ var repeatUnitSelect = document.getElementById("ralph-repeat-unit");
3960
+ var repeatDowRow = document.getElementById("ralph-custom-dow-row");
3961
+ var cronPreview = document.getElementById("ralph-cron-preview");
3962
+
3963
+ function updateRepeatUI() {
3964
+ if (!repeatSelect) return;
3965
+ var val = repeatSelect.value;
3966
+ var isScheduled = val !== "none";
3967
+ if (repeatTimeRow) repeatTimeRow.style.display = isScheduled ? "" : "none";
3968
+ if (repeatCustom) repeatCustom.style.display = val === "custom" ? "" : "none";
3969
+ if (cronPreview) cronPreview.style.display = isScheduled ? "" : "none";
3970
+ if (isScheduled) {
3971
+ var cron = buildWizardCron();
3972
+ var humanEl = document.getElementById("ralph-cron-human");
3973
+ var cronEl = document.getElementById("ralph-cron-expr");
3974
+ if (humanEl) humanEl.textContent = cronToHumanText(cron);
3975
+ if (cronEl) cronEl.textContent = cron || "";
3976
+ }
3977
+ }
3978
+
3979
+ if (repeatSelect) {
3980
+ repeatSelect.addEventListener("change", updateRepeatUI);
3981
+ }
3982
+ if (repeatUnitSelect) {
3983
+ repeatUnitSelect.addEventListener("change", function () {
3984
+ if (repeatDowRow) repeatDowRow.style.display = this.value === "week" ? "" : "none";
3985
+ updateRepeatUI();
3986
+ });
3987
+ }
3988
+
3989
+ var timeInput = document.getElementById("ralph-time");
3990
+ if (timeInput) timeInput.addEventListener("change", updateRepeatUI);
3991
+
3992
+ // DOW buttons in custom repeat
3993
+ var customDowBtns = document.querySelectorAll("#ralph-custom-repeat .sched-dow-btn");
3994
+ for (var di = 0; di < customDowBtns.length; di++) {
3995
+ customDowBtns[di].addEventListener("click", function () {
3996
+ this.classList.toggle("active");
3997
+ updateRepeatUI();
3230
3998
  });
3231
3999
  }
3232
4000
 
@@ -3284,7 +4052,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3284
4052
  '<span class="ralph-sticky-label">Ralph</span>' +
3285
4053
  '<span class="ralph-sticky-status" id="ralph-sticky-status">Ready</span>' +
3286
4054
  '<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>' +
4055
+ '<button class="ralph-sticky-action ralph-sticky-start" title="' + (wizardData.cron ? 'Schedule' : 'Start loop') + '">' + iconHtml(wizardData.cron ? "calendar-clock" : "play") + '</button>' +
3288
4056
  '<button class="ralph-sticky-action ralph-sticky-dismiss" title="Cancel and discard">' + iconHtml("x") + '</button>' +
3289
4057
  '</div>' +
3290
4058
  '</div>';
@@ -3371,7 +4139,24 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3371
4139
  var modal = document.getElementById("ralph-preview-modal");
3372
4140
  if (!modal) return;
3373
4141
  modal.classList.remove("hidden");
4142
+
4143
+ // Set name from wizard data
4144
+ var nameEl = document.getElementById("ralph-preview-name");
4145
+ if (nameEl) {
4146
+ var name = (wizardData && wizardData.name) || "Ralph Loop";
4147
+ nameEl.textContent = name;
4148
+ }
4149
+
4150
+ // Update run button label based on cron
4151
+ var runBtn = document.getElementById("ralph-preview-run");
4152
+ if (runBtn) {
4153
+ var hasCron = wizardData && wizardData.cron;
4154
+ runBtn.innerHTML = iconHtml(hasCron ? "calendar-clock" : "play") + " " + (hasCron ? "Schedule" : "Run now");
4155
+ runBtn.disabled = !(ralphFilesReady && ralphFilesReady.bothReady);
4156
+ }
4157
+
3374
4158
  showRalphPreviewTab("prompt");
4159
+ refreshIcons();
3375
4160
  }
3376
4161
 
3377
4162
  function closeRalphPreviewModal() {
@@ -3380,7 +4165,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3380
4165
  }
3381
4166
 
3382
4167
  function showRalphPreviewTab(tab) {
3383
- var tabs = document.querySelectorAll(".ralph-tab");
4168
+ var tabs = document.querySelectorAll("#ralph-preview-modal .ralph-tab");
3384
4169
  for (var i = 0; i < tabs.length; i++) {
3385
4170
  if (tabs[i].getAttribute("data-tab") === tab) {
3386
4171
  tabs[i].classList.add("active");
@@ -3392,19 +4177,44 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3392
4177
  if (!body) return;
3393
4178
  var content = tab === "prompt" ? ralphPreviewContent.prompt : ralphPreviewContent.judge;
3394
4179
  if (typeof marked !== "undefined" && marked.parse) {
3395
- body.innerHTML = DOMPurify.sanitize(marked.parse(content));
4180
+ body.innerHTML = '<div class="md-content">' + DOMPurify.sanitize(marked.parse(content)) + '</div>';
3396
4181
  } else {
3397
4182
  body.textContent = content;
3398
4183
  }
3399
4184
  }
3400
4185
 
3401
4186
  // Preview modal listeners
3402
- var previewCloseBtn = document.getElementById("ralph-preview-close");
3403
- if (previewCloseBtn) previewCloseBtn.addEventListener("click", closeRalphPreviewModal);
3404
-
3405
4187
  var previewBackdrop = document.querySelector("#ralph-preview-modal .confirm-backdrop");
3406
4188
  if (previewBackdrop) previewBackdrop.addEventListener("click", closeRalphPreviewModal);
3407
4189
 
4190
+ // Run now button in preview modal
4191
+ var previewRunBtn = document.getElementById("ralph-preview-run");
4192
+ if (previewRunBtn) {
4193
+ previewRunBtn.addEventListener("click", function (e) {
4194
+ e.stopPropagation();
4195
+ closeRalphPreviewModal();
4196
+ // Trigger the same flow as the sticky start button
4197
+ var stickyStart = document.querySelector(".ralph-sticky-start");
4198
+ if (stickyStart) {
4199
+ stickyStart.click();
4200
+ }
4201
+ });
4202
+ }
4203
+
4204
+ // Delete/cancel button in preview modal
4205
+ var previewDeleteBtn = document.getElementById("ralph-preview-delete");
4206
+ if (previewDeleteBtn) {
4207
+ previewDeleteBtn.addEventListener("click", function (e) {
4208
+ e.stopPropagation();
4209
+ closeRalphPreviewModal();
4210
+ // Trigger the same flow as the sticky dismiss button
4211
+ var stickyDismiss = document.querySelector(".ralph-sticky-dismiss");
4212
+ if (stickyDismiss) {
4213
+ stickyDismiss.click();
4214
+ }
4215
+ });
4216
+ }
4217
+
3408
4218
  var previewTabs = document.querySelectorAll(".ralph-tab");
3409
4219
  for (var ti = 0; ti < previewTabs.length; ti++) {
3410
4220
  previewTabs[ti].addEventListener("click", function() {
@@ -3421,12 +4231,103 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3421
4231
  sendTerminalCommand: function (cmd) { sendTerminalCommand(cmd); },
3422
4232
  });
3423
4233
 
4234
+ // --- Scheduler ---
4235
+ initScheduler({
4236
+ get ws() { return ws; },
4237
+ get connected() { return connected; },
4238
+ get activeSessionId() { return activeSessionId; },
4239
+ basePath: basePath,
4240
+ currentSlug: currentSlug,
4241
+ openRalphWizard: function () { openRalphWizard(); },
4242
+ getProjects: function () { return cachedProjects; },
4243
+ });
4244
+
3424
4245
  // --- Remove project ---
4246
+ var pendingRemoveSlug = null;
4247
+ var pendingRemoveName = null;
4248
+
3425
4249
  function confirmRemoveProject(slug, name) {
3426
- showConfirm("Remove project \"" + name + "\"?", function () {
4250
+ // First check if the project has tasks/schedules
4251
+ pendingRemoveSlug = slug;
4252
+ pendingRemoveName = name;
4253
+ if (ws && ws.readyState === 1) {
4254
+ ws.send(JSON.stringify({ type: "remove_project_check", slug: slug }));
4255
+ }
4256
+ }
4257
+
4258
+ function handleRemoveProjectCheckResult(msg) {
4259
+ var slug = msg.slug || pendingRemoveSlug;
4260
+ var name = msg.name || pendingRemoveName || slug;
4261
+ if (!slug) return;
4262
+
4263
+ if (msg.count > 0) {
4264
+ // Project has tasks — show dialog with options
4265
+ showRemoveProjectTaskDialog(slug, name, msg.count);
4266
+ } else {
4267
+ // No tasks — simple confirm
4268
+ showConfirm('Remove project "' + name + '"?', function () {
4269
+ if (ws && ws.readyState === 1) {
4270
+ ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
4271
+ }
4272
+ });
4273
+ }
4274
+ pendingRemoveSlug = null;
4275
+ pendingRemoveName = null;
4276
+ }
4277
+
4278
+ function showRemoveProjectTaskDialog(slug, name, taskCount) {
4279
+ // Build list of other projects to move tasks to
4280
+ var otherProjects = cachedProjects.filter(function (p) { return p.slug !== slug; });
4281
+
4282
+ var modal = document.createElement("div");
4283
+ modal.className = "remove-project-task-modal";
4284
+ modal.innerHTML =
4285
+ '<div class="remove-project-task-backdrop"></div>' +
4286
+ '<div class="remove-project-task-dialog">' +
4287
+ '<div class="remove-project-task-title">Remove project "' + (name || slug) + '"</div>' +
4288
+ '<div class="remove-project-task-text">This project has <strong>' + taskCount + '</strong> task' + (taskCount > 1 ? 's' : '') + '/schedule' + (taskCount > 1 ? 's' : '') + '.</div>' +
4289
+ '<div class="remove-project-task-options">' +
4290
+ (otherProjects.length > 0
4291
+ ? '<div class="remove-project-task-label">Move tasks to:</div>' +
4292
+ '<select class="remove-project-task-select" id="rpt-move-target">' +
4293
+ otherProjects.map(function (p) {
4294
+ return '<option value="' + p.slug + '">' + (p.title || p.project || p.slug) + '</option>';
4295
+ }).join("") +
4296
+ '</select>' +
4297
+ '<button class="remove-project-task-btn move" id="rpt-move-btn">Move &amp; Remove</button>'
4298
+ : '') +
4299
+ '<button class="remove-project-task-btn delete" id="rpt-delete-btn">Delete all &amp; Remove</button>' +
4300
+ '<button class="remove-project-task-btn cancel" id="rpt-cancel-btn">Cancel</button>' +
4301
+ '</div>' +
4302
+ '</div>';
4303
+
4304
+ document.body.appendChild(modal);
4305
+
4306
+ var backdrop = modal.querySelector(".remove-project-task-backdrop");
4307
+ var moveBtn = modal.querySelector("#rpt-move-btn");
4308
+ var deleteBtn = modal.querySelector("#rpt-delete-btn");
4309
+ var cancelBtn = modal.querySelector("#rpt-cancel-btn");
4310
+ var selectEl = modal.querySelector("#rpt-move-target");
4311
+
4312
+ function close() { modal.remove(); }
4313
+ backdrop.addEventListener("click", close);
4314
+ cancelBtn.addEventListener("click", close);
4315
+
4316
+ if (moveBtn) {
4317
+ moveBtn.addEventListener("click", function () {
4318
+ var targetSlug = selectEl ? selectEl.value : null;
4319
+ if (ws && ws.readyState === 1 && targetSlug) {
4320
+ ws.send(JSON.stringify({ type: "remove_project", slug: slug, moveTasksTo: targetSlug }));
4321
+ }
4322
+ close();
4323
+ });
4324
+ }
4325
+
4326
+ deleteBtn.addEventListener("click", function () {
3427
4327
  if (ws && ws.readyState === 1) {
3428
4328
  ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
3429
4329
  }
4330
+ close();
3430
4331
  });
3431
4332
  }
3432
4333
 
@@ -3649,4 +4550,8 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
3649
4550
  // --- Init ---
3650
4551
  lucide.createIcons();
3651
4552
  connect();
3652
- inputEl.focus();
4553
+ if (!currentSlug) {
4554
+ showHomeHub();
4555
+ } else {
4556
+ inputEl.focus();
4557
+ }