bloby-bot 0.53.5 → 0.53.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.53.5",
3
+ "version": "0.53.7",
4
4
  "releaseNotes": [
5
5
  "1. New Morphy animation system: config-driven sprites loaded from /morphy/*.json",
6
6
  "2. Swapped teleporting (splash) and headphones (bubble + chat) to the new format",
@@ -112,12 +112,15 @@ const MIME_TYPES: Record<string, string> = {
112
112
  const SW_JS = `// Service worker — app-shell caching + push notifications
113
113
  // Caching strategy:
114
114
  // Hashed assets (/assets/*-AbCd12.js) → cache-first (immutable)
115
- // Navigation (HTML) → stale-while-revalidate (precached on install)
116
- // Static assets (img/video/fonts) stale-while-revalidate
117
- // JS/CSS modules → stale-while-revalidate
115
+ // Navigation (HTML) → network-first (cache = offline fallback only)
116
+ // Static assets / JS / CSS modules network-first (cache = offline fallback only)
118
117
  // API, WebSocket, Vite internals → network-only (no cache)
118
+ // Network-first (NOT stale-while-revalidate) on navigations + modules: Bloby is a LIVE-EDIT
119
+ // tool, so the browser must always see the current app and its build errors — never a stale
120
+ // cached shell that masks a broken (or just-fixed) frontend and produces the confusing
121
+ // "normal refresh is broken but hard refresh works" split. Cache is a pure offline fallback.
119
122
 
120
- var CACHE = 'bloby-v16';
123
+ var CACHE = 'bloby-v17';
121
124
  var HASHED_RE = new RegExp('/assets/.+-[a-zA-Z0-9]{6,}[.](js|css)$');
122
125
 
123
126
  // Precache the HTML shell on install so the cache is never empty.
@@ -182,18 +185,11 @@ self.addEventListener('fetch', function(event) {
182
185
  if (request.mode === 'navigate') {
183
186
  console.log('[SW] navigate →', url.pathname);
184
187
  if (url.pathname === '/' || url.pathname === '/index.html') {
188
+ // Network-first: always fetch the live dashboard; only fall back to cache when offline.
185
189
  event.respondWith(caches.open(CACHE).then(function(c) {
186
- return c.match('/').then(function(hit) {
187
- console.log('[SW] cache hit for /:', !!hit);
188
- var net = fetch(request)
189
- .then(function(r) {
190
- console.log('[SW] network response for /:', r.status);
191
- if (r.ok) c.put('/', r.clone());
192
- return r;
193
- })
194
- .catch(function(err) { console.warn('[SW] network failed, using cache:', err.message); return hit; });
195
- return hit || net;
196
- });
190
+ return fetch(request)
191
+ .then(function(r) { if (r.ok) c.put('/', r.clone()); return r; })
192
+ .catch(function(err) { console.warn('[SW] / network failed, using cache:', err.message); return c.match('/'); });
197
193
  }));
198
194
  return;
199
195
  }
@@ -202,14 +198,11 @@ self.addEventListener('fetch', function(event) {
202
198
  return;
203
199
  }
204
200
 
205
- // Everything else → stale-while-revalidate
201
+ // Everything else (JS/CSS modules, static assets) network-first, cache as offline fallback.
206
202
  event.respondWith(caches.open(CACHE).then(function(c) {
207
- return c.match(request).then(function(hit) {
208
- var net = fetch(request)
209
- .then(function(r) { if (r.ok) c.put(request, r.clone()); return r; })
210
- .catch(function() { return hit; });
211
- return hit || net;
212
- });
203
+ return fetch(request)
204
+ .then(function(r) { if (r.ok) c.put(request, r.clone()); return r; })
205
+ .catch(function() { return c.match(request); });
213
206
  }));
214
207
  });
215
208
 
@@ -530,6 +523,15 @@ export async function startSupervisor() {
530
523
  return;
531
524
  }
532
525
 
526
+ // Workspace guard — injected by the supervisor into the dashboard HTML (see the Vite proxy
527
+ // below). Auto-reloads into the "backend down" interstitial when the backend gives up, and
528
+ // replaces Vite's raw error overlay with a friendly one. Served here so it's always current.
529
+ if (req.url === '/bloby/workspace-guard.js') {
530
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
531
+ res.end(fs.readFileSync(path.join(PKG_DIR, 'supervisor', 'workspace-guard.js')));
532
+ return;
533
+ }
534
+
533
535
  // Service worker — served from embedded constant (supervisor/ is always updated)
534
536
  if (req.url === '/sw.js' || req.url === '/bloby/sw.js') {
535
537
  res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
@@ -1798,11 +1800,37 @@ mint();
1798
1800
 
1799
1801
  // Everything else → proxy to dashboard Vite dev server
1800
1802
  console.log(`[supervisor] → dashboard Vite :${vitePorts.dashboard} | ${req.method} ${(req.url || '').split('?')[0]}`);
1803
+ const GUARD_TAG = '<script defer src="/bloby/workspace-guard.js"></script>';
1801
1804
  const proxy = http.request(
1802
1805
  { host: '127.0.0.1', port: vitePorts.dashboard, path: req.url, method: req.method, headers: req.headers },
1803
1806
  (proxyRes) => {
1804
- res.writeHead(proxyRes.statusCode!, proxyRes.headers);
1805
- proxyRes.pipe(res);
1807
+ const ct = String(proxyRes.headers['content-type'] || '');
1808
+ const enc = String(proxyRes.headers['content-encoding'] || '');
1809
+ // Inject the workspace guard into dashboard HTML *documents* only (and only when
1810
+ // uncompressed — Vite dev serves plain HTML). Assets, HMR, JSON, etc. stream through
1811
+ // untouched. The guard auto-reloads into the "backend down" interstitial and replaces
1812
+ // Vite's raw error overlay — supervisor-side so it reaches every workspace, no edits needed.
1813
+ if (!ct.includes('text/html') || enc) {
1814
+ res.writeHead(proxyRes.statusCode!, proxyRes.headers);
1815
+ proxyRes.pipe(res);
1816
+ return;
1817
+ }
1818
+ const chunks: Buffer[] = [];
1819
+ proxyRes.on('data', (c: Buffer) => chunks.push(c));
1820
+ proxyRes.on('end', () => {
1821
+ let html = Buffer.concat(chunks).toString('utf-8');
1822
+ if (!html.includes('workspace-guard.js')) {
1823
+ html = html.includes('</head>') ? html.replace('</head>', GUARD_TAG + '</head>') : GUARD_TAG + html;
1824
+ }
1825
+ const headers = { ...proxyRes.headers };
1826
+ // Body length changed — drop both framing headers and let Node set content-length
1827
+ // from the single res.end() write.
1828
+ delete headers['content-length'];
1829
+ delete headers['transfer-encoding'];
1830
+ res.writeHead(proxyRes.statusCode!, headers);
1831
+ res.end(html);
1832
+ });
1833
+ proxyRes.on('error', () => { try { res.destroy(); } catch {} });
1806
1834
  },
1807
1835
  );
1808
1836
  proxy.on('error', (e) => {
@@ -0,0 +1,123 @@
1
+ (function () {
2
+ // Injected by the supervisor into the workspace dashboard HTML (and ONLY there — never the
3
+ // chat PWA or the supervisor's own interstitials). Two jobs, both aimed at non-technical users:
4
+ //
5
+ // 1. Backend-down auto-detect — when the workspace backend crash-loops and gives up, the
6
+ // supervisor serves a full "backend down" interstitial on the next navigation. Poll for
7
+ // that state and reload automatically so the user doesn't have to press F5 to discover
8
+ // their app's server broke. We reload ONLY on the terminal "dead" (gave-up) state, never
9
+ // during a normal 1-2s restart (which reports alive:false, dead:false).
10
+ //
11
+ // 2. Friendly frontend-error overlay — Vite's dev server throws a raw red error box on a
12
+ // compile/import error, covering the whole screen (even the chat). We hide Vite's overlay
13
+ // and show a friendly one instead: the error text behind a "copy" button + a pointer to
14
+ // the chat. It auto-clears when the agent fixes the error (Vite removes its overlay).
15
+ if (window.__blobyWorkspaceGuard) return;
16
+ window.__blobyWorkspaceGuard = true;
17
+
18
+ /* ── 1. Backend-down auto-detect ──────────────────────────────────────── */
19
+ var reloading = false;
20
+ function pollBackend() {
21
+ fetch('/__bloby/backend-status', { cache: 'no-store' })
22
+ .then(function (r) { return r.json(); })
23
+ .then(function (s) { if (s && s.dead && !reloading) { reloading = true; location.reload(); } })
24
+ .catch(function () {});
25
+ }
26
+ setInterval(pollBackend, 4000);
27
+ pollBackend();
28
+
29
+ /* ── 2. Friendly frontend-error overlay ───────────────────────────────── */
30
+ // Hide Vite's built-in overlay (kept enabled so we can read the error text from its shadow DOM).
31
+ var hideStyle = document.createElement('style');
32
+ hideStyle.textContent = 'vite-error-overlay{display:none!important}';
33
+ (document.head || document.documentElement).appendChild(hideStyle);
34
+
35
+ var overlay = null;
36
+ var dismissed = false;
37
+ var lastErr = '';
38
+
39
+ function readViteError() {
40
+ var ov = document.querySelector('vite-error-overlay');
41
+ if (!ov || !ov.shadowRoot) return '';
42
+ var sr = ov.shadowRoot, parts = [];
43
+ ['.message-body', '.file', '.frame'].forEach(function (sel) {
44
+ var el = sr.querySelector(sel);
45
+ if (el && el.textContent && el.textContent.trim()) parts.push(el.textContent.trim());
46
+ });
47
+ return parts.join('\n\n');
48
+ }
49
+
50
+ function buildOverlay() {
51
+ var d = document.createElement('div');
52
+ d.id = '__bloby_fe_error';
53
+ // z-index 99990: below the chat widget (99998/99999) so the user can still open chat, above the app.
54
+ d.setAttribute('style', 'position:fixed;inset:0;z-index:99990;background:#0a0a0b;color:#e4e4e7;display:flex;align-items:center;justify-content:center;padding:1.5rem;font-family:system-ui,-apple-system,sans-serif');
55
+ d.innerHTML =
56
+ '<div style="text-align:center;max-width:480px;width:100%">' +
57
+ '<div style="position:relative;width:160px;height:160px;margin:0 auto 1.2rem">' +
58
+ '<div style="position:absolute;inset:-18px;background:radial-gradient(circle,rgba(1,102,255,.18) 0%,transparent 60%);filter:blur(18px)"></div>' +
59
+ '<video autoplay loop muted playsinline style="position:relative;width:100%;height:100%;object-fit:contain;border-radius:50%">' +
60
+ '<source src="/what-happened.webm" type="video/webm"><source src="/what-happened.mp4" type="video/mp4">' +
61
+ '</video>' +
62
+ '</div>' +
63
+ '<h1 style="font-size:1.5rem;font-weight:700;margin:0 0 .6rem;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">A screen in your app has an error</h1>' +
64
+ '<p style="color:#e4e4e7;font-size:1rem;line-height:1.6;margin:0 0 .4rem">Your latest change didn\'t compile.</p>' +
65
+ '<p style="color:#a1a1aa;font-size:.93rem;line-height:1.6;margin:0 0 1.2rem">Ask your agent to fix it — the chat is in the bottom corner. Copy the error so it can debug faster.</p>' +
66
+ '<div><button id="__bloby_fe_copy" style="font:inherit;cursor:pointer;border:none;border-radius:10px;padding:.65rem 1.2rem;font-size:.9rem;font-weight:600;background:linear-gradient(135deg,#0166FF,#0069FE);color:#fff">Copy error for your agent</button> ' +
67
+ '<button id="__bloby_fe_dismiss" style="font:inherit;cursor:pointer;border-radius:10px;padding:.65rem 1.1rem;font-size:.9rem;font-weight:600;border:1px solid #27272a;background:#18181b;color:#e4e4e7">Dismiss</button></div>' +
68
+ '</div>';
69
+ document.body.appendChild(d);
70
+
71
+ var copyBtn = d.querySelector('#__bloby_fe_copy');
72
+ copyBtn.addEventListener('click', function () {
73
+ var text = 'A screen in my app has a frontend build error. Find and fix the root cause. Error:\n\n' + (lastErr || '(no details captured)');
74
+ function ok() { copyBtn.textContent = '✓ Copied — paste it to your agent'; setTimeout(function () { copyBtn.textContent = 'Copy error for your agent'; }, 2600); }
75
+ function fb() { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); ok(); } catch (e) {} document.body.removeChild(ta); }
76
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(ok).catch(fb); else fb();
77
+ });
78
+ d.querySelector('#__bloby_fe_dismiss').addEventListener('click', function () {
79
+ dismissed = true;
80
+ if (overlay) { overlay.remove(); overlay = null; }
81
+ });
82
+ return d;
83
+ }
84
+
85
+ var start = Date.now();
86
+ var sawError = false;
87
+
88
+ function appLooksBroken() {
89
+ // 1. Compile/import error → Vite renders <vite-error-overlay>. Immediate, reliable.
90
+ if (document.querySelector('vite-error-overlay')) return true;
91
+ var root = document.getElementById('root');
92
+ if (!root) return false; // not the dashboard shell — don't interfere
93
+ // Healthy = React rendered real content into #root (and it isn't the workspace template's
94
+ // own "Your app crashed" fallback, which we supersede with the friendly screen).
95
+ var mounted = root.children.length > 0 && (root.textContent || '').indexOf('Your app crashed') === -1;
96
+ if (mounted) return false;
97
+ // Not mounted: if we already saw a hard error, judge immediately; otherwise give a cold
98
+ // load a generous grace window before declaring a black screen (avoids false positives).
99
+ if (sawError) return true;
100
+ return (Date.now() - start) > 8000;
101
+ }
102
+
103
+ function evaluate() {
104
+ if (appLooksBroken()) {
105
+ var err = readViteError();
106
+ if (err) lastErr = err;
107
+ if (!overlay && !dismissed) overlay = buildOverlay();
108
+ } else {
109
+ // App healthy (or recovered) — drop our overlay and re-arm for the next episode.
110
+ dismissed = false;
111
+ if (overlay) { overlay.remove(); overlay = null; }
112
+ }
113
+ }
114
+
115
+ // Triggers: Vite appends <vite-error-overlay> directly under <body> (childList catches it
116
+ // instantly + cheaply); a steady tick handles the timeout-based black-screen detection and
117
+ // recovery; window errors flip sawError so real load failures surface fast.
118
+ new MutationObserver(evaluate).observe(document.body, { childList: true });
119
+ setInterval(evaluate, 1500);
120
+ window.addEventListener('error', function () { sawError = true; evaluate(); });
121
+ window.addEventListener('unhandledrejection', function () { sawError = true; evaluate(); });
122
+ evaluate();
123
+ })();
@@ -1,12 +1,14 @@
1
1
  // Service worker — app-shell caching + push notifications
2
2
  // Caching strategy:
3
3
  // Hashed assets (/assets/*-AbCd12.js) → cache-first (immutable)
4
- // Navigation (HTML) → stale-while-revalidate (precached on install)
5
- // Static assets (img/video/fonts) stale-while-revalidate
6
- // JS/CSS modules → stale-while-revalidate
4
+ // Navigation (HTML) → network-first (cache = offline fallback only)
5
+ // Static assets / JS / CSS modules network-first (cache = offline fallback only)
7
6
  // API, WebSocket, Vite internals → network-only (no cache)
7
+ // Network-first on navigations + modules so a live-edit always shows the current app and its
8
+ // build errors, never a stale cached shell. Cache is a pure offline fallback. (Mirror of the
9
+ // supervisor's SW_JS in supervisor/index.ts — keep in sync.)
8
10
 
9
- const CACHE = 'bloby-v7';
11
+ const CACHE = 'bloby-v17';
10
12
 
11
13
  // Precache the HTML shell on install so the cache is never empty.
12
14
  // Without this, the first navigation isn't intercepted (SW wasn't
@@ -70,18 +72,11 @@ self.addEventListener('fetch', (event) => {
70
72
  if (request.mode === 'navigate') {
71
73
  console.log('[SW] navigate →', url.pathname);
72
74
  if (url.pathname === '/' || url.pathname === '/index.html') {
75
+ // Network-first: always fetch the live dashboard; cache only as offline fallback.
73
76
  event.respondWith(caches.open(CACHE).then(c =>
74
- c.match('/').then(hit => {
75
- console.log('[SW] cache hit for /:', !!hit);
76
- const net = fetch(request)
77
- .then(r => {
78
- console.log('[SW] network response for /:', r.status);
79
- if (r.ok) c.put('/', r.clone());
80
- return r;
81
- })
82
- .catch(err => { console.warn('[SW] network failed, using cache:', err.message); return hit; });
83
- return hit || net;
84
- })
77
+ fetch(request)
78
+ .then(r => { if (r.ok) c.put('/', r.clone()); return r; })
79
+ .catch(err => { console.warn('[SW] / network failed, using cache:', err.message); return c.match('/'); })
85
80
  ));
86
81
  return;
87
82
  }
@@ -90,16 +85,12 @@ self.addEventListener('fetch', (event) => {
90
85
  return;
91
86
  }
92
87
 
93
- // ── Everything else → stale-while-revalidate ────────────────────
94
- // Serves cached version instantly, refreshes cache in background.
88
+ // ── Everything else → network-first, cache as offline fallback ───
95
89
  // Covers: JS/CSS modules, images, video, fonts, manifest, etc.
96
90
  event.respondWith(caches.open(CACHE).then(c =>
97
- c.match(request).then(hit => {
98
- const net = fetch(request)
99
- .then(r => { if (r.ok) c.put(request, r.clone()); return r; })
100
- .catch(() => hit);
101
- return hit || net;
102
- })
91
+ fetch(request)
92
+ .then(r => { if (r.ok) c.put(request, r.clone()); return r; })
93
+ .catch(() => c.match(request))
103
94
  ));
104
95
  });
105
96