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