bloby-bot 0.53.3 → 0.53.6
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
package/supervisor/backend.ts
CHANGED
|
@@ -8,6 +8,10 @@ let child: ChildProcess | null = null;
|
|
|
8
8
|
let restarts = 0;
|
|
9
9
|
let lastSpawnTime = 0;
|
|
10
10
|
let intentionallyStopped = false;
|
|
11
|
+
// True once the backend has crash-looped past MAX_RESTARTS and given up — i.e. it's down and
|
|
12
|
+
// will NOT come back without the user fixing the code. The supervisor shows the "backend down"
|
|
13
|
+
// interstitial in this state. Cleared on every spawn attempt (a deliberate restart is "trying again").
|
|
14
|
+
let gaveUp = false;
|
|
11
15
|
const MAX_RESTARTS = 3;
|
|
12
16
|
const STABLE_THRESHOLD = 30_000; // 30s — if backend ran this long, it wasn't a crash loop
|
|
13
17
|
|
|
@@ -39,6 +43,7 @@ export function spawnBackend(port: number): ChildProcess {
|
|
|
39
43
|
const backendPath = path.join(WORKSPACE_DIR, 'backend', 'index.ts');
|
|
40
44
|
lastSpawnTime = Date.now();
|
|
41
45
|
intentionallyStopped = false;
|
|
46
|
+
gaveUp = false;
|
|
42
47
|
|
|
43
48
|
// Clear log file on each restart — only keeps current run
|
|
44
49
|
try { fs.writeFileSync(LOG_FILE, ''); } catch {}
|
|
@@ -106,6 +111,7 @@ export function spawnBackend(port: number): ChildProcess {
|
|
|
106
111
|
log.info(`Restarting backend (${restarts}/${MAX_RESTARTS}, delay ${delay}ms)...`);
|
|
107
112
|
setTimeout(() => spawnBackend(port), delay);
|
|
108
113
|
} else {
|
|
114
|
+
gaveUp = true;
|
|
109
115
|
log.error('Backend failed too many times. Use Bloby chat to debug.');
|
|
110
116
|
}
|
|
111
117
|
});
|
|
@@ -183,6 +189,23 @@ export function isBackendAlive(): boolean {
|
|
|
183
189
|
return child !== null && child.exitCode === null;
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
/** True when the backend has crash-looped past MAX_RESTARTS and given up — down and not
|
|
193
|
+
* coming back without a code fix. Drives the supervisor's "backend down" interstitial. */
|
|
194
|
+
export function isBackendDead(): boolean {
|
|
195
|
+
return gaveUp;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Read the tail of the backend log (default 100 lines) for the "copy logs" debug helper. */
|
|
199
|
+
export function readBackendLogTail(maxLines = 100): string {
|
|
200
|
+
try {
|
|
201
|
+
const text = fs.readFileSync(LOG_FILE, 'utf-8');
|
|
202
|
+
const lines = text.split('\n');
|
|
203
|
+
return lines.slice(-maxLines).join('\n').trim();
|
|
204
|
+
} catch {
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
186
209
|
export function isBackendStopping(): boolean {
|
|
187
210
|
return stopPromise !== null;
|
|
188
211
|
}
|
package/supervisor/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { log } from '../shared/logger.js';
|
|
|
11
11
|
import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel, startNamedTunnel, restartNamedTunnel } from './tunnel.js';
|
|
12
12
|
import { createWorkerApp } from '../worker/index.js';
|
|
13
13
|
import { closeDb, getSession, getSetting } from '../worker/db.js';
|
|
14
|
-
import { spawnBackend, stopBackend, restartBackend, getBackendPort, isBackendAlive, isBackendStopping, setBackendEnv } from './backend.js';
|
|
14
|
+
import { spawnBackend, stopBackend, restartBackend, getBackendPort, isBackendAlive, isBackendStopping, isBackendDead, readBackendLogTail, setBackendEnv } from './backend.js';
|
|
15
15
|
import { handleAgentQuery, type AgentQueryRequest } from './agent-api.js';
|
|
16
16
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
17
17
|
import {
|
|
@@ -69,6 +69,8 @@ const PLATFORM_ASSETS = new Set([
|
|
|
69
69
|
'/pi-logo.svg',
|
|
70
70
|
'/codex.svg',
|
|
71
71
|
'/manifest.json',
|
|
72
|
+
'/what-happened.webm',
|
|
73
|
+
'/what-happened.mp4',
|
|
72
74
|
]);
|
|
73
75
|
|
|
74
76
|
// Directory-prefix platform assets — anything under these is served from supervisor/public/.
|
|
@@ -250,6 +252,77 @@ const RECOVERING_HTML = `<!DOCTYPE html><html style="background:#222122"><head><
|
|
|
250
252
|
</div><script>setTimeout(function(){location.reload()},3000)</script>
|
|
251
253
|
<script src="/bloby/widget.js"></script></body></html>`;
|
|
252
254
|
|
|
255
|
+
/** Interstitial shown (by the supervisor, not the workspace) when the workspace backend has
|
|
256
|
+
* crash-looped and given up. Replaces proxying the dashboard SPA to Vite — which would 503 on
|
|
257
|
+
* every /app/api call and, for the common workspace-lock template, misread "no backend" as
|
|
258
|
+
* "no password set" and show the lock-setup screen. Embeds the Bloby chat widget so the user
|
|
259
|
+
* can ask the agent to fix it inline, a "copy logs" button (last 100 backend log lines baked in
|
|
260
|
+
* at render time), and a poll that reloads into the real dashboard once the backend is back. */
|
|
261
|
+
function backendDownPage(logTail: string): string {
|
|
262
|
+
// Embed logs as a JS string literal; escape `<` so a stray `</script>` in the logs can't break out.
|
|
263
|
+
const logs = JSON.stringify(logTail && logTail.length ? logTail : '(no backend logs were captured)').replace(/</g, '\\u003c');
|
|
264
|
+
return `<!DOCTYPE html>
|
|
265
|
+
<html lang="en"><head>
|
|
266
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
267
|
+
<title>Backend down · Bloby</title>
|
|
268
|
+
<style>
|
|
269
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
270
|
+
body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:#0a0a0b;color:#e4e4e7;display:flex;align-items:center;justify-content:center;min-height:100dvh;padding:1.5rem;overflow:hidden}
|
|
271
|
+
.c{text-align:center;max-width:480px;width:100%;animation:fade-up .6s ease-out both}
|
|
272
|
+
.video-wrap{position:relative;width:200px;height:200px;margin:0 auto 1.4rem;display:flex;align-items:center;justify-content:center}
|
|
273
|
+
.video-wrap::before{content:'';position:absolute;inset:-20px;background:radial-gradient(circle,rgba(1,102,255,0.18) 0%,transparent 60%);filter:blur(20px);animation:glow 3s ease-in-out infinite}
|
|
274
|
+
.video-wrap video{position:relative;width:100%;height:100%;object-fit:contain;pointer-events:none;border-radius:50%}
|
|
275
|
+
h1{font-size:1.55rem;font-weight:700;margin-bottom:.6rem;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
|
276
|
+
p{color:#a1a1aa;line-height:1.6;margin-bottom:.5rem;font-size:.95rem}
|
|
277
|
+
.lead{color:#e4e4e7;font-size:1rem}
|
|
278
|
+
.actions{margin-top:1.3rem}
|
|
279
|
+
button{font:inherit;cursor:pointer;border-radius:10px;padding:.65rem 1.2rem;font-size:.9rem;font-weight:600;border:none;background:linear-gradient(135deg,#0166FF,#0069FE);color:#fff;transition:filter .15s}
|
|
280
|
+
button:hover{filter:brightness(1.12)}
|
|
281
|
+
.sub{font-size:.82rem;color:#71717a;display:inline-flex;align-items:center;gap:.5rem;background:#18181b;border:1px solid #27272a;border-radius:9999px;padding:.35rem .9rem;margin-top:1.1rem}
|
|
282
|
+
.sub .dot{width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#0166FF,#009AFE);box-shadow:0 0 8px rgba(1,102,255,.6);animation:pulse 1.6s ease-in-out infinite}
|
|
283
|
+
.badge{display:block;font-size:.7rem;color:#52525b;margin-top:1.3rem}
|
|
284
|
+
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.85)}}
|
|
285
|
+
@keyframes glow{0%,100%{opacity:.55;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}}
|
|
286
|
+
@keyframes fade-up{0%{opacity:0;transform:translateY(12px)}100%{opacity:1;transform:translateY(0)}}
|
|
287
|
+
</style></head>
|
|
288
|
+
<body><div class="c">
|
|
289
|
+
<div class="video-wrap"><video autoplay loop muted playsinline>
|
|
290
|
+
<source src="/what-happened.webm" type="video/webm">
|
|
291
|
+
<source src="/what-happened.mp4" type="video/mp4">
|
|
292
|
+
</video></div>
|
|
293
|
+
<h1>Your app's backend is down</h1>
|
|
294
|
+
<p class="lead">The workspace server crashed and couldn't restart on its own.</p>
|
|
295
|
+
<p>Ask your agent to fix it — the chat is right here in the corner. Tap below to copy the logs so it can debug faster.</p>
|
|
296
|
+
<div class="actions"><button id="copyBtn">Copy logs for your agent</button></div>
|
|
297
|
+
<div class="sub"><span class="dot"></span><span id="statusText">Watching for recovery…</span></div>
|
|
298
|
+
<span class="badge">Powered by Bloby</span>
|
|
299
|
+
</div>
|
|
300
|
+
<script>
|
|
301
|
+
(function(){
|
|
302
|
+
var LOGS = ${logs};
|
|
303
|
+
var btn = document.getElementById('copyBtn'), statusEl = document.getElementById('statusText');
|
|
304
|
+
btn.addEventListener('click', function(){
|
|
305
|
+
var text = 'My workspace backend crashed and will not start. Find and fix the root cause. Last backend logs:\\n\\n' + LOGS;
|
|
306
|
+
function ok(){ btn.textContent = '✓ Copied — paste it to your agent'; setTimeout(function(){ btn.textContent = 'Copy logs for your agent'; }, 2600); }
|
|
307
|
+
function fallback(){ 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){ btn.textContent='Copy failed — open the logs manually'; } document.body.removeChild(ta); }
|
|
308
|
+
if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(ok).catch(fallback); } else { fallback(); }
|
|
309
|
+
});
|
|
310
|
+
var attempt = 0;
|
|
311
|
+
function retry(){
|
|
312
|
+
attempt++;
|
|
313
|
+
fetch('/__bloby/backend-status', { cache:'no-store' })
|
|
314
|
+
.then(function(r){ return r.json(); })
|
|
315
|
+
.then(function(s){ if (s && s.alive) { location.reload(); } else { schedule(); } })
|
|
316
|
+
.catch(schedule);
|
|
317
|
+
}
|
|
318
|
+
function schedule(){ statusEl.textContent = 'Watching for recovery… (checked ' + attempt + 'x)'; setTimeout(retry, Math.min(4000, 1500 + attempt*250)); }
|
|
319
|
+
setTimeout(retry, 2500);
|
|
320
|
+
})();
|
|
321
|
+
</script>
|
|
322
|
+
<script src="/bloby/widget.js"></script>
|
|
323
|
+
</body></html>`;
|
|
324
|
+
}
|
|
325
|
+
|
|
253
326
|
/** Kill any stale process holding a port. Ensures clean startup after crashes/updates. */
|
|
254
327
|
function killPort(port: number): void {
|
|
255
328
|
try {
|
|
@@ -457,6 +530,15 @@ export async function startSupervisor() {
|
|
|
457
530
|
return;
|
|
458
531
|
}
|
|
459
532
|
|
|
533
|
+
// Workspace guard — injected by the supervisor into the dashboard HTML (see the Vite proxy
|
|
534
|
+
// below). Auto-reloads into the "backend down" interstitial when the backend gives up, and
|
|
535
|
+
// replaces Vite's raw error overlay with a friendly one. Served here so it's always current.
|
|
536
|
+
if (req.url === '/bloby/workspace-guard.js') {
|
|
537
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
|
|
538
|
+
res.end(fs.readFileSync(path.join(PKG_DIR, 'supervisor', 'workspace-guard.js')));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
460
542
|
// Service worker — served from embedded constant (supervisor/ is always updated)
|
|
461
543
|
if (req.url === '/sw.js' || req.url === '/bloby/sw.js') {
|
|
462
544
|
res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
|
|
@@ -464,6 +546,15 @@ export async function startSupervisor() {
|
|
|
464
546
|
return;
|
|
465
547
|
}
|
|
466
548
|
|
|
549
|
+
// Backend liveness for the "backend down" interstitial's recovery poll. Supervisor-served
|
|
550
|
+
// (not proxied) so it answers even when the workspace backend is dead, and independent of
|
|
551
|
+
// whatever routes the user's backend happens to define.
|
|
552
|
+
if (req.url === '/__bloby/backend-status') {
|
|
553
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
554
|
+
res.end(JSON.stringify({ alive: isBackendAlive(), dead: isBackendDead() }));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
467
558
|
// App API routes → proxy to user's backend server
|
|
468
559
|
if (req.url?.startsWith('/app/api')) {
|
|
469
560
|
const backendPath = req.url.replace(/^\/app/, '');
|
|
@@ -1696,13 +1787,57 @@ mint();
|
|
|
1696
1787
|
} catch { /* fall through to Vite */ }
|
|
1697
1788
|
}
|
|
1698
1789
|
|
|
1790
|
+
// Workspace backend has crash-looped and given up → serve the "backend down" interstitial
|
|
1791
|
+
// for dashboard DOCUMENT navigations, instead of proxying to Vite (which serves the user's
|
|
1792
|
+
// SPA that then 503s on every /app/api call and, for the common workspace-lock template,
|
|
1793
|
+
// misreads the dead backend as "no password set" and shows the lock-setup screen). Scoped to
|
|
1794
|
+
// top-level navigations only (not assets/HMR/XHR) and only when the backend has truly given
|
|
1795
|
+
// up — never during a normal 1–2s restart. The chat PWA (/bloby/*) is served earlier and is
|
|
1796
|
+
// unaffected.
|
|
1797
|
+
const wantsHtml = req.method === 'GET' && (
|
|
1798
|
+
req.headers['sec-fetch-dest'] === 'document' ||
|
|
1799
|
+
req.headers['sec-fetch-mode'] === 'navigate' ||
|
|
1800
|
+
(!req.headers['sec-fetch-dest'] && String(req.headers['accept'] || '').includes('text/html'))
|
|
1801
|
+
);
|
|
1802
|
+
if (wantsHtml && isBackendDead()) {
|
|
1803
|
+
res.writeHead(503, { 'Content-Type': 'text/html', 'Cache-Control': 'no-store, no-cache, must-revalidate' });
|
|
1804
|
+
res.end(backendDownPage(readBackendLogTail(100)));
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1699
1808
|
// Everything else → proxy to dashboard Vite dev server
|
|
1700
1809
|
console.log(`[supervisor] → dashboard Vite :${vitePorts.dashboard} | ${req.method} ${(req.url || '').split('?')[0]}`);
|
|
1810
|
+
const GUARD_TAG = '<script defer src="/bloby/workspace-guard.js"></script>';
|
|
1701
1811
|
const proxy = http.request(
|
|
1702
1812
|
{ host: '127.0.0.1', port: vitePorts.dashboard, path: req.url, method: req.method, headers: req.headers },
|
|
1703
1813
|
(proxyRes) => {
|
|
1704
|
-
|
|
1705
|
-
proxyRes.
|
|
1814
|
+
const ct = String(proxyRes.headers['content-type'] || '');
|
|
1815
|
+
const enc = String(proxyRes.headers['content-encoding'] || '');
|
|
1816
|
+
// Inject the workspace guard into dashboard HTML *documents* only (and only when
|
|
1817
|
+
// uncompressed — Vite dev serves plain HTML). Assets, HMR, JSON, etc. stream through
|
|
1818
|
+
// untouched. The guard auto-reloads into the "backend down" interstitial and replaces
|
|
1819
|
+
// Vite's raw error overlay — supervisor-side so it reaches every workspace, no edits needed.
|
|
1820
|
+
if (!ct.includes('text/html') || enc) {
|
|
1821
|
+
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
1822
|
+
proxyRes.pipe(res);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const chunks: Buffer[] = [];
|
|
1826
|
+
proxyRes.on('data', (c: Buffer) => chunks.push(c));
|
|
1827
|
+
proxyRes.on('end', () => {
|
|
1828
|
+
let html = Buffer.concat(chunks).toString('utf-8');
|
|
1829
|
+
if (!html.includes('workspace-guard.js')) {
|
|
1830
|
+
html = html.includes('</head>') ? html.replace('</head>', GUARD_TAG + '</head>') : GUARD_TAG + html;
|
|
1831
|
+
}
|
|
1832
|
+
const headers = { ...proxyRes.headers };
|
|
1833
|
+
// Body length changed — drop both framing headers and let Node set content-length
|
|
1834
|
+
// from the single res.end() write.
|
|
1835
|
+
delete headers['content-length'];
|
|
1836
|
+
delete headers['transfer-encoding'];
|
|
1837
|
+
res.writeHead(proxyRes.statusCode!, headers);
|
|
1838
|
+
res.end(html);
|
|
1839
|
+
});
|
|
1840
|
+
proxyRes.on('error', () => { try { res.destroy(); } catch {} });
|
|
1706
1841
|
},
|
|
1707
1842
|
);
|
|
1708
1843
|
proxy.on('error', (e) => {
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
function sync() {
|
|
86
|
+
var ov = document.querySelector('vite-error-overlay');
|
|
87
|
+
if (ov) {
|
|
88
|
+
var err = readViteError();
|
|
89
|
+
if (err) lastErr = err;
|
|
90
|
+
if (!overlay && !dismissed) overlay = buildOverlay();
|
|
91
|
+
} else {
|
|
92
|
+
// Error cleared (agent fixed it) — drop our overlay and re-arm for the next episode.
|
|
93
|
+
dismissed = false;
|
|
94
|
+
if (overlay) { overlay.remove(); overlay = null; }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Vite appends <vite-error-overlay> directly under <body>; observing body's childList is enough
|
|
99
|
+
// and far lighter than a full-document subtree observer.
|
|
100
|
+
var obs = new MutationObserver(sync);
|
|
101
|
+
obs.observe(document.body, { childList: true });
|
|
102
|
+
sync();
|
|
103
|
+
})();
|