Frontend Architecture
The moltis web UI is a TypeScript single-page application built with Preact and Vite.
Directory Layout
crates/web/
├── ui/ # TypeScript source & tooling
│ ├── src/ # Application source
│ │ ├── app.tsx # Main entry point
│ │ ├── login-app.tsx # Login page entry
│ │ ├── onboarding-app.tsx # Onboarding wizard entry
│ │ ├── types/ # Shared type definitions
│ │ ├── stores/ # Preact Signal stores
│ │ ├── components/ # Reusable Preact components
│ │ │ └── forms/ # Form field & layout components
│ │ ├── pages/ # Page components
│ │ │ ├── sections/ # Settings page sections
│ │ │ ├── channels/ # Channel modal sub-components
│ │ │ └── chat/ # Chat page sub-modules
│ │ ├── providers/ # Provider setup sub-modules
│ │ ├── sessions/ # Session management sub-modules
│ │ ├── onboarding/ # Onboarding step components
│ │ ├── ws/ # WebSocket handler sub-modules
│ │ ├── hooks/ # Custom Preact hooks
│ │ └── locales/ # i18n translations (en, fr, zh)
│ ├── e2e/ # Playwright E2E tests
│ ├── vite.config.ts # Vite build configuration
│ ├── tsconfig.json # TypeScript strict config
│ └── package.json # Dependencies & scripts
├── src/
│ ├── assets/ # Served static assets
│ │ ├── dist/ # Vite build output (committed)
│ │ ├── css/ # Stylesheets (Tailwind + custom)
│ │ ├── js/ # E2E test shims + share page
│ │ ├── icons/ # Favicons & PWA icons
│ │ └── sw.js # Service worker
│ └── templates/ # Askama HTML templates
Build Pipeline
TypeScript → JavaScript (Vite)
Source files in ui/src/ are compiled and bundled by Vite into
src/assets/dist/. Three entry points produce three bundles:
dist/main.js— main app (chat, settings, all pages)dist/login.js— login pagedist/onboarding.js— onboarding wizard
cd crates/web/ui
npm run build # Production build → ../src/assets/dist/
npm run dev # Watch mode (rebuilds on file changes)
The dist/ output is committed to git (unminified, no source maps)
so that cargo build works without Node.js installed. This mirrors the
approach used for the committed Tailwind CSS output.
CSS (Tailwind)
Tailwind CSS is built separately from the TypeScript pipeline:
cd crates/web/ui
npm run build:css # input.css → ../src/assets/css/style.css
npm run watch:css # Watch mode
The output style.css is committed unminified (one rule per line) so
diffs merge cleanly.
Service Worker
The service worker is built from TypeScript via esbuild:
cd crates/web/ui
npm run build:sw # src/sw.ts → ../src/assets/sw.js
Full Build
cd crates/web/ui
npm run build:all # Vite + Tailwind + service worker
Technology Stack
| Layer | Technology |
|---|---|
| UI framework | Preact (lightweight React alternative) |
| Templating | JSX with typed Props interfaces |
| State management | Preact Signals |
| Build tool | Vite with @preact/preset-vite |
| Type checking | TypeScript strict mode (tsc --noEmit) |
| Linting/formatting | Biome |
| CSS | Tailwind CSS v4 |
| i18n | i18next (en, fr, zh) |
| Charts | uPlot |
| Terminal | xterm.js |
| Syntax highlighting | Shiki (bundled, lazy-loaded) |
| E2E testing | Playwright |
Type Safety
The codebase enforces strict TypeScript with zero tolerance for any:
tsc --noEmitruns in CI and local-validate (must pass with 0 errors)- 107 typed RPC methods via
RpcMethodMap— callingsendRpc("models.list", {})infers the response type asModelInfo[] - 28 WebSocket events via
WsEventNameenum with typed payload discriminated unions ChannelTypeenum for channel type comparisons (no raw strings)targetValue(e)/targetChecked(e)helpers eliminate(e.target as HTMLInputElement).valuecasts
Shared Component Library
Reusable components in components/forms/:
- Form fields:
TextField,TextAreaField,SelectField,CheckboxField - Layout:
SectionHeading,SubHeading,SettingsCard,DangerZone - Lists:
ListItem,Badge,EmptyState,Loading,CopyButton - Navigation:
TabBar - State:
useSaveState()hook,SaveButton,StatusMessage
Asset Serving
The Rust moltis-web crate serves assets with three-tier resolution:
- Dev filesystem —
MOLTIS_ASSETS_DIRenv var or auto-detected from the crate source tree (cargo rundev mode) - External share dir —
share_dir()/web/for packaged deployments - Embedded fallback —
include_dir!compiled into the binary
HTML templates are rendered by Askama
with server-injected data (window.__MOLTIS__, the “gon” pattern).
E2E Test Compatibility
E2E tests dynamically import individual JS modules (e.g.,
await import("js/state.js")) to inspect and mock internal app state.
With Vite bundling, individual modules don’t exist as standalone files.
Shim layer: small proxy files in src/assets/js/ re-export from
window.__moltis_modules (populated by app.tsx at startup). This
lets tests import modules at their original paths without changes.
The shims are only loaded by E2E tests, never by the production app.
Development Workflow
After changing TypeScript source files:
cd crates/web/ui
# 1. Type check
npx tsc --noEmit
# 2. Lint and format
biome check --write src/
# 3. Build (commits dist/ output)
npm run build
# 4. Run E2E tests
npx playwright test --project default
For CSS changes, also run npm run build:css and commit style.css.