@zooid/web 0.10.0 → 0.12.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/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>Zooid</title>
8
- <script type="module" crossorigin src="/assets/index-B2FkL9sD.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-C_9y-fTg.css">
8
+ <script type="module" crossorigin src="/assets/index-BSXXmV89.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-Lm6-iv02.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/dist/sw.js ADDED
@@ -0,0 +1,113 @@
1
+ // Zooid service worker. Deliberately imports no application code — no bundler
2
+ // integration, no build-graph coupling. Push and notificationclick, plus the
3
+ // two lifecycle handlers that make an update to those actually take effect.
4
+
5
+ const ROOM_PATH = /\/room\/([^/?#]+)/;
6
+
7
+ function roomOf(url) {
8
+ const m = ROOM_PATH.exec(url);
9
+ return m ? decodeURIComponent(m[1]) : null;
10
+ }
11
+
12
+ function titleFor(p) {
13
+ return p.room_name || p.room_id;
14
+ }
15
+
16
+ // The three agent events are, by definition, things you asked to be told
17
+ // about *while you are away* — a turn you're waiting on, an approval that
18
+ // blocks the agent, an error. A macOS/Chrome banner auto-dismisses after a
19
+ // few seconds, so an unattended one is a notification that never happened.
20
+ // These stay on screen until dismissed; ordinary chat messages keep the
21
+ // transient default, as chat everywhere else does.
22
+ const PERSISTENT_TYPES = [
23
+ "dev.zooid.approval_request",
24
+ "dev.zooid.turn.end",
25
+ "dev.zooid.error",
26
+ ];
27
+
28
+ function bodyFor(p) {
29
+ // Agent events carry no `body` prose of their own. Rendering
30
+ // `${sender}: ${undefined}` is the failure this branch exists to prevent.
31
+ switch (p.type) {
32
+ case "dev.zooid.approval_request":
33
+ return `${p.sender_display_name || "An agent"} needs approval`;
34
+ case "dev.zooid.turn.end":
35
+ // The prose itself never pushes — agent messages are `m.notice`, which
36
+ // `.m.rule.suppress_notices` silences so a chatty turn doesn't fire one
37
+ // push per chunk. turn.end carries a preview of the final message so the
38
+ // notification says what the agent actually said, not just that it stopped.
39
+ return p.preview
40
+ ? `${p.sender_display_name || "An agent"}: ${p.preview}`
41
+ : `${p.sender_display_name || "An agent"} finished`;
42
+ case "dev.zooid.error":
43
+ return p.body || `${p.sender_display_name || "An agent"} hit an error`;
44
+ default:
45
+ return p.sender_display_name ? `${p.sender_display_name}: ${p.body || ""}` : p.body || "New message";
46
+ }
47
+ }
48
+
49
+ // This worker caches nothing and holds no versioned app state — it answers
50
+ // push and notificationclick, nothing else. So there is no half-updated app
51
+ // to guard against, and the default "wait until every tab of the origin is
52
+ // closed" only means a shipped fix to notification behaviour sits inert for
53
+ // anyone who keeps a tab open indefinitely. Take over immediately instead.
54
+ self.addEventListener("install", () => self.skipWaiting());
55
+ self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
56
+
57
+ self.addEventListener("push", (event) => {
58
+ event.waitUntil(
59
+ (async () => {
60
+ let payload;
61
+ try {
62
+ payload = event.data.json();
63
+ } catch {
64
+ // userVisibleOnly is a contract: a push that renders nothing gets the
65
+ // origin penalised, so always show *something*.
66
+ await self.registration.showNotification("Zooid", { body: "New activity" });
67
+ return;
68
+ }
69
+
70
+ const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
71
+
72
+ // Suppression (§5): a visible client already looking at this room.
73
+ const looking = windows.some(
74
+ (c) => c.visibilityState === "visible" && roomOf(c.url) === payload.room_id,
75
+ );
76
+ if (looking) return;
77
+
78
+ // Sound (§12). A worker cannot play audio, so a live client plays the
79
+ // real cue and the notification stays silent; with no client at all the
80
+ // OS default sound is the ceiling.
81
+ let cued = false;
82
+ if (payload.sound && windows.length > 0) {
83
+ for (const c of windows) c.postMessage({ type: "sound", roomId: payload.room_id });
84
+ cued = true;
85
+ }
86
+
87
+ await self.registration.showNotification(titleFor(payload), {
88
+ body: bodyFor(payload),
89
+ tag: payload.event_id,
90
+ data: { roomId: payload.room_id },
91
+ silent: cued || !payload.sound,
92
+ requireInteraction: PERSISTENT_TYPES.indexOf(payload.type) !== -1,
93
+ });
94
+ })(),
95
+ );
96
+ });
97
+
98
+ self.addEventListener("notificationclick", (event) => {
99
+ event.notification.close();
100
+ const roomId = event.notification.data && event.notification.data.roomId;
101
+ event.waitUntil(
102
+ (async () => {
103
+ const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
104
+ if (windows.length > 0) {
105
+ // No React Router closure survives out here — the page navigates itself.
106
+ await windows[0].focus();
107
+ windows[0].postMessage({ type: "navigate", roomId });
108
+ return;
109
+ }
110
+ await self.clients.openWindow(`/room/${roomId}`);
111
+ })(),
112
+ );
113
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zooid/web",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "files": [
@@ -56,6 +56,7 @@
56
56
  "@vitejs/plugin-react": "^4.3.0",
57
57
  "@vitest/browser": "3.2.4",
58
58
  "@vitest/coverage-v8": "3.2.4",
59
+ "fake-indexeddb": "^6.2.5",
59
60
  "happy-dom": "^20.9.0",
60
61
  "jsdom": "^25.0.0",
61
62
  "msw": "^2.4.0",