--- description: >- The React + Vite frontend (`app/src/`) - architecture, state, services, providers, routing, components, hooks. icon: browsers --- # 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](frontend.md#architecture-overview) | Provider chain, build, layout, conventions | | [State Management](frontend.md#state-management) | Redux Toolkit slices, selectors, persistence | | [Services Layer](frontend.md#services-layer) | `apiClient`, `socketService`, `coreRpcClient` | | [Providers](frontend.md#providers) | `ThemeProvider`, `CoreState`, `Socket`, `ChatRuntime` providers | | [Pages & Routing](frontend.md#pages-routing) | `HashRouter`, route guards, main routes | | [Components](frontend.md#components) | UI / settings component patterns | | [Hooks & Utilities](frontend.md#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](../architecture.md) - Tauri shell: [Tauri Shell](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:` 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 = 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("/users/me"); const result = await apiClient.post("/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({ 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](frontend.md#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](frontend.md#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](frontend.md#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 `` 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//`. 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](frontend.md#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("/users/me"); const result = await callCoreRpc({ method: "openhuman.app_state_snapshot", }); ``` ---