feat(webview): native OS notifications from embedded webview apps (#714)#727
feat(webview): native OS notifications from embedded webview apps (#714)#727M3gA-Mind wants to merge 22 commits intotinyhumansai:mainfrom
Conversation
tinyhumansai#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>
…nsai#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>
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>
📝 WalkthroughWalkthroughThis PR implements native OS notifications from embedded webview applications by adding backend RPC handlers, persistence infrastructure, frontend notification UI, permission management, and webview notification API interception via Tauri integration and CEF injection. Changes
Sequence DiagramsequenceDiagram
participant User
participant WebviewApp as Webview (CEF)
participant TauriRuntime as Tauri Runtime
participant Backend as Backend RPC
participant Store as SQLite Store
participant Frontend as Frontend UI
participant Triage as Triage Pipeline
participant OS as OS Notification
User->>WebviewApp: Slack message arrives
WebviewApp->>WebviewApp: Calls window.Notification("Msg from `@john`")
Note over WebviewApp: ua_spoof.js intercepts, extracts title/body
WebviewApp->>TauriRuntime: recipe event: kind='notify'
TauriRuntime->>Frontend: webview_recipe_event
Frontend->>Backend: notification:ingest {provider,title,body,raw_payload}
Backend->>Backend: Check provider enabled
alt disabled
Backend-->>Frontend: {skipped:true, reason:"..."}
else enabled
Backend->>Store: insert notification (status=unread)
Backend-->>Frontend: {id, skipped:false}
Backend->>Triage: spawn async run_triage()
Triage->>Store: update_triage {importance_score, action}
Triage->>Backend: apply_decision if meets threshold
end
Frontend->>Frontend: dispatch addNotification()
Frontend->>OS: invoke tauri notification plugin
OS->>User: Display native OS notification
User->>OS: Click notification
OS->>TauriRuntime: notification:click event
TauriRuntime->>Frontend: listener receives {account_id, provider}
Frontend->>Frontend: setActiveAccount(account_id)
Frontend->>Frontend: setFocusedAccount(account_id)
Frontend->>TauriRuntime: invoke activate_main_window
TauriRuntime->>User: Bring OpenHuman to foreground, show Slack webview
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes The changes span multiple heterogeneous domains (Rust backend RPC/store/triage, TypeScript/React frontend UI/state/services, CEF/webview interception, settings routing, type definitions) with significant logic density in the store module (SQL schema/queries), RPC handlers (async triage spawning, filtering, settings persistence), frontend Redux integration, and webview API interception. Multiple file categories require separate reasoning despite related functionality. Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src-tauri/src/webview_accounts/mod.rs`:
- Around line 265-275: The info-level log in log::info! is currently emitting
raw_title (user-facing notification text) which may contain PII; remove
raw_title from the info log call in webview_accounts::notify-cef code path (the
log::info! invocation) and either omit it entirely or replace it with a
non-sensitive placeholder/metadata (e.g., title length or a redacted token) and,
if you still need the full title for debugging, log the raw_title at debug or
trace level instead (move it to log::debug! or log::trace! referencing
raw_title). Ensure the format string and argument list for log::info! are
updated to match the removed/replaced field.
- Around line 256-307: The code currently inserts a notification_routes entry
for every notification (using notification_route_key and
WebviewAccountsState::notification_routes) before checking payload.silent or
whether builder.show() succeeds, causing unbounded growth; change the logic so
entries are only stored for notifications that will be routable later: either
move the insert to after the payload.silent check and after builder.show()
returns Ok(), or if you must insert earlier, ensure you remove the route on
payload.silent and on Err(e) from builder.show(). Use the existing symbols
notification_route_key, NotificationRoute, payload.silent, builder.show(), and
state.notification_routes.lock().unwrap().insert/remove to implement the fix.
In `@app/src/services/webviewAccountService.ts`:
- Around line 123-135: ensureNotificationPermission currently sets
permissionChecked = true before the async invokes, so a transient failure leaves
the session marked checked; move the mutation of permissionChecked into the
success path inside ensureNotificationPermission: only set permissionChecked =
true after the initial invoke('webview_notification_permission_state') completes
successfully (and likewise after the follow-up
invoke('webview_notification_permission_request') if performed), and keep the
try/catch so that on error permissionChecked remains false and future calls will
retry; references: ensureNotificationPermission, permissionChecked,
invoke('webview_notification_permission_state'),
invoke('webview_notification_permission_request').
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c9119250-e554-4f60-8ade-e9024ddf561f
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lockapp/src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
app/src-tauri/capabilities/default.jsonapp/src-tauri/src/lib.rsapp/src-tauri/src/webview_accounts/mod.rsapp/src/services/webviewAccountService.ts
tinyhumansai#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 tinyhumansai#679
- 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.
- 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.
…ction signature - Removed unnecessary line breaks in the webview_notification_set_dnd function for improved readability.
Feat/679 notification bypass
…anagement - 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.
- 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.
Feat/718 notification routing
…nner 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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
…ssions 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>
…fication # Conflicts: # Cargo.lock # app/src-tauri/Cargo.lock # app/src-tauri/src/lib.rs # app/src-tauri/src/webview_accounts/mod.rs # app/src-tauri/vendor/tauri-cef # src/core/all.rs # src/openhuman/mod.rs
…ai#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
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.
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (1)
app/src/services/webviewAccountService.ts (1)
135-143:⚠️ Potential issue | 🟠 MajorOnly mark the session permission bootstrap complete after the request path succeeds.
permissionCheckedflips immediately after the state probe. Ifwebview_notification_permission_requestthrows once, lateropenWebviewAccount()calls will skip permission bootstrap for the rest of the session.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/services/webviewAccountService.ts` around lines 135 - 143, The bug is that permissionChecked is set to true immediately after probing state so a subsequent failing call to invoke('webview_notification_permission_request') will leave the session marked complete; modify ensureNotificationPermission so that permissionChecked is only set to true after the request path succeeds (i.e., set permissionChecked = true after a successful invoke('webview_notification_permission_request') or when state === 'granted'), and ensure any thrown error from invoke('webview_notification_permission_request') does not flip permissionChecked (so leave permissionChecked false and rethrow or handle the error); update references in ensureNotificationPermission and callers like openWebviewAccount if needed to rely on the corrected permissionChecked behavior.
🧹 Nitpick comments (5)
app/src/components/settings/panels/NotificationsPanel.tsx (1)
12-23: Consider adding error handling for resilience.The async operations lack error handling:
getBypassPrefs()rejection will leave the component in loading state forever (line 15 won't execute)- If
setGlobalDnd(next)fails, the UI shows the toggled state but the backend wasn't updated♻️ Suggested improvement
useEffect(() => { - getBypassPrefs().then(prefs => { - if (prefs) setDnd(prefs.global_dnd); - setLoading(false); - }); + getBypassPrefs() + .then(prefs => { + if (prefs) setDnd(prefs.global_dnd); + }) + .catch(err => { + console.warn('[NotificationsPanel] Failed to load preferences:', err); + }) + .finally(() => setLoading(false)); }, []); const handleDndToggle = async () => { const next = !dnd; setDnd(next); - await setGlobalDnd(next); + try { + await setGlobalDnd(next); + } catch (err) { + console.warn('[NotificationsPanel] Failed to update DND:', err); + setDnd(!next); // Revert optimistic update + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/components/settings/panels/NotificationsPanel.tsx` around lines 12 - 23, Add robust error handling around the async calls in useEffect and handleDndToggle: wrap the getBypassPrefs() call inside a try/catch/finally in the useEffect so any rejection is caught, ensure setLoading(false) runs in finally, and log or surface the error; in handleDndToggle wrap setGlobalDnd(next) in try/catch and on failure revert the optimistic UI change by calling setDnd(prev) (or re-read state) and surface/log the error so the UI and backend remain consistent. Ensure you update functions referenced (useEffect, getBypassPrefs, setLoading, setDnd, handleDndToggle, setGlobalDnd) only by adding try/catch/finally and revert logic, not by changing their signatures.app/src/services/notificationService.ts (1)
17-22: Prefer named interfaces for the RPC contracts in this file.
opts, the ingest payload, and the settings shapes are all anonymous object types right now. Extracting named interfaces will make the API surface easier to reuse and keep it in line with the repo's TS convention.As per coding guidelines:
**/*.{ts,tsx}: Preferinterfacefor defining object shapes in TypeScript.Also applies to: 57-80, 92-97
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/services/notificationService.ts` around lines 17 - 22, Replace the anonymous object types with named interfaces: define e.g. interface NotificationFetchOptions { provider?: string; limit?: number; offset?: number; min_score?: number } and use it in the fetchNotifications signature (fetchNotifications(opts?: NotificationFetchOptions)), and similarly extract interfaces for the ingest payload (e.g. NotificationIngestPayload) and settings shape (e.g. NotificationSettings) used elsewhere in this file (including the other function signatures referenced around the later blocks). Update function parameters and return types to reference these interfaces so the RPC contract is explicit and reusable, and export the interfaces if they need to be consumed externally.app/src/components/settings/panels/NotificationRoutingPanel.tsx (1)
19-24: Extract a shared settings interface instead of repeating the object shape.The same settings structure is duplicated in the state type and the patch type. A named interface will keep this file aligned with the repo's TS conventions and give you one source of truth.
As per coding guidelines:
**/*.{ts,tsx}: Preferinterfacefor defining object shapes in TypeScript.Also applies to: 52-56
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/components/settings/panels/NotificationRoutingPanel.tsx` around lines 19 - 24, Extract the repeated object shape into a named interface (e.g., NotificationSetting) and replace the inline type annotations currently used in the useState declaration for settings and the patch type referenced around the patch-handling code (the inline Record<string, { enabled: boolean; importance_threshold: number; route_to_orchestrator: boolean }> and the identical shape used later at lines ~52-56); update the useState generic to Record<string, NotificationSetting> and update the patch type to use NotificationSetting so there is a single source of truth for the settings shape and it follows the project's preference for interfaces.src/openhuman/notifications/store.rs (1)
108-153: Consider adding an upper bound onlimitto prevent excessive memory usage.While
limitis guaranteed to be numeric (parsed fromu64), there's no cap on its value. A client requestinglimit: 999999999could cause memory pressure.♻️ Proposed fix
pub fn list( config: &Config, limit: usize, offset: usize, provider_filter: Option<&str>, min_score: Option<f32>, ) -> Result<Vec<IntegrationNotification>> { + const MAX_LIMIT: usize = 500; + let limit = limit.min(MAX_LIMIT); with_connection(config, |conn| {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/notifications/store.rs` around lines 108 - 153, In list(), clamp the input limit to a reasonable maximum before building the SQL to avoid unbounded allocations: introduce a constant (e.g., MAX_LIMIT), compute let limit = std::cmp::min(limit, MAX_LIMIT); and use that clamped value when formatting the LIMIT/OFFSET fragment; update any type conversions so the formatted limit is a usize/u64 matching MAX_LIMIT and ensure the change is applied inside the with_connection closure (function name: list, variables: limit, MAX_LIMIT, with_connection) so the query uses the safe bounded limit.src/openhuman/notifications/rpc.rs (1)
30-34: Consider adding entry-point debug logging for observability.Per coding guidelines, handlers should have debug/trace logs at entry points. Adding a brief log here would help trace request flow:
tracing::debug!( provider = %req.provider, "[notifications::rpc] handle_ingest called" );Similar entry-point logging could be added to
handle_list,handle_settings_get, andhandle_settings_setfor consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/notifications/rpc.rs` around lines 30 - 34, Add entry-point debug logging to the notification RPC handlers: in handle_ingest, after deserializing into req, call tracing::debug! to log that handle_ingest was called and include the provider field (e.g., provider = %req.provider) and a short context tag like "[notifications::rpc] handle_ingest called"; also add analogous tracing::debug! calls at the start of handle_list, handle_settings_get, and handle_settings_set so each handler logs its entry and any small identifying fields (e.g., provider or request id) to improve observability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/components/notifications/NotificationCenter.tsx`:
- Around line 57-58: The current providers pill list is computed from the
filtered items array, so when selectedProvider is applied via
fetchNotifications({provider}) the providers list collapses; instead compute
providers from an unfiltered source (e.g., originalNotifications, allItems, or a
providers list returned by the API) and stop deriving it from items. Update the
providers calculation (the Array.from(new Set(items.map(n =>
n.provider))).sort() expression) to use the unfiltered state or a dedicated
providers array, and ensure code around selectedProvider and fetchNotifications
uses that unfiltered source so the pill row (and the providers.length > 1 check)
remains correct even when items is filtered.
- Line 5: The component NotificationCenter currently imports markReadAction and
setNotifications but never dispatches loading/error actions, so update the fetch
effect inside NotificationCenter to dispatch setNotificationsLoading(true)
before starting the RPC, then on success dispatch setNotifications(payload) and
setNotificationsLoading(false), and on failure dispatch
setNotificationsError(error) (and setNotificationsLoading(false)); ensure you
reference the existing action creators setNotificationsLoading and
setNotificationsError and the imported setNotifications/markReadAction so the
loading and error branches in the render become reachable.
In `@app/src/components/settings/panels/NotificationRoutingPanel.tsx`:
- Around line 50-66: The optimistic update in updateSetting must be rolled back
if persistence fails: capture the previous provider state (e.g., store a copy of
settings[provider] as prevState before calling setSettings), perform the
optimistic setSettings({ ... }), then wrap await setNotificationSettings({
provider, ...next }) in try/catch; on catch restore the previous state via
setSettings(prev => ({ ...prev, [provider]: prevState })) and rethrow or return
a rejected promise so callers can observe the failure; also ensure updateSetting
preserves and propagates the rejection (don’t swallow it) so callers can await
or handle errors.
In `@app/src/components/settings/SettingsHome.tsx`:
- Around line 130-163: Update the settings navigation types and routing logic to
include the new notifications routes: add 'notifications' and
'notification-routing' to the SettingsRoute union in useSettingsNavigation.ts;
update getCurrentRoute() to detect URL paths for '/settings/notifications' and
'/settings/notification-routing' (returning the corresponding SettingsRoute
values) and ensure any switch/case or path-matching logic covers those two new
routes; and extend getBreadcrumbs() to return appropriate breadcrumb arrays when
route is 'notifications' or 'notification-routing' (matching the pattern used by
other routes) so the panels NotificationsPanel and NotificationRoutingPanel are
discoverable and have breadcrumbs.
In `@app/src/services/notificationService.ts`:
- Around line 57-70: ingestNotification currently types its response as { id:
string } even though callCoreRpc (openhuman.notification_ingest) may return {
skipped: true } for disabled-provider flows; update the return type of
ingestNotification to a discriminated union (e.g. { id: string } | { skipped:
true }) and adjust the call signature to callCoreRpc<{ id: string } | { skipped:
true }>, then ensure any downstream usage of ingestNotification checks for the
skipped branch (inspect result.skipped or result.id) so callers no longer assume
an id exists.
In `@docs/SUMMARY.md`:
- Around line 1-127: This SUMMARY.md should either be removed from the PR or
cleaned up and committed: either drop the file from the commit (exclude it from
the PR/diff) or update SUMMARY.md by (1) removing the local worktree path
reference (/Volumes/Extreme SSD/...) near the top, (2) fixing capitalization
from "macos" to "macOS" in the Architecture Notes, and (3) removing the
self-referential deletion note about deleting SUMMARY.md before opening the PR;
then re-stage the cleaned SUMMARY.md (or remove it) and update the commit so the
PR reflects the chosen option.
In `@scripts/install.sh`:
- Around line 296-299: The dry-run branch currently treats a missing release
asset as success; update the branch that checks DRY_RUN to return a non-zero
exit when no compatible ${OS}/${ARCH} asset is found by replacing the current
exit 0 with a failing exit code (e.g., exit 1) and use a more appropriate error
logging call (swap log_warn for log_error) so installer smoke tests detect the
packaging regression; locate the conditional that references DRY_RUN and
log_warn and change the exit code and log level accordingly.
In `@scripts/setup-dev-codesign.sh`:
- Around line 77-80: The openssl pkcs12 invocation currently includes an
unconditional -legacy flag which overrides the earlier probe and
PKCS12_LEGACY_ARGS handling; remove the literal -legacy token from the openssl
pkcs12 command and rely solely on expanding "${PKCS12_LEGACY_ARGS[@]}" so that
-legacy is only passed when the probe added it to PKCS12_LEGACY_ARGS (ensure the
command still uses the array expansion as shown).
In `@src/openhuman/notifications/schemas.rs`:
- Around line 52-95: Update the "ingest" ControllerSchema outputs so clients can
represent the disabled-provider skip path: change the existing FieldSchema for
"id" in the "ingest" ControllerSchema to required: false (since skipped
responses won’t include an id) and add a new FieldSchema named "skipped" with
ty: TypeSchema::Bool and required: false (with a comment like "true when
provider is disabled and ingest was skipped"). This ensures the schema covers
both the normal { id: string } response and the { skipped: true } response used
by openhuman.notification_ingest.
In `@src/openhuman/notifications/store.rs`:
- Around line 283-284: The code currently silently falls back to Utc::now() when
parsing received_at_str fails; change the parse call for received_at
(DateTime<Utc>) to capture the parse error and emit a warning (e.g., using
log::warn! or tracing::warn!) that includes the offending received_at_str and
the parse error, then return Utc::now() as the fallback; specifically replace
the unwrap_or_else on received_at_str.parse() with a closure or match that logs
the warning with received_at_str and the error before returning Utc::now().
---
Duplicate comments:
In `@app/src/services/webviewAccountService.ts`:
- Around line 135-143: The bug is that permissionChecked is set to true
immediately after probing state so a subsequent failing call to
invoke('webview_notification_permission_request') will leave the session marked
complete; modify ensureNotificationPermission so that permissionChecked is only
set to true after the request path succeeds (i.e., set permissionChecked = true
after a successful invoke('webview_notification_permission_request') or when
state === 'granted'), and ensure any thrown error from
invoke('webview_notification_permission_request') does not flip
permissionChecked (so leave permissionChecked false and rethrow or handle the
error); update references in ensureNotificationPermission and callers like
openWebviewAccount if needed to rely on the corrected permissionChecked
behavior.
---
Nitpick comments:
In `@app/src/components/settings/panels/NotificationRoutingPanel.tsx`:
- Around line 19-24: Extract the repeated object shape into a named interface
(e.g., NotificationSetting) and replace the inline type annotations currently
used in the useState declaration for settings and the patch type referenced
around the patch-handling code (the inline Record<string, { enabled: boolean;
importance_threshold: number; route_to_orchestrator: boolean }> and the
identical shape used later at lines ~52-56); update the useState generic to
Record<string, NotificationSetting> and update the patch type to use
NotificationSetting so there is a single source of truth for the settings shape
and it follows the project's preference for interfaces.
In `@app/src/components/settings/panels/NotificationsPanel.tsx`:
- Around line 12-23: Add robust error handling around the async calls in
useEffect and handleDndToggle: wrap the getBypassPrefs() call inside a
try/catch/finally in the useEffect so any rejection is caught, ensure
setLoading(false) runs in finally, and log or surface the error; in
handleDndToggle wrap setGlobalDnd(next) in try/catch and on failure revert the
optimistic UI change by calling setDnd(prev) (or re-read state) and surface/log
the error so the UI and backend remain consistent. Ensure you update functions
referenced (useEffect, getBypassPrefs, setLoading, setDnd, handleDndToggle,
setGlobalDnd) only by adding try/catch/finally and revert logic, not by changing
their signatures.
In `@app/src/services/notificationService.ts`:
- Around line 17-22: Replace the anonymous object types with named interfaces:
define e.g. interface NotificationFetchOptions { provider?: string; limit?:
number; offset?: number; min_score?: number } and use it in the
fetchNotifications signature (fetchNotifications(opts?:
NotificationFetchOptions)), and similarly extract interfaces for the ingest
payload (e.g. NotificationIngestPayload) and settings shape (e.g.
NotificationSettings) used elsewhere in this file (including the other function
signatures referenced around the later blocks). Update function parameters and
return types to reference these interfaces so the RPC contract is explicit and
reusable, and export the interfaces if they need to be consumed externally.
In `@src/openhuman/notifications/rpc.rs`:
- Around line 30-34: Add entry-point debug logging to the notification RPC
handlers: in handle_ingest, after deserializing into req, call tracing::debug!
to log that handle_ingest was called and include the provider field (e.g.,
provider = %req.provider) and a short context tag like "[notifications::rpc]
handle_ingest called"; also add analogous tracing::debug! calls at the start of
handle_list, handle_settings_get, and handle_settings_set so each handler logs
its entry and any small identifying fields (e.g., provider or request id) to
improve observability.
In `@src/openhuman/notifications/store.rs`:
- Around line 108-153: In list(), clamp the input limit to a reasonable maximum
before building the SQL to avoid unbounded allocations: introduce a constant
(e.g., MAX_LIMIT), compute let limit = std::cmp::min(limit, MAX_LIMIT); and use
that clamped value when formatting the LIMIT/OFFSET fragment; update any type
conversions so the formatted limit is a usize/u64 matching MAX_LIMIT and ensure
the change is applied inside the with_connection closure (function name: list,
variables: limit, MAX_LIMIT, with_connection) so the query uses the safe bounded
limit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0d117cbe-ad13-4919-be51-41a8b76abe67
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (26)
app/src-tauri/Cargo.tomlapp/src-tauri/src/webview_accounts/mod.rsapp/src-tauri/src/webview_accounts/ua_spoof.jsapp/src/AppRoutes.tsxapp/src/components/notifications/NotificationCard.tsxapp/src/components/notifications/NotificationCenter.tsxapp/src/components/settings/SettingsHome.tsxapp/src/components/settings/panels/NotificationRoutingPanel.tsxapp/src/components/settings/panels/NotificationsPanel.tsxapp/src/pages/Notifications.tsxapp/src/pages/Settings.tsxapp/src/services/notificationService.tsapp/src/services/webviewAccountService.tsapp/src/store/index.tsapp/src/store/notificationsSlice.tsapp/src/types/notifications.tsdocs/SUMMARY.mdscripts/install.shscripts/setup-dev-codesign.shsrc/openhuman/agent/triage/envelope.rssrc/openhuman/notifications/mod.rssrc/openhuman/notifications/rpc.rssrc/openhuman/notifications/schemas.rssrc/openhuman/notifications/store.rssrc/openhuman/notifications/types.rstests/json_rpc_e2e.rs
✅ Files skipped from review due to trivial changes (4)
- app/src/pages/Notifications.tsx
- app/src-tauri/Cargo.toml
- src/openhuman/notifications/mod.rs
- app/src/types/notifications.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src-tauri/src/webview_accounts/mod.rs
| { | ||
| id: 'notifications', | ||
| title: 'Notifications', | ||
| description: 'Do Not Disturb and per-account notification controls', | ||
| icon: ( | ||
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| strokeWidth={2} | ||
| d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" | ||
| /> | ||
| </svg> | ||
| ), | ||
| onClick: () => navigateToSettings('notifications'), | ||
| dangerous: false, | ||
| }, | ||
| { | ||
| id: 'notification-routing', | ||
| title: 'Notification Routing', | ||
| description: 'AI importance scoring and orchestrator escalation for integration alerts', | ||
| icon: ( | ||
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| strokeWidth={2} | ||
| d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" | ||
| /> | ||
| </svg> | ||
| ), | ||
| onClick: () => navigateToSettings('notification-routing'), | ||
| dangerous: false, | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify if SettingsRoute type includes the new notification routes
rg -n "type SettingsRoute" app/src/components/settings/hooks/useSettingsNavigation.ts -A 30 | head -40
# Check if any handling exists for notification routes
rg -n "notification" app/src/components/settings/hooks/useSettingsNavigation.tsRepository: tinyhumansai/openhuman
Length of output: 712
🏁 Script executed:
#!/bin/bash
# Get the full implementation of getCurrentRoute() and getBreadcrumbs()
rg -n "getCurrentRoute|getBreadcrumbs" app/src/components/settings/hooks/useSettingsNavigation.ts -A 15 | head -80
# Check if there are switch statements that might need updates
rg -n "switch|case" app/src/components/settings/hooks/useSettingsNavigation.ts -B 2 -A 8Repository: tinyhumansai/openhuman
Length of output: 4082
🏁 Script executed:
#!/bin/bash
# Search for notification-related settings pages
fd -type f "notification" app/src/components/settings/ 2>/dev/null || echo "No notification settings files found"
# Look for any notification settings page implementation
rg -l "notification" app/src/components/settings/ 2>/dev/null | head -10
# Check the actual type definition and confirm the string fallback
sed -n '104,113p' app/src/components/settings/hooks/useSettingsNavigation.tsRepository: tinyhumansai/openhuman
Length of output: 567
Add missing route definitions for notifications features.
The 'notifications' and 'notification-routing' routes are not defined in the SettingsRoute type union in app/src/components/settings/hooks/useSettingsNavigation.ts, and the corresponding panels (NotificationsPanel.tsx and NotificationRoutingPanel.tsx) exist but are not wired into the navigation system.
Even though navigateToSettings() accepts SettingsRoute | string, allowing the code to compile, the missing type definitions mean:
getCurrentRoute()will not properly detect these routes from the URL pathgetBreadcrumbs()will not provide breadcrumbs for these routes (falls to default empty array)
Add 'notifications' | 'notification-routing' to the SettingsRoute type, add corresponding path checks in getCurrentRoute(), and add breadcrumb cases in getBreadcrumbs().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/components/settings/SettingsHome.tsx` around lines 130 - 163, Update
the settings navigation types and routing logic to include the new notifications
routes: add 'notifications' and 'notification-routing' to the SettingsRoute
union in useSettingsNavigation.ts; update getCurrentRoute() to detect URL paths
for '/settings/notifications' and '/settings/notification-routing' (returning
the corresponding SettingsRoute values) and ensure any switch/case or
path-matching logic covers those two new routes; and extend getBreadcrumbs() to
return appropriate breadcrumb arrays when route is 'notifications' or
'notification-routing' (matching the pattern used by other routes) so the panels
NotificationsPanel and NotificationRoutingPanel are discoverable and have
breadcrumbs.
| "ingest" => ControllerSchema { | ||
| namespace: "notification", | ||
| function: "ingest", | ||
| description: "Ingest a new notification from an embedded webview integration. \ | ||
| Immediately persists the record and kicks off background triage scoring.", | ||
| inputs: vec![ | ||
| FieldSchema { | ||
| name: "provider", | ||
| ty: TypeSchema::String, | ||
| comment: "Provider slug, e.g. \"gmail\", \"slack\", \"whatsapp\".", | ||
| required: true, | ||
| }, | ||
| FieldSchema { | ||
| name: "account_id", | ||
| ty: TypeSchema::Option(Box::new(TypeSchema::String)), | ||
| comment: "Webview account identifier (optional).", | ||
| required: false, | ||
| }, | ||
| FieldSchema { | ||
| name: "title", | ||
| ty: TypeSchema::String, | ||
| comment: "Short notification title / subject.", | ||
| required: true, | ||
| }, | ||
| FieldSchema { | ||
| name: "body", | ||
| ty: TypeSchema::String, | ||
| comment: "Notification body or preview text.", | ||
| required: true, | ||
| }, | ||
| FieldSchema { | ||
| name: "raw_payload", | ||
| ty: TypeSchema::Ref("JsonObject"), | ||
| comment: "Full raw event payload from the source for downstream use.", | ||
| required: true, | ||
| }, | ||
| ], | ||
| outputs: vec![FieldSchema { | ||
| name: "id", | ||
| ty: TypeSchema::String, | ||
| comment: "UUID of the newly created notification record.", | ||
| required: true, | ||
| }], | ||
| }, |
There was a problem hiding this comment.
Keep the ingest schema aligned with the disabled-provider skip path.
The new e2e coverage exercises openhuman.notification_ingest returning { skipped: true } when a provider is disabled, but this schema still says the only successful output is id: string. Any schema-driven client, docs view, or controller tooling will assume id is always present and miss the skip case.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/notifications/schemas.rs` around lines 52 - 95, Update the
"ingest" ControllerSchema outputs so clients can represent the disabled-provider
skip path: change the existing FieldSchema for "id" in the "ingest"
ControllerSchema to required: false (since skipped responses won’t include an
id) and add a new FieldSchema named "skipped" with ty: TypeSchema::Bool and
required: false (with a comment like "true when provider is disabled and ingest
was skipped"). This ensures the schema covers both the normal { id: string }
response and the { skipped: true } response used by
openhuman.notification_ingest.
…ansai#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
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
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (1)
app/src/services/webviewAccountService.ts (1)
135-146:⚠️ Potential issue | 🟠 MajorDeduplicate the permission bootstrap and only request on
"default".Two concurrent
openWebviewAccount()calls can both enterensureNotificationPermission()beforepermissionCheckedflips, so this can issue duplicate OS permission requests. It also callswebview_notification_permission_requestfor"denied", andpermissionCheckedis set before that second invoke succeeds, so a transient request failure suppresses retries for the rest of the session.🛠️ Possible fix
-let permissionChecked = false; +let permissionCheckPromise: Promise<void> | null = null; async function ensureNotificationPermission(): Promise<void> { - if (permissionChecked) return; + if (permissionCheckPromise) return permissionCheckPromise; try { - const state = await invoke<string>('webview_notification_permission_state'); - permissionChecked = true; - log('notification permission state=%s', state); - if (state === 'granted') return; - const next = await invoke<string>('webview_notification_permission_request'); - log('notification permission after request=%s', next); + permissionCheckPromise = (async () => { + const state = await invoke<string>('webview_notification_permission_state'); + log('notification permission state=%s', state); + if (state !== 'default') return; + const next = await invoke<string>('webview_notification_permission_request'); + log('notification permission after request=%s', next); + })(); + await permissionCheckPromise; } catch (err) { + permissionCheckPromise = null; errLog('notification permission check failed: %o', err); } }Also applies to: 716-716
🧹 Nitpick comments (1)
src/openhuman/notifications/schemas.rs (1)
278-279: Rename stale test name for clarity.The test name says “three functions” but asserts five; renaming avoids confusion during failures.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/notifications/schemas.rs` around lines 278 - 279, Rename the stale test function all_controller_schemas_covers_three_functions to reflect the actual assertion count (e.g., all_controller_schemas_covers_five_functions) so the test name matches its expectations; update the function identifier in the test declaration and any references to it to avoid confusion when the test fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src-tauri/src/cdp/session.rs`:
- Around line 146-154: The injected script passed to
cdp.call("Page.addScriptToEvaluateOnNewDocument", ...) currently forces
Notification.permission to "granted" and always resolves
Notification.requestPermission(), which hides the real OS/browser state; change
this so the script consults the real permission state provided by our backend
(do not hardcode "granted") and proxy requestPermission() calls back to our
permission path instead of resolving locally. Locate the injection in session.rs
(the cdp.call invocation and the session_id usage) and replace the hardcoded
shim with one that reads a backend-supplied permission value (or queries it via
an IPC/binding) and routes Notification.requestPermission() through the same
backend permission-check/flow so returned promises reflect the true OS
permission ("default", "denied", or "granted").
In `@app/src/components/notifications/NotificationCenter.tsx`:
- Around line 21-39: The provider pill list (allProviders) can become stale when
a new notification from a previously unseen provider is added in real-time;
update allProviders whenever a notification is added rather than only on
unfiltered fetches. Concretely, in the place that handles incoming notifications
(the same handler that calls setNotifications or addNotification inside
NotificationCenter), check the new notification's provider against allProviders
and, if missing, call setAllProviders(prev => Array.from(new Set([...prev,
newProvider])).sort()); ensure this check runs for single-item additions as well
as batched adds so the pill row is updated immediately.
In `@app/src/components/settings/hooks/useSettingsNavigation.ts`:
- Around line 32-34: getCurrentRoute in useSettingsNavigation doesn't handle the
newly added SettingsRoute entries ('notifications' and 'notification-routing'),
so paths like /settings/notifications fall through to 'home'; update the route
resolution logic inside the getCurrentRoute function to explicitly map
'/settings/notifications' -> 'notifications' and
'/settings/notification-routing' -> 'notification-routing' (use the
SettingsRoute type/enum and any existing pathname parsing used for other
settings routes to keep behavior consistent with the rest of the function).
In `@src/core/all.rs`:
- Around line 148-149: The CLI namespace for "notification" is registered via
controllers.extend(crate::openhuman::notifications::all_notifications_registered_controllers())
but namespace_description() still returns None for "notification", so add a
description entry for "notification" in the namespace_description() function
(match the pattern used for other namespaces) and update/add unit tests to
assert namespace_description("notification") returns the expected string;
reference the controllers.extend(...) call and the namespace_description()
function and ensure tests cover the new mapping.
In `@src/openhuman/notifications/schemas.rs`:
- Around line 89-103: The ingest response schema's outputs vector (the
FieldSchema entries shown) is missing the optional "reason" field returned on
skipped notifications; add a FieldSchema with name: "reason", ty:
TypeSchema::Option(Box::new(TypeSchema::String)), comment: "Optional reason why
the notification was skipped (e.g., provider disabled).", and required: false so
schema-driven clients/docs include the skip reason alongside "skipped" and "id".
- Around line 217-223: The outputs for the settings_set endpoint are missing the
returned normalized settings: update the outputs Vec in the FieldSchema for
settings_set to include a second FieldSchema with name "settings", type matching
the settings schema (e.g., TypeSchema::Ref or the specific settings TypeSchema),
a descriptive comment (e.g., "Normalized settings returned after save."), and
required: true so registry metadata matches the handler return value; modify the
outputs Vec alongside the existing "ok" FieldSchema in the settings_set
definition to reflect both returned fields.
In `@src/openhuman/notifications/store.rs`:
- Around line 47-72: Add debug/trace instrumentation around the notifications
store entry points and DB operations: in with_connection, log entry/exit
(function start and before returning), log the resolved db_path, log before and
after Connection::open, log success/failure of create_dir_all, and log
before/after conn.execute_batch and before calling f(&conn); use the crate's
logging (tracing::debug/trace or log::debug) to record these checkpoints and
include contextual details (db_path.display(), parent.display(), error contexts)
so callers like with_connection, the migration block (conn.execute_batch), and
the invocation f(&conn) are observable; apply the same pattern to the other
store functions mentioned (lines 80-105, 116-152, 163-185, 203-252) at their
entry/exit points, DB calls, branches and error paths.
- Around line 47-72: The with_connection helper opens a new rusqlite Connection
without a busy timeout, causing transient "database is locked" errors under
concurrent access; after creating conn in with_connection (the Connection::open
call) set a busy timeout (e.g., Duration::from_secs(5) or reuse the existing
SQLITE_BUSY_TIMEOUT constant used in memory/tree/store.rs) by calling the
connection's busy_timeout(...) method and propagate any error (with_context
similarly) before running conn.execute_batch(SCHEMA) and invoking f(&conn).
- Around line 293-294: The code silently drops malformed scored_at values by
converting scored_at_str.parse().ok() into None; change the behavior in the
data-loading function that calls row.get(11)? (the code creating scored_at_str
and scored_at) so that parse failures are surfaced instead of silently returning
None — either return an Err with a clear context (including the offending
scored_at_str) or log a warning/error that includes scored_at_str and the parse
error and preserve the original string in the struct (or a separate
"raw_scored_at" field) so on-disk data issues are visible; update the handling
of scored_at (the scored_at_str -> scored_at conversion) accordingly.
- Around line 163-172: The update_triage flow currently ignores the affected-row
count from conn.execute so it returns Ok(()) even when no row matches the given
id; modify the block inside with_connection that calls conn.execute in
update_triage to capture the returned affected row count, and if it is 0 return
a Contextual error (e.g., via anyhow::bail or .with_context) indicating the
notification id was not found; do the same fix for mark_read (the other
conn.execute block) so both functions fail when no rows were updated rather than
silently succeeding.
---
Nitpick comments:
In `@src/openhuman/notifications/schemas.rs`:
- Around line 278-279: Rename the stale test function
all_controller_schemas_covers_three_functions to reflect the actual assertion
count (e.g., all_controller_schemas_covers_five_functions) so the test name
matches its expectations; update the function identifier in the test declaration
and any references to it to avoid confusion when the test fails.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2a3bc41b-4ea1-4fc7-b302-ad474544e756
⛔ Files ignored due to path filters (1)
app/src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (14)
app/src-tauri/src/cdp/emulation.rsapp/src-tauri/src/cdp/session.rsapp/src-tauri/vendor/tauri-cefapp/src/components/notifications/NotificationCenter.tsxapp/src/components/settings/hooks/useSettingsNavigation.tsapp/src/components/settings/panels/NotificationRoutingPanel.tsxapp/src/services/notificationService.tsapp/src/services/webviewAccountService.tsdocs/SUMMARY.mdscripts/setup-dev-codesign.shsrc/core/all.rssrc/openhuman/mod.rssrc/openhuman/notifications/schemas.rssrc/openhuman/notifications/store.rs
✅ Files skipped from review due to trivial changes (4)
- src/openhuman/mod.rs
- app/src-tauri/vendor/tauri-cef
- scripts/setup-dev-codesign.sh
- app/src-tauri/src/cdp/emulation.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- app/src/components/settings/panels/NotificationRoutingPanel.tsx
- docs/SUMMARY.md
- app/src/services/notificationService.ts
| cdp.call( | ||
| "Page.addScriptToEvaluateOnNewDocument", | ||
| json!({ | ||
| "source": "Object.defineProperty(Notification,'permission',{get:()=>'granted'});\ | ||
| Notification.requestPermission=()=>Promise.resolve('granted');" | ||
| }), | ||
| Some(&session_id), | ||
| ) | ||
| .await?; |
There was a problem hiding this comment.
Don't force the embedded page's Notification API to "granted".
This makes every provider observe a successful permission state/request even when the OS is still "default" or "denied", so apps like Slack/Gmail can suppress their own permission UI while native toasts are still blocked. Feed the real permission state into this shim, and route requestPermission() through the same backend permission path instead of always resolving "granted".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src-tauri/src/cdp/session.rs` around lines 146 - 154, The injected script
passed to cdp.call("Page.addScriptToEvaluateOnNewDocument", ...) currently
forces Notification.permission to "granted" and always resolves
Notification.requestPermission(), which hides the real OS/browser state; change
this so the script consults the real permission state provided by our backend
(do not hardcode "granted") and proxy requestPermission() calls back to our
permission path instead of resolving locally. Locate the injection in session.rs
(the cdp.call invocation and the session_id usage) and replace the hardcoded
shim with one that reads a backend-supplied permission value (or queries it via
an IPC/binding) and routes Notification.requestPermission() through the same
backend permission-check/flow so returned promises reflect the true OS
permission ("default", "denied", or "granted").
| // All providers seen across unfiltered loads — kept separate so the filter | ||
| // pill row doesn't collapse when a provider filter is active. | ||
| const [allProviders, setAllProviders] = useState<string[]>([]); | ||
|
|
||
| // Fetch on mount and when provider filter changes. | ||
| useEffect(() => { | ||
| let cancelled = false; | ||
| const load = async () => { | ||
| dispatch(setNotificationsLoading(true)); | ||
| try { | ||
| const result = await fetchNotifications({ provider: selectedProvider, limit: 100 }); | ||
| if (!cancelled) { | ||
| dispatch(setNotifications(result)); | ||
| // Accumulate providers only from unfiltered loads so the pill row | ||
| // stays stable when a filter is active. | ||
| if (!selectedProvider) { | ||
| const seen = Array.from(new Set(result.items.map(n => n.provider))).sort(); | ||
| setAllProviders(seen); | ||
| } |
There was a problem hiding this comment.
Keep the provider pill list synced with live notifications.
allProviders only updates after an unfiltered fetch. If addNotification pushes the first item from a new provider while this screen is open, that notification shows up in items but the new filter pill never appears until a later full reload.
💡 Low-friction fix
- const providers = allProviders;
+ const providers =
+ selectedProvider === undefined
+ ? Array.from(new Set([...allProviders, ...items.map(n => n.provider)])).sort()
+ : allProviders;Also applies to: 78-78
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/components/notifications/NotificationCenter.tsx` around lines 21 -
39, The provider pill list (allProviders) can become stale when a new
notification from a previously unseen provider is added in real-time; update
allProviders whenever a notification is added rather than only on unfiltered
fetches. Concretely, in the place that handles incoming notifications (the same
handler that calls setNotifications or addNotification inside
NotificationCenter), check the new notification's provider against allProviders
and, if missing, call setAllProviders(prev => Array.from(new Set([...prev,
newProvider])).sort()); ensure this check runs for single-item additions as well
as batched adds so the pill row is updated immediately.
| | 'local-model-debug' | ||
| | 'notifications' | ||
| | 'notification-routing'; |
There was a problem hiding this comment.
New settings routes are declared but not resolved in navigation state
Lines 33-34 add new SettingsRoute values, but getCurrentRoute (Line 67 onward) never maps /settings/notifications or /settings/notification-routing. Those pages fall through to 'home', which breaks breadcrumbs and can misroute back-navigation behavior.
Proposed fix
diff --git a/app/src/components/settings/hooks/useSettingsNavigation.ts b/app/src/components/settings/hooks/useSettingsNavigation.ts
@@
if (path.includes('/settings/ai')) return 'ai';
+ if (path.includes('/settings/notifications')) return 'notifications';
+ if (path.includes('/settings/notification-routing')) return 'notification-routing';
if (path.includes('/settings/local-model-debug')) return 'local-model-debug';
@@
case 'developer-options':
+ case 'notifications':
+ case 'notification-routing':
return [settingsCrumb];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/components/settings/hooks/useSettingsNavigation.ts` around lines 32 -
34, getCurrentRoute in useSettingsNavigation doesn't handle the newly added
SettingsRoute entries ('notifications' and 'notification-routing'), so paths
like /settings/notifications fall through to 'home'; update the route resolution
logic inside the getCurrentRoute function to explicitly map
'/settings/notifications' -> 'notifications' and
'/settings/notification-routing' -> 'notification-routing' (use the
SettingsRoute type/enum and any existing pathname parsing used for other
settings routes to keep behavior consistent with the rest of the function).
| // Integration notification ingest, triage, and per-provider settings | ||
| controllers.extend(crate::openhuman::notifications::all_notifications_registered_controllers()); |
There was a problem hiding this comment.
Add CLI namespace description for notification (and test coverage).
After registering the new namespace, namespace_description() still returns None for "notification", so CLI help won’t describe these controllers.
🔧 Proposed fix
pub fn namespace_description(namespace: &str) -> Option<&'static str> {
match namespace {
+ "notification" => Some(
+ "Integration notifications: ingest, list, read-state, and provider-level settings.",
+ ),
"about_app" => Some("Catalog the app's user-facing capabilities and where to find them."),
"app_state" => Some("Expose core-owned app shell state for frontend polling."),
"auth" => Some("Manage app session and provider credentials."), fn namespace_description_known_namespaces() {
+ assert!(namespace_description("notification").is_some());
assert!(namespace_description("memory").is_some());
assert!(namespace_description("memory_tree").is_some());
assert!(namespace_description("billing").is_some());Also applies to: 202-203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/core/all.rs` around lines 148 - 149, The CLI namespace for "notification"
is registered via
controllers.extend(crate::openhuman::notifications::all_notifications_registered_controllers())
but namespace_description() still returns None for "notification", so add a
description entry for "notification" in the namespace_description() function
(match the pattern used for other namespaces) and update/add unit tests to
assert namespace_description("notification") returns the expected string;
reference the controllers.extend(...) call and the namespace_description()
function and ensure tests cover the new mapping.
| outputs: vec![ | ||
| FieldSchema { | ||
| name: "id", | ||
| ty: TypeSchema::Option(Box::new(TypeSchema::String)), | ||
| comment: "UUID of the newly created notification record. Absent when skipped.", | ||
| required: false, | ||
| }, | ||
| FieldSchema { | ||
| name: "skipped", | ||
| ty: TypeSchema::Bool, | ||
| comment: | ||
| "True when the provider is disabled and the notification was not stored.", | ||
| required: true, | ||
| }, | ||
| ], |
There was a problem hiding this comment.
ingest schema is missing the optional reason output used in skip responses.
On disabled providers, runtime returns reason alongside skipped; the schema currently does not model that field, so schema-driven clients/docs lose that signal.
🔧 Proposed schema fix
outputs: vec![
FieldSchema {
name: "id",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "UUID of the newly created notification record. Absent when skipped.",
required: false,
},
FieldSchema {
name: "skipped",
ty: TypeSchema::Bool,
comment:
"True when the provider is disabled and the notification was not stored.",
required: true,
},
+ FieldSchema {
+ name: "reason",
+ ty: TypeSchema::Option(Box::new(TypeSchema::String)),
+ comment: "Optional skip reason, e.g. \"provider_disabled\".",
+ required: false,
+ },
],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| outputs: vec![ | |
| FieldSchema { | |
| name: "id", | |
| ty: TypeSchema::Option(Box::new(TypeSchema::String)), | |
| comment: "UUID of the newly created notification record. Absent when skipped.", | |
| required: false, | |
| }, | |
| FieldSchema { | |
| name: "skipped", | |
| ty: TypeSchema::Bool, | |
| comment: | |
| "True when the provider is disabled and the notification was not stored.", | |
| required: true, | |
| }, | |
| ], | |
| outputs: vec![ | |
| FieldSchema { | |
| name: "id", | |
| ty: TypeSchema::Option(Box::new(TypeSchema::String)), | |
| comment: "UUID of the newly created notification record. Absent when skipped.", | |
| required: false, | |
| }, | |
| FieldSchema { | |
| name: "skipped", | |
| ty: TypeSchema::Bool, | |
| comment: | |
| "True when the provider is disabled and the notification was not stored.", | |
| required: true, | |
| }, | |
| FieldSchema { | |
| name: "reason", | |
| ty: TypeSchema::Option(Box::new(TypeSchema::String)), | |
| comment: "Optional skip reason, e.g. \"provider_disabled\".", | |
| required: false, | |
| }, | |
| ], |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/notifications/schemas.rs` around lines 89 - 103, The ingest
response schema's outputs vector (the FieldSchema entries shown) is missing the
optional "reason" field returned on skipped notifications; add a FieldSchema
with name: "reason", ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "Optional reason why the notification was skipped (e.g., provider
disabled).", and required: false so schema-driven clients/docs include the skip
reason alongside "skipped" and "id".
| outputs: vec![FieldSchema { | ||
| name: "ok", | ||
| ty: TypeSchema::Bool, | ||
| comment: "True when settings were saved.", | ||
| required: true, | ||
| }], | ||
| }, |
There was a problem hiding this comment.
settings_set output schema is incomplete (settings missing).
Line 217 defines only ok, but the handler returns both ok and normalized settings. Please include settings in schema outputs to keep registry metadata accurate.
🔧 Proposed schema fix
- outputs: vec![FieldSchema {
- name: "ok",
- ty: TypeSchema::Bool,
- comment: "True when settings were saved.",
- required: true,
- }],
+ outputs: vec![
+ FieldSchema {
+ name: "ok",
+ ty: TypeSchema::Bool,
+ comment: "True when settings were saved.",
+ required: true,
+ },
+ FieldSchema {
+ name: "settings",
+ ty: TypeSchema::Ref("NotificationSettings"),
+ comment: "Persisted settings after clamp/normalization.",
+ required: true,
+ },
+ ],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| outputs: vec![FieldSchema { | |
| name: "ok", | |
| ty: TypeSchema::Bool, | |
| comment: "True when settings were saved.", | |
| required: true, | |
| }], | |
| }, | |
| outputs: vec![ | |
| FieldSchema { | |
| name: "ok", | |
| ty: TypeSchema::Bool, | |
| comment: "True when settings were saved.", | |
| required: true, | |
| }, | |
| FieldSchema { | |
| name: "settings", | |
| ty: TypeSchema::Ref("NotificationSettings"), | |
| comment: "Persisted settings after clamp/normalization.", | |
| required: true, | |
| }, | |
| ], | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/notifications/schemas.rs` around lines 217 - 223, The outputs
for the settings_set endpoint are missing the returned normalized settings:
update the outputs Vec in the FieldSchema for settings_set to include a second
FieldSchema with name "settings", type matching the settings schema (e.g.,
TypeSchema::Ref or the specific settings TypeSchema), a descriptive comment
(e.g., "Normalized settings returned after save."), and required: true so
registry metadata matches the handler return value; modify the outputs Vec
alongside the existing "ok" FieldSchema in the settings_set definition to
reflect both returned fields.
| fn with_connection<T>(config: &Config, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> { | ||
| let db_path = config | ||
| .workspace_dir | ||
| .join("notifications") | ||
| .join("notifications.db"); | ||
|
|
||
| if let Some(parent) = db_path.parent() { | ||
| std::fs::create_dir_all(parent).with_context(|| { | ||
| format!( | ||
| "[notifications::store] failed to create dir {}", | ||
| parent.display() | ||
| ) | ||
| })?; | ||
| } | ||
|
|
||
| let conn = Connection::open(&db_path).with_context(|| { | ||
| format!( | ||
| "[notifications::store] failed to open DB at {}", | ||
| db_path.display() | ||
| ) | ||
| })?; | ||
|
|
||
| conn.execute_batch(SCHEMA) | ||
| .context("[notifications::store] schema migration failed")?; | ||
|
|
||
| f(&conn) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add debug/trace instrumentation around store entry points and DB operations.
This new store opens/migrates the DB and performs inserts, queries, and updates with almost no debug/trace checkpoints. That will make notification-delivery issues much harder to reconstruct in production. As per coding guidelines, "Rust code must use log / tracing at debug or trace level; add substantial development-oriented logs at entry/exit points, branch decisions, external calls, retries, state transitions, and error handling paths".
Also applies to: 80-105, 116-152, 163-185, 203-252
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/notifications/store.rs` around lines 47 - 72, Add debug/trace
instrumentation around the notifications store entry points and DB operations:
in with_connection, log entry/exit (function start and before returning), log
the resolved db_path, log before and after Connection::open, log success/failure
of create_dir_all, and log before/after conn.execute_batch and before calling
f(&conn); use the crate's logging (tracing::debug/trace or log::debug) to record
these checkpoints and include contextual details (db_path.display(),
parent.display(), error contexts) so callers like with_connection, the migration
block (conn.execute_batch), and the invocation f(&conn) are observable; apply
the same pattern to the other store functions mentioned (lines 80-105, 116-152,
163-185, 203-252) at their entry/exit points, DB calls, branches and error
paths.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether this store configures any SQLite busy timeout or related lock-handling today.
rg -n -C2 --type=rust 'busy_timeout|PRAGMA\s+busy_timeout|journal_mode' src/openhuman/notifications/store.rsRepository: tinyhumansai/openhuman
Length of output: 48
🏁 Script executed:
# Check for other SQLite Connection opens in the codebase to see how busy_timeout is handled elsewhere
rg -n 'Connection::open|busy_timeout' --type=rust -A2 | head -100Repository: tinyhumansai/openhuman
Length of output: 7731
🏁 Script executed:
# Look at the full notifications/store.rs file to understand the module structure and usage patterns
wc -l src/openhuman/notifications/store.rsRepository: tinyhumansai/openhuman
Length of output: 106
🏁 Script executed:
# Check whether the notifications store module is exposed and used by RPC handlers
rg -n 'notifications.*store|use.*notifications' --type=rust src/core_server/ | head -20Repository: tinyhumansai/openhuman
Length of output: 122
🏁 Script executed:
# Check the notifications module's public API and entry points
fd 'notifications' --type f --path '*/src/openhuman/*' | head -10Repository: tinyhumansai/openhuman
Length of output: 299
🏁 Script executed:
# Check the memory/tree/store.rs pattern with busy_timeout
sed -n '340,355p' src/openhuman/memory/tree/store.rsRepository: tinyhumansai/openhuman
Length of output: 816
🏁 Script executed:
# Find where SQLITE_BUSY_TIMEOUT is defined
rg -n 'SQLITE_BUSY_TIMEOUT\s*=' --type=rustRepository: tinyhumansai/openhuman
Length of output: 48
🏁 Script executed:
# Check how notifications store is actually used (look for imports/calls)
rg -n 'notifications::store' --type=rust | head -20Repository: tinyhumansai/openhuman
Length of output: 1687
🏁 Script executed:
# Search for where notifications store is actually invoked (not just internal error messages)
rg -n 'with_connection|notifications.*insert|notifications.*list|notifications.*update_triage' --type=rust src/ | grep -v 'store.rs' | head -20Repository: tinyhumansai/openhuman
Length of output: 2313
🏁 Script executed:
# Check if SQLITE_BUSY_TIMEOUT is inline or from a dependency
rg -n 'busy_timeout\(' --type=rust -B2 -A2Repository: tinyhumansai/openhuman
Length of output: 555
🏁 Script executed:
# Look for any API handler or module that calls notifications functions
rg -n 'pub.*fn.*notification|notifications::|Notification' --type=rust src/openhuman/ | grep -v test | head -30Repository: tinyhumansai/openhuman
Length of output: 3202
🏁 Script executed:
# Check notifications/rpc.rs to see how it calls store operations
sed -n '1,100p' src/openhuman/notifications/rpc.rsRepository: tinyhumansai/openhuman
Length of output: 3967
🏁 Script executed:
# Check SQLITE_BUSY_TIMEOUT definition
rg -n 'const.*SQLITE_BUSY_TIMEOUT|SQLITE_BUSY_TIMEOUT\s*=' --type=rust -B1 -A1Repository: tinyhumansai/openhuman
Length of output: 280
🏁 Script executed:
# Check if subconscious store also lacks busy_timeout
sed -n '20,35p' src/openhuman/subconscious/store.rsRepository: tinyhumansai/openhuman
Length of output: 607
Configure a SQLite busy timeout on each new connection.
This helper opens a fresh connection for every store operation. The notifications store is accessed concurrently (from RPC handlers and background triage tasks), and without a busy timeout configured, transient SQLite file locking can surface as immediate database is locked failures. This follows the established pattern in src/openhuman/memory/tree/store.rs (which uses SQLITE_BUSY_TIMEOUT = Duration::from_secs(5)).
Suggested change
let conn = Connection::open(&db_path).with_context(|| {
format!(
"[notifications::store] failed to open DB at {}",
db_path.display()
)
})?;
+ conn
+ .busy_timeout(std::time::Duration::from_secs(5))
+ .context("[notifications::store] failed to configure sqlite busy timeout")?;
conn.execute_batch(SCHEMA)
.context("[notifications::store] schema migration failed")?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/notifications/store.rs` around lines 47 - 72, The
with_connection helper opens a new rusqlite Connection without a busy timeout,
causing transient "database is locked" errors under concurrent access; after
creating conn in with_connection (the Connection::open call) set a busy timeout
(e.g., Duration::from_secs(5) or reuse the existing SQLITE_BUSY_TIMEOUT constant
used in memory/tree/store.rs) by calling the connection's busy_timeout(...)
method and propagate any error (with_context similarly) before running
conn.execute_batch(SCHEMA) and invoking f(&conn).
| with_connection(config, |conn| { | ||
| let now = Utc::now().to_rfc3339(); | ||
| conn.execute( | ||
| "UPDATE integration_notifications | ||
| SET importance_score = ?1, triage_action = ?2, triage_reason = ?3, scored_at = ?4 | ||
| WHERE id = ?5", | ||
| params![score, action, reason, now, id], | ||
| ) | ||
| .context("[notifications::store] update_triage failed")?; | ||
| Ok(()) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect whether update_triage checks the affected-row count from rusqlite::Connection::execute.
rg -n -C3 --type=rust 'fn update_triage|let updated = conn\.execute|conn\.execute\(' src/openhuman/notifications/store.rsRepository: tinyhumansai/openhuman
Length of output: 1922
🏁 Script executed:
#!/bin/bash
# Read the full update_triage function to confirm current state
sed -n '155,175p' src/openhuman/notifications/store.rsRepository: tinyhumansai/openhuman
Length of output: 661
🏁 Script executed:
#!/bin/bash
# Check the test for update_triage to see if it validates error cases
sed -n '369,420p' src/openhuman/notifications/store.rsRepository: tinyhumansai/openhuman
Length of output: 2015
🏁 Script executed:
#!/bin/bash
# Compare mark_read (similar UPDATE operation) to see if it has the same pattern
sed -n '176,195p' src/openhuman/notifications/store.rsRepository: tinyhumansai/openhuman
Length of output: 764
Fail update_triage when the target notification does not exist.
The function ignores the affected-row count from conn.execute(), so a stale id returns Ok(()) even though nothing was updated. The caller would incorrectly believe the triage persisted when the record is already gone. The test only covers the happy path and does not validate this error condition.
Suggested change
let now = Utc::now().to_rfc3339();
- conn.execute(
+ let updated = conn.execute(
"UPDATE integration_notifications
SET importance_score = ?1, triage_action = ?2, triage_reason = ?3, scored_at = ?4
WHERE id = ?5",
params![score, action, reason, now, id],
)
.context("[notifications::store] update_triage failed")?;
+ if updated == 0 {
+ anyhow::bail!("[notifications::store] update_triage missing notification: {id}");
+ }
Ok(())
})
}Note: mark_read has the same issue at lines 177–189.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| with_connection(config, |conn| { | |
| let now = Utc::now().to_rfc3339(); | |
| conn.execute( | |
| "UPDATE integration_notifications | |
| SET importance_score = ?1, triage_action = ?2, triage_reason = ?3, scored_at = ?4 | |
| WHERE id = ?5", | |
| params![score, action, reason, now, id], | |
| ) | |
| .context("[notifications::store] update_triage failed")?; | |
| Ok(()) | |
| with_connection(config, |conn| { | |
| let now = Utc::now().to_rfc3339(); | |
| let updated = conn.execute( | |
| "UPDATE integration_notifications | |
| SET importance_score = ?1, triage_action = ?2, triage_reason = ?3, scored_at = ?4 | |
| WHERE id = ?5", | |
| params![score, action, reason, now, id], | |
| ) | |
| .context("[notifications::store] update_triage failed")?; | |
| if updated == 0 { | |
| anyhow::bail!("[notifications::store] update_triage missing notification: {id}"); | |
| } | |
| Ok(()) | |
| }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/notifications/store.rs` around lines 163 - 172, The
update_triage flow currently ignores the affected-row count from conn.execute so
it returns Ok(()) even when no row matches the given id; modify the block inside
with_connection that calls conn.execute in update_triage to capture the returned
affected row count, and if it is 0 return a Contextual error (e.g., via
anyhow::bail or .with_context) indicating the notification id was not found; do
the same fix for mark_read (the other conn.execute block) so both functions fail
when no rows were updated rather than silently succeeding.
| let scored_at_str: Option<String> = row.get(11)?; | ||
| let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| s.parse().ok()); |
There was a problem hiding this comment.
Don't silently erase malformed scored_at values.
Unlike received_at, a bad scored_at is converted to None with no signal. That makes a persisted notification look unscored and hides on-disk data issues.
Suggested change
let scored_at_str: Option<String> = row.get(11)?;
- let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| s.parse().ok());
+ let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| {
+ s.parse().map_err(|e| {
+ tracing::warn!(
+ raw = %s,
+ error = %e,
+ "[notifications::store] invalid scored_at, using None"
+ );
+ e
+ }).ok()
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let scored_at_str: Option<String> = row.get(11)?; | |
| let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| s.parse().ok()); | |
| let scored_at_str: Option<String> = row.get(11)?; | |
| let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| { | |
| s.parse().map_err(|e| { | |
| tracing::warn!( | |
| raw = %s, | |
| error = %e, | |
| "[notifications::store] invalid scored_at, using None" | |
| ); | |
| e | |
| }).ok() | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/notifications/store.rs` around lines 293 - 294, The code
silently drops malformed scored_at values by converting
scored_at_str.parse().ok() into None; change the behavior in the data-loading
function that calls row.get(11)? (the code creating scored_at_str and scored_at)
so that parse failures are surfaced instead of silently returning None — either
return an Err with a clear context (including the offending scored_at_str) or
log a warning/error that includes scored_at_str and the parse error and preserve
the original string in the struct (or a separate "raw_scored_at" field) so
on-disk data issues are visible; update the handling of scored_at (the
scored_at_str -> scored_at conversion) accordingly.
Summary
tauri-cefnotification intercept intotauri-plugin-notificationso embedded apps (Slack, Discord, Gmail, WhatsApp) produce real native OS toasts instead of silently dropping them[Slack] New message from Alice) for clear attribution; honoursilent,icon, andtagfrom the Web Notification APINotificationRoutekeyed by{provider}:{account_id}:{tag_or_uuid}to enable click-to-focus routing once platform delegates (UNUserNotificationCenter / notify-rust) are wiredwebview_notification_permission_state/webview_notification_permission_request) so the frontend sees"granted" | "denied" | "default"on both CEF and wry runtimesProblem
Embedded webview apps call
window.Notificationinside CEF but never produce native macOS/Windows toasts. The CEF runtime intercepts the web Notification API but the intercept was dropped on the floor — no bridge totauri-plugin-notification, no click routing back to the originating account, no permission query/request pipeline.Solution
Rust (
app/src-tauri/src/webview_accounts/mod.rs)NotificationRoutestruct:provider,account_id,tag,created_atnotification_routes: Mutex<HashMap<String, NotificationRoute>>onWebviewAccountStateclear_notification_routes(account_id)— purged on close/purgeforward_native_notification(app, provider, account_id, payload)— prefixes title, respectssilent/icon/tag, falls back to monotonic timestamp for untagged payloadswebview_notification_permission_state/webview_notification_permission_request— map 4-varianttauri::plugin::PermissionStateonto 3-value web API shape; non-CEF stubs always return"default"setup:tauri_runtime_cef::notification::registerwith handler callingforward_native_notification;unregisteron account closeFrontend (
app/src/services/webviewAccountService.ts)ensureNotificationPermission(accountId)— invokes permission state, requests if"default", runs once per session; called afteropenWebviewAccounthandleNotificationClick+listen('notification:click', …)— dispatchessetActiveAccountand invokesactivate_main_windowwhen the dormant platform click hook firesCapabilities — added
notification:allow-notify,notification:allow-request-permission,notification:allow-is-permission-grantedSubmission Checklist
.app, open Slack/Discord/Gmail in a webview account, trigger a notification, confirm provider-prefixed OS toast appears.webview_accounts/mod.rsImpact
notification:clickyet — follow-up work to wire UNUserNotificationCenter / notify-rust delegates.Related
Summary by CodeRabbit
Release Notes