clay-server 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. package/package.json +47 -0
@@ -0,0 +1,649 @@
1
+ import { copyToClipboard } from './utils.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+
4
+ var ctx;
5
+ var basePath = "/";
6
+ var onboardingBanner, onboardingText, onboardingClose, onboardingDismissed;
7
+ var notifAlertEnabled, notifSoundEnabled, notifPermission;
8
+ var audioCtx = null;
9
+
10
+ export function isNotifAlertEnabled() { return notifAlertEnabled; }
11
+ export function isNotifSoundEnabled() { return notifSoundEnabled; }
12
+ export function getNotifPermission() { return notifPermission; }
13
+
14
+ export function showOnboarding(html) {
15
+ onboardingText.innerHTML = html;
16
+ onboardingBanner.classList.remove("hidden");
17
+ refreshIcons();
18
+ }
19
+
20
+ export function hideOnboarding() {
21
+ onboardingBanner.classList.add("hidden");
22
+ }
23
+
24
+ export function playDoneSound() {
25
+ try {
26
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
27
+ if (audioCtx.state === "suspended") audioCtx.resume();
28
+ var osc = audioCtx.createOscillator();
29
+ var gain = audioCtx.createGain();
30
+ osc.type = "sine";
31
+ osc.frequency.value = 880;
32
+ gain.gain.value = 0.1;
33
+ osc.connect(gain);
34
+ gain.connect(audioCtx.destination);
35
+ osc.start();
36
+ gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
37
+ osc.stop(audioCtx.currentTime + 0.3);
38
+ } catch(e) {}
39
+ }
40
+
41
+ export function showDoneNotification() {
42
+ var lastAssistant = ctx.messagesEl.querySelector(".msg-assistant:last-of-type .md-content");
43
+ var preview = lastAssistant ? lastAssistant.textContent.substring(0, 100) : "Response ready";
44
+
45
+ var sessionTitle = "Claude";
46
+ var activeItem = ctx.sessionListEl.querySelector(".session-item.active");
47
+ if (activeItem) {
48
+ var textEl = activeItem.querySelector(".session-item-text");
49
+ if (textEl) sessionTitle = textEl.textContent || "Claude";
50
+ else sessionTitle = activeItem.textContent || "Claude";
51
+ }
52
+
53
+ var n = new Notification(sessionTitle, {
54
+ body: preview,
55
+ tag: "claude-done",
56
+ });
57
+
58
+ n.onclick = function() {
59
+ window.focus();
60
+ n.close();
61
+ };
62
+
63
+ setTimeout(function() { n.close(); }, 5000);
64
+ }
65
+
66
+ export function initNotifications(_ctx) {
67
+ ctx = _ctx;
68
+ basePath = ctx.basePath || "/";
69
+ var $ = ctx.$;
70
+
71
+ // --- Mobile viewport (iOS keyboard handling) ---
72
+ if (window.visualViewport) {
73
+ var layout = $("layout");
74
+ function onViewportChange() {
75
+ layout.style.height = window.visualViewport.height + "px";
76
+ document.documentElement.scrollTop = 0;
77
+ ctx.scrollToBottom();
78
+ }
79
+ window.visualViewport.addEventListener("resize", onViewportChange);
80
+ window.visualViewport.addEventListener("scroll", onViewportChange);
81
+ }
82
+
83
+ // --- Update banner ---
84
+ (function () {
85
+ var banner = $("update-banner");
86
+ var closeBtn = $("update-banner-close");
87
+ var howBtn = $("update-how");
88
+ var updateNowBtn = $("update-now");
89
+ if (!banner) return;
90
+
91
+ // Build popover (manual update instructions)
92
+ var popover = document.createElement("div");
93
+ popover.id = "update-popover";
94
+ popover.innerHTML =
95
+ '<div class="popover-label">Run in your terminal:</div>' +
96
+ '<div class="popover-cmd">' +
97
+ '<code>npx clay-server@latest</code>' +
98
+ '<button class="popover-copy" title="Copy">' + iconHtml("copy") + '</button>' +
99
+ '</div>';
100
+ banner.appendChild(popover);
101
+ refreshIcons();
102
+
103
+ var copyBtn = popover.querySelector(".popover-copy");
104
+ copyBtn.addEventListener("click", function () {
105
+ copyToClipboard("npx clay-server@latest").then(function () {
106
+ copyBtn.classList.add("copied");
107
+ copyBtn.innerHTML = iconHtml("check");
108
+ refreshIcons();
109
+ setTimeout(function () {
110
+ copyBtn.classList.remove("copied");
111
+ copyBtn.innerHTML = iconHtml("copy");
112
+ refreshIcons();
113
+ }, 1500);
114
+ });
115
+ });
116
+
117
+ // "Update now" button — trigger server-side update + restart
118
+ if (updateNowBtn) {
119
+ updateNowBtn.addEventListener("click", function () {
120
+ if (ctx.ws && ctx.connected) {
121
+ ctx.ws.send(JSON.stringify({ type: "update_now" }));
122
+ updateNowBtn.textContent = "Updating...";
123
+ updateNowBtn.disabled = true;
124
+ }
125
+ });
126
+ }
127
+
128
+ // "?" button — toggle manual instructions popover
129
+ howBtn.addEventListener("click", function (e) {
130
+ e.stopPropagation();
131
+ popover.classList.toggle("visible");
132
+ });
133
+
134
+ document.addEventListener("click", function (e) {
135
+ if (!popover.contains(e.target) && e.target !== howBtn) {
136
+ popover.classList.remove("visible");
137
+ }
138
+ });
139
+
140
+ if (closeBtn) {
141
+ closeBtn.addEventListener("click", function () {
142
+ banner.classList.add("hidden");
143
+ popover.classList.remove("visible");
144
+ });
145
+ }
146
+ })();
147
+
148
+ // --- Settings: Check for updates ---
149
+ (function () {
150
+ var settingsUpdateCheck = $("settings-update-check");
151
+
152
+ function setUpdateBtn(label, spin, disabled) {
153
+ if (!settingsUpdateCheck) return;
154
+ var icon = settingsUpdateCheck.querySelector(".lucide, [data-lucide]");
155
+ if (icon) {
156
+ icon.setAttribute("data-lucide", spin ? "loader" : "refresh-cw");
157
+ if (spin) icon.classList.add("icon-spin-inline");
158
+ else icon.classList.remove("icon-spin-inline");
159
+ }
160
+ // Update text node
161
+ settingsUpdateCheck.innerHTML = "";
162
+ var i = document.createElement("i");
163
+ i.setAttribute("data-lucide", spin ? "loader" : (disabled ? "check" : "refresh-cw"));
164
+ settingsUpdateCheck.appendChild(i);
165
+ settingsUpdateCheck.appendChild(document.createTextNode(" " + label));
166
+ settingsUpdateCheck.disabled = disabled;
167
+ refreshIcons();
168
+ if (spin) {
169
+ var newIcon = settingsUpdateCheck.querySelector(".lucide");
170
+ if (newIcon) newIcon.classList.add("icon-spin-inline");
171
+ }
172
+ }
173
+
174
+ if (settingsUpdateCheck) {
175
+ settingsUpdateCheck.addEventListener("click", function (e) {
176
+ e.stopPropagation();
177
+ if (ctx.ws && ctx.connected) {
178
+ ctx.ws.send(JSON.stringify({ type: "check_update" }));
179
+ }
180
+ setUpdateBtn("Checking…", true, true);
181
+ setTimeout(function () {
182
+ if (settingsUpdateCheck.disabled) {
183
+ setUpdateBtn("Up to date", false, true);
184
+ setTimeout(function () {
185
+ setUpdateBtn("Check for updates", false, false);
186
+ }, 1500);
187
+ }
188
+ }, 2000);
189
+ });
190
+ }
191
+
192
+ // --- Footer status (title bar) ---
193
+ var footerStatus = $("footer-status");
194
+ if (footerStatus) {
195
+ footerStatus.addEventListener("click", function (e) {
196
+ e.stopPropagation();
197
+ if (ctx.toggleStatusPanel) ctx.toggleStatusPanel();
198
+ });
199
+ }
200
+ })();
201
+
202
+ // --- Onboarding banner (HTTPS / Push) ---
203
+ onboardingBanner = $("onboarding-banner");
204
+ onboardingText = $("onboarding-banner-text");
205
+ onboardingClose = $("onboarding-banner-close");
206
+ onboardingDismissed = localStorage.getItem("onboarding-dismissed");
207
+
208
+ if (onboardingClose) {
209
+ onboardingClose.addEventListener("click", function () {
210
+ hideOnboarding();
211
+ localStorage.setItem("onboarding-dismissed", "1");
212
+ onboardingDismissed = "1";
213
+ });
214
+ }
215
+
216
+ // Suggest HTTPS setup for push notification support
217
+ if (location.protocol !== "https:" && location.hostname !== "localhost") {
218
+ if (!onboardingDismissed) {
219
+ showOnboarding(
220
+ iconHtml("bell-ring") +
221
+ ' Get alerts on your phone when Claude is done. <a href="/setup">Set up HTTPS</a>'
222
+ );
223
+ }
224
+ }
225
+
226
+ // --- Tooltip ---
227
+ var tooltipEl = document.createElement("div");
228
+ tooltipEl.className = "tooltip";
229
+ document.body.appendChild(tooltipEl);
230
+ var tooltipTimer = null;
231
+
232
+ function showTooltipAt(target) {
233
+ tooltipEl.textContent = target.dataset.tip;
234
+ tooltipEl.style.top = "-9999px";
235
+ tooltipEl.style.left = "0";
236
+ tooltipEl.style.right = "";
237
+ tooltipEl.style.transform = "";
238
+ tooltipEl.classList.add("visible");
239
+ var tipW = tooltipEl.offsetWidth;
240
+ var rect = target.getBoundingClientRect();
241
+ var centerX = rect.left + rect.width / 2;
242
+ var leftPos = centerX - tipW / 2;
243
+ tooltipEl.style.top = (rect.bottom + 8) + "px";
244
+ if (leftPos + tipW > window.innerWidth - 8) {
245
+ tooltipEl.style.left = "";
246
+ tooltipEl.style.right = "8px";
247
+ } else {
248
+ tooltipEl.style.left = Math.max(8, leftPos) + "px";
249
+ tooltipEl.style.right = "";
250
+ }
251
+ }
252
+
253
+ function hideTooltip() {
254
+ tooltipEl.classList.remove("visible");
255
+ clearTimeout(tooltipTimer);
256
+ }
257
+
258
+ document.addEventListener("click", function (e) {
259
+ var target = e.target.closest("[data-tip]");
260
+ if (target) {
261
+ showTooltipAt(target);
262
+ clearTimeout(tooltipTimer);
263
+ tooltipTimer = setTimeout(hideTooltip, 2000);
264
+ } else {
265
+ hideTooltip();
266
+ }
267
+ });
268
+
269
+ document.addEventListener("mouseover", function (e) {
270
+ var target = e.target.closest("[data-tip]");
271
+ if (target) {
272
+ clearTimeout(tooltipTimer);
273
+ showTooltipAt(target);
274
+ }
275
+ });
276
+
277
+ document.addEventListener("mouseout", function (e) {
278
+ var target = e.target.closest("[data-tip]");
279
+ if (target) {
280
+ hideTooltip();
281
+ }
282
+ });
283
+
284
+ // --- iOS Safari detection ---
285
+ var isIOSSafari = (function () {
286
+ var ua = navigator.userAgent;
287
+ var isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
288
+ var isSafari = isIOS && /Safari/.test(ua) && !/CriOS|FxiOS|OPiOS|EdgiOS/.test(ua);
289
+ return isSafari;
290
+ })();
291
+ var isStandalone = window.matchMedia("(display-mode:standalone)").matches || navigator.standalone;
292
+
293
+ // --- Browser notifications ---
294
+ notifPermission = ("Notification" in window) ? Notification.permission : "denied";
295
+ notifAlertEnabled = localStorage.getItem("notif-alert") !== "0";
296
+ notifSoundEnabled = localStorage.getItem("notif-sound") !== "0";
297
+
298
+ var notifBtn = $("notif-btn");
299
+ var notifMenu = $("notif-menu");
300
+ var notifToggleAlert = $("notif-toggle-alert");
301
+ var notifToggleSound = $("notif-toggle-sound");
302
+
303
+ if (notifAlertEnabled && "Notification" in window && Notification.permission === "denied") {
304
+ notifAlertEnabled = false;
305
+ localStorage.setItem("notif-alert", "0");
306
+ }
307
+ notifToggleAlert.checked = notifAlertEnabled;
308
+ notifToggleSound.checked = notifSoundEnabled;
309
+
310
+ notifBtn.addEventListener("click", function (e) {
311
+ e.stopPropagation();
312
+ var open = notifMenu.classList.toggle("hidden");
313
+ notifBtn.classList.toggle("active", !open);
314
+ });
315
+
316
+ document.addEventListener("click", function (e) {
317
+ if (!notifMenu.contains(e.target) && e.target !== notifBtn) {
318
+ notifMenu.classList.add("hidden");
319
+ notifBtn.classList.remove("active");
320
+ }
321
+ });
322
+
323
+ var notifBlockedHint = $("notif-blocked-hint");
324
+
325
+ notifToggleAlert.addEventListener("change", function () {
326
+ notifAlertEnabled = notifToggleAlert.checked;
327
+ localStorage.setItem("notif-alert", notifAlertEnabled ? "1" : "0");
328
+ notifBlockedHint.classList.add("hidden");
329
+ if (notifAlertEnabled && notifPermission !== "granted") {
330
+ if ("Notification" in window && Notification.permission === "denied") {
331
+ notifAlertEnabled = false;
332
+ notifToggleAlert.checked = false;
333
+ localStorage.setItem("notif-alert", "0");
334
+ notifBlockedHint.classList.remove("hidden");
335
+ refreshIcons();
336
+ return;
337
+ }
338
+ Notification.requestPermission().then(function (p) {
339
+ notifPermission = p;
340
+ if (p !== "granted") {
341
+ notifAlertEnabled = false;
342
+ notifToggleAlert.checked = false;
343
+ localStorage.setItem("notif-alert", "0");
344
+ notifBlockedHint.classList.remove("hidden");
345
+ refreshIcons();
346
+ }
347
+ });
348
+ }
349
+ });
350
+
351
+ // --- Notification help modal ---
352
+ var notifHelpModal = $("notif-help-modal");
353
+ var notifHelpClose = $("notif-help-close");
354
+ var notifLearnMore = $("notif-learn-more");
355
+ var notifUrlCopy = $("notif-url-copy");
356
+ var notifSettingsUrl = $("notif-settings-url");
357
+
358
+ // Detect browser and set correct settings URL
359
+ (function () {
360
+ var url = "chrome://settings/content/notifications";
361
+ var ua = navigator.userAgent;
362
+ if (ua.indexOf("Firefox") !== -1) url = "about:preferences#privacy";
363
+ else if (ua.indexOf("Edg/") !== -1) url = "edge://settings/content/notifications";
364
+ else if (ua.indexOf("Arc") !== -1) url = "arc://settings/content/notifications";
365
+ else if (isIOSSafari) url = "Settings > Safari > Notifications";
366
+ notifSettingsUrl.textContent = url;
367
+ })();
368
+
369
+ notifLearnMore.addEventListener("click", function (e) {
370
+ e.preventDefault();
371
+ notifHelpModal.classList.remove("hidden");
372
+ refreshIcons();
373
+ });
374
+
375
+ notifHelpClose.addEventListener("click", function () {
376
+ notifHelpModal.classList.add("hidden");
377
+ });
378
+
379
+ notifHelpModal.querySelector(".confirm-backdrop").addEventListener("click", function () {
380
+ notifHelpModal.classList.add("hidden");
381
+ });
382
+
383
+ notifUrlCopy.addEventListener("click", function () {
384
+ copyToClipboard(notifSettingsUrl.textContent).then(function () {
385
+ notifUrlCopy.classList.add("copied");
386
+ notifUrlCopy.innerHTML = iconHtml("check");
387
+ refreshIcons();
388
+ setTimeout(function () {
389
+ notifUrlCopy.classList.remove("copied");
390
+ notifUrlCopy.innerHTML = iconHtml("copy");
391
+ refreshIcons();
392
+ }, 1500);
393
+ });
394
+ });
395
+
396
+ notifToggleSound.addEventListener("change", function () {
397
+ notifSoundEnabled = notifToggleSound.checked;
398
+ localStorage.setItem("notif-sound", notifSoundEnabled ? "1" : "0");
399
+ });
400
+
401
+ // --- Push notifications toggle ---
402
+ var notifPushRow = $("notif-push-row");
403
+ var notifTogglePush = $("notif-toggle-push");
404
+ var pushAvailable = ("serviceWorker" in navigator) &&
405
+ (location.protocol === "https:" || location.hostname === "localhost");
406
+
407
+ // On iOS Safari (not in PWA mode), replace the push toggle with an info hint
408
+ if (isIOSSafari && !isStandalone) {
409
+ var infoRow = document.createElement("div");
410
+ infoRow.className = "notif-option notif-ios-info";
411
+ infoRow.style.display = "flex";
412
+ infoRow.innerHTML =
413
+ '<span><i data-lucide="smartphone" style="width:14px;height:14px"></i> Push notifications</span>' +
414
+ '<button class="notif-ios-info-btn" title="Info"><i data-lucide="info" style="width:14px;height:14px"></i></button>';
415
+ notifPushRow.parentNode.replaceChild(infoRow, notifPushRow);
416
+
417
+ var iosHint = document.createElement("div");
418
+ iosHint.id = "notif-ios-hint";
419
+ iosHint.className = "hidden";
420
+ iosHint.innerHTML =
421
+ 'To enable push notifications on iOS, tap <strong>Share</strong> ' +
422
+ '<i data-lucide="share" style="width:12px;height:12px;vertical-align:-2px"></i> ' +
423
+ 'then <strong>Add to Home Screen</strong>. ' +
424
+ 'Push notifications work inside the installed app.';
425
+ infoRow.parentNode.insertBefore(iosHint, infoRow.nextSibling);
426
+
427
+ infoRow.querySelector(".notif-ios-info-btn").addEventListener("click", function (e) {
428
+ e.preventDefault();
429
+ e.stopPropagation();
430
+ iosHint.classList.toggle("hidden");
431
+ refreshIcons();
432
+ });
433
+ refreshIcons();
434
+ } else if (pushAvailable) {
435
+ notifPushRow.style.display = "flex";
436
+ }
437
+
438
+ function sendPushSubscription(sub) {
439
+ var prevEndpoint = localStorage.getItem("push-endpoint");
440
+ window._pushSubscription = sub;
441
+ localStorage.setItem("push-endpoint", sub.endpoint);
442
+ var json = sub.toJSON();
443
+ var payload = { subscription: json };
444
+ if (prevEndpoint && prevEndpoint !== sub.endpoint) {
445
+ payload.replaceEndpoint = prevEndpoint;
446
+ }
447
+ if (ctx.ws && ctx.ws.readyState === 1) {
448
+ ctx.ws.send(JSON.stringify({ type: "push_subscribe", subscription: json, replaceEndpoint: payload.replaceEndpoint || null }));
449
+ } else {
450
+ fetch(basePath + "api/push-subscribe", {
451
+ method: "POST", headers: { "Content-Type": "application/json" },
452
+ credentials: "same-origin", body: JSON.stringify(payload),
453
+ });
454
+ }
455
+ }
456
+
457
+ function subscribePush() {
458
+ navigator.serviceWorker.ready.then(function (reg) {
459
+ return fetch(basePath + "api/vapid-public-key", { cache: "no-store" })
460
+ .then(function (r) { return r.json(); })
461
+ .then(function (data) {
462
+ if (!data.publicKey) throw new Error("No VAPID key");
463
+ var raw = atob(data.publicKey.replace(/-/g, "+").replace(/_/g, "/"));
464
+ var key = new Uint8Array(raw.length);
465
+ for (var i = 0; i < raw.length; i++) key[i] = raw.charCodeAt(i);
466
+ return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
467
+ });
468
+ }).then(function (sub) {
469
+ sendPushSubscription(sub);
470
+ localStorage.setItem("notif-push", "1");
471
+ hideOnboarding();
472
+ localStorage.setItem("onboarding-dismissed", "1");
473
+ // Show a welcome notification so the user knows it works
474
+ navigator.serviceWorker.ready.then(function (reg) {
475
+ reg.showNotification("\ud83c\udf89 Welcome to Clay!", {
476
+ body: "\ud83d\udd14 You\u2019ll be notified when Claude responds.",
477
+ tag: "claude-welcome",
478
+ });
479
+ }).catch(function () {});
480
+ }).catch(function () {
481
+ notifTogglePush.checked = false;
482
+ localStorage.setItem("notif-push", "0");
483
+ notifBlockedHint.classList.remove("hidden");
484
+ refreshIcons();
485
+ });
486
+ }
487
+
488
+ function unsubscribePush() {
489
+ if (window._pushSubscription) {
490
+ window._pushSubscription.unsubscribe().catch(function () {});
491
+ window._pushSubscription = null;
492
+ }
493
+ localStorage.setItem("notif-push", "0");
494
+ }
495
+
496
+ notifTogglePush.addEventListener("change", function () {
497
+ if (notifTogglePush.checked) {
498
+ notifBlockedHint.classList.add("hidden");
499
+ subscribePush();
500
+ } else {
501
+ unsubscribePush();
502
+ }
503
+ });
504
+
505
+ // --- Service Worker registration & push state sync ---
506
+ (function initServiceWorker() {
507
+ if (!("serviceWorker" in navigator)) return;
508
+ if (location.protocol !== "https:" && location.hostname !== "localhost") return;
509
+
510
+ navigator.serviceWorker.register("/sw.js")
511
+ .then(function () { return navigator.serviceWorker.ready; })
512
+ .then(function (reg) {
513
+ // Fetch current VAPID key to detect key changes
514
+ var vapidPromise = fetch(basePath + "api/vapid-public-key", { cache: "no-store" })
515
+ .then(function (r) { return r.json(); })
516
+ .then(function (d) { return d.publicKey || null; })
517
+ .catch(function () { return null; });
518
+
519
+ return Promise.all([reg.pushManager.getSubscription(), vapidPromise]).then(function (results) {
520
+ var sub = results[0];
521
+ var serverKey = results[1];
522
+
523
+ // If subscription exists but VAPID key changed, unsubscribe and re-subscribe
524
+ if (sub && serverKey) {
525
+ var savedKey = localStorage.getItem("vapid-key");
526
+ if (savedKey && savedKey !== serverKey) {
527
+ sub.unsubscribe().catch(function () {});
528
+ sub = null;
529
+ }
530
+ }
531
+ if (serverKey) localStorage.setItem("vapid-key", serverKey);
532
+
533
+ if (sub) {
534
+ window._pushSubscription = sub;
535
+ notifTogglePush.checked = true;
536
+ sendPushSubscription(sub);
537
+ hideOnboarding();
538
+ } else if (serverKey && localStorage.getItem("notif-push") === "1") {
539
+ // Had push enabled but subscription is gone (VAPID key change), re-subscribe
540
+ var raw = atob(serverKey.replace(/-/g, "+").replace(/_/g, "/"));
541
+ var key = new Uint8Array(raw.length);
542
+ for (var i = 0; i < raw.length; i++) key[i] = raw.charCodeAt(i);
543
+ reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key })
544
+ .then(function (newSub) {
545
+ sendPushSubscription(newSub);
546
+ notifTogglePush.checked = true;
547
+ }).catch(function () {
548
+ notifTogglePush.checked = false;
549
+ localStorage.setItem("notif-push", "0");
550
+ });
551
+ } else {
552
+ notifTogglePush.checked = false;
553
+ localStorage.setItem("notif-push", "0");
554
+ // Standalone (PWA) without push: redirect to setup for push onboarding
555
+ // Skip if setup was just completed (setup-done flag)
556
+ var isStandalone = window.matchMedia("(display-mode:standalone)").matches || navigator.standalone;
557
+ if (isStandalone && !localStorage.getItem("setup-done")) {
558
+ var isTailscale = /^100\./.test(location.hostname);
559
+ location.href = "/setup" + (isTailscale ? "" : "?mode=lan");
560
+ return;
561
+ }
562
+ // Browser: show onboarding banner
563
+ if (!onboardingDismissed) {
564
+ showOnboarding(
565
+ iconHtml("bell-ring") +
566
+ ' Get notified when Claude responds. ' +
567
+ '<button class="onboarding-cta" id="onboarding-enable-push">Enable push notifications</button>'
568
+ );
569
+ var enableBtn = $("onboarding-enable-push");
570
+ if (enableBtn) {
571
+ enableBtn.addEventListener("click", function () {
572
+ subscribePush();
573
+ notifTogglePush.checked = true;
574
+ hideOnboarding();
575
+ localStorage.setItem("onboarding-dismissed", "1");
576
+ });
577
+ }
578
+ }
579
+ }
580
+ });
581
+ })
582
+ .catch(function () {});
583
+ })();
584
+
585
+ // --- Debug panel ---
586
+ (function () {
587
+ var debugBtn = $("debug-btn");
588
+ var debugMenu = $("debug-menu");
589
+ if (!debugBtn || !debugMenu) return;
590
+
591
+ var debugToggleUpdate = $("debug-toggle-update");
592
+ var debugToggleOnboarding = $("debug-toggle-onboarding");
593
+
594
+ debugBtn.addEventListener("click", function (e) {
595
+ e.stopPropagation();
596
+ var open = debugMenu.classList.toggle("hidden");
597
+ debugBtn.classList.toggle("active", !open);
598
+
599
+ // Sync toggle states with current banner visibility
600
+ var updateBanner = $("update-banner");
601
+ if (debugToggleUpdate && updateBanner) {
602
+ debugToggleUpdate.checked = !updateBanner.classList.contains("hidden");
603
+ }
604
+ if (debugToggleOnboarding && onboardingBanner) {
605
+ debugToggleOnboarding.checked = !onboardingBanner.classList.contains("hidden");
606
+ }
607
+ });
608
+
609
+ document.addEventListener("click", function (e) {
610
+ if (!debugMenu.contains(e.target) && e.target !== debugBtn) {
611
+ debugMenu.classList.add("hidden");
612
+ debugBtn.classList.remove("active");
613
+ }
614
+ });
615
+
616
+ if (debugToggleUpdate) {
617
+ debugToggleUpdate.addEventListener("change", function () {
618
+ var banner = $("update-banner");
619
+ if (!banner) return;
620
+ if (debugToggleUpdate.checked) {
621
+ // Trigger real update check from server (debug mode uses v0.0.9)
622
+ if (ctx.ws && ctx.connected) {
623
+ ctx.ws.send(JSON.stringify({ type: "check_update" }));
624
+ }
625
+ } else {
626
+ banner.classList.add("hidden");
627
+ }
628
+ refreshIcons();
629
+ });
630
+ }
631
+
632
+ if (debugToggleOnboarding) {
633
+ debugToggleOnboarding.addEventListener("change", function () {
634
+ if (debugToggleOnboarding.checked) {
635
+ if (!onboardingText.innerHTML.trim()) {
636
+ showOnboarding(
637
+ iconHtml("bell-ring") +
638
+ ' Get alerts on your phone when Claude is done. <a href="/setup">Set up HTTPS</a>'
639
+ );
640
+ } else {
641
+ onboardingBanner.classList.remove("hidden");
642
+ }
643
+ } else {
644
+ hideOnboarding();
645
+ }
646
+ });
647
+ }
648
+ })();
649
+ }