Files
openhuman/docs/SUMMARY.md
T
3bb714bf96 feat(webview): native OS notifications from embedded webview apps (#714) (#727)
* 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>
2026-04-22 14:13:35 -07:00

8.0 KiB

Issue #714 — Native OS Notifications from Embedded Webviews

Branch: feat/714-native-os-notifications Base: upstream/main Upstream: tinyhumansai/openhuman Origin (fork): oxoxDev/openhuman


Problem

Embedded webview apps (Slack, Discord, Gmail, WhatsApp) call window.Notification inside CEF but never produce native macOS/Windows toasts. The CEF runtime intercepts the web Notification API, but the intercept dropped on the floor — no bridge to tauri-plugin-notification, no click routing back to the originating account, no permission query/request pipeline.

Solution

Wire the tauri-cef notification intercept into tauri-plugin-notification, prefix each toast with the provider label (e.g. [Slack] New message from Alice), honour silent / icon / tag, and record a NotificationRoute keyed by {provider}:{account_id}:{tag_or_uuid} so a future platform click hook can emit notification:click and route focus back to the correct account. Also round-trip the OS notification permission via new invokes so the frontend sees the same "granted" | "denied" | "default" triple as the web API on both CEF and wry runtimes.

Commits (in order)

50b831ad feat(webview_accounts): native OS notifications from embedded webviews (#714)

Rust backend — the core of the feature.

  • app/src-tauri/src/webview_accounts/mod.rs (+141 / -3)

    • NotificationRoute struct: provider, account_id, tag, created_at
    • notification_routes: Mutex<HashMap<String, NotificationRoute>> on WebviewAccountState
    • clear_notification_routes(account_id) — purged on close / purge
    • forward_native_notification(app, provider, account_id, payload):
      • Prefixes title with [Provider]
      • Respects silent (records route, skips toast)
      • Passes icon through to builder
      • Uses tag as dedup key, falls back to monotonic timestamp
    • tag_or_uuid helper — tag is the web API's dedup key; timestamp fallback ensures untagged payloads route uniquely
    • webview_notification_permission_state / _request — map tauri::plugin::PermissionState (Granted | Denied | Prompt | PromptWithRationale) onto "granted" | "denied" | "default"
    • permission_state_str helper
    • Non-cef (wry) stubs return "default" so frontend calls same invoke names on both runtimes
    • CEF registration in setup: tauri_runtime_cef::notification::register with handler that calls forward_native_notification; unregister on account close
  • app/src-tauri/src/lib.rs (+2)

    • Added webview_notification_permission_state and webview_notification_permission_request to the invoke handler list.
  • app/src-tauri/capabilities/default.json (+3)

    • Added notification:allow-notify, notification:allow-request-permission, notification:allow-is-permission-granted so the plugin can be invoked from the webview context.

97ef390f feat(accounts): wire notification permission + click bridge (#714)

Frontend — permission round-trip + dormant click listener.

  • app/src/services/webviewAccountService.ts (+59 / -1)
    • ensureNotificationPermission(accountId) — invokes webview_notification_permission_state, requests if "default", runs once per session on first account open. Desktop plugin auto-grants today, but shape matches web API so future platform prompts slot in without UI change.
    • handleNotificationClick + listen('notification:click', …) — dispatches setActiveAccount and invokes activate_main_window when the (currently dormant) platform click hook emits the event. Contract matches Rust NotificationRoute shape so Rust emit side is a one-liner when UNUserNotificationCenter / notify-rust on_response is wired.
    • openWebviewAccount now calls void ensureNotificationPermission(accountId) after the account opens.

e6f60180 chore: sync Cargo.lock to 0.52.26 after version bump

  • Cargo.lock + app/src-tauri/Cargo.lock (+2 / -2 each)
    • Picked up pending 0.52.26 version bump while building. No dependency graph change.

Quality Gates (all passed)

Gate Result Time
yarn compile (tsc --noEmit) pass 32.30s
yarn lint (eslint) pass 63.65s
yarn rust:format:check pass
cargo check --features cef --no-default-features pass 22.21s
cargo check --features wry --no-default-features pass 6m 29s (cold)

Skipped:

  • yarn format:check — flags only app/src/pages/Home.tsx (local build-tag pill #714, skip-worktree flagged, per workflow Phase 3 Step 6). Confirmed via git ls-files -v | grep '^S 'S app/src/pages/Home.tsx.
  • cargo clippy — pre-existing errors in src/slack_scanner/extract.rs (type_complexity) and src/lib.rs:212 (unnecessary_map_or) unrelated to this feature. Verified with git diff upstream/main -- app/src-tauri/src/lib.rs shows only the 2-line invoke handler addition.

Not yet done:

  • Manual verification in built .app bundle with real Slack/Discord/Gmail notifications. Requires yarn macOS:build:debug (~10 min), install, open, trigger notifications, confirm provider-prefixed titles fire natively.

Key Files for Teammate Review

File Role
app/src-tauri/src/webview_accounts/mod.rs Core Rust logic — intercept handler, route table, permission commands
app/src-tauri/src/lib.rs Invoke handler registration
app/src-tauri/capabilities/default.json Notification plugin capabilities
app/src/services/webviewAccountService.ts Frontend permission round-trip + click bridge
app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs (vendored, unchanged) — source of the register/unregister/dispatch API used here

Architecture Notes

Route keying

{provider}:{account_id}:{tag_or_uuid} — tag is the web Notifications API dedup key (second new Notification(title, { tag }) with same tag replaces the first). When absent, fall back to Instant::now() monotonic timestamp so every untagged payload routes uniquely. This matches browser semantics and prevents map collisions when two accounts of the same provider fire untagged notifications simultaneously.

Permission shape

tauri::plugin::PermissionState has 4 variants but the web API only has 3. Map:

  • Granted"granted"
  • Denied"denied"
  • Prompt, PromptWithRationale"default"

Non-cef runtime stubs always return "default" — prevents invoke name mismatch between runtimes so frontend doesn't need a feature flag.

Dormant click listener

notification:click listener is registered frontend-side but Rust doesn't emit it yet. UNUserNotificationCenter (macOS) and notify-rust on_response (Linux/Windows) callbacks are the platform hooks that will emit once wired. The route table is already populated by the notification dispatch path so the emit side is a one-liner:

let route = state.notification_routes.lock().unwrap().get(&route_key).cloned();
if let Some(r) = route {
    app.emit("notification:click", &r)?;
}

Next Steps for Teammate

  1. Manual verification — build .app, test Slack/Discord/Gmail toasts, confirm title prefix, confirm silent / icon / tag all honoured.
  2. Platform click hooks — wire UNUserNotificationCenter delegate (macOS) and notify-rust on_response (Linux/Windows) to emit notification:click with the stored NotificationRoute. Route table already exists; emit is one line.
  3. PR — template headings required: ## Summary, ## Problem, ## Solution, ## Submission Checklist, ## Impact, ## Related. Closes #714.

Local State Caveats

  • Home.tsx build-tag pillskip-worktree flag set on app/src/pages/Home.tsx with inline #714 pill (top-right, fixed). Per-clone, does NOT travel with branch. If teammate pulls this branch into their own clone, no pill appears locally. If they want one, Phase 3 Step 6 of .claude/rules/00-workflow.md has the snippet.
  • Cargo.lock — version bumped to 0.52.26 locally. Separate commit e6f60180 so diff review is clean.