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.
Files changed (71) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-http.js +4 -2
  10. package/lib/project-loop.js +110 -48
  11. package/lib/project-mate-interaction.js +4 -0
  12. package/lib/project-notifications.js +210 -0
  13. package/lib/project-sessions.js +5 -2
  14. package/lib/project-user-message.js +2 -1
  15. package/lib/project.js +26 -2
  16. package/lib/public/app.js +1193 -8517
  17. package/lib/public/css/command-palette.css +14 -0
  18. package/lib/public/css/loop.css +301 -0
  19. package/lib/public/css/notifications-center.css +190 -0
  20. package/lib/public/css/rewind.css +6 -0
  21. package/lib/public/index.html +89 -35
  22. package/lib/public/modules/app-connection.js +160 -0
  23. package/lib/public/modules/app-cursors.js +473 -0
  24. package/lib/public/modules/app-debate-ui.js +389 -0
  25. package/lib/public/modules/app-dm.js +627 -0
  26. package/lib/public/modules/app-favicon.js +212 -0
  27. package/lib/public/modules/app-header.js +229 -0
  28. package/lib/public/modules/app-home-hub.js +600 -0
  29. package/lib/public/modules/app-loop-ui.js +589 -0
  30. package/lib/public/modules/app-loop-wizard.js +439 -0
  31. package/lib/public/modules/app-messages.js +1560 -0
  32. package/lib/public/modules/app-misc.js +299 -0
  33. package/lib/public/modules/app-notifications.js +372 -0
  34. package/lib/public/modules/app-panels.js +888 -0
  35. package/lib/public/modules/app-projects.js +798 -0
  36. package/lib/public/modules/app-rate-limit.js +451 -0
  37. package/lib/public/modules/app-rendering.js +597 -0
  38. package/lib/public/modules/app-skills-install.js +234 -0
  39. package/lib/public/modules/command-palette.js +27 -4
  40. package/lib/public/modules/input.js +31 -20
  41. package/lib/public/modules/scheduler-config.js +1532 -0
  42. package/lib/public/modules/scheduler-history.js +79 -0
  43. package/lib/public/modules/scheduler.js +33 -1554
  44. package/lib/public/modules/session-search.js +13 -1
  45. package/lib/public/modules/sidebar-mates.js +812 -0
  46. package/lib/public/modules/sidebar-mobile.js +1269 -0
  47. package/lib/public/modules/sidebar-projects.js +1449 -0
  48. package/lib/public/modules/sidebar-sessions.js +986 -0
  49. package/lib/public/modules/sidebar.js +232 -4591
  50. package/lib/public/modules/store.js +27 -0
  51. package/lib/public/modules/ws-ref.js +7 -0
  52. package/lib/public/style.css +1 -0
  53. package/lib/sdk-bridge.js +96 -717
  54. package/lib/sdk-message-processor.js +587 -0
  55. package/lib/sdk-message-queue.js +42 -0
  56. package/lib/sdk-skill-discovery.js +131 -0
  57. package/lib/server-admin.js +712 -0
  58. package/lib/server-auth.js +737 -0
  59. package/lib/server-dm.js +221 -0
  60. package/lib/server-mates.js +281 -0
  61. package/lib/server-palette.js +110 -0
  62. package/lib/server-settings.js +479 -0
  63. package/lib/server-skills.js +280 -0
  64. package/lib/server.js +246 -2755
  65. package/lib/sessions.js +11 -4
  66. package/lib/users-auth.js +146 -0
  67. package/lib/users-permissions.js +118 -0
  68. package/lib/users-preferences.js +210 -0
  69. package/lib/users.js +48 -398
  70. package/lib/ws-schema.js +498 -0
  71. package/package.json +1 -1
@@ -0,0 +1,299 @@
1
+ // app-misc.js - Modals (image, paste, confirm), force PIN, PWA install, extension bridge
2
+ // Extracted from app.js (PR-34)
3
+
4
+ import { refreshIcons, iconHtml } from './icons.js';
5
+ import { escapeHtml, copyToClipboard } from './utils.js';
6
+ import { getWs } from './ws-ref.js';
7
+ import { updateBrowserTabList } from './context-sources.js';
8
+
9
+ // --- Module-owned state ---
10
+ var confirmCallback = null;
11
+ var _extRequestCallbacks = {};
12
+
13
+ export function initMisc() {
14
+ // --- Confirm modal listeners ---
15
+ var confirmModal = document.getElementById("confirm-modal");
16
+ var confirmOk = document.getElementById("confirm-ok");
17
+ var confirmCancel = document.getElementById("confirm-cancel");
18
+
19
+ confirmOk.addEventListener("click", function () {
20
+ if (confirmCallback) confirmCallback();
21
+ hideConfirm();
22
+ });
23
+
24
+ confirmCancel.addEventListener("click", hideConfirm);
25
+ confirmModal.querySelector(".confirm-backdrop").addEventListener("click", hideConfirm);
26
+
27
+ // --- PWA install prompt ---
28
+ (function () {
29
+ var installPill = document.getElementById("pwa-install-pill");
30
+ var modal = document.getElementById("pwa-install-modal");
31
+ var confirmBtn = document.getElementById("pwa-modal-confirm");
32
+ var cancelBtn = document.getElementById("pwa-modal-cancel");
33
+ if (!installPill || !modal) return;
34
+
35
+ // Already standalone -- never show
36
+ if (document.documentElement.classList.contains("pwa-standalone")) return;
37
+
38
+ // Show pill on mobile browsers (the primary target for PWA install)
39
+ var isMobile = /Mobi|Android|iPad|iPhone|iPod/.test(navigator.userAgent) ||
40
+ (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
41
+ if (isMobile) {
42
+ installPill.classList.remove("hidden");
43
+ }
44
+
45
+ // Also show on desktop if beforeinstallprompt fires
46
+ window.addEventListener("beforeinstallprompt", function (e) {
47
+ e.preventDefault();
48
+ installPill.classList.remove("hidden");
49
+ });
50
+
51
+ function openModal() {
52
+ modal.classList.remove("hidden");
53
+ lucide.createIcons({ nodes: [modal] });
54
+ }
55
+
56
+ function closeModal() {
57
+ modal.classList.add("hidden");
58
+ }
59
+
60
+ installPill.addEventListener("click", openModal);
61
+ cancelBtn.addEventListener("click", closeModal);
62
+ modal.querySelector(".pwa-modal-backdrop").addEventListener("click", closeModal);
63
+
64
+ confirmBtn.addEventListener("click", function () {
65
+ // Builtin cert (*.d.clay.studio): open PWA setup guide
66
+ if (location.hostname.endsWith(".d.clay.studio")) {
67
+ closeModal();
68
+ location.href = "/pwa";
69
+ return;
70
+ }
71
+ // mkcert / other: redirect to onboarding setup page
72
+ var port = parseInt(location.port, 10);
73
+ var setupUrl;
74
+ if (!port) {
75
+ // Standard port (443/80), behind a reverse proxy with real cert
76
+ setupUrl = location.protocol + "//" + location.hostname + "/setup";
77
+ } else {
78
+ // Non-standard port, Clay serving directly with onboarding server on port+1
79
+ setupUrl = "http://" + location.hostname + ":" + (port + 1) + "/setup";
80
+ }
81
+ location.href = setupUrl;
82
+ });
83
+
84
+ // Hide after install
85
+ window.addEventListener("appinstalled", function () {
86
+ installPill.classList.add("hidden");
87
+ closeModal();
88
+ });
89
+ })();
90
+
91
+ // --- Extension bridge window message listener ---
92
+ window.addEventListener("message", function(event) {
93
+ if (event.source !== window) return;
94
+ if (!event.data || event.data.source !== "clay-chrome-extension") return;
95
+ var msg = event.data.payload;
96
+
97
+ if (msg.type === "clay_ext_tab_list") {
98
+ updateBrowserTabList(msg.tabs);
99
+ // Also inform server about tab list
100
+ var ws = getWs();
101
+ if (ws && ws.readyState === 1) {
102
+ ws.send(JSON.stringify({
103
+ type: "browser_tab_list",
104
+ tabs: msg.tabs
105
+ }));
106
+ }
107
+ }
108
+ if (msg.type === "clay_ext_result") {
109
+ handleExtensionResult(msg.requestId, msg.result);
110
+ }
111
+ });
112
+ }
113
+
114
+ export function showImageModal(src) {
115
+ var modal = document.getElementById("image-modal");
116
+ var img = document.getElementById("image-modal-img");
117
+ if (!modal || !img) return;
118
+ img.src = src;
119
+ modal.classList.remove("hidden");
120
+ refreshIcons(modal);
121
+ }
122
+
123
+ export function closeImageModal() {
124
+ var modal = document.getElementById("image-modal");
125
+ if (modal) modal.classList.add("hidden");
126
+ }
127
+
128
+ export function showPasteModal(text) {
129
+ var modal = document.getElementById("paste-modal");
130
+ var body = document.getElementById("paste-modal-body");
131
+ if (!modal || !body) return;
132
+ body.textContent = text;
133
+ modal.classList.remove("hidden");
134
+ }
135
+
136
+ export function closePasteModal() {
137
+ var modal = document.getElementById("paste-modal");
138
+ if (modal) modal.classList.add("hidden");
139
+ }
140
+
141
+ export function showConfirm(text, onConfirm, okLabel, destructive) {
142
+ var confirmText = document.getElementById("confirm-text");
143
+ var confirmOk = document.getElementById("confirm-ok");
144
+ var confirmModal = document.getElementById("confirm-modal");
145
+ confirmText.textContent = text;
146
+ confirmCallback = onConfirm;
147
+ confirmOk.textContent = okLabel || "Delete";
148
+ confirmOk.className = "confirm-btn " + (destructive === false ? "confirm-ok" : "confirm-delete");
149
+ confirmModal.classList.remove("hidden");
150
+ }
151
+
152
+ export function hideConfirm() {
153
+ var confirmModal = document.getElementById("confirm-modal");
154
+ confirmModal.classList.add("hidden");
155
+ confirmCallback = null;
156
+ }
157
+
158
+ export function showForceChangePinOverlay() {
159
+ var ov = document.createElement("div");
160
+ ov.id = "force-change-pin-overlay";
161
+ ov.style.cssText = "position:fixed;inset:0;background:var(--bg,#0e0e10);z-index:99999;display:flex;align-items:center;justify-content:center;flex-direction:column";
162
+ ov.innerHTML = '<div style="width:100%;max-width:380px;padding:24px;text-align:center">' +
163
+ '<h2 style="margin:0 0 8px;color:var(--text,#fff);font-size:22px">Set your new PIN</h2>' +
164
+ '<p style="margin:0 0 24px;color:var(--text-secondary,#aaa);font-size:14px">Your temporary PIN has expired. Please set a new 6-digit PIN to continue.</p>' +
165
+ '<div style="display:flex;gap:8px;justify-content:center;margin-bottom:16px" id="fcp-boxes">' +
166
+ '<input class="fcp-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off" style="width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid var(--border,#333);border-radius:10px;background:var(--bg-alt,#f5f5f5);color:var(--text,#fff);outline:none">' +
167
+ '<input class="fcp-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off" style="width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid var(--border,#333);border-radius:10px;background:var(--bg-alt,#f5f5f5);color:var(--text,#fff);outline:none">' +
168
+ '<input class="fcp-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off" style="width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid var(--border,#333);border-radius:10px;background:var(--bg-alt,#f5f5f5);color:var(--text,#fff);outline:none">' +
169
+ '<input class="fcp-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off" style="width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid var(--border,#333);border-radius:10px;background:var(--bg-alt,#f5f5f5);color:var(--text,#fff);outline:none">' +
170
+ '<input class="fcp-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off" style="width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid var(--border,#333);border-radius:10px;background:var(--bg-alt,#f5f5f5);color:var(--text,#fff);outline:none">' +
171
+ '<input class="fcp-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off" style="width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid var(--border,#333);border-radius:10px;background:var(--bg-alt,#f5f5f5);color:var(--text,#fff);outline:none">' +
172
+ '</div>' +
173
+ '<button id="fcp-save" disabled style="width:100%;padding:12px;border:none;border-radius:10px;background:var(--accent,#7c3aed);color:#fff;font-size:15px;font-weight:600;cursor:pointer;opacity:0.5">Save PIN</button>' +
174
+ '<div id="fcp-err" style="margin-top:12px;color:#ef4444;font-size:13px"></div>' +
175
+ '</div>';
176
+ document.body.appendChild(ov);
177
+
178
+ var boxes = ov.querySelectorAll(".fcp-digit");
179
+ var saveBtn = ov.querySelector("#fcp-save");
180
+ var errEl = ov.querySelector("#fcp-err");
181
+ var pinValues = ["", "", "", "", "", ""];
182
+
183
+ function setDigit(idx, v) {
184
+ pinValues[idx] = v;
185
+ boxes[idx].value = v ? "\u2022" : "";
186
+ boxes[idx].classList.toggle("filled", v.length > 0);
187
+ }
188
+
189
+ function getPin() {
190
+ return pinValues.join("");
191
+ }
192
+
193
+ function updateBtn() {
194
+ var ready = getPin().length === 6;
195
+ saveBtn.disabled = !ready;
196
+ saveBtn.style.opacity = ready ? "1" : "0.5";
197
+ }
198
+
199
+ for (var i = 0; i < boxes.length; i++) {
200
+ (function (idx) {
201
+ boxes[idx].addEventListener("input", function () {
202
+ var raw = this.value.replace(/[^0-9]/g, "");
203
+ if (!raw) { setDigit(idx, ""); updateBtn(); return; }
204
+ var v = raw.charAt(raw.length - 1);
205
+ setDigit(idx, v);
206
+ if (v && idx < 5) boxes[idx + 1].focus();
207
+ updateBtn();
208
+ });
209
+ boxes[idx].addEventListener("keydown", function (e) {
210
+ if (e.key === "Backspace") {
211
+ if (!pinValues[idx] && idx > 0) {
212
+ setDigit(idx - 1, "");
213
+ boxes[idx - 1].focus();
214
+ } else {
215
+ setDigit(idx, "");
216
+ }
217
+ updateBtn();
218
+ }
219
+ if (e.key === "ArrowLeft" && idx > 0) boxes[idx - 1].focus();
220
+ if (e.key === "ArrowRight" && idx < 5) boxes[idx + 1].focus();
221
+ if (e.key === "Enter" && !saveBtn.disabled) doSave();
222
+ e.stopPropagation();
223
+ });
224
+ boxes[idx].addEventListener("keyup", function (e) { e.stopPropagation(); });
225
+ boxes[idx].addEventListener("keypress", function (e) { e.stopPropagation(); });
226
+ boxes[idx].addEventListener("paste", function (e) {
227
+ e.preventDefault();
228
+ var text = (e.clipboardData || window.clipboardData).getData("text").replace(/[^0-9]/g, "").substring(0, 6);
229
+ for (var j = 0; j < text.length && (idx + j) < 6; j++) {
230
+ setDigit(idx + j, text.charAt(j));
231
+ }
232
+ if (text.length > 0) {
233
+ var focusIdx = Math.min(idx + text.length, 5);
234
+ boxes[focusIdx].focus();
235
+ }
236
+ updateBtn();
237
+ });
238
+ boxes[idx].addEventListener("focus", function () { this.select(); });
239
+ })(i);
240
+ }
241
+ boxes[0].focus();
242
+
243
+ function doSave() {
244
+ var pin = getPin();
245
+ if (pin.length !== 6) return;
246
+ saveBtn.disabled = true;
247
+ saveBtn.style.opacity = "0.5";
248
+ errEl.textContent = "";
249
+ fetch("/api/user/pin", {
250
+ method: "PUT",
251
+ headers: { "Content-Type": "application/json" },
252
+ body: JSON.stringify({ newPin: pin }),
253
+ }).then(function (r) { return r.json(); }).then(function (d) {
254
+ if (d.ok) {
255
+ ov.remove();
256
+ return;
257
+ }
258
+ errEl.textContent = d.error || "Failed to save PIN";
259
+ saveBtn.disabled = false;
260
+ saveBtn.style.opacity = "1";
261
+ }).catch(function () {
262
+ errEl.textContent = "Connection error";
263
+ saveBtn.disabled = false;
264
+ saveBtn.style.opacity = "1";
265
+ });
266
+ }
267
+ saveBtn.addEventListener("click", doSave);
268
+ }
269
+
270
+ export function sendExtensionCommand(command, args, requestId) {
271
+ window.postMessage({
272
+ source: "clay-page",
273
+ payload: {
274
+ type: "clay_ext_command",
275
+ command: command,
276
+ args: args,
277
+ requestId: requestId
278
+ }
279
+ }, "*");
280
+ }
281
+
282
+ export function handleExtensionResult(requestId, result) {
283
+ // Check local callback first (for server-initiated requests)
284
+ var cb = _extRequestCallbacks[requestId];
285
+ if (cb) {
286
+ delete _extRequestCallbacks[requestId];
287
+ cb(result);
288
+ return;
289
+ }
290
+ // Forward to server
291
+ var ws = getWs();
292
+ if (ws && ws.readyState === 1) {
293
+ ws.send(JSON.stringify({
294
+ type: "extension_result",
295
+ requestId: requestId,
296
+ result: result
297
+ }));
298
+ }
299
+ }
@@ -0,0 +1,372 @@
1
+ // app-notifications.js - Notification banners (Apple banner style)
2
+ // New notifications appear as individual banners top-right, auto-dismiss after 3s.
3
+ // Bell button click shows all stored notifications as banners.
4
+
5
+ import { refreshIcons, iconHtml } from './icons.js';
6
+ import { escapeHtml } from './utils.js';
7
+ import { store } from './store.js';
8
+ import { getWs } from './ws-ref.js';
9
+ import { openDm } from './app-dm.js';
10
+ import { getCachedProjects } from './app-projects.js';
11
+ import { switchProject } from './app-projects.js';
12
+ var notifications = [];
13
+ var unreadCount = 0;
14
+ var bannerContainer = null;
15
+ var bellBtn = null;
16
+ var badgeEl = null;
17
+
18
+ // ========================================================
19
+ // Init
20
+ // ========================================================
21
+
22
+ export function initAppNotifications() {
23
+ bellBtn = document.getElementById("notif-center-btn");
24
+ badgeEl = document.getElementById("notif-center-badge");
25
+
26
+ // Create banner container
27
+ bannerContainer = document.createElement("div");
28
+ bannerContainer.className = "notif-banner-container";
29
+ document.body.appendChild(bannerContainer);
30
+
31
+ if (bellBtn) {
32
+ bellBtn.addEventListener("click", function (e) {
33
+ e.stopPropagation();
34
+ showAllBanners();
35
+ });
36
+ }
37
+ }
38
+
39
+ // ========================================================
40
+ // Show all stored notifications as banners
41
+ // ========================================================
42
+
43
+ function showAllBanners() {
44
+ // Clear existing banners first
45
+ if (bannerContainer) bannerContainer.innerHTML = "";
46
+
47
+ if (notifications.length === 0) {
48
+ showBanner({
49
+ id: "_empty",
50
+ type: "info",
51
+ title: randomEmptyMessage(),
52
+ body: "",
53
+ slug: "",
54
+ }, 3000);
55
+ return;
56
+ }
57
+
58
+ for (var i = 0; i < notifications.length; i++) {
59
+ showBanner(notifications[i], false);
60
+ }
61
+ }
62
+
63
+ // ========================================================
64
+ // Banner
65
+ // ========================================================
66
+
67
+ function showBanner(notif, autoDismissMs) {
68
+ if (!bannerContainer) return;
69
+
70
+ var isEmpty = notif.id === "_empty";
71
+ var projectIcon = isEmpty ? null : getProjectIcon(notif.slug);
72
+ var projectName = isEmpty ? "" : getProjectName(notif.slug);
73
+ var isPermission = notif.type === "permission_request" && notif.meta && notif.meta.requestId;
74
+
75
+ var banner = document.createElement("div");
76
+ banner.className = "notif-banner" + (isPermission ? " notif-banner-permission" : "");
77
+ if (!isEmpty) banner.setAttribute("data-notif-id", notif.id);
78
+
79
+ var iconHtmlStr = projectIcon
80
+ ? '<span class="notif-banner-emoji">' + projectIcon + '</span>'
81
+ : iconHtml(isEmpty ? "check-circle" : "folder");
82
+
83
+ // Format permission title as "Can I ..." style
84
+ if (isPermission && notif.meta) {
85
+ var permInfo = formatPermissionInfo(notif.meta.toolName, notif.meta.toolInput);
86
+ if (permInfo) {
87
+ notif = Object.assign({}, notif, { title: "Can I " + permInfo.verb + (permInfo.target ? " " + permInfo.target : "") + "?" });
88
+ }
89
+ }
90
+
91
+ var actionsHtml = "";
92
+ if (isPermission) {
93
+ actionsHtml =
94
+ '<div class="notif-banner-actions">' +
95
+ '<button class="notif-banner-allow">Sure</button>' +
96
+ '<button class="notif-banner-always">Always allow</button>' +
97
+ '<button class="notif-banner-deny">No</button>' +
98
+ '<button class="notif-banner-goto" title="Go to session">' + iconHtml("external-link") + '</button>' +
99
+ '</div>';
100
+ }
101
+
102
+ banner.innerHTML =
103
+ '<div class="notif-banner-icon">' + iconHtmlStr + '</div>' +
104
+ '<div class="notif-banner-body">' +
105
+ (projectName ? '<div class="notif-banner-project">' + escapeHtml(projectName) + '</div>' : '') +
106
+ '<div class="notif-banner-title">' + escapeHtml(notif.title) + '</div>' +
107
+ (notif.body ? '<div class="notif-banner-text">' + escapeHtml(notif.body) + '</div>' : '') +
108
+ actionsHtml +
109
+ '</div>' +
110
+ (!isEmpty ? '<button class="notif-banner-close">' + iconHtml("x") + '</button>' : '');
111
+
112
+ bannerContainer.appendChild(banner);
113
+ refreshIcons();
114
+
115
+ requestAnimationFrame(function () {
116
+ banner.classList.add("show");
117
+ });
118
+
119
+ if (!isEmpty) {
120
+ // Click banner body -> navigate + dismiss
121
+ banner.addEventListener("click", function (e) {
122
+ if (e.target.closest(".notif-banner-close")) return;
123
+ removeBanner(banner);
124
+ dismissNotif(notif.id);
125
+ navigateToNotification(notif);
126
+ });
127
+
128
+ // Close button -> dismiss
129
+ var closeBtn = banner.querySelector(".notif-banner-close");
130
+ if (closeBtn) {
131
+ closeBtn.addEventListener("click", function (e) {
132
+ e.stopPropagation();
133
+ removeBanner(banner);
134
+ dismissNotif(notif.id);
135
+ });
136
+ }
137
+
138
+ // Permission actions
139
+ if (isPermission) {
140
+ var sureBtn = banner.querySelector(".notif-banner-allow");
141
+ var alwaysBtn = banner.querySelector(".notif-banner-always");
142
+ var noBtn = banner.querySelector(".notif-banner-deny");
143
+ var gotoBtn = banner.querySelector(".notif-banner-goto");
144
+
145
+ if (sureBtn) {
146
+ sureBtn.addEventListener("click", function (e) {
147
+ e.stopPropagation();
148
+ sendPermissionResponse(notif.meta.requestId, true, notif.slug);
149
+ removeBanner(banner);
150
+ dismissNotif(notif.id);
151
+ });
152
+ }
153
+ if (alwaysBtn) {
154
+ alwaysBtn.addEventListener("click", function (e) {
155
+ e.stopPropagation();
156
+ sendPermissionResponse(notif.meta.requestId, "always", notif.slug);
157
+ removeBanner(banner);
158
+ dismissNotif(notif.id);
159
+ });
160
+ }
161
+ if (noBtn) {
162
+ noBtn.addEventListener("click", function (e) {
163
+ e.stopPropagation();
164
+ sendPermissionResponse(notif.meta.requestId, false, notif.slug);
165
+ removeBanner(banner);
166
+ dismissNotif(notif.id);
167
+ });
168
+ }
169
+ if (gotoBtn) {
170
+ gotoBtn.addEventListener("click", function (e) {
171
+ e.stopPropagation();
172
+ navigateToNotification(notif);
173
+ removeBanner(banner);
174
+ dismissNotif(notif.id);
175
+ });
176
+ }
177
+ }
178
+ }
179
+
180
+ // Auto-dismiss (number = ms, false = stay)
181
+ if (typeof autoDismissMs === "number") {
182
+ setTimeout(function () { removeBanner(banner); }, autoDismissMs);
183
+ } else if (autoDismissMs === true) {
184
+ setTimeout(function () { removeBanner(banner); }, 3000);
185
+ }
186
+ }
187
+
188
+ function removeBanner(banner) {
189
+ if (!banner || !banner.parentNode) return;
190
+ banner.classList.remove("show");
191
+ banner.classList.add("hide");
192
+ setTimeout(function () {
193
+ if (banner.parentNode) banner.parentNode.removeChild(banner);
194
+ }, 300);
195
+ }
196
+
197
+ function sendPermissionResponse(requestId, decision, slug) {
198
+ var ws = getWs();
199
+ if (ws && ws.readyState === 1) {
200
+ var d = decision === "always" ? "allow_always" : decision ? "allow" : "deny";
201
+ var msg = { type: "permission_response", requestId: requestId, decision: d };
202
+ if (slug) msg.targetSlug = slug;
203
+ ws.send(JSON.stringify(msg));
204
+ }
205
+ }
206
+
207
+ function dismissNotif(id) {
208
+ var ws = getWs();
209
+ if (ws && ws.readyState === 1) {
210
+ ws.send(JSON.stringify({ type: "notification_dismiss", ids: [id] }));
211
+ }
212
+ }
213
+
214
+ // ========================================================
215
+ // Message handlers
216
+ // ========================================================
217
+
218
+ export function handleNotificationsState(msg) {
219
+ notifications = msg.notifications || [];
220
+ unreadCount = msg.unreadCount || 0;
221
+ updateBadge();
222
+
223
+ // Check for pending session navigation after project switch
224
+ try {
225
+ var pendingSession = sessionStorage.getItem("pending-notif-session");
226
+ if (pendingSession) {
227
+ sessionStorage.removeItem("pending-notif-session");
228
+ var ws = getWs();
229
+ if (ws && ws.readyState === 1) {
230
+ ws.send(JSON.stringify({ type: "switch_session", id: parseInt(pendingSession, 10) }));
231
+ }
232
+ }
233
+ } catch (e) {}
234
+ }
235
+
236
+ export function handleNotificationCreated(msg) {
237
+ var notif = msg.notification;
238
+
239
+ // Auto-dismiss if it's for the session the user is currently viewing
240
+ var activeSession = store.getState().activeSessionId || null;
241
+ console.log("[notif] created:", notif.type, "sessionId=" + notif.sessionId + "(" + typeof notif.sessionId + ")", "active=" + activeSession + "(" + typeof activeSession + ")", "match=" + (notif.sessionId == activeSession));
242
+ if (notif.sessionId && String(notif.sessionId) === String(activeSession)) {
243
+ dismissNotif(notif.id);
244
+ return;
245
+ }
246
+
247
+ notifications.unshift(notif);
248
+ unreadCount = msg.unreadCount;
249
+ updateBadge();
250
+
251
+ var _autoDismiss = notif.type === "permission_request" ? false : true;
252
+ showBanner(notif, _autoDismiss);
253
+ }
254
+
255
+ export function handleNotificationDismissed(msg) {
256
+ var ids = msg.ids || [];
257
+ notifications = notifications.filter(function (n) { return ids.indexOf(n.id) === -1; });
258
+ unreadCount = msg.unreadCount;
259
+ updateBadge();
260
+ // Remove corresponding banners if visible
261
+ if (bannerContainer) {
262
+ for (var i = 0; i < ids.length; i++) {
263
+ var el = bannerContainer.querySelector('[data-notif-id="' + ids[i] + '"]');
264
+ if (el) removeBanner(el);
265
+ }
266
+ }
267
+ }
268
+
269
+ export function handleNotificationDismissedAll() {
270
+ notifications = [];
271
+ unreadCount = 0;
272
+ updateBadge();
273
+ if (bannerContainer) bannerContainer.innerHTML = "";
274
+ }
275
+
276
+ // ========================================================
277
+ // Badge
278
+ // ========================================================
279
+
280
+ function updateBadge() {
281
+ if (!badgeEl) return;
282
+ if (unreadCount > 0) {
283
+ badgeEl.textContent = unreadCount > 99 ? "99+" : String(unreadCount);
284
+ badgeEl.classList.remove("hidden");
285
+ } else {
286
+ badgeEl.classList.add("hidden");
287
+ }
288
+ }
289
+
290
+ // ========================================================
291
+ // Navigation
292
+ // ========================================================
293
+
294
+ function navigateToNotification(notif) {
295
+ if (notif.mateId) {
296
+ openDm(notif.mateId);
297
+ return;
298
+ }
299
+
300
+ var currentSlug = store.getState().currentSlug || "";
301
+ var needsProjectSwitch = notif.slug && notif.slug !== currentSlug;
302
+
303
+ if (needsProjectSwitch) {
304
+ // Store target session for after project switch
305
+ if (notif.sessionId) {
306
+ try { sessionStorage.setItem("pending-notif-session", notif.sessionId); } catch (e) {}
307
+ }
308
+ switchProject(notif.slug);
309
+ } else if (notif.sessionId) {
310
+ var ws = getWs();
311
+ if (ws && ws.readyState === 1) {
312
+ ws.send(JSON.stringify({ type: "switch_session", id: notif.sessionId }));
313
+ }
314
+ }
315
+ }
316
+
317
+ // ========================================================
318
+ // Helpers
319
+ // ========================================================
320
+
321
+ function formatPermissionInfo(toolName, toolInput) {
322
+ if (!toolName) return null;
323
+ var input = toolInput && typeof toolInput === "object" ? toolInput : {};
324
+ var verb = "use " + toolName;
325
+ var target = "";
326
+ var shortPath = function (p) { return p ? p.split(/[/\\]/).pop() : ""; };
327
+
328
+ switch (toolName) {
329
+ case "Write": verb = "write to"; target = shortPath(input.file_path); break;
330
+ case "Edit": verb = "edit"; target = shortPath(input.file_path); break;
331
+ case "Read": verb = "read"; target = shortPath(input.file_path); break;
332
+ case "Bash": verb = "run"; target = input.description || (input.command || "").substring(0, 80); break;
333
+ case "Grep": verb = "search"; target = input.pattern || ""; break;
334
+ case "Glob": verb = "search for files in"; target = input.pattern || ""; break;
335
+ case "WebFetch": verb = "fetch"; target = input.url || ""; break;
336
+ case "WebSearch": verb = "search the web for"; target = input.query || ""; break;
337
+ }
338
+ return { verb: verb, target: target };
339
+ }
340
+
341
+ var EMPTY_MESSAGES = [
342
+ "Quiet. Too quiet.",
343
+ "Nothing. Suspiciously nothing.",
344
+ "Inbox zero. Brag about it.",
345
+ "No notifications. Are you even working?",
346
+ "All clear. For now.",
347
+ "The void stares back.",
348
+ "Notification-free since just now.",
349
+ "You have 0 problems. Allegedly.",
350
+ "Tumbleweeds.",
351
+ "Your agents are napping.",
352
+ ];
353
+
354
+ function randomEmptyMessage() {
355
+ return EMPTY_MESSAGES[Math.floor(Math.random() * EMPTY_MESSAGES.length)];
356
+ }
357
+
358
+ function getProjectIcon(slug) {
359
+ var projects = getCachedProjects();
360
+ for (var i = 0; i < projects.length; i++) {
361
+ if (projects[i].slug === slug) return projects[i].icon || null;
362
+ }
363
+ return null;
364
+ }
365
+
366
+ function getProjectName(slug) {
367
+ var projects = getCachedProjects();
368
+ for (var i = 0; i < projects.length; i++) {
369
+ if (projects[i].slug === slug) return projects[i].title || projects[i].name || slug;
370
+ }
371
+ return slug;
372
+ }