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 +1 -1
- package/supervisor/index.ts +52 -24
- package/supervisor/workspace-guard.js +123 -0
- package/workspace/client/public/sw.js +14 -23
package/package.json
CHANGED
package/supervisor/index.ts
CHANGED
|
@@ -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) →
|
|
116
|
-
// Static assets
|
|
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-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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 →
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
1805
|
-
proxyRes.
|
|
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) →
|
|
5
|
-
// Static assets
|
|
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-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 →
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|