index.html paints a PraisonAI wordmark and Starting… from markup, and a guard reveals a failure notice when the bundle cannot load.
Quick Start
1
See the boot indicator
index.html paints the indicator inside #root on the first frame, before app.js runs.2
Load the guard before the bundle
A classic
boot-guard.js loads before the deferred app.js module, so its error listener is installed while the fetch is in flight.3
Let the app retire the indicator
mount() clears #root and appends the chat in the same statement pair, so the indicator is gone the instant the app paints.How It Works
boot-guard.js installs its error listener in the capture phase, then either steps aside when the app mounts or reveals the failure notice when the bundle cannot load.
What you see
A wordmark and one sentence tell the user the app is launching, not broken. On a slow cold start the user seesPraisonAI and Starting…. If the bundle is refused, the note is replaced by: “PraisonAI could not start: its code could not be loaded. Close the app and open it again.” The whole point is that a slow launch can no longer be mistaken for a broken install.
“The app is starting” is not something the view models model — so a wordmark plus one sentence, which owns no view-model state and duplicates no layout, is honest where a fake topbar or grey bubbles would not be.
Why it is not a skeleton
A skeleton would claim a layout the boot frame cannot yet know. A fake topbar or grey message bubbles duplicate a view they have no state for. The boot indicator paints one wordmark and one sentence — nothing that pretends to be a real view.Why it is static
A spinner would assert liveness nothing can make beforeapp.js runs.
There is no download-progress event on <script>, and no way to ask how much of a module has parsed. The compositor keeps animating even when the main thread is dead — a moving spinner during a hung boot is a lie, so the indicator stays still.
How removal works
mount() clears and appends in the same statement pair, so there is no frame with both the indicator and the app, and none with neither.
renderFatal clears the same way. On a boot fast enough for the first presented frame to already contain the app, the indicator is gone before it is ever painted.
Theme awareness
Every colour is a:root token, so a dark device gets a dark boot screen that hands over to a dark app.
--ground, --ink, --soft, and --bad drive the boot rules, so prefers-color-scheme: dark swaps the boot screen along with the app.
Safe-area insets
The.boot container pads with --inset-*, falling back to env(safe-area-inset-*), because nothing has run yet to write the shell’s real numbers.
This is why a landscape viewport or large font scale does not push the wordmark into a status bar or camera cutout.
The failure notice
boot-guard.js speaks only for a bundle that cannot load — every degraded-page failure is ignored.
Resource errors do not bubble, so the guard listens for
error in the capture phase.
The ordering is the whole guarantee:
boot-guard.js loads before the deferred app.js, without defer / async / type=module. <script type=module> is implicitly deferred, so a listener installed later can miss the fetch’s error event when a service worker or memory cache answers instantly. Pinned by boot-screen.test.ts::"the guard is loaded before the bundle, and not deferred" and web-boot.test.mjs.The guard is an external file, not an inline
<script>, because index.html ships script-src 'self' — an inline script or an onerror= attribute would be blocked. Same reason as register-sw.js.Common Patterns
The guard hands off tocrash.ts cleanly: once mount() runs, the boot indicator is gone and any later error belongs to the crash handler.
crash.ts, because mount() never ran to install it — so the guard is the only thing that can speak.
Best Practices
Load the guard before the bundle, undeferred
Load the guard before the bundle, undeferred
<script type=module> is implicitly deferred. Put boot-guard.js first as a classic script so its error listener is armed before app.js is even fetched — a service worker can answer instantly and a late listener would miss the failure.Listen in the capture phase
Listen in the capture phase
Resource
error events do not bubble. addEventListener("error", handler, true) is the only way to hear a <script src> fail.Ignore degraded-page failures
Ignore degraded-page failures
A stylesheet, an icon, or
register-sw.js declining is a degraded page, not a broken app. Only app.js failing — or a throw whose target is window — reveals the failure notice.Keep every boot colour a token
Keep every boot colour a token
Drive the boot screen from
:root tokens so a dark device gets a dark boot. A hardcoded colour becomes a flash at handoff.Clear and append together
Clear and append together
Retire the indicator with the same statement pair that appends the app, so no frame shows both or neither.
What this does not cover
The pre-WebView time is unreachable from the web layer. The gap fromam start to navigationStart is Android window setup, not something a painted frame can shorten — only a native window background or a splash on MainActivity’s theme could cover it. The boot indicator owns everything from the first web frame onward, and nothing before it.
Related
Boot Failures
What
createApp reports once the bundle has parsed and run.Architecture
Where the boot indicator sits in the overall boot order.
Overview
The launch experience and retained chat.
Shipping to Stores
Packaging the WebView app for release.

