clay-server 2.27.0-beta.8 → 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-debate.js +19 -12
- 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 -8521
- 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,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
|
+
}
|