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