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.
- package/bin/cli.js +31 -17
- package/lib/config.js +7 -4
- package/lib/project.js +343 -15
- package/lib/public/app.js +1039 -134
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/clay-logo.png +0 -0
- package/lib/public/css/base.css +18 -1
- package/lib/public/css/filebrowser.css +1 -0
- package/lib/public/css/home-hub.css +455 -0
- package/lib/public/css/icon-strip.css +6 -5
- package/lib/public/css/loop.css +141 -23
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/mobile-nav.css +38 -12
- package/lib/public/css/overlays.css +205 -169
- package/lib/public/css/playbook.css +264 -0
- package/lib/public/css/profile.css +268 -0
- package/lib/public/css/scheduler-modal.css +1429 -0
- package/lib/public/css/scheduler.css +1305 -0
- package/lib/public/css/sidebar.css +305 -11
- package/lib/public/css/sticky-notes.css +23 -19
- package/lib/public/css/stt.css +155 -0
- package/lib/public/css/title-bar.css +14 -6
- package/lib/public/favicon-banded-32.png +0 -0
- package/lib/public/favicon-banded.png +0 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-banded-76.png +0 -0
- package/lib/public/icon-banded-96.png +0 -0
- package/lib/public/index.html +336 -44
- package/lib/public/modules/ascii-logo.js +442 -0
- package/lib/public/modules/markdown.js +18 -0
- package/lib/public/modules/notifications.js +50 -63
- package/lib/public/modules/playbook.js +578 -0
- package/lib/public/modules/profile.js +357 -0
- package/lib/public/modules/project-settings.js +1 -9
- package/lib/public/modules/scheduler.js +2826 -0
- package/lib/public/modules/server-settings.js +1 -1
- package/lib/public/modules/sidebar.js +376 -32
- package/lib/public/modules/stt.js +272 -0
- package/lib/public/modules/terminal.js +32 -0
- package/lib/public/modules/theme.js +3 -10
- package/lib/public/style.css +6 -0
- package/lib/public/sw.js +82 -3
- package/lib/public/wordmark-banded-20.png +0 -0
- package/lib/public/wordmark-banded-32.png +0 -0
- package/lib/public/wordmark-banded-64.png +0 -0
- package/lib/public/wordmark-banded-80.png +0 -0
- package/lib/scheduler.js +402 -0
- package/lib/sdk-bridge.js +3 -2
- package/lib/server.js +124 -3
- package/lib/sessions.js +35 -2
- 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 + "¤t=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
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
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
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
|
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-
|
|
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
|
|
2544
|
+
var updatePillWrap = $("update-pill-wrap");
|
|
1958
2545
|
var updateVersion = $("update-version");
|
|
1959
|
-
if (
|
|
2546
|
+
if (updatePillWrap && updateVersion && msg.version) {
|
|
1960
2547
|
updateVersion.textContent = "v" + msg.version;
|
|
1961
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
2892
|
-
if (!
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
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
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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:
|
|
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 ===
|
|
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 =
|
|
3810
|
+
wizardData.name = "";
|
|
3127
3811
|
wizardData.task = taskEl ? taskEl.value.trim() : "";
|
|
3128
|
-
wizardData.maxIterations = iterEl ? parseInt(iterEl.value, 10) ||
|
|
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 <
|
|
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
|
-
//
|
|
3226
|
-
var
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
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
|
-
|
|
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 & Remove</button>'
|
|
4298
|
+
: '') +
|
|
4299
|
+
'<button class="remove-project-task-btn delete" id="rpt-delete-btn">Delete all & 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
|
-
|
|
4553
|
+
if (!currentSlug) {
|
|
4554
|
+
showHomeHub();
|
|
4555
|
+
} else {
|
|
4556
|
+
inputEl.focus();
|
|
4557
|
+
}
|