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