Bug
Clicking the "+" button in the sidebar to open a new project shows nothing — no dialog, no error message.
Root Cause
Three compounding issues:
1. Stale client bundle after redeployment
When the server is redeployed with a new build, all Vite chunk hashes change (e.g. dialog-select-directory-utHQcNsA.js → dialog-select-directory-CRC4imRH.js). Browser tabs that were open before the redeployment still have the old main JS bundle loaded, which tries to dynamically import chunks with old hashes that no longer exist on the server.
2. Silent failure via void import(...) pattern
The dynamic imports in layout.tsx use the void import(...) pattern which discards the promise. When the import fails (404 on stale chunk), the rejection becomes an unhandled promise rejection — no error dialog, no console message visible to the user, no recovery mechanism.
// layout.tsx:1507 — the failing code
void import("@/components/dialog-select-directory").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(/* ... */)
})
3. SPA fallback masks the 404
In instance.ts (embedded UI path), the catch-all route falls back to index.html for any missing path, including .js chunk files:
embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
This means a missing .js chunk actually returns HTML content instead of a proper 404, causing a confusing MIME type error rather than a clean import failure.
4. No cache-busting headers on HTML
Neither instance.ts nor server.ts set Cache-Control headers on HTML responses, so browsers may cache stale index.html that references old chunk hashes.
Why tests didn't catch this
- E2E tests always start fresh with the current build — they never test "old client + new server" scenarios
- The
void import(...) anti-pattern silently swallows errors as unhandled rejections — no test asserts on this
- The SPA fallback bug in
instance.ts actually masks the 404 — making it even harder to detect during development
- No integration test verifies behavior when a lazy chunk returns 404
Fix
- Fix SPA fallback in
instance.ts: Only fall back to index.html for extensionless routes (matching server.ts behavior), so stale .js requests properly 404
- Add
Cache-Control: no-cache to HTML responses: Both instance.ts and server.ts now set cache headers on HTML, ensuring browsers revalidate on each navigation
- Global stale-module detection in
entry.tsx: Listen for unhandledrejection events matching dynamic import failures and show a persistent "new version available" overlay with a Reload button — covers all 15+ dynamic import sites across the app
Bug
Clicking the "+" button in the sidebar to open a new project shows nothing — no dialog, no error message.
Root Cause
Three compounding issues:
1. Stale client bundle after redeployment
When the server is redeployed with a new build, all Vite chunk hashes change (e.g.
dialog-select-directory-utHQcNsA.js→dialog-select-directory-CRC4imRH.js). Browser tabs that were open before the redeployment still have the old main JS bundle loaded, which tries to dynamically import chunks with old hashes that no longer exist on the server.2. Silent failure via
void import(...)patternThe dynamic imports in
layout.tsxuse thevoid import(...)pattern which discards the promise. When the import fails (404 on stale chunk), the rejection becomes an unhandled promise rejection — no error dialog, no console message visible to the user, no recovery mechanism.3. SPA fallback masks the 404
In
instance.ts(embedded UI path), the catch-all route falls back toindex.htmlfor any missing path, including.jschunk files:This means a missing
.jschunk actually returns HTML content instead of a proper 404, causing a confusing MIME type error rather than a clean import failure.4. No cache-busting headers on HTML
Neither
instance.tsnorserver.tssetCache-Controlheaders on HTML responses, so browsers may cache staleindex.htmlthat references old chunk hashes.Why tests didn't catch this
void import(...)anti-pattern silently swallows errors as unhandled rejections — no test asserts on thisinstance.tsactually masks the 404 — making it even harder to detect during developmentFix
instance.ts: Only fall back toindex.htmlfor extensionless routes (matchingserver.tsbehavior), so stale.jsrequests properly 404Cache-Control: no-cacheto HTML responses: Bothinstance.tsandserver.tsnow set cache headers on HTML, ensuring browsers revalidate on each navigationentry.tsx: Listen forunhandledrejectionevents matching dynamic import failures and show a persistent "new version available" overlay with a Reload button — covers all 15+ dynamic import sites across the app