* feat(webview_accounts): native OS notifications from embedded webviews (#714) Forward CEF notification intercept payloads to tauri-plugin-notification, prefixing the title with the provider label so the source of each toast is obvious at a glance. Honour `silent` (skip toast, still record route), `icon` (passed through to the native builder), and `tag` (used as the dedup key, with a monotonic timestamp fallback for untagged payloads). Record a NotificationRoute keyed by `{provider}:{account_id}:{tag_or_uuid}` so a future click hook (UNUserNotificationCenter / notify-rust on_response) can route the OS click back to the source account. Entries are cleared on webview_account_close / _purge to bound map growth. Expose webview_notification_permission_state / _request commands mapping tauri::plugin::PermissionState onto the web API triple. Non-cef stubs return "default" so the frontend can call the same invoke names on both runtimes. Wire notification:allow-* capabilities so the plugin can be invoked from the webview. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(accounts): wire notification permission + click bridge (#714) Round-trip the OS notification permission once per session on first account open via the new invoke pair. Attach a dormant notification:click listener that dispatches setActiveAccount and brings the main window to front when a platform click hook starts emitting the event — contract matches the Rust NotificationRoute shape so the emit side is a one-liner. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: sync Cargo.lock to 0.52.26 after version bump Lockfile picked up the pending 0.52.26 version bump from Cargo.toml while building the notification feature. No dependency graph change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(notifications): add notification bypass for embedded webview apps (#679) - Add NotificationBypassPrefs (global DND, per-account mute, bypass-when-focused) to WebviewAccountsState with thread-safe AtomicBool window focus tracking - Evaluate all three bypass conditions inside forward_native_notification before showing OS toast; each suppression path logs at debug with [notify-bypass] prefix - Add four new Tauri commands: webview_notification_set_dnd, webview_notification_mute_account, webview_notification_get_bypass_prefs, webview_set_focused_account - Wire window focus tracking in setup hook via on_window_event Focused handler - Frontend: add setAccountMuted, setGlobalDnd, getBypassPrefs, setFocusedAccount helpers in webviewAccountService; sync focused account on open + click - Add NotificationsPanel settings page with Global DND toggle - Register NotificationsPanel at /settings/notifications Closes #679 * feat(notifications): integrate notifications feature into app - Added Notifications page and routing to AppRoutes. - Introduced NotificationRoutingPanel in Settings for managing notification settings. - Updated SettingsHome to include navigation for notification routing. - Integrated notifications reducer into the store for state management. - Enhanced Rust backend to support notification handling from embedded webviews. This commit lays the groundwork for a comprehensive notification system within the application. * refactor(notifications): clean up code formatting and structure - Simplified JSX structure in NotificationCard for better readability. - Consolidated fetchNotifications call in NotificationCenter for cleaner syntax. - Improved formatting in NotificationRoutingPanel and notificationsSlice for consistency. - Enhanced Rust code readability by streamlining function signatures and logic. These changes enhance code maintainability and readability across the notifications feature. * refactor(webview_accounts): simplify webview_notification_set_dnd function signature - Removed unnecessary line breaks in the webview_notification_set_dnd function for improved readability. * feat(notifications): implement provider-level notification settings management - Added `getNotificationSettings` and `setNotificationSettings` functions to manage notification settings for providers. - Enhanced `NotificationRoutingPanel` to display and update settings for Gmail, Slack, Discord, and WhatsApp. - Introduced new RPC endpoints for retrieving and updating notification settings. - Updated database schema to store notification settings persistently. This commit establishes a robust system for managing notification preferences, improving user control over notifications. * refactor(notifications): improve code formatting and readability - Enhanced formatting in NotificationRoutingPanel for better clarity. - Streamlined function signatures in notificationService and Rust backend. - Improved readability of assertions in tests by adjusting line breaks. These changes contribute to a more maintainable and comprehensible codebase for the notifications feature. * chore(vendor): bump tauri-cef to fix Slack notification permission banner Updates the tauri-cef submodule to 55db2d6 which adds a navigator.permissions.query shim in the CEF render process. Slack checks this API (not just Notification.permission) to decide whether to show its "needs your permission" banner — the shim returns "granted" for notifications queries so the banner no longer appears. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(vendor): bump tauri-cef for cargo fmt fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(vendor): bump tauri-cef — native V8 permissions.query shim Switches from context.eval() to a proper PermissionsQueryV8Handler so the navigator.permissions.query fix actually runs in on_context_created. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(notifications): patch navigator.permissions.query in ua_spoof.js The V8 set_value_bykey approach in cef-helper's on_context_created does not stick on CEF platform objects (Chromium's V8 binding layer silently ignores property writes on native wrappers like Permissions). The init script path via frame.execute_java_script runs in the fully-initialised JS context where navigator.permissions IS writable, matching how ua_spoof.js already overrides navigator.userAgent successfully. Slack checks navigator.permissions.query({ name: 'notifications' }) before showing its "needs permission" banner — patching it here to return "granted" removes the banner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(notifications): use Object.defineProperty to shim navigator.permissions Two-layer fix for the Slack "needs permission to enable notifications" banner: 1. cef-helper (submodule update to 99a2686): context.eval() in on_context_created installs Object.defineProperty(navigator, 'permissions', ...) before any page JS runs. 2. ua_spoof.js: same Object.defineProperty pattern as belt-and-suspenders for frames that reload or trigger permission checks after on_load_end. Simple property assignment on Blink platform objects is silently ignored; Object.defineProperty on the navigator wrapper itself (the same mechanism already used for navigator.userAgent) works correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(notifications): address CodeRabbit review issues on PR #727 - Move raw_title out of log::info! (PII risk) — log title_chars at info, raw_title at debug only - Fix permissionChecked set before async invoke in ensureNotificationPermission so transient failures allow retry on next account open * fix(cef): enable webview-data-url feature for CEF placeholder URL The CEF backend uses a data: URL as the initial webview location so CDP can attach before the real provider URL loads. Tauri's add_child rejects data: URLs unless the webview-data-url feature is enabled. * fix(notifications): address remaining CodeRabbit issues on PR #727 - cdp/emulation: bump Chrome UA 124→136 to pass Slack browser check - cdp/session: inject Page.addScriptToEvaluateOnNewDocument to stub Notification.permission as "granted" and silence provider banners - notifications/mod.rs + core/all.rs: wire notifications domain into the controller registry (fixes unknown-method in json_rpc_e2e tests) - notifications/schemas: add skipped bool output to ingest schema - notifications/store: add tracing::warn on datetime parse failure - notificationService: union return type for ingestNotification - webviewAccountService: narrow union before accessing result.id - NotificationCenter: drive loading/error from fetch effect; track allProviders separately so filter pills don't collapse on selection - NotificationRoutingPanel: rollback optimistic update on save failure - useSettingsNavigation: add notifications/notification-routing routes - scripts/install.sh: remove silent dry-run exit 0 on asset failure - scripts/setup-dev-codesign.sh: remove unconditional -legacy flag - docs/SUMMARY.md: remove worktree path, fix macOS capitalisation, remove self-referential deletion note * chore: apply prettier + cargo fmt + fix useEffect dep warning Auto-apply formatting changes flagged by the pre-push hook: - prettier reformatted NotificationCenter.tsx and notificationService.ts - cargo fmt reformatted all.rs, openhuman/mod.rs, notifications/schemas.rs - NotificationRoutingPanel: move providers array to module scope so useEffect dependency array is satisfied without exhaustive-deps warning * feat(notifications): enhance notification management and permissions - Added new commands for managing notification preferences, including setting global Do Not Disturb (DND), muting specific accounts, and retrieving current bypass preferences. - Implemented a notification permission state handler to ensure consistent behavior across different environments. - Updated the JavaScript shim for notification permissions to handle both Notification and PushManager states, ensuring compatibility with various providers. - Refactored the WebviewAccountsState to include a new structure for managing notification bypass preferences, improving the overall notification handling logic. * update agents * fix(notifications): complete schema + navigation metadata for ingest/settings routes - app/src/components/settings/hooks/useSettingsNavigation.ts: resolve the new `/settings/notifications` and `/settings/notification-routing` URLs to their SettingsRoute values and feed them into breadcrumbs so the new panels don't silently fall through to `'home'`. Addresses CodeRabbit on useSettingsNavigation.ts:34. - src/openhuman/notifications/schemas.rs: add the optional `reason` output on `notification.ingest` (populated alongside `skipped=true` by the runtime) and the normalized `settings` output on `notification.settings_set` so schema-driven clients see the full response shape. Addresses CodeRabbit on schemas.rs:103 and schemas.rs:217. - src/core/all.rs: add a `notification` namespace_description so CLI help covers the new controllers, plus a test assertion. Addresses CodeRabbit on src/core/all.rs:149. * fix(notifications): trace DB entry, surface empty update matches, warn on bad scored_at - Add `tracing::trace!` checkpoints around the `with_connection` DB open and schema migration so notification-delivery issues are reconstructible from logs. - `update_triage` and `mark_read` now inspect `Connection::execute`'s affected-row count: log a `warn!` when the update matched zero rows (row deleted between ingest and scoring / client passed a stale id), `debug!` on the normal path. - `scored_at` parsing no longer silently drops malformed values — log a `warn!` with the raw value and parse error before treating the row as unscored, matching the existing behavior for `received_at`. Addresses CodeRabbit on store.rs (lines 72, 172, 294). * fix(webview): respect silent notifications, multi-host CDP fallback, shim idempotency - webview_accounts/mod.rs: honor the Web Notification `silent` flag. Previously we only logged it and still called `builder.show()`, so pages that marked a notification silent still produced an OS toast. Mirror event still fires so the in-app center updates; only the OS toast is suppressed. Also picks up a prior cargo-fmt rewrap. - cdp/target.rs: `browser_ws_url()` now continues the host loop when `resp.json()` fails instead of early-returning via `?`. A malformed response from the first host (CDP_HOST) no longer prevents the `localhost` fallback from being tried. - webview_accounts/ua_spoof.js: guard the Notification wrapper behind `window.__OH_NOTIF_SHIM` so repeated evaluations of the script (Page.addScriptToEvaluateOnNewDocument + frame-level re-injections) don't stack wrappers onto the same page globals or re-proxy `Function.prototype.toString`. Addresses CodeRabbit on webview_accounts/mod.rs:377, cdp/target.rs:33, and ua_spoof.js:176. * update agents * update --------- Co-authored-by: oxoxDev <nikhil@tinyhumans.ai> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
14 KiB
OpenHuman
AI assistant for communities — React + Tauri v2 desktop app with a Rust core (JSON-RPC / CLI).
Narrative architecture: docs/ARCHITECTURE.md. Frontend: docs/src/README.md. Tauri shell: docs/src-tauri/README.md.
Repository layout
| Path | Role |
|---|---|
app/ |
Yarn workspace openhuman-app: Vite + React (app/src/), Tauri desktop host (app/src-tauri/), Vitest tests |
src/ (root) |
Rust lib openhuman_core + openhuman CLI binary — core_server, openhuman::* domains, MCP routing |
Cargo.toml (root) |
Core crate; cargo build --bin openhuman produces the sidecar staged by app's core:stage |
docs/ |
Architecture and module guides |
Commands assume the repo root; yarn dev delegates to the app workspace.
Runtime scope
- Shipped product: desktop — Windows, macOS, Linux.
- Tauri host (
app/src-tauri): desktop-only (compile_error!for other targets). No Android/iOS branches. - Core binary (
openhuman): spawned as a sidecar; the UI talks to it over HTTP (core_rpc_relay+core_rpcclient), not by duplicating domain logic.
Where logic lives
- Rust core: business logic, execution, domains, RPC, persistence, CLI. Authoritative.
- Tauri + React (
app/): UX, screens, navigation, bridging to the core. Presents and orchestrates only.
Commands (from repo root)
yarn dev # Frontend + Tauri dev
yarn tauri dev # Desktop with Tauri (loads env via scripts/load-dotenv.sh)
yarn build # Production UI build
yarn typecheck # Typecheck (app workspace)
yarn lint # ESLint
yarn format # Prettier write
yarn format:check # Prettier check
cd app && yarn core:stage # Stage openhuman binary next to Tauri resources
# Rust — core library + CLI
cargo check --manifest-path Cargo.toml
cargo build --manifest-path Cargo.toml --bin openhuman
# Rust — Tauri shell
cargo check --manifest-path app/src-tauri/Cargo.toml
Tests: Vitest in app/ (yarn test:unit, yarn test:coverage); Rust via cargo test.
Quality: ESLint + Prettier + Husky in app.
Configuration
.env.example— Rust core, Tauri shell, backend URL, logging, proxy, storage, AI binary overrides. Load viasource scripts/load-dotenv.sh.app/.env.example—VITE_*(core RPC URL, backend URL, Sentry DSN, dev helpers). Copy toapp/.env.local.
Frontend config is centralized in app/src/utils/config.ts. Read VITE_* there and re-export — never import.meta.env directly elsewhere.
Rust config uses a TOML Config struct (src/openhuman/config/schema/types.rs) with env overrides (src/openhuman/config/schema/load.rs).
Testing
Unit (Vitest)
- Co-locate as
*.test.ts/*.test.tsxunderapp/src/**. - Config:
app/test/vitest.config.ts; setup:app/src/test/setup.ts. - Run:
yarn test:unit,yarn test:coverage. - Prefer behavior over implementation. Use helpers in
app/src/test/. No real network, no time flakes.
Shared mock backend
Used by both unit and Rust tests.
- Core:
scripts/mock-api-core.mjs· server:scripts/mock-api-server.mjs· E2E wrapper:app/test/e2e/mock-server.ts. - Admin:
GET /__admin/health,POST /__admin/reset,POST /__admin/behavior,GET /__admin/requests. - Run manually:
yarn mock:api.
E2E (WDIO — dual platform)
Full guide: docs/E2E-TESTING.md.
- Linux (CI):
tauri-driver(WebDriver :4444). - macOS (local): Appium Mac2 (XCUITest :4723) on the
.appbundle. - Specs:
app/test/e2e/specs/*.spec.ts. Helpers inapp/test/e2e/helpers/. Config:app/test/wdio.conf.ts.
yarn test:e2e:build
bash app/scripts/e2e-run-spec.sh test/e2e/specs/smoke.spec.ts smoke
yarn test:e2e:all:flows
docker compose -f e2e/docker-compose.yml run --rm e2e # Linux E2E on macOS
Use element-helpers.ts (clickNativeButton, waitForWebView, clickToggle) — never raw XCUIElementType*. Assert UI outcomes and mock effects.
Deterministic core-sidecar reset
app/scripts/e2e-run-spec.sh creates and cleans a temp OPENHUMAN_WORKSPACE by default. OPENHUMAN_WORKSPACE redirects core config + storage away from ~/.openhuman.
Rust tests with mock
yarn test:rust
bash scripts/test-rust-with-mock.sh --test json_rpc_e2e
Frontend (app/src/)
Provider chain (App.tsx):
Redux → PersistGate → UserProvider → SocketProvider → AIProvider → SkillProvider → HashRouter → AppRoutes.
State (store/): Redux Toolkit slices — auth, user, socket, ai, skills, team, etc. Prefer Redux (persisted where configured) over ad-hoc localStorage.
Services (services/): singletons — apiClient, socketService, coreRpcClient (HTTP bridge to core), domain api/* clients.
MCP (lib/mcp/): JSON-RPC transport, validation, types over Socket.io. Tooling is driven by the backend + skills system.
Routing (AppRoutes.tsx): hash routes /, /onboarding, /mnemonic, /home, /intelligence, /skills, /conversations, /invites, /agents, /settings/*. No /login.
AI config: bundled prompts in src/openhuman/agent/prompts/ (also bundled via app/src-tauri/tauri.conf.json resources). Loaders in app/src/lib/ai/ use ?raw imports, optional remote fetch, and ai_get_config / ai_refresh_config in Tauri.
Tauri shell (app/src-tauri/)
Thin desktop host: window management, daemon health, core process lifecycle (core_process, CoreProcessHandle), JSON-RPC relay (core_rpc_relay, core_rpc).
Registered IPC (see docs/src-tauri/02-commands.md): greet, write_ai_config_file, ai_get_config, ai_refresh_config, core_rpc_relay, window commands, openhuman_* daemon helpers.
Rust core (src/)
openhuman/— Domain logic (memory, channels, config, cron, skills, webhooks, …). RPC controllers in per-domainrpc.rs; useRpcOutcome<T>perAGENTS.md.- Module layout rule: new functionality goes in a dedicated subdirectory (
openhuman/<domain>/mod.rs+ siblings). Do not add new standalone*.rsfiles atsrc/openhuman/root. - Controller schema contract: shared types in
src/core/mod.rs(ControllerSchema,FieldSchema,TypeSchema). - Domain schema files: per-domain
schemas.rs(e.g.src/openhuman/cron/schemas.rs), exported from domainmod.rs. - Controller-only exposure: expose features to CLI and JSON-RPC via the controller registry. Do not add domain branches in
src/core/cli.rs/src/core/jsonrpc.rs. - Light
mod.rs: keep domainmod.rsexport-focused. Operational code inops.rs,store.rs,types.rs, etc. core_server/— Transport only: Axum/HTTP, JSON-RPC envelope, CLI parsing, dispatch. No heavy logic.
Controller migration checklist
src/openhuman/<domain>/mod.rs: addmod schemas;, re-exportall_controller_schemas as all_<domain>_controller_schemasandall_registered_controllers as all_<domain>_registered_controllers.src/openhuman/<domain>/schemas.rsdefinesschemas,all_controller_schemas,all_registered_controllers, andhandle_*fns delegating to domainrpc.rs.- Wire exports into
src/core/all.rs. Remove migrated branches fromsrc/rpc/dispatch.rs.
Event bus (src/core/event_bus/)
Typed pub/sub + in-process typed request/response. Both singletons — use module-level functions; never construct EventBus / NativeRegistry directly.
- Broadcast (
publish_global/subscribe_global) — fire-and-forget. Many subscribers, no return. - Native request/response (
register_native_global/request_native_global) — one-to-one typed dispatch keyed by method string. Zero serialization — trait objects,mpsc::Sender,oneshot::Senderpass through unchanged. Internal-only; JSON-RPC-facing work goes throughsrc/core/all.rs.
Core types (all in src/core/event_bus/):
| Type | File | Purpose |
|---|---|---|
DomainEvent |
events.rs |
#[non_exhaustive] enum of all cross-module events |
EventBus |
bus.rs |
Singleton over tokio::sync::broadcast; ctor is pub(crate) |
NativeRegistry / NativeRequestError |
native_request.rs |
Typed request/response registry by method name |
EventHandler |
subscriber.rs |
Async trait with optional domains() filter |
SubscriptionHandle |
subscriber.rs |
RAII — drops cancel the subscriber |
TracingSubscriber |
tracing.rs |
Built-in debug logger |
Singleton API: init_global(capacity), publish_global(event), subscribe_global(handler), register_native_global(method, handler), request_native_global(method, req), global() / native_registry().
Domains: agent, memory, channel, cron, skill, tool, webhook, system.
Each domain owns a bus.rs with its EventHandler impls — e.g. cron/bus.rs (CronDeliverySubscriber), webhooks/bus.rs (WebhookRequestSubscriber), channels/bus.rs (ChannelInboundSubscriber). Convention: <Purpose>Subscriber + name() returning "<domain>::<purpose>".
Adding events: add variants to DomainEvent, extend the domain() match, create <domain>/bus.rs, register subscribers at startup, publish via publish_global.
Adding a native handler: define request/response types in the domain (owned fields, Arcs, channels — not borrows; Send + 'static, not Serialize). Register at startup keyed by "<domain>.<verb>". Callers dispatch via request_native_global.
Tests: re-register the same method to override; or construct a fresh NativeRegistry::new() for isolation.
Design
Premium, calm visual language — ocean primary #4A83DD, sage / amber / coral semantics, Inter + Cabinet Grotesk + JetBrains Mono, Tailwind with custom radii/spacing/shadows. See docs/DESIGN_GUIDELINES.md.
Shell vs app code
Tauri/Rust in the shell is a delivery vehicle (windowing, process lifecycle, IPC). Keep UI behavior and product logic in TypeScript/React (app/). Only grow Rust in the shell for hard platform/security reasons.
Git workflow
- Issues and PRs on upstream tinyhumansai/openhuman — not a fork — unless explicitly told otherwise.
- Issue templates:
.github/ISSUE_TEMPLATE/feature.md,.github/ISSUE_TEMPLATE/bug.md. PR template:.github/PULL_REQUEST_TEMPLATE.md. AI-authored text should follow them verbatim. - PRs target
main.
Coding philosophy
- Unix-style modules: small, sharp-responsibility units composed through clear boundaries.
- Tests before the next layer: ship unit tests for new/changed behavior before stacking features. Untested code is incomplete.
- Docs with code: new/changed behavior ships with matching rustdoc / code comments; update
AGENTS.mdor architecture docs when rules or user-visible behavior change.
Debug logging (must follow)
- Default to verbose diagnostics on new/changed flows so issues are easy to trace end-to-end.
- Log entry/exit, branches, external calls, retries/timeouts, state transitions, errors.
- Use stable grep-friendly prefixes (
[domain],[rpc],[ui-flow]) and correlation fields (request IDs, method names, entity IDs). - Rust:
log/tracingatdebug/trace.app/: namespaceddebug+ dev-only detail. - Never log secrets or full PII — redact.
- Changes lacking diagnosis logging are incomplete.
Feature design workflow
Specify → prove in Rust → prove over RPC → surface in the UI → test.
- Specify against the current codebase — ground in existing domains, controller/registry patterns, JSON-RPC naming (
openhuman.<namespace>_<function>). No parallel architectures. - Implement in Rust — domain logic under
src/openhuman/<domain>/, schemas + handlers in the registry, unit tests until correct in isolation. - JSON-RPC E2E — extend
tests/json_rpc_e2e.rs/scripts/test-rust-with-mock.shso RPC methods match what the UI will call. - UI in Tauri app — React screens/state using
core_rpc_relay/coreRpcClient. Keep rules in the core. - App unit tests — Vitest.
- App E2E — desktop specs for user-visible flows.
Capability catalog: when a change adds/removes/renames a user-facing feature, update src/openhuman/about_app/ in the same work.
Planning rule: up front, define the E2E scenarios (core RPC + app) that cover the full intended scope — happy paths, failure modes, auth gates, regressions. Not testable end-to-end ⇒ incomplete spec or too-large cut.
Key patterns
- File size: prefer ≤ ~500 lines; split growing modules.
- Pre-merge (code changes): Prettier, ESLint,
tsc --noEmitinapp/;cargo fmt+cargo checkfor changed Rust. - No dynamic imports in production
app/srccode — staticimport/import typeonly. Noimport(),React.lazy(() => import(...)),await import(...). For heavy optional paths, use a static import and guard the call site withtry/catchor a runtime check. Exceptions: Vitest harness patterns in*.test.ts/__tests__/test/setup.ts; ambienttypeof import('…')in.d.ts; config files (e.g.tailwind.config.jsJSDoc). - Dual socket sync: when changing the realtime protocol, keep
socketService/ MCP transport aligned with core socket behavior (seedocs/ARCHITECTURE.mddual-socket section).
Platform notes
- Vendored CEF-aware
tauri-cli: runtime is CEF; only the vendored CLI atapp/src-tauri/vendor/tauri-cef/crates/tauri-clibundles Chromium intoContents/Frameworks/. Stock@tauri-apps/cliproduces a broken bundle (panic incef::library_loader::LibraryLoader::new).yarn dev:appand allcargo tauriscripts callyarn tauri:ensurewhich runsscripts/ensure-tauri-cli.sh. If overwritten, reinstall withcargo install --locked --path app/src-tauri/vendor/tauri-cef/crates/tauri-cli. - macOS deep links: often require a built
.appbundle, not justtauri dev. window.__TAURI__: not present at module load; guard Tauri usage.- Core sidecar: must be staged so
core_rpccan reach theopenhumanbinary (seescripts/stage-core-sidecar.mjs).