Web App (PWA)

The same PraisonAI Mobile bundle, as an installable, offline-capable PWA in the browser

The same mobile bundle boots in a browser tab as an installable PWA, deployed to GitHub Pages, with the remote engine by default and the whole shell cached for an offline reload.

Diagram

Web App

๐ŸŒ Browser Tab

๐Ÿ“ฑ PWA Manifest

โš™๏ธ Service Worker

๐Ÿ’พ Versioned Cache

๐Ÿ’ฌ App Boots Offline

PraisonAI Mobile ships to three targets from one bundle: iOS and Android through the Tauri shell, and the web as a Progressive Web App. The web build is the same dist/ the Tauri shell serves โ€” the manifest, icons, and service worker are inert inside the shell, so one build covers all three.

Quick Start

  1. Build the bundle
    bash
    git clone https://github.com/MervinPraison/PraisonAI
    cd PraisonAI/src/praisonai-mobile
    npm install
    npm run build

    The build writes dist/ โ€” the page, stylesheet, manifest, icons, the entry app.js plus its eager chunks, and a sw.js whose cache name is a hash of exactly those bytes.

  2. Serve dist/
    bash
    npx serve dist

    Open the printed URL. The composer and Send button render, and the app runs against the remote engine by default.

  3. Install it

    The browser shows an Install app prompt because the manifest and service worker are present. Installed, it opens standalone in portrait โ€” no browser chrome โ€” using the icons from manifest.webmanifest.

  4. Reload offline

    Cut the network and reload. The shell still renders: the service worker serves the last index.html and the precached assets from its versioned cache.


The Manifest

app/manifest.webmanifest is what turns the tab into an installable app. Every URL is relative (./) so a project site served from a subpath resolves correctly.

FieldValueWhy
idai.praison.mobileStable identity across installs.
namePraisonAIFull install name.
short_namePraisonAIHome-screen label.
start_url./Relative, so a subpath install opens the right page.
scope./Confines the app to its own subpath.
displaystandaloneOpens without browser chrome.
orientationportraitLocks to portrait, matching the phone shell.
background_color#ffffffSplash background before first paint.
theme_color#1f7a63Toolbar / status-bar tint.

Icons are read from the Tauri icon set at build time, so the manifest cannot name an icon the shell does not have โ€” a missing one fails the build, not a phone’s home screen.


How Offline Works

The service worker picks a strategy from the request, never touching cross-origin traffic.

Diagram

yes

no

yes

no

yes

no

๐ŸŒ fetch

cross-origin?

โžก๏ธ passthrough
engine untouched

navigation?

๐Ÿ”„ network-first
fallback cached index.html

precached?

โšก cache-first

๐Ÿ“ฅ fetch + cache-on-use

RequestStrategy
NavigationNetwork-first; on failure, the cached index.html. A user online gets the current page; one offline gets the last one that loaded.
Precached assetCache-first. These bytes are the ones the cache name was derived from.
Other same-origin (lazy chunk)Fetch, then cache on use into the same versioned cache.
Cross-origin (engine at 127.0.0.1:*, an https: engine)Never touched โ€” passthrough.

How Redeploys Work

A new build is a new cache, and the old one is deleted on activate โ€” so a redeploy never pins a user to a stale app.

Diagram

๐Ÿ”จ new build

#๏ธโƒฃ sha256 of precache

๐Ÿท๏ธ praisonai-mobile-hash

๐Ÿ“ฆ install: precache

โ™ป๏ธ activate: delete old
praisonai-mobile-*

โœ… clients.claim

  • The cache name is praisonai-mobile-<buildId>, where buildId is the first 16 hex of a sha256 over the precached bytes.
  • On activate, every praisonai-mobile-* cache that is not the current one is deleted, then clients.claim() runs.
  • Navigation is always network-first, so a new index.html surfaces on the first online visit rather than waiting for a cache to expire.

Which Engine the Web Picks

A browser tab has a server to talk to, so the web defaults to the remote engine.

ts
import { defaultEngineIdFor } from "praisonai-mobile/app/main";

defaultEngineIdFor("web");   // "remote-http"  โ€” a browser tab has a server
defaultEngineIdFor("tauri"); // "praisonai-ts" โ€” a device runs in-process

defaultEngineIdFor only decides the very first launch; a persisted engineId always wins. The web picks remote-http because a browser has no reason to fetch ~1.4 MB of engine before first paint. See Engines โ†’ First-Launch Default.


Content Security Policy

The browser sees only the <meta> CSP in index.html; Tauri appends its own, so inside the shell both apply and the effective policy is their intersection.

html
<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:* https:; manifest-src 'self'; worker-src 'self'; base-uri 'self'; form-action 'self'"
/>
DirectiveWhat it allowsWhy
script-src 'self'External scripts only โ€” no inline, no eval.Every script, including the SW registration, is an external file, so there is nothing to whitelist and no hash to keep in step.
connect-src localhttp://127.0.0.1:, http://localhost:Reach a local engine.
connect-src remotehttps:Reach a remote HTTPS engine.
connect-src IPCipc:, http://ipc.localhostInert in a browser, but keeps Android’s IPC channel unblocked once the Tauri shell appends its own tag.

Every URL Is Relative

GitHub Pages serves project sites from a subpath (e.g. /PraisonAI/), so every URL in the app is relative.

An absolute /app.js or /manifest.webmanifest would resolve to the wrong site. The boot proof (tools/web-boot.test.mjs) serves dist/ under /PraisonAI/ to pin exactly this โ€” the manifest is served as application/manifest+json, icons return 200 as image/png, and the service worker activates scoped to the subpath.


Deploying to GitHub Pages

The mobile-web.yml workflow builds, boots the app in a real browser, and deploys โ€” but only main deploys, and only after one manual repo setting.

Diagram

yes

no

โฌ†๏ธ push to main

๐Ÿ”จ build dist/

๐Ÿงช boot proof
headless Chromium

๐Ÿ“ฆ upload mobile-web-dist

Pages source =
GitHub Actions?

๐Ÿš€ deploy-pages

โญ๏ธ SKIP with notice
never fails

StepWhat it does
Build praisonai-tsIt is a file: link, so its dist/ must exist before the mobile package installs.
npm ci, install Chromium, npm run buildProduce dist/.
npm run test:webServe dist/ under /PraisonAI/ and drive first paint, install, an offline reload, and a blocked inline script.
Upload mobile-web-distThe built site is always available as an artifact, even when the deploy is skipped.
Deploy (main only)actions/deploy-pages, gated on the Pages source.

Choosing the Web Build vs. Tauri

Both ship from the same bundle; pick by what the target needs.

Diagram

No install, no store review,
instant cross-device

On-device engine, OS integration,
offline before ever online

What do you need?

๐ŸŒ Web PWA
remote-http

๐Ÿ“ฑ Tauri shell
praisonai-ts

ChooseWhen
Web PWANo install or store review needed; instant access on any device; a server is available for the remote engine.
Tauri shellAn on-device engine, OS integration (safe-area, keyboard insets, back gesture), or offline-first before the app has ever been online.

Best Practices

Never use absolute URLs

The app is designed to live under a subpath on GitHub Pages. An absolute /app.js resolves to the wrong site. Keep start_url, scope, links, icons, and scripts ./-relative.

Never precache lazy chunks

Splitting exists so a browser does not pay ~1.4 MB it may never use. Precaching lazy chunks undoes that. The service worker caches each one on first use into the same versioned cache instead.

Never register the service worker inside Tauri

register-sw.js skips registration when __TAURI_INTERNALS__ is present, on non-http(s) origins, and where serviceWorker is absent. Inside the shell the app IS the offline copy; a worker caching custom-protocol responses would put a second copy between the shell and its assets.

Let a failed registration degrade, not break

If registration fails, the app still boots from the network. register-sw.js logs a warning rather than throwing โ€” a degraded page, not a broken one.

Trust the content-hash cache name

The cache name is a sha256 of the precached bytes, so two builds of one version differ the moment a source file does. activate deletes any older praisonai-mobile-* cache, so a redeploy never serves a mismatched pair of old and new chunks.


Related