Skip to content

Commit bed4608

Browse files
frenchie4111claude
andauthored
feat: diagnostic logging + recovery banner on Monaco worker construction failure (#42)
When Monaco's web workers fail to construct, the renderer's worker subsystem is poisoned for the whole session — every diff editor renders without highlighting until reload. Today the failure is invisible to users and silent in logs. Wrap each worker construction in `safeCreateWorker` that captures the failure (label, phase, message, stack, userAgent, resolved Vite URL) to console.error (which already lands in debug.log via renderer log forwarding) and dispatches a `monaco:worker-failed` window event. Also hooks the worker's runtime `error` event so workers that die on first message are captured the same way. Add a `MonacoWorkerFailedBanner` mounted at the top of DesktopApp that listens for the event and offers a one-click `window.location.reload()` plus a dismiss-for-now button. No mitigations or retries — the point is to capture data so the root cause can be diagnosed; masking the symptom would rob us of that. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 16b1264 commit bed4608

3 files changed

Lines changed: 111 additions & 5 deletions

File tree

src/renderer/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ResolveRepoModal } from './components/ResolveRepoModal'
3838
import { RepoAddErrorModal } from './components/RepoAddErrorModal'
3939
import { ReportIssueScreen, onOpenReportIssue, type OpenReportIssueDetail } from './components/ReportIssueScreen'
4040
import { AddBackendModal } from './components/AddBackendModal'
41+
import { MonacoWorkerFailedBanner } from './components/MonacoWorkerFailedBanner'
4142
import iconUrl from '../../resources/icon.png'
4243
import { PerfMonitorHUD } from './components/PerfMonitorHUD'
4344
import { focusTerminalById } from './components/XTerminal'
@@ -1025,6 +1026,7 @@ const setQuestStep = useCallback((next: QuestStep) => {
10251026
return (
10261027
<HotkeysProvider bindings={resolvedHotkeys}>
10271028
<div className="flex h-full flex-col">
1029+
<MonacoWorkerFailedBanner />
10281030
{/* Update-ready banner */}
10291031
{updaterStatus?.state === 'downloaded' && !updateBannerDismissed && (
10301032
<div className="bg-success/15 border-b border-success/30 pl-20 pr-4 py-2.5 drag-region flex items-center gap-3 shrink-0">
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useEffect, useState } from 'react'
2+
import { AlertTriangle, X } from 'lucide-react'
3+
4+
export function MonacoWorkerFailedBanner(): JSX.Element | null {
5+
const [failed, setFailed] = useState<string[]>([])
6+
const [dismissed, setDismissed] = useState(false)
7+
8+
useEffect(() => {
9+
const handler = (e: Event) => {
10+
const detail = (e as CustomEvent<{ label: string }>).detail
11+
if (!detail?.label) return
12+
setFailed((prev) => (prev.includes(detail.label) ? prev : [...prev, detail.label]))
13+
}
14+
window.addEventListener('monaco:worker-failed', handler)
15+
return () => window.removeEventListener('monaco:worker-failed', handler)
16+
}, [])
17+
18+
if (failed.length === 0 || dismissed) return null
19+
20+
return (
21+
<div className="bg-warning/15 border-b border-warning/40 text-warning px-3 py-1.5 text-xs flex items-center gap-3 drag-region shrink-0">
22+
<AlertTriangle size={12} className="shrink-0" />
23+
<span className="flex-1">
24+
Diff highlighting and syntax tools are temporarily unavailable (Monaco workers failed to
25+
load: {failed.join(', ')}). Reload this window to restore.
26+
</span>
27+
<button
28+
onClick={() => window.location.reload()}
29+
className="px-2 py-0.5 rounded bg-warning/30 hover:bg-warning/50 text-warning transition-colors cursor-pointer no-drag"
30+
>
31+
Reload window
32+
</button>
33+
<button
34+
onClick={() => setDismissed(true)}
35+
title="Hide this banner (problem will persist until reload)"
36+
className="text-warning/70 hover:text-warning cursor-pointer no-drag"
37+
>
38+
<X size={12} />
39+
</button>
40+
</div>
41+
)
42+
}

src/renderer/monaco-setup.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,76 @@ import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
77
import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
88
import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
99
import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
10+
import EditorWorkerUrl from 'monaco-editor/esm/vs/editor/editor.worker?worker&url'
11+
import JsonWorkerUrl from 'monaco-editor/esm/vs/language/json/json.worker?worker&url'
12+
import CssWorkerUrl from 'monaco-editor/esm/vs/language/css/css.worker?worker&url'
13+
import HtmlWorkerUrl from 'monaco-editor/esm/vs/language/html/html.worker?worker&url'
14+
import TsWorkerUrl from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker&url'
15+
16+
const workerUrlByLabel: Record<string, string> = {
17+
editor: EditorWorkerUrl,
18+
json: JsonWorkerUrl,
19+
css: CssWorkerUrl,
20+
scss: CssWorkerUrl,
21+
less: CssWorkerUrl,
22+
html: HtmlWorkerUrl,
23+
handlebars: HtmlWorkerUrl,
24+
razor: HtmlWorkerUrl,
25+
typescript: TsWorkerUrl,
26+
javascript: TsWorkerUrl
27+
}
28+
29+
// If a Monaco worker fails to construct, Monaco falls back to loading
30+
// the worker source inline on the main thread. That fallback ALSO fails
31+
// because the worker chunk is ESM and the inline-parse hits `export`,
32+
// at which point Monaco's worker subsystem is poisoned for the entire
33+
// renderer session — every diff editor renders without highlighting
34+
// until full reload. We can't fix that here; we just capture enough
35+
// detail to root-cause it and emit a window event so the UI can offer
36+
// a one-click reload.
37+
function safeCreateWorker(label: string, factory: () => Worker): Worker {
38+
try {
39+
const worker = factory()
40+
worker.addEventListener('error', (ev) => {
41+
const detail = {
42+
label,
43+
phase: 'runtime',
44+
message: ev.message,
45+
filename: (ev as ErrorEvent).filename,
46+
lineno: (ev as ErrorEvent).lineno,
47+
workerUrl: workerUrlByLabel[label]
48+
}
49+
// eslint-disable-next-line no-console
50+
console.error(`[monaco] worker '${label}' runtime error`, detail)
51+
window.dispatchEvent(new CustomEvent('monaco:worker-failed', { detail }))
52+
})
53+
return worker
54+
} catch (err) {
55+
const detail = {
56+
label,
57+
phase: 'construct',
58+
message: err instanceof Error ? err.message : String(err),
59+
stack: err instanceof Error ? err.stack : undefined,
60+
userAgent: navigator.userAgent,
61+
workerUrl: workerUrlByLabel[label]
62+
}
63+
// eslint-disable-next-line no-console
64+
console.error(`[monaco] worker '${label}' construction failed`, detail)
65+
window.dispatchEvent(new CustomEvent('monaco:worker-failed', { detail }))
66+
throw err
67+
}
68+
}
1069

1170
;(self as unknown as { MonacoEnvironment: monaco.Environment }).MonacoEnvironment = {
1271
getWorker(_workerId, label) {
13-
if (label === 'json') return new JsonWorker()
14-
if (label === 'css' || label === 'scss' || label === 'less') return new CssWorker()
15-
if (label === 'html' || label === 'handlebars' || label === 'razor') return new HtmlWorker()
16-
if (label === 'typescript' || label === 'javascript') return new TsWorker()
17-
return new EditorWorker()
72+
if (label === 'json') return safeCreateWorker('json', () => new JsonWorker())
73+
if (label === 'css' || label === 'scss' || label === 'less')
74+
return safeCreateWorker(label, () => new CssWorker())
75+
if (label === 'html' || label === 'handlebars' || label === 'razor')
76+
return safeCreateWorker(label, () => new HtmlWorker())
77+
if (label === 'typescript' || label === 'javascript')
78+
return safeCreateWorker(label, () => new TsWorker())
79+
return safeCreateWorker('editor', () => new EditorWorker())
1880
}
1981
}
2082

0 commit comments

Comments
 (0)