clay-server 2.7.2 → 2.8.2

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 (55) hide show
  1. package/bin/cli.js +31 -17
  2. package/lib/config.js +7 -4
  3. package/lib/project.js +343 -15
  4. package/lib/public/app.js +1039 -134
  5. package/lib/public/apple-touch-icon-dark.png +0 -0
  6. package/lib/public/apple-touch-icon.png +0 -0
  7. package/lib/public/clay-logo.png +0 -0
  8. package/lib/public/css/base.css +18 -1
  9. package/lib/public/css/filebrowser.css +1 -0
  10. package/lib/public/css/home-hub.css +455 -0
  11. package/lib/public/css/icon-strip.css +6 -5
  12. package/lib/public/css/loop.css +141 -23
  13. package/lib/public/css/messages.css +2 -0
  14. package/lib/public/css/mobile-nav.css +38 -12
  15. package/lib/public/css/overlays.css +205 -169
  16. package/lib/public/css/playbook.css +264 -0
  17. package/lib/public/css/profile.css +268 -0
  18. package/lib/public/css/scheduler-modal.css +1429 -0
  19. package/lib/public/css/scheduler.css +1305 -0
  20. package/lib/public/css/sidebar.css +305 -11
  21. package/lib/public/css/sticky-notes.css +23 -19
  22. package/lib/public/css/stt.css +155 -0
  23. package/lib/public/css/title-bar.css +14 -6
  24. package/lib/public/favicon-banded-32.png +0 -0
  25. package/lib/public/favicon-banded.png +0 -0
  26. package/lib/public/icon-192-dark.png +0 -0
  27. package/lib/public/icon-192.png +0 -0
  28. package/lib/public/icon-512-dark.png +0 -0
  29. package/lib/public/icon-512.png +0 -0
  30. package/lib/public/icon-banded-76.png +0 -0
  31. package/lib/public/icon-banded-96.png +0 -0
  32. package/lib/public/index.html +336 -44
  33. package/lib/public/modules/ascii-logo.js +442 -0
  34. package/lib/public/modules/markdown.js +18 -0
  35. package/lib/public/modules/notifications.js +50 -63
  36. package/lib/public/modules/playbook.js +578 -0
  37. package/lib/public/modules/profile.js +357 -0
  38. package/lib/public/modules/project-settings.js +1 -9
  39. package/lib/public/modules/scheduler.js +2826 -0
  40. package/lib/public/modules/server-settings.js +1 -1
  41. package/lib/public/modules/sidebar.js +376 -32
  42. package/lib/public/modules/stt.js +272 -0
  43. package/lib/public/modules/terminal.js +32 -0
  44. package/lib/public/modules/theme.js +3 -10
  45. package/lib/public/style.css +6 -0
  46. package/lib/public/sw.js +82 -3
  47. package/lib/public/wordmark-banded-20.png +0 -0
  48. package/lib/public/wordmark-banded-32.png +0 -0
  49. package/lib/public/wordmark-banded-64.png +0 -0
  50. package/lib/public/wordmark-banded-80.png +0 -0
  51. package/lib/scheduler.js +402 -0
  52. package/lib/sdk-bridge.js +3 -2
  53. package/lib/server.js +124 -3
  54. package/lib/sessions.js +35 -2
  55. package/package.json +1 -1
@@ -0,0 +1,442 @@
1
+ // --- Animated ASCII Logo (Canvas particle system) ---
2
+ // Midjourney-style scatter-in / color-sweep / shatter / reassemble animation
3
+
4
+ var ASCII_LINES = [
5
+ "________/\\\\\\\\\\\\\\\\\\__/\\\\\\_________________/\\\\\\\\\\\\\\\\\\_____/\\\\\\________/\\\\\\",
6
+ " _____/\\\\\\////////__\\/\\\\\\_______________/\\\\\\\\\\\\\\\\\\\\\\\\\\__\\///\\\\\\____/\\\\\\/_",
7
+ " ___/\\\\\\/___________\\/\\\\\\______________/\\\\\\/////////\\\\\\___\\///\\\\\\/\\\\\\/___",
8
+ " __/\\\\\\_____________\\/\\\\\\_____________\\/\\\\\\_______\\/\\\\\\_____\\///\\\\\\/_____",
9
+ " _\\/\\\\\\_____________\\/\\\\\\_____________\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\_______\\/\\\\\\______",
10
+ " _\\//\\\\\\____________\\/\\\\\\_____________\\/\\\\\\/////////\\\\\\_______\\/\\\\\\______",
11
+ " __\\///\\\\\\__________\\/\\\\\\_____________\\/\\\\\\_______\\/\\\\\\_______\\/\\\\\\______",
12
+ " ____\\////\\\\\\\\\\\\\\\\\\_\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\_\\/\\\\\\_______\\/\\\\\\_______\\/\\\\\\______",
13
+ " _______\\/////////__\\///////////////__\\///________\\///________\\///_______",
14
+ ];
15
+
16
+ // Tri-accent gradient stops: Green → Indigo → Terracotta (matches CLI)
17
+ var GRADIENT_STOPS = [
18
+ [9, 229, 163],
19
+ [88, 87, 252],
20
+ [254, 113, 80],
21
+ ];
22
+
23
+ // Animation phase durations (seconds)
24
+ var PHASE_SCATTER_IN = 1.8;
25
+ var PHASE_COLOR_SWEEP = 0.9;
26
+ var PHASE_HOLD = 2.5;
27
+ var PHASE_SHATTER = 1.4;
28
+ var PHASE_REASSEMBLE = 1.8;
29
+
30
+ var canvas = null;
31
+ var ctx = null;
32
+ var particles = [];
33
+ var phase = "idle";
34
+ var phaseTime = 0;
35
+ var animId = null;
36
+ var lastTime = 0;
37
+ var charWidth = 0;
38
+ var lineHeight = 0;
39
+ var fontSize = 0;
40
+ var offsetX = 0;
41
+ var offsetY = 0;
42
+ var centerX = 0;
43
+ var centerY = 0;
44
+ var maxCol = 0;
45
+ var glyphCache = {}; // key: char → offscreen canvas (white glyph)
46
+ var running = false;
47
+
48
+ function getGradientColor(row, totalRows) {
49
+ var t = totalRows > 1 ? row / (totalRows - 1) : 0;
50
+ var r, g, b;
51
+ if (t <= 0.5) {
52
+ var s = t * 2;
53
+ r = Math.round(GRADIENT_STOPS[0][0] + (GRADIENT_STOPS[1][0] - GRADIENT_STOPS[0][0]) * s);
54
+ g = Math.round(GRADIENT_STOPS[0][1] + (GRADIENT_STOPS[1][1] - GRADIENT_STOPS[0][1]) * s);
55
+ b = Math.round(GRADIENT_STOPS[0][2] + (GRADIENT_STOPS[1][2] - GRADIENT_STOPS[0][2]) * s);
56
+ } else {
57
+ var s = (t - 0.5) * 2;
58
+ r = Math.round(GRADIENT_STOPS[1][0] + (GRADIENT_STOPS[2][0] - GRADIENT_STOPS[1][0]) * s);
59
+ g = Math.round(GRADIENT_STOPS[1][1] + (GRADIENT_STOPS[2][1] - GRADIENT_STOPS[1][1]) * s);
60
+ b = Math.round(GRADIENT_STOPS[1][2] + (GRADIENT_STOPS[2][2] - GRADIENT_STOPS[1][2]) * s);
61
+ }
62
+ return [r, g, b];
63
+ }
64
+
65
+ function easeOutBack(t) {
66
+ var s = 1.4;
67
+ var t1 = t - 1;
68
+ return t1 * t1 * ((s + 1) * t1 + s) + 1;
69
+ }
70
+
71
+ function easeOutCubic(t) {
72
+ var t1 = t - 1;
73
+ return t1 * t1 * t1 + 1;
74
+ }
75
+
76
+ function lerp(a, b, t) {
77
+ return a + (b - a) * t;
78
+ }
79
+
80
+ function buildParticles() {
81
+ particles = [];
82
+ maxCol = 0;
83
+ for (var row = 0; row < ASCII_LINES.length; row++) {
84
+ var line = ASCII_LINES[row];
85
+ for (var col = 0; col < line.length; col++) {
86
+ var ch = line[col];
87
+ if (ch === " ") continue;
88
+ if (col > maxCol) maxCol = col;
89
+ var gc = getGradientColor(row, ASCII_LINES.length);
90
+ particles.push({
91
+ char: ch,
92
+ row: row,
93
+ col: col,
94
+ targetX: 0,
95
+ targetY: 0,
96
+ x: 0,
97
+ y: 0,
98
+ vx: 0,
99
+ vy: 0,
100
+ opacity: 0,
101
+ rotation: 0,
102
+ vr: 0,
103
+ finalR: gc[0],
104
+ finalG: gc[1],
105
+ finalB: gc[2],
106
+ r: 220,
107
+ g: 220,
108
+ b: 220,
109
+ scatterX: 0,
110
+ scatterY: 0,
111
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ function computeLayout() {
117
+ if (!canvas) return;
118
+ var container = canvas.parentElement;
119
+ var dpr = window.devicePixelRatio || 1;
120
+ var w = container.clientWidth;
121
+ var h = container.clientHeight;
122
+
123
+ canvas.style.width = w + "px";
124
+ canvas.style.height = h + "px";
125
+ canvas.width = Math.round(w * dpr);
126
+ canvas.height = Math.round(h * dpr);
127
+
128
+ ctx = canvas.getContext("2d");
129
+ ctx.scale(dpr, dpr);
130
+
131
+ // Find longest line
132
+ var maxLen = 0;
133
+ for (var i = 0; i < ASCII_LINES.length; i++) {
134
+ if (ASCII_LINES[i].length > maxLen) maxLen = ASCII_LINES[i].length;
135
+ }
136
+
137
+ // Responsive font size
138
+ fontSize = Math.floor(w / maxLen * 1.3);
139
+ fontSize = Math.max(8, Math.min(fontSize, 28));
140
+
141
+ ctx.font = "bold " + fontSize + "px 'Roboto Mono', Menlo, Monaco, Consolas, monospace";
142
+ charWidth = ctx.measureText("M").width;
143
+ lineHeight = fontSize * 1.2;
144
+
145
+ var totalW = maxLen * charWidth;
146
+ var totalH = ASCII_LINES.length * lineHeight;
147
+ offsetX = (w - totalW) / 2;
148
+ offsetY = (h - totalH) / 2;
149
+ centerX = w / 2;
150
+ centerY = h / 2;
151
+
152
+ // Recompute target positions
153
+ for (var i = 0; i < particles.length; i++) {
154
+ var p = particles[i];
155
+ p.targetX = offsetX + p.col * charWidth;
156
+ p.targetY = offsetY + p.row * lineHeight + lineHeight * 0.8;
157
+ }
158
+
159
+ // Build glyph cache — pre-render each unique char as white on offscreen canvas
160
+ glyphCache = {};
161
+ var dpr2 = window.devicePixelRatio || 1;
162
+ var pad = Math.ceil(fontSize * 0.3);
163
+ var gw = Math.ceil(charWidth * dpr2) + pad * 2;
164
+ var gh = Math.ceil(fontSize * 1.6 * dpr2) + pad * 2;
165
+ var fontStr = "bold " + fontSize + "px 'Roboto Mono', Menlo, Monaco, Consolas, monospace";
166
+ var chars = {};
167
+ for (var i = 0; i < particles.length; i++) chars[particles[i].char] = 1;
168
+ var keys = Object.keys(chars);
169
+ for (var i = 0; i < keys.length; i++) {
170
+ var ch = keys[i];
171
+ var oc = document.createElement("canvas");
172
+ oc.width = gw;
173
+ oc.height = gh;
174
+ var ox = oc.getContext("2d");
175
+ ox.scale(dpr2, dpr2);
176
+ ox.font = fontStr;
177
+ ox.textBaseline = "alphabetic";
178
+ ox.fillStyle = "#fff";
179
+ ox.fillText(ch, pad / dpr2, (gh - pad) / dpr2);
180
+ glyphCache[ch] = oc;
181
+ }
182
+ }
183
+
184
+ function randomScatter(p) {
185
+ var canvasW = canvas.width / (window.devicePixelRatio || 1);
186
+ var canvasH = canvas.height / (window.devicePixelRatio || 1);
187
+ p.scatterX = (Math.random() - 0.5) * canvasW * 2;
188
+ p.scatterY = (Math.random() - 0.5) * canvasH * 2;
189
+ p.x = p.scatterX;
190
+ p.y = p.scatterY;
191
+ p.opacity = 0;
192
+ p.rotation = (Math.random() - 0.5) * Math.PI * 4;
193
+ p.vx = 0;
194
+ p.vy = 0;
195
+ p.vr = 0;
196
+ p.r = 220;
197
+ p.g = 220;
198
+ p.b = 220;
199
+ }
200
+
201
+ function setPhase(newPhase) {
202
+ phase = newPhase;
203
+ phaseTime = 0;
204
+
205
+ if (phase === "scatter-in") {
206
+ for (var i = 0; i < particles.length; i++) {
207
+ randomScatter(particles[i]);
208
+ }
209
+ } else if (phase === "shatter") {
210
+ for (var i = 0; i < particles.length; i++) {
211
+ var p = particles[i];
212
+ var angle = Math.atan2(p.targetY - centerY, p.targetX - centerX);
213
+ angle += (Math.random() - 0.5) * 0.6;
214
+ var speed = 250 + Math.random() * 450;
215
+ p.vx = Math.cos(angle) * speed;
216
+ p.vy = Math.sin(angle) * speed;
217
+ p.vr = (Math.random() - 0.5) * 8;
218
+ }
219
+ } else if (phase === "reassemble") {
220
+ // particles keep their current positions from shatter
221
+ for (var i = 0; i < particles.length; i++) {
222
+ var p = particles[i];
223
+ p.scatterX = p.x;
224
+ p.scatterY = p.y;
225
+ p.vx = 0;
226
+ p.vy = 0;
227
+ }
228
+ }
229
+ }
230
+
231
+ function updateParticles(dt) {
232
+ phaseTime += dt;
233
+
234
+ if (phase === "scatter-in") {
235
+ for (var i = 0; i < particles.length; i++) {
236
+ var p = particles[i];
237
+ // Stagger by column — delay proportional to column position
238
+ var delay = (p.col / (maxCol || 1)) * 0.6;
239
+ var localT = Math.max(0, phaseTime - delay) / (PHASE_SCATTER_IN - 0.6);
240
+ localT = Math.min(localT, 1);
241
+ var ease = easeOutBack(localT);
242
+
243
+ p.x = lerp(p.scatterX, p.targetX, ease);
244
+ p.y = lerp(p.scatterY, p.targetY, ease);
245
+ p.opacity = Math.min(1, localT * 3);
246
+ p.rotation = lerp(p.rotation, 0, localT);
247
+ }
248
+ if (phaseTime >= PHASE_SCATTER_IN) {
249
+ // Snap to targets
250
+ for (var i = 0; i < particles.length; i++) {
251
+ var p = particles[i];
252
+ p.x = p.targetX;
253
+ p.y = p.targetY;
254
+ p.opacity = 1;
255
+ p.rotation = 0;
256
+ }
257
+ setPhase("color-sweep");
258
+ }
259
+ } else if (phase === "color-sweep") {
260
+ var sweepProgress = phaseTime / PHASE_COLOR_SWEEP;
261
+ for (var i = 0; i < particles.length; i++) {
262
+ var p = particles[i];
263
+ var colNorm = p.col / (maxCol || 1);
264
+ var localT = (sweepProgress - colNorm * 0.6) / 0.4;
265
+ localT = Math.max(0, Math.min(1, localT));
266
+ var ease = easeOutCubic(localT);
267
+ p.r = Math.round(lerp(220, p.finalR, ease));
268
+ p.g = Math.round(lerp(220, p.finalG, ease));
269
+ p.b = Math.round(lerp(220, p.finalB, ease));
270
+ }
271
+ if (phaseTime >= PHASE_COLOR_SWEEP) {
272
+ for (var i = 0; i < particles.length; i++) {
273
+ var p = particles[i];
274
+ p.r = p.finalR;
275
+ p.g = p.finalG;
276
+ p.b = p.finalB;
277
+ }
278
+ setPhase("hold");
279
+ }
280
+ } else if (phase === "hold") {
281
+ var breath = Math.sin(phaseTime * Math.PI * 0.8) * 0.04;
282
+ for (var i = 0; i < particles.length; i++) {
283
+ var p = particles[i];
284
+ p.opacity = 0.96 + breath;
285
+ // Subtle micro-jitter
286
+ p.x = p.targetX + Math.sin(phaseTime * 2.3 + i * 0.7) * 0.4;
287
+ p.y = p.targetY + Math.cos(phaseTime * 1.9 + i * 0.5) * 0.3;
288
+ }
289
+ if (phaseTime >= PHASE_HOLD) {
290
+ // Reset to exact positions before shatter
291
+ for (var i = 0; i < particles.length; i++) {
292
+ var p = particles[i];
293
+ p.x = p.targetX;
294
+ p.y = p.targetY;
295
+ p.opacity = 1;
296
+ }
297
+ setPhase("shatter");
298
+ }
299
+ } else if (phase === "shatter") {
300
+ var drag = 0.97;
301
+ for (var i = 0; i < particles.length; i++) {
302
+ var p = particles[i];
303
+ p.vx *= drag;
304
+ p.vy *= drag;
305
+ p.x += p.vx * dt;
306
+ p.y += p.vy * dt;
307
+ p.rotation += p.vr * dt;
308
+ p.vr *= drag;
309
+
310
+ // Fade out based on distance from center
311
+ var dx = p.x - centerX;
312
+ var dy = p.y - centerY;
313
+ var dist = Math.sqrt(dx * dx + dy * dy);
314
+ var maxDist = Math.max(centerX, centerY) * 0.8;
315
+ p.opacity = Math.max(0, 1 - dist / maxDist);
316
+ }
317
+ if (phaseTime >= PHASE_SHATTER) {
318
+ setPhase("reassemble");
319
+ }
320
+ } else if (phase === "reassemble") {
321
+ for (var i = 0; i < particles.length; i++) {
322
+ var p = particles[i];
323
+ var delay = ((maxCol - p.col) / (maxCol || 1)) * 0.5;
324
+ var localT = Math.max(0, phaseTime - delay) / (PHASE_REASSEMBLE - 0.5);
325
+ localT = Math.min(localT, 1);
326
+ var ease = easeOutBack(localT);
327
+
328
+ p.x = lerp(p.scatterX, p.targetX, ease);
329
+ p.y = lerp(p.scatterY, p.targetY, ease);
330
+ p.opacity = Math.min(1, localT * 2.5);
331
+ p.rotation = lerp(p.rotation, 0, localT * localT);
332
+ }
333
+ if (phaseTime >= PHASE_REASSEMBLE) {
334
+ for (var i = 0; i < particles.length; i++) {
335
+ var p = particles[i];
336
+ p.x = p.targetX;
337
+ p.y = p.targetY;
338
+ p.opacity = 1;
339
+ p.rotation = 0;
340
+ }
341
+ setPhase("hold");
342
+ }
343
+ }
344
+ }
345
+
346
+ function drawFrame() {
347
+ if (!ctx) return;
348
+ var dpr = window.devicePixelRatio || 1;
349
+ var w = canvas.width / dpr;
350
+ var h = canvas.height / dpr;
351
+ ctx.clearRect(0, 0, w, h);
352
+
353
+ var pad = Math.ceil(fontSize * 0.3);
354
+ var drawW = (glyphCache._w || (Math.ceil(charWidth * dpr) + pad * 2)) / dpr;
355
+ var drawH = (glyphCache._h || (Math.ceil(fontSize * 1.6 * dpr) + pad * 2)) / dpr;
356
+ var padCSS = pad / dpr;
357
+
358
+ // Use globalCompositeOperation to tint white glyphs
359
+ // 1) Draw glyphs as white (source-over)
360
+ // 2) Multiply with color overlay
361
+ // Instead, use simpler approach: drawImage with globalAlpha, then tint via composite
362
+
363
+ for (var i = 0; i < particles.length; i++) {
364
+ var p = particles[i];
365
+ if (p.opacity <= 0.01) continue;
366
+
367
+ var gc = glyphCache[p.char];
368
+ if (!gc) continue;
369
+
370
+ var dx = p.x - padCSS;
371
+ var dy = p.y - drawH + padCSS;
372
+
373
+ ctx.globalAlpha = p.opacity;
374
+
375
+ if (Math.abs(p.rotation) > 0.01) {
376
+ ctx.save();
377
+ ctx.translate(p.x + charWidth / 2, p.y - fontSize / 3);
378
+ ctx.rotate(p.rotation);
379
+ ctx.drawImage(gc, -drawW / 2, -drawH / 2, drawW, drawH);
380
+ ctx.restore();
381
+ } else {
382
+ ctx.drawImage(gc, dx, dy, drawW, drawH);
383
+ }
384
+ }
385
+
386
+ // Tint pass — multiply white glyphs with particle colors
387
+ ctx.globalCompositeOperation = "source-atop";
388
+ for (var i = 0; i < particles.length; i++) {
389
+ var p = particles[i];
390
+ if (p.opacity <= 0.01) continue;
391
+ ctx.globalAlpha = 1;
392
+ ctx.fillStyle = "rgb(" + p.r + "," + p.g + "," + p.b + ")";
393
+ var dx = p.x - padCSS;
394
+ var dy = p.y - drawH + padCSS;
395
+ ctx.fillRect(dx, dy, drawW, drawH);
396
+ }
397
+ ctx.globalCompositeOperation = "source-over";
398
+ }
399
+
400
+ function tick(now) {
401
+ if (!running) return;
402
+ var dt = Math.min((now - lastTime) / 1000, 0.05);
403
+ lastTime = now;
404
+ updateParticles(dt);
405
+ drawFrame();
406
+ animId = requestAnimationFrame(tick);
407
+ }
408
+
409
+ var resizeObserver = null;
410
+
411
+ export function initAsciiLogo(canvasEl) {
412
+ canvas = canvasEl;
413
+ buildParticles();
414
+ computeLayout();
415
+
416
+ if (typeof ResizeObserver !== "undefined") {
417
+ resizeObserver = new ResizeObserver(function () {
418
+ computeLayout();
419
+ });
420
+ resizeObserver.observe(canvas.parentElement);
421
+ }
422
+ }
423
+
424
+ export function startLogoAnimation() {
425
+ if (running) {
426
+ // Reset to beginning
427
+ cancelAnimationFrame(animId);
428
+ }
429
+ running = true;
430
+ computeLayout();
431
+ setPhase("scatter-in");
432
+ lastTime = performance.now();
433
+ animId = requestAnimationFrame(tick);
434
+ }
435
+
436
+ export function stopLogoAnimation() {
437
+ running = false;
438
+ if (animId) {
439
+ cancelAnimationFrame(animId);
440
+ animId = null;
441
+ }
442
+ }
@@ -26,8 +26,26 @@ export function renderMarkdown(text) {
26
26
  return DOMPurify.sanitize(marked.parse(text));
27
27
  }
28
28
 
29
+ /**
30
+ * parseEmojis — no-op stub.
31
+ * Emoji rendering is now handled by the Twemoji COLR font via CSS @font-face.
32
+ * No DOM manipulation needed — the font renders emoji glyphs directly.
33
+ * This stub is kept so existing callers don't break.
34
+ */
35
+ export function parseEmojis() {}
36
+
37
+ var langAliases = { jsonl: "json", dotenv: "bash" };
38
+
29
39
  export function highlightCodeBlocks(el) {
30
40
  el.querySelectorAll("pre code:not(.hljs):not(.language-mermaid)").forEach(function (block) {
41
+ var cls = Array.from(block.classList).find(function (c) { return c.startsWith("language-"); });
42
+ if (cls) {
43
+ var lang = cls.replace("language-", "");
44
+ if (langAliases[lang]) {
45
+ block.classList.remove(cls);
46
+ block.classList.add("language-" + langAliases[lang]);
47
+ }
48
+ }
31
49
  hljs.highlightElement(block);
32
50
  });
33
51
  el.querySelectorAll("pre:not(.has-copy-btn):not([data-mermaid-processed])").forEach(function (pre) {
@@ -3,7 +3,7 @@ import { iconHtml, refreshIcons } from './icons.js';
3
3
 
4
4
  var ctx;
5
5
  var basePath = "/";
6
- var onboardingBanner, onboardingText, onboardingClose, onboardingDismissed;
6
+ var onboardingPill, onboardingText, onboardingClose, onboardingDismissed;
7
7
  var notifAlertEnabled, notifSoundEnabled, notifPermission;
8
8
  var audioCtx = null;
9
9
 
@@ -13,12 +13,12 @@ export function getNotifPermission() { return notifPermission; }
13
13
 
14
14
  export function showOnboarding(html) {
15
15
  onboardingText.innerHTML = html;
16
- onboardingBanner.classList.remove("hidden");
16
+ onboardingPill.classList.remove("hidden");
17
17
  refreshIcons();
18
18
  }
19
19
 
20
20
  export function hideOnboarding() {
21
- onboardingBanner.classList.add("hidden");
21
+ onboardingPill.classList.add("hidden");
22
22
  }
23
23
 
24
24
  export function playDoneSound() {
@@ -80,69 +80,57 @@ export function initNotifications(_ctx) {
80
80
  window.visualViewport.addEventListener("scroll", onViewportChange);
81
81
  }
82
82
 
83
- // --- Update banner ---
83
+ // --- Update pill badge ---
84
84
  (function () {
85
- var banner = $("update-banner");
86
- var closeBtn = $("update-banner-close");
87
- var howBtn = $("update-how");
85
+ var pillWrap = $("update-pill-wrap");
86
+ var pillBtn = $("update-pill");
87
+ var popover = $("update-popover");
88
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();
89
+ if (!pillWrap) return;
102
90
 
103
91
  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");
92
+ if (copyBtn) {
93
+ copyBtn.addEventListener("click", function (e) {
94
+ e.stopPropagation();
95
+ copyToClipboard("npx clay-server@latest").then(function () {
96
+ copyBtn.classList.add("copied");
97
+ copyBtn.innerHTML = iconHtml("check");
112
98
  refreshIcons();
113
- }, 1500);
99
+ setTimeout(function () {
100
+ copyBtn.classList.remove("copied");
101
+ copyBtn.innerHTML = iconHtml("copy");
102
+ refreshIcons();
103
+ }, 1500);
104
+ });
114
105
  });
115
- });
106
+ }
116
107
 
117
108
  // "Update now" button — trigger server-side update + restart
118
109
  if (updateNowBtn) {
119
- updateNowBtn.addEventListener("click", function () {
110
+ updateNowBtn.addEventListener("click", function (e) {
111
+ e.stopPropagation();
120
112
  if (ctx.ws && ctx.connected) {
121
113
  ctx.ws.send(JSON.stringify({ type: "update_now" }));
122
- updateNowBtn.textContent = "Updating...";
114
+ var textNode = updateNowBtn.lastChild;
115
+ if (textNode) textNode.textContent = " Updating...";
123
116
  updateNowBtn.disabled = true;
124
117
  }
125
118
  });
126
119
  }
127
120
 
128
- // "?" button toggle manual instructions popover
129
- howBtn.addEventListener("click", function (e) {
130
- e.stopPropagation();
131
- popover.classList.toggle("visible");
132
- });
121
+ // Toggle popover on pill click
122
+ if (pillBtn) {
123
+ pillBtn.addEventListener("click", function (e) {
124
+ e.stopPropagation();
125
+ popover.classList.toggle("visible");
126
+ });
127
+ }
133
128
 
134
129
  document.addEventListener("click", function (e) {
135
- if (!popover.contains(e.target) && e.target !== howBtn) {
130
+ if (!popover.contains(e.target) && e.target !== pillBtn && !pillBtn.contains(e.target)) {
136
131
  popover.classList.remove("visible");
137
132
  }
138
133
  });
139
-
140
- if (closeBtn) {
141
- closeBtn.addEventListener("click", function () {
142
- banner.classList.add("hidden");
143
- popover.classList.remove("visible");
144
- });
145
- }
146
134
  })();
147
135
 
148
136
  // --- Settings: Check for updates ---
@@ -199,10 +187,10 @@ export function initNotifications(_ctx) {
199
187
  }
200
188
  })();
201
189
 
202
- // --- Onboarding banner (HTTPS / Push) ---
203
- onboardingBanner = $("onboarding-banner");
204
- onboardingText = $("onboarding-banner-text");
205
- onboardingClose = $("onboarding-banner-close");
190
+ // --- Onboarding pill badge (HTTPS / Push) ---
191
+ onboardingPill = $("onboarding-pill");
192
+ onboardingText = $("onboarding-pill-text");
193
+ onboardingClose = $("onboarding-pill-close");
206
194
  onboardingDismissed = localStorage.getItem("onboarding-dismissed");
207
195
 
208
196
  if (onboardingClose) {
@@ -218,7 +206,7 @@ export function initNotifications(_ctx) {
218
206
  if (!onboardingDismissed) {
219
207
  showOnboarding(
220
208
  iconHtml("bell-ring") +
221
- ' Get alerts on your phone when Claude is done. <a href="/setup">Set up HTTPS</a>'
209
+ ' <a href="/setup">Set up HTTPS</a>'
222
210
  );
223
211
  }
224
212
  }
@@ -563,8 +551,7 @@ export function initNotifications(_ctx) {
563
551
  if (!onboardingDismissed) {
564
552
  showOnboarding(
565
553
  iconHtml("bell-ring") +
566
- ' Get notified when Claude responds. ' +
567
- '<button class="onboarding-cta" id="onboarding-enable-push">Enable push notifications</button>'
554
+ ' <button class="onboarding-cta" id="onboarding-enable-push">Enable push</button>'
568
555
  );
569
556
  var enableBtn = $("onboarding-enable-push");
570
557
  if (enableBtn) {
@@ -596,13 +583,13 @@ export function initNotifications(_ctx) {
596
583
  var open = debugMenu.classList.toggle("hidden");
597
584
  debugBtn.classList.toggle("active", !open);
598
585
 
599
- // Sync toggle states with current banner visibility
600
- var updateBanner = $("update-banner");
601
- if (debugToggleUpdate && updateBanner) {
602
- debugToggleUpdate.checked = !updateBanner.classList.contains("hidden");
586
+ // Sync toggle states with current pill visibility
587
+ var updatePillWrap = $("update-pill-wrap");
588
+ if (debugToggleUpdate && updatePillWrap) {
589
+ debugToggleUpdate.checked = !updatePillWrap.classList.contains("hidden");
603
590
  }
604
- if (debugToggleOnboarding && onboardingBanner) {
605
- debugToggleOnboarding.checked = !onboardingBanner.classList.contains("hidden");
591
+ if (debugToggleOnboarding && onboardingPill) {
592
+ debugToggleOnboarding.checked = !onboardingPill.classList.contains("hidden");
606
593
  }
607
594
  });
608
595
 
@@ -615,15 +602,15 @@ export function initNotifications(_ctx) {
615
602
 
616
603
  if (debugToggleUpdate) {
617
604
  debugToggleUpdate.addEventListener("change", function () {
618
- var banner = $("update-banner");
619
- if (!banner) return;
605
+ var pillWrap = $("update-pill-wrap");
606
+ if (!pillWrap) return;
620
607
  if (debugToggleUpdate.checked) {
621
608
  // Trigger real update check from server (debug mode uses v0.0.9)
622
609
  if (ctx.ws && ctx.connected) {
623
610
  ctx.ws.send(JSON.stringify({ type: "check_update" }));
624
611
  }
625
612
  } else {
626
- banner.classList.add("hidden");
613
+ pillWrap.classList.add("hidden");
627
614
  }
628
615
  refreshIcons();
629
616
  });
@@ -635,10 +622,10 @@ export function initNotifications(_ctx) {
635
622
  if (!onboardingText.innerHTML.trim()) {
636
623
  showOnboarding(
637
624
  iconHtml("bell-ring") +
638
- ' Get alerts on your phone when Claude is done. <a href="/setup">Set up HTTPS</a>'
625
+ ' <a href="/setup">Set up HTTPS</a>'
639
626
  );
640
627
  } else {
641
- onboardingBanner.classList.remove("hidden");
628
+ onboardingPill.classList.remove("hidden");
642
629
  }
643
630
  } else {
644
631
  hideOnboarding();