> For the complete documentation index, see [llms.txt](https://tinyhumans.gitbook.io/openhuman/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tinyhumans.gitbook.io/openhuman/developing/architecture/frontend.md).

# Frontend (app/src/)

The OpenHuman desktop UI: a Vite + React 19 tree under `app/src/` (pnpm workspace `openhuman-app`). It uses Redux Toolkit with persistence for session state, talks to the in-process Rust core over JSON-RPC (`coreRpcClient` → local HTTP, with the Tauri `relay_http_rpc` command as a fallback relay) and socket.io (`socketService`), and reaches the cloud backend via REST (`apiClient`). Heavy logic lives in the core, not here.

This is one consolidated reference. Use the table of contents above (or your reader's outline) to jump between sections.

## Quick reference

| Section                                | Covers                                                          |
| -------------------------------------- | --------------------------------------------------------------- |
| [Architecture](#architecture-overview) | Provider chain, build, layout, conventions                      |
| [State Management](#state-management)  | Redux Toolkit slices, selectors, persistence                    |
| [Services Layer](#services-layer)      | `apiClient`, `socketService`, `coreRpcClient`                   |
| [Providers](#providers)                | `ThemeProvider`, `CoreState`, `Socket`, `ChatRuntime` providers |
| [Pages & Routing](#pages-routing)      | `HashRouter`, route guards, main routes                         |
| [Components](#components)              | UI / settings component patterns                                |
| [Hooks & Utilities](#hooks-utilities)  | Shared hooks, helpers, config                                   |

## Scale

| Metric                                  | Value                                                                     |
| --------------------------------------- | ------------------------------------------------------------------------- |
| TypeScript / TSX files under `app/src/` | \~1700 (`find app/src -name '*.ts' -o -name '*.tsx' \| wc -l` to refresh) |
| Test runner                             | Vitest (`app/test/vitest.config.ts`)                                      |

## Directory layout

```
app/src/
├── App.tsx                 # Provider chain + HashRouter shell (desktop + mobile shells)
├── AppRoutes.tsx           # Desktop route table (AppRoutesIOS.tsx for mobile)
├── main.tsx                # Entry (polyfills, Sentry, store, styles)
├── store/                  # Redux slices, selectors, userScopedStorage persistence
├── providers/              # ThemeProvider, CoreStateProvider, SocketProvider, ChatRuntimeProvider
├── services/               # apiClient, socketService, coreRpcClient, transport/, api/* (~50 modules)
├── lib/                    # AI prompt loaders, i18n, MCP helpers, platform, tunnel crypto
├── pages/                  # Route-level screens (incl. onboarding/, ios/, dev/)
├── features/               # Feature verticals (human/, conversations/, meet/, voice/)
├── components/             # Shared UI (incl. settings/, layout/shell/, accounts/)
├── agentworld/             # tiny.place Agent World surface (/agent-world/*)
├── hooks/                  # App hooks
├── utils/                  # Config, Tauri command wrappers, routing utilities
└── assets/                 # Icons and static assets
```

## Architecture overview

### System architecture

OpenHuman’s desktop UI is a **React 19** app (`app/src/`) that:

* Uses **Redux Toolkit** with persistence for session-related state
* Connects to the backend with **REST** (`apiClient`) and to the local core with **Socket.io** (`socketService` → core socket endpoint)
* Calls the **Rust core** (embedded in the Tauri host as a tokio task) over HTTP via **`coreRpcClient`** (JSON-RPC methods implemented in repo root `src/openhuman/`); non-loopback plain-http runtimes are relayed through the Tauri **`relay_http_rpc`** command
* Leaves **AI prompts** to the core: bundled `src/openhuman/agent/prompts` (repo root) ship as Tauri resources and are read core-side, not by the frontend
* Uses a **minimal MCP-style** helper layer under `lib/mcp/` (transport, validation)

### Entry points

| File                    | Purpose                                                                          |
| ----------------------- | -------------------------------------------------------------------------------- |
| `app/src/main.tsx`      | React root, polyfills, Sentry boundary, store, global styles                     |
| `app/src/App.tsx`       | Provider chain (see below) + desktop/mobile shells, Settings modal overlay       |
| `app/src/AppRoutes.tsx` | `HashRouter` routes, `ProtectedRoute` / `PublicRoute` / `DefaultRedirect` guards |

### Provider chain

*Generated from `app/src/App.tsx` by `scripts/generate-architecture-docs.mjs`. Do not edit by hand — run `pnpm docs:generate` to refresh.*

| #  | Component              | Role                                                                        |
| -- | ---------------------- | --------------------------------------------------------------------------- |
| 1  | `Sentry.ErrorBoundary` | Crash boundary; renders ErrorFallbackScreen                                 |
| 2  | `Provider`             | Redux store; enables useAppSelector / dispatch app-wide                     |
| 3  | `PersistGate`          | Holds UI until persisted Redux slices rehydrate                             |
| 4  | `ThemeProvider`        | Theme tokens and dark-mode handling                                         |
| 5  | `I18nProvider`         | Localization context consumed via useT                                      |
| 6  | `BootCheckGate`        | Blocks render until the core boot snapshot resolves                         |
| 7  | `CoreStateProvider`    | Core app snapshot: auth, session, onboarding state                          |
| 8  | `SocketProvider`       | Core socket.io events; desktop only (mobile uses the TunnelTransport relay) |
| 9  | `ChatRuntimeProvider`  | Chat runtime events, tool timeline, and approvals                           |
| 10 | `Router`               | HashRouter navigation for all routes                                        |
| 11 | `CommandProvider`      | Command palette context                                                     |
| 12 | `ServiceBlockingGate`  | Blocks the shell until required services are configured                     |

**Why this order**

1. Redux `Provider` is outermost so `useAppSelector` / dispatch work everywhere.
2. `PersistGate` rehydrates persisted slices before children assume stable auth/session.
3. `BootCheckGate` / `CoreStateProvider` resolve the core boot snapshot (auth, onboarding) before feature providers mount.
4. `SocketProvider` (desktop only) and `ChatRuntimeProvider` depend on that core state for realtime events and approvals.
5. `Router` supplies navigation to all routes.

### Module relationships (simplified)

```
App.tsx
  ├─ Redux store + persistor
  ├─ ThemeProvider / I18nProvider - theme tokens, useT() localization
  ├─ BootCheckGate - waits for the core boot snapshot
  ├─ CoreStateProvider - auth/session/onboarding snapshot (fetchCoreAppSnapshot RPC)
  ├─ SocketProvider - socket.io connection to the local core (desktop only)
  ├─ ChatRuntimeProvider - chat streaming, tool timeline, approvals → Redux
  └─ AppShell (desktop or mobile)
       ├─ AppRoutes - PublicRoute / ProtectedRoute / DefaultRedirect
       ├─ SettingsModal - overlay mounted when the URL is /settings/*
       └─ WebviewHost - active connected-app CEF webview overlay
```

### Services layer (conceptual)

```
services/
  ├─ apiClient        → REST to a URL resolved at runtime via `services/backendUrl#getBackendUrl`
  ├─ backendUrl       → Calls `openhuman.config_resolve_api_url`; falls back to VITE_BACKEND_URL only outside Tauri
  ├─ socketService    → Socket.io to the local core (base URL derived from the RPC URL); MCP-style envelopes
  ├─ coreRpcClient    → JSON-RPC over HTTP to the local openhuman core; `relay_http_rpc` fallback for non-loopback http
  └─ transport/       → ConnectionProfile transports for iOS/remote (LanHttp, Tunnel, CloudHttp)
```

#### Runtime config precedence

The desktop app does not bake the core RPC URL or the API host into the bundle as a hard requirement. At runtime the app resolves them in this order (highest first):

1. **Welcome-screen RPC URL field**, saved via `utils/configPersistence` and restored on next launch. End users configure a self-hosted core address here, not by hand-editing `config.toml` or `.env` files.
2. **Tauri `core_rpc_url` command**, the port the embedded core is listening on for this process.
3. **`VITE_OPENHUMAN_CORE_RPC_URL`**, build-time fallback for development.
4. The hardcoded `http://127.0.0.1:7788/rpc` default.

Once the RPC handshake succeeds, `services/backendUrl` calls `openhuman.config_resolve_api_url` to pull `api_url` (and other safe client fields) from the loaded core `Config`. `VITE_BACKEND_URL` is only used as a web fallback when the app runs outside Tauri.

Components that need the backend URL should call `useBackendUrl()` (or `getBackendUrl()` from non-React code), they must not import the static `BACKEND_URL` constant from `utils/config`, which represents the build-time value only.

### Related docs

* Rust architecture: [Architecture](https://github.com/tinyhumansai/openhuman/tree/main/gitbooks/developing/architecture.md)
* Tauri shell: [Tauri Shell](/openhuman/developing/architecture/tauri-shell.md)

## State Management

The application uses Redux Toolkit with Redux-Persist. There is no single root persist config: each slice that persists wraps its own reducer with `persistReducer` in **`store/index.ts`**, whitelisting exactly the fields that should survive a restart.

### Storage backends

* **`userScopedStorage`** (`store/userScopedStorage.ts`) — the default storage for persisted slices. Blobs are keyed `${userId}:persist:<key>` so state never leaks across users on logout/login (#900).
* **Plain `localStorage`** — used only for pre-login, device-wide slices (`coreMode`, `locale`, `theme`) that must survive user switches.

### Slices

Authoritative list = the `reducer` map in `store/index.ts`. One-line purposes:

| Slice                | Purpose                                                                 | Persisted?                                                     |
| -------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------- |
| `accounts`           | Connected web-app (CEF webview) accounts + rail ordering                | `accounts`, `order`, `lastActiveAccountId` (not the active id) |
| `agentProfiles`      | Agent profile data                                                      | no                                                             |
| `announcement`       | Harness-init announcement banner, seen ids                              | `shownIds`                                                     |
| `backendMeet`        | Backend-driven Google Meet call state (join/leave, transcript, replies) | no                                                             |
| `channelConnections` | Messaging channel connections (WhatsApp, Slack, …)                      | connections + migration/default-channel fields                 |
| `chatRuntime`        | Streaming buffers, tool timelines, inference status, artifacts          | only `artifactsByThread` (ready snapshots)                     |
| `companion`          | Companion overlay state                                                 | no                                                             |
| `connectivity`       | navigator.onLine + backend/core health status                           | no                                                             |
| `coreMode`           | Pre-login core mode selection (embedded / self-hosted / cloud)          | `mode` (plain localStorage)                                    |
| `layout`             | Two-pane layout geometry (sidebar visibility, dragged widths)           | `panels`                                                       |
| `locale`             | UI language                                                             | `current` (plain localStorage)                                 |
| `mascot`             | Mascot appearance / voice selection                                     | `color`, `voiceId`, `customMascotGifUrl`, `selectedMascotId`   |
| `notifications`      | Notification items + preferences                                        | `items`, `preferences`                                         |
| `persona`            | Cosmetic persona display name + description (SOUL.md lives in the core) | `displayName`, `description`                                   |
| `providerSurfaces`   | Provider webview surface state                                          | no                                                             |
| `ptt`                | Push-to-talk hotkey + session prefs (`isHeld` deliberately excluded)    | `shortcut`, `speakReplies`, `showOverlay`                      |
| `socket`             | Per-user socket connection status / socket ids                          | no (reconnects on boot)                                        |
| `theme`              | Theme mode, font size, message view mode, custom themes                 | plain localStorage                                             |
| `thread`             | Chat thread list + per-thread message caches                            | only `selectedThreadId`                                        |
| `userErrors`         | User-actionable runtime errors (#3931)                                  | no (in-memory only)                                            |

Ephemeral chat state (streaming buffers, tool timelines) must **not** survive a restart — the UI would try to resume a turn whose live driver is gone. The one exception, agent-generated artifacts, goes through the `artifactsReadyOnlyTransform` in `store/index.ts` (pure logic in `store/artifactsPersistFilter.ts`).

### Typed hooks

**File:** `store/hooks.ts`

```typescript
// Use these instead of plain useDispatch/useSelector
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```

### Best practices

1. **Always use typed hooks** — `useAppDispatch` and `useAppSelector`.
2. **Use selectors for derived state** — see `store/socketSelectors.ts`, `store/connectivitySelectors.ts`, `store/userErrorsSelectors.ts`.
3. **Whitelist persistence per slice** — never persist transient/loading state; add a per-slice `persistReducer` in `store/index.ts`.
4. **Prefer Redux over ad-hoc `localStorage`** — plain localStorage is reserved for the pre-login slices noted above.
5. In dev / E2E builds the store is exposed as `window.__OPENHUMAN_STORE__` so WDIO specs can assert backing state; production bundles do not expose it.

***

## Services Layer

The application uses singleton services for external communication. This prevents connection leaks and provides consistent API access.

### Service architecture

```
app/src/services/
  ├─ apiClient (HTTP REST)
  │   └─ backend URL resolved at runtime (services/backendUrl)
  ├─ socketService (Socket.io)
  │   └─ connects to the local core's socket endpoint (base derived from the RPC URL)
  ├─ coreRpcClient.ts
  │   ├─ direct webview fetch → local openhuman core (JSON-RPC over HTTP)
  │   └─ invoke('relay_http_rpc', …) fallback for non-loopback plain-http runtimes
  ├─ coreCommandClient.ts - typed wrappers over core RPC methods
  ├─ transport/ - ConnectionProfile transports (LanHttp, Tunnel, CloudHttp) for iOS/remote
  └─ services/api/* - domain API modules (~50 files, see below)
```

### API Client (`services/apiClient.ts`)

Fetch-based HTTP REST client for backend communication with typed request/response handling and error handling. The backend URL is resolved at runtime (`services/backendUrl`), not baked in.

```typescript
import apiClient from "../services/apiClient";

const user = await apiClient.get<User>("/users/me");
const result = await apiClient.post<LoginResponse>("/auth/login", {
  email,
  password,
});
```

### Domain API modules (`services/api/`)

\~50 domain-scoped modules, one per feature surface, each wrapping either backend REST endpoints or core RPC methods. Representative examples:

* `authApi` / `userApi` — auth + user profile
* `threadApi`, `threadGoalApi`, `threadUsageApi` — chat threads
* `agentProfilesApi`, `agentTeamApi`, `agentWorkApi`, `subagentApi` — agents
* `skillsApi`, `skillRegistryApi`, `flowsApi`, `workflowRunsApi`, `todosApi` — skills & automation
* `channelConnectionsApi`, `mcpClientsApi`, `mcpSetupApi`, `tunnelsApi` — connections
* `memoryTimelineApi`, `memoryFreshnessApi`, `graphCentralityApi`, `namespaceOverviewApi` — memory/graph
* `billingApi`, `creditsApi`, `referralApi`, `rewardsApi`, `inviteApi` — commerce
* `voiceSettingsApi`, `voiceInstallApi`, `aiSettingsApi`, `modelCouncilApi` — AI/voice config

For the full list, `ls app/src/services/api/`. New feature surfaces get their own module here rather than growing `apiClient`.

### Socket Service (`services/socketService.ts`)

Socket.io client singleton connected to the **local core's** socket endpoint (base URL derived from the resolved RPC URL via `coreSocket.ts`; authenticated with the core RPC token). It ingests realtime core events — chat/meet/channel/companion updates — and dispatches them into Redux (`socketSlice`, `backendMeetSlice`, `channelConnectionsSlice`, `companionSlice`, `connectivitySlice`). It also hosts the MCP-style transport (`SocketIOMCPTransportImpl` from `lib/mcp`).

Keep `socketService` and the core socket behavior aligned (the "dual socket sync" rule in AGENTS.md). Connection lifecycle is owned by `providers/SocketProvider.tsx`; on mobile the provider is not mounted at all — events arrive through the `TunnelTransport` relay instead.

### Core RPC (`services/coreRpcClient.ts`)

The Rust core runs **in-process** inside the Tauri host (no sidecar). The UI calls JSON-RPC methods on it over local HTTP:

```typescript
import { callCoreRpc } from "../services/coreRpcClient";

const result = await callCoreRpc<MyType>({
  method: "openhuman.some_method",
  params: {
    /* … */
  },
  timeoutMs: 60_000, // optional per-call override (default 30s)
  suppressAuthExpiredEvent: false, // narrow reads can opt out of global sign-out on 401
});
```

How a call flows:

1. **URL + token resolution** — the RPC URL follows the precedence in [Runtime config precedence](#runtime-config-precedence); the per-launch bearer token comes from the Tauri `core_rpc_token` command (or the stored token for self-hosted cores).
2. **Direct fetch** — the webview `fetch()`es the JSON-RPC envelope straight to the core (loopback http or any https URL).
3. **Shell relay fallback** — plain `http://` to a **non-loopback** host is active mixed content and Chromium blocks it (#3865). `rpcUrlNeedsShellRelay()` detects this and routes the call through `invoke('relay_http_rpc', { url, token, body })`, implemented in **`app/src-tauri/src/core_rpc.rs`**, which returns `{ status, body }` re-wrapped as a `Response`.
4. **Transport override** — iOS/remote connection profiles install a `CoreTransport` (`setActiveCoreTransport`) so the same `callCoreRpc` surface rides LAN/tunnel/cloud transports.

Errors are classified into a stable `CoreRpcError.kind` (`auth_expired`, `transport`, `timeout`, `rate_limited`, …) — callers branch on `kind`, never on message regexes. An `auth_expired` classification broadcasts `core-rpc-auth-expired`, which `CoreStateProvider` turns into a session clear.

### Best Practices

1. **Use singletons** — never create multiple service instances.
2. **Keep Tauri IPC and RPC calls in services** — do not scatter `invoke()` or raw fetches through components.
3. **Clean up on unmount** — disconnect in `useEffect` cleanup.
4. **Handle errors via `CoreRpcError.kind`** — retry only transient failures.

***

## Providers

React context providers (`app/src/providers/`) manage service lifecycle and expose core-owned state. The full nesting (including gates that live in `components/`) is the generated [provider chain](#provider-chain) above. There is **no** `UserProvider`, `AIProvider`, or `SkillProvider` — auth/user state lives in `CoreStateProvider`, AI configuration lives in the Rust core, and skills execute in the core (the frontend QuickJS skills engine was removed).

### ThemeProvider (`providers/ThemeProvider.tsx`)

Applies theme tokens and dark-mode handling from the persisted `theme` slice (mode, font size, custom themes).

### CoreStateProvider (`providers/CoreStateProvider.tsx`)

The authoritative auth/session/onboarding context. Fetches the core app snapshot (`fetchCoreAppSnapshot()` RPC), exposes it via `useCoreState()` (`{ snapshot, isBootstrapping, refresh }`), and clears the session on the global `core-rpc-auth-expired` event. It follows a **turn-boundary refetch contract**: after every agent reply completes (`chat_done` in `ChatRuntimeProvider`) it refetches the user state (debounced 750ms) and merges it into the snapshot via `patchSnapshot` — see `providers/README.md`.

### SocketProvider (`providers/SocketProvider.tsx`)

Owns the socket.io connection to the local core: connects once core state is ready, updates the `socket` slice, and tears down on unmount. Desktop only — `App.tsx` skips it on mobile, where events arrive through the `TunnelTransport` relay.

### ChatRuntimeProvider (`providers/ChatRuntimeProvider.tsx`)

Subscribes to chat runtime socket events (message streaming, tool calls, subagent lifecycle, approval requests) and reduces them into the `chatRuntime` slice — per-thread tool timelines, streaming buffers, artifacts, and approval state consumed by the chat surface and the mascot.

### Gates and shell-level contexts (in `components/`)

* **`BootCheckGate`** (`components/BootCheckGate/`) — blocks render until the core boot snapshot resolves.
* **`CommandProvider`** (`components/commands/`) — command palette context.
* **`ServiceBlockingGate`** (`components/daemon/`) — blocks the shell until required services are configured.

### Context vs Redux

| Use Context For                    | Use Redux For                      |
| ---------------------------------- | ---------------------------------- |
| Service instances (socket, client) | Serializable state (status, data)  |
| Methods (emit, on, off)            | Persisted state (sessions, tokens) |
| Derived values                     | Complex state logic                |

Example: `SocketProvider` owns the socket instance; Redux stores connection status in `socketSlice`.

***

## Human Mascot Surface

The Human page (`app/src/features/human/HumanPage.tsx`) renders the main `YellowMascot` beside the conversation sidebar. The mascot face still comes from `useHumanMascot`, which subscribes to chat lifecycle events for thinking, speaking, acknowledgement, and error states.

Sub-agent delegation is visualized by `SubMascotLayer`. It does not introduce a new socket protocol. Instead, it reads the selected or active thread's `chatRuntime.toolTimelineByThread` entries that `ChatRuntimeProvider` already builds from `subagent_spawned`, `subagent_completed`, `subagent_failed`, `subagent_iteration_start`, `subagent_tool_call`, and `subagent_tool_result`.

Lifecycle mapping:

| Runtime timeline state | Sub-mascot state                                                     |
| ---------------------- | -------------------------------------------------------------------- |
| `running`              | Small colored mascot in a thinking face with a short activity bubble |
| `success`              | Same mascot resolves to a happy face and completion bubble           |
| `error`                | Same mascot resolves to a concerned face and failure bubble          |

Activity bubble text is intentionally compact: current child tool call, child iteration, the delegation prompt excerpt, or final status. The thread timeline remains the authoritative detailed view; sub-mascots are only the glanceable orchestration layer around the main mascot.

***

## Pages & Routing

The application uses HashRouter with protected and public route guards. Desktop routes live in **`app/src/AppRoutes.tsx`**; on mobile (iOS/Android) `AppRoutesIOS.tsx` renders a reduced Human/Chat/Settings set instead.

### Route map

Current desktop routes (read `AppRoutes.tsx` for the authoritative table — the file is heavily commented with the rationale for each redirect):

```
/                      → Welcome (PublicRoute; redirects to /home if logged in)
/auth                  → WebCallbackPage (auth callback)
/callback/:kind[/:status] → WebCallbackPage (generic OAuth/provider callbacks)
/onboarding/*          → Onboarding stepper (ProtectedRoute)
/human                 → HumanPage (mascot surface)
/brain                 → Brain (memory knowledge-graph)
/flows                 → FlowsPage · /flows/draft → draft canvas · /flows/:id → FlowCanvasPage
/orchestration         → OrchestrationPage (TinyPlace multi-agent coordination)
/workflows/run         → WorkflowsRun (single-purpose Skill runner)
/connections           → Skills page (connections hub)
/chat/:threadId?       → Accounts (unified chat: agent + connected web apps)
/invites               → Invites
/feedback              → Feedback
/notifications         → Notifications
/rewards               → Rewards
/ptt-overlay           → PttOverlayPage (push-to-talk overlay window)
/dev/agent-insights    → dev-only preview
/agent-world/*         → AgentWorld (tiny.place A2A social network)
*                      → DefaultRedirect
```

Back-compat redirects (all `Navigate replace`, query params preserved):

```
/home        → /chat                     /skills      → /connections
/activity    → /settings/notifications   /channels    → /connections?tab=messaging
/intelligence→ /settings/notifications   /routines    → /settings/automations
/workflows   → /settings/automations     /webhooks    → /settings/integrations#webhooks
/brain/tinyplace-orchestration → /orchestration
```

There is **no** `/login` route — authentication flows through the Welcome page, the `/auth` callback, and deep links. Desktop **Settings is not an inline route**: when the URL is `/settings/*`, `AppShellDesktop` keeps rendering the *background* location and mounts `SettingsModal` on top (see [Settings](#settings)). Note that `/agents` does not exist; the agent-social surface is `/agent-world/*`.

### Route guards

All three guards read `useCoreState()` (not Redux auth state) and render `RouteLoadingScreen` while bootstrapping:

* **`ProtectedRoute`** (`components/ProtectedRoute.tsx`) — `({ children, requireAuth = true, redirectTo })`; without a session token, navigates to `redirectTo || '/'`. Onboarding gating is *not* done here — an effect in `AppShellDesktop` (App.tsx) forces non-onboarding routes back to `/onboarding` while `onboarding_completed` is false, and bounces off it once complete.
* **`PublicRoute`** (`components/PublicRoute.tsx`) — redirects signed-in users to `/home` (which forwards to `/chat`).
* **`DefaultRedirect`** (`components/DefaultRedirect.tsx`) — signed out → `/`; signed in but onboarding incomplete → `/onboarding`; otherwise → `/chat`. Waits for `snapshot.currentUser` to avoid the post-login race.

### Onboarding Flow (`pages/onboarding/`)

A routed stepper (`Onboarding.tsx` mounts nested routes inside `OnboardingLayout`):

```
/onboarding/welcome         → WelcomePage
/onboarding/runtime-choice  → RuntimeChoicePage
  ├── cloud  → /chat
  └── custom → /onboarding/custom/inference → voice → oauth → search
               → embeddings → (activity) → vault → /chat
```

Each custom step offers **Default** (let OpenHuman manage it) vs **Configure** (inline controls, or a deep-link callout to Settings for domains not yet embedded). Pages live in `pages/onboarding/pages/`; the legacy Composio/skills/context-gathering steps (`pages/onboarding/steps/`) are retired from the default flow but remain on disk. Completion is tracked by the core's `onboarding_completed` flag, enforced by the AppShell onboarding gate. After onboarding, `AppWalkthrough` (Joyride) runs the post-onboarding tour.

### Settings

Settings is a full `/settings/*` URL surface, presented on desktop as a **modal overlay** and on iOS as a full page. The old `SettingsPanelLayout` / `useSettingsAnimation` / `ProfilePanel` modal system is gone.

* **`components/settings/settingsRouteRegistry.ts`** — single declarative source of truth for every settings destination (id/route slug, i18n keys, section, sidebar `navGroup`, `devOnly`, search keywords). Navigation menus, breadcrumbs, and settings search all derive from it.
* **`components/settings/settingsRouteElements.tsx`** — maps registry entries to panel `<Route>` elements.
* **`components/settings/modal/`** — `SettingsModal` (mounted by `AppShellDesktop` whenever the path is a settings path; `settingsOverlay.ts` computes `{ settingsOpen, baseLocation }` so the page behind stays rendered), `SettingsModalFrame` (backdrop / Esc / focus / close), `SettingsModalLayout` (routed two-column layout).
* **`components/settings/layout/`** — two-pane chrome: `SettingsLayout`, `SettingsSidebar` (grouped by `SettingsNavGroup`: general, assistant, data, connections, knowledge & memory, agents & autonomy, models & inference, automation & integrations, diagnostics & logs), `SettingsSubNav`, `SettingsIndexRedirect`.
* **`components/settings/panels/`** — \~50 leaf panels (`AccountPanel`, `AppearancePanel`, `AIPanel`, `AgentsPanel`, `AgentAccessPanel`, `AutonomyPanel`, `BillingPanel`, `CronJobsPanel`, `IntegrationsPanel`, `McpServerPanel`, `NotificationsTabbedPanel`, `PrivacyPanel`, `DeveloperOptionsPanel`, …). Adding a panel = add the component + a registry entry; nav, breadcrumbs, and search pick it up automatically.
* **`components/settings/search/`** — settings search bar + registry-derived index.

### HashRouter vs BrowserRouter

The app uses HashRouter for desktop compatibility:

```typescript
// App.tsx
import { HashRouter } from "react-router-dom";

// URLs look like: app://localhost/#/home
// Instead of: app://localhost/home
```

**Why HashRouter:**

1. Tauri deep links work with hash-based URLs
2. No server configuration needed
3. Works with file:// protocol
4. Prevents 404 on direct URL access

### Deep Link Handling

Deep links are handled before routing:

```typescript
// main.tsx
import("./utils/desktopDeepLinkListener").then((m) => {
  m.setupDesktopDeepLinkListener().catch(console.error);
});
```

The listener intercepts `openhuman://` URLs (e.g. auth handoff), exchanges tokens through the Rust side (bypassing CORS), stores the session, and navigates to the right route. See `utils/desktopDeepLinkListener.ts`.

***

## Components

Shared UI lives in `app/src/components/`; feature-specific UI lives in `app/src/features/<vertical>/`. Highlights:

```
components/
├── ProtectedRoute / PublicRoute / DefaultRedirect   # Route guards
├── layout/shell/            # RootShellLayout, AppSidebar, SidebarSlot (two-pane app chrome)
├── settings/                # Settings registry, modal, layout, panels, search (see above)
├── accounts/                # WebviewHost + connected-app (CEF webview) surfaces
├── BootCheckGate/, daemon/  # Boot + service gates in the provider chain
├── commands/                # CommandProvider (command palette)
├── Announcement/, upsell/, userErrors/, walkthrough/  # Shell-level overlays
├── keyring/, mcp-setup/, InitProgressScreen/          # Consent + init overlays
└── intelligence/            # Memory/vault surfaces (ObsidianVaultSection, VaultHealthChecklist, WorkflowsTab, …)
```

Conventions:

* **Modal via portal** — shell modals (Settings, link modal) render above routed content; the Settings modal uses the backgroundLocation pattern rather than unmounting the page underneath.
* **Controlled modals** — parents own `isOpen` state and pass `onClose`.
* **i18n everywhere** — all user-facing text goes through `useT()` (`lib/i18n/I18nContext`); CI enforces locale parity.
* **No dynamic imports** in production `app/src` code — static `import` / `import type` only.

***

## Hooks & Utilities

### Custom Hooks (`hooks/`)

\~40 app-level hooks. Representative examples:

* **`useUser`** — thin wrapper over `useCoreState()`; returns `{ user: snapshot.currentUser, isLoading, error, refetch }`. There is no standalone user store.
* **`useBackendUrl`** — runtime backend URL resolution (see [Runtime config precedence](#runtime-config-precedence)).
* **`useThreadQueries`** — chat thread fetching.
* **`useDaemonHealth` / `useDaemonLifecycle`** — core service health.
* **`useDictationHotkey` / `usePttHotkey`** — global hotkey managers.
* **`useDeveloperMode`**, **`useMediaQuery`**, **`useEscapeKey`**, **`useStickToBottom`** — UI utilities.
* Feature hooks: `useFlowRunProgress`, `useWorkflowBuilderChat`, `useConsciousItems`, `useSubconscious`, `useIntelligenceStats`, `useCostDashboard`, ….

Feature-local hooks live next to their feature under `features/*/`.

### Utilities

#### Configuration (`utils/config.ts`)

Centralized build-time environment variable access — **never read `import.meta.env` directly elsewhere**. These constants only carry the value baked into the bundle; for the **runtime** URL the app actually talks to, see `services/backendUrl` and `hooks/useBackendUrl`.

```typescript
// Build-time fallback only (used outside Tauri).
export const BACKEND_URL = /* VITE_BACKEND_URL || default */;
// Core RPC build-time fallback.
export const CORE_RPC_URL = /* VITE_OPENHUMAN_CORE_RPC_URL || 'http://127.0.0.1:7788/rpc' */;
// Dev flags, e.g.
export const DEV_FORCE_ONBOARDING = /* dev-only VITE_DEV_FORCE_ONBOARDING */;
```

> **Do not** import `BACKEND_URL` directly to make API calls. Resolve the URL at runtime so the core's `api_url` (via `openhuman.config_resolve_api_url`) takes effect:
>
> ```typescript
> // React components
> import { useBackendUrl } from "../hooks/useBackendUrl";
> const backendUrl = useBackendUrl();
>
> // Non-React code
> import { getBackendUrl } from "../services/backendUrl";
> const backendUrl = await getBackendUrl();
> ```

#### Desktop Deep Link Listener (`utils/desktopDeepLinkListener.ts`)

Handles incoming `openhuman://` deep links via the Tauri deep-link plugin: parses the URL, performs the Rust-side token exchange (bypasses CORS), stores the session, and navigates. Set up lazily from `main.tsx` so the Tauri IPC bridge is ready first.

#### URL Opener (`utils/openUrl.ts`)

Cross-platform URL opening — tries the Tauri opener plugin, falls back to `window.open`. Always use this instead of raw `window.open` so links open in the system browser.

#### Tauri command wrappers (`utils/tauriCommands/`)

Typed wrappers around `invoke(...)`, including the bridge-gap-aware `isTauri()` guard (checks `__TAURI_INTERNALS__.invoke` is actually wired, not merely that the app runs under Tauri). Use it — never check `window.__TAURI__` directly.

### Polyfills (`polyfills.ts`)

Node.js globals (`Buffer`, `process`, `util`) polyfilled for the browser. Several browser-side modules use Node APIs — e.g. voice/PTT audio encoding (`features/voice/pttAudio.ts`, `wavEncoder.ts`), mascot Rive asset caching (`features/human/Mascot/`), the Meet mascot frame producer, and tool-timeline formatting.

Two layers provide them:

1. **`vite-plugin-node-polyfills`** in `app/vite.config.ts` (`buffer`, `process`, `util`, `os`, `crypto`, `stream`, plus `Buffer`/`process`/`global` globals).
2. **`polyfills.ts`**, imported **first** in `main.tsx`, which synchronously assigns `Buffer`/`process`/`util` onto `globalThis`/`window`/`global`/`self` before any dependent module executes.

### Best Practices

#### Hook dependencies & cleanup

```typescript
useEffect(() => {
  on("event", handler);
  return () => off("event", handler);
}, [on, off, handler]);
```

Always include dependencies and always clean up subscriptions.

#### Error handling

Wrap Tauri/utility calls in try-catch with a fallback:

```typescript
try {
  await openUrl(url);
} catch (error) {
  console.error("Failed to open URL:", error);
}
```

#### Type safety

Use TypeScript generics for API and RPC calls:

```typescript
const user = await apiClient.get<User>("/users/me");
const result = await callCoreRpc<Snapshot>({
  method: "openhuman.app_state_snapshot",
});
```

***
