Files
openhuman/docs/TAURI_CEF_FINDINGS_AND_CHANGES.md
T

18 KiB
Raw Blame History

Tauri CEF Notification Findings And Changes

Scope

This note summarizes:

  • what was found in tauri-cef
  • what was missing for webview notification permission and delivery
  • what was changed in tauri-cef
  • what was changed in openhuman-cursor
  • how the setup was debugged and verified

Relevant codebases:

  • (external clone, not in this repo) — standalone tauri-cef repo
  • . (repo root)
  • vendored submodule: app/src-tauri/vendor/tauri-cef

Initial Findings In tauri-cef

1. Browser-side permission acceptance existed

tauri-runtime-cef already had browser-process logic that accepted Chromium notification permission requests:

  • crates/tauri-runtime-cef/src/permissions.rs
  • crates/tauri-runtime-cef/src/cef_impl.rs

This meant CEF could accept CEF_PERMISSION_TYPE_NOTIFICATIONS.

2. Renderer-side granted state was not wired into the real runtime path

Slack and similar apps do not rely only on the browser permission callback. They also inspect browser-visible JavaScript state:

  • Notification.permission
  • Notification.requestPermission()
  • navigator.permissions.query({ name: "notifications" })

A renderer-side shim for this behavior existed only in:

  • cef-helper/src/notification.rs

But the actual runtime app path used by tauri-runtime-cef did not attach that renderer process handler in the default TauriApp path. As a result, web apps could still observe notification state as "prompt" instead of "granted".

3. Notification permission looked partially implemented, not end-to-end

There was a notification IPC path and registry in tauri-runtime-cef, but the setup was incomplete unless the embedder registered a handler:

  • browser process received notification IPC
  • runtime exposed notification::register(...)
  • without an app-side registration, notifications could still be dropped

4. The old behavior was not sufficient for Slack

In practice, Slack kept behaving as if notifications still needed to be enabled because the renderer-visible granted state was not consistently exposed on the real runtime path.

5. Additional root cause found during live debugging

Later live DevTools inspection revealed a second, more concrete failure mode in openhuman-cursor:

  • tauri-plugin-notification injects its own JavaScript init script into every Tauri webview
  • that init script overwrote window.Notification
  • the replacement implementation forwarded notification calls to:
    • plugin:notification|notify
    • over Tauri IPC at http://ipc.localhost/...

For external web pages such as Slack, this is the wrong path:

  • the page is not supposed to use Tauri IPC as its notification transport
  • the page should stay on the native CEF notification path
  • when the plugin shim won, calls failed with 500 and console errors such as:
    • POST http://ipc.localhost/plugin%3Anotification%7Cnotify 500
    • Origin header is not a valid URL

That meant the plugins JS shim was effectively undoing the CEF notification interception fix inside external webviews.

Changes Made In tauri-cef

These changes were made in the standalone tauri-cef repo and pushed there first.

1. Moved notification permission shims into the real runtime path

Renderer-side notification permission shims were added to tauri-runtime-cef so they run on the real Tauri CEF runtime path instead of only in the standalone helper.

Files involved:

  • (external tauri-cef repo)/crates/tauri-runtime-cef/src/notification.rs
  • (external tauri-cef repo)/crates/tauri-runtime-cef/src/cef_impl.rs
  • (external tauri-cef repo)/crates/tauri-runtime-cef/src/lib.rs

Behavior provided by the shim:

  • Notification.permission resolves to granted
  • Notification.requestPermission() resolves to granted
  • navigator.permissions.query({ name: "notifications" }) reports granted
  • notification calls are forwarded through the CEF runtime notification path

2. Hooked the render process handler into TauriApp

The runtime app path now installs the render process handler needed for the notification shim.

3. Started the helper process with a real app object

run_cef_helper_process() was updated to launch CEF with an app object instead of None, so the notification renderer setup is available consistently.

4. Added notification handler cleanup on browser close

In the vendored tauri-cef inside openhuman-cursor, an additional lifecycle cleanup was added:

  • app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs

Added on browser close:

crate::notification::unregister(browser_id);

This prevents stale notification handlers from remaining registered after a webview is destroyed.

5. Standalone tauri-cef commit

The standalone tauri-cef repo was updated and pushed with:

  • branch: feat/cef
  • commit: c8ece7c78
  • message: Fix CEF notification permission shims

The openhuman-cursor vendored submodule was then updated to the corresponding submodule commit:

  • c8ece7c784b8cdff16dc552f6892a0c9982ef1ba

Changes Made In openhuman-cursor

1. Enabled shell-side webview notifications by default

File:

  • app/src-tauri/src/notification_settings/mod.rs

Change:

  • default notification toggle changed to enabled:
AtomicBool::new(true)

This ensures notifications are not disabled by default at the app layer.

2. Added a Slack debugging bypass for internal CDP attachment

File:

  • app/src-tauri/src/webview_accounts/mod.rs

Environment flag added:

  • OPENHUMAN_DISABLE_SLACK_SCANNER=1

When set for Slack accounts, the app now:

  • skips Slack scanner startup
  • skips the long-lived CDP session for Slack
  • loads the real Slack URL directly instead of going through the placeholder data: path used for the CDP bootstrap flow

Expected logs:

  • [webview-accounts] skipping CDP session via OPENHUMAN_DISABLE_SLACK_SCANNER ...
  • [webview-accounts] slack scanner disabled via OPENHUMAN_DISABLE_SLACK_SCANNER ...

This was added only to make manual DevTools inspection possible without the app attaching to the same Slack target.

3. Vendored tauri-plugin-notification and removed its JS init script

To stop window.Notification from being overwritten inside external webviews, tauri-plugin-notification was vendored into the repo and switched from a git dependency to a path dependency.

New vendored path:

  • app/src-tauri/vendor/tauri-plugin-notification

Two changes were made:

  1. The plugin dependency in:

    • app/src-tauri/Cargo.toml now points to the vendored path.
  2. The plugin init function in:

    • app/src-tauri/vendor/tauri-plugin-notification/src/lib.rs no longer calls:
.js_init_script(...)

This keeps the Rust-side desktop notification API available through NotificationExt, but stops the plugin from globally replacing window.Notification in embedded external pages.

The result is:

  • app Rust code can still fire native notifications
  • external webviews like Slack no longer get forced onto the Tauri IPC notification path
  • the native CEF notification shim remains authoritative inside the external webview

Verification Performed

Build verification

In app/src-tauri:

  • cargo fmt passed
  • cargo check --features cef passed for the main notification changes
  • cargo check --features cef also passed after vendoring tauri-plugin-notification and removing its JS init script

One later cargo check attempt for the Slack DevTools bypass work was blocked by another running Cargo process holding the build lock, but the changes were localized and formatting completed.

Runtime verification

The app was run in dev mode and the CEF DevTools target list was checked through:

  • http://localhost:9222/json/list
  • http://127.0.0.1:9222/json/list
  • earlier in one run, http://[::1]:9222/json/list

Observed targets included:

  • Slack page target
  • Slack service worker target
  • OpenHuman page target

This confirmed:

  • Slack was running inside the CEF webview
  • CEF remote debugging was active

Slack scanner contention diagnosis

At first, the main reason DevTools inspection failed for Slack was that the app itself was attaching to the Slack target over CDP:

  • the Slack scanner auto-attached
  • the long-lived per-account CDP session also attached

This was why manual DevTools sessions disconnected even when the target existed.

The debugging bypass confirmed this by producing logs that both internal attachment paths were skipped.

Final DevTools Findings

After the Slack-specific CDP paths were disabled, DevTools could still disconnect even for the OpenHuman page target. That showed the remaining issue was not Slack-specific.

Live verification showed:

  • the active backend was on 127.0.0.1:9222
  • localhost was inconsistent during checks
  • Brave already had multiple established connections to 127.0.0.1:9222
  • the running OpenHuman app was listening on that port

Most likely explanation:

  • multiple existing DevTools frontend sessions in Brave were already connected
  • reopening new inspector tabs caused connection churn
  • 127.0.0.1 was more reliable than localhost

Recommended DevTools usage:

  1. Close all existing DevTools tabs for localhost:9222 and 127.0.0.1:9222.
  2. Reopen only one inspector at a time.
  3. Prefer the exact 127.0.0.1 websocket host advertised by /json/list.

Later Runtime And Helper Findings

1. The real helper bundle was stale at first

During live verification, the main app binary contained the new notification instrumentation but the bundled macOS helper apps did not.

That turned out to be a packaging issue:

  • the installed cargo-tauri binary was stale
  • helper executables are bundled by the CLI/bundler
  • rebuilding the app alone was not enough to refresh the helper binaries

To prevent this from recurring, the local CLI bootstrap script was updated:

  • scripts/ensure-tauri-cli.sh

It now reinstalls vendored cargo-tauri when vendored tauri-cef sources are newer than the installed CLI binary.

2. The live macOS renderer path uses cef-helper

An important debugging detail was that the actual live renderer helper on macOS was using:

  • app/src-tauri/vendor/tauri-cef/cef-helper/src/notification.rs

and not only the runtime-side notification file in:

  • app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs

So the helper-side shim was instrumented directly with:

  • install logs
  • execution logs
  • debug markers
  • manual test entry points

Helper-side logs added:

  • [cef-helper-notify] on_context_created ...
  • [cef-helper-notify] installed shims ...
  • [cef-helper-notify] execute source=...

Helper-side debug markers added:

  • window.__OPENHUMAN_CEF_NOTIFICATION_SHIM
  • window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN
  • Notification.__openhuman_cef

Manual helper entry points added:

  • window.__openhumanFireNotification(title, options)
  • window.__OPENHUMAN_CEF_NOTIFICATION_CONSTRUCTOR

3. Helper shim installation was verified in the real Slack page

After rebuilding the helper bundle, live DevTools checks showed:

  • window.__OPENHUMAN_CEF_NOTIFICATION_SHIM === true
  • window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN pointing at the Slack page URL

This proved that the CEF helper render shim was actually installing in the Slack page context.

4. Manual helper-triggered notifications work end-to-end

The following manual trigger was verified to reach the app:

window.__openhumanFireNotification("Slack CEF test", {
  body: "Manual helper path"
})

Observed runtime logs:

  • [cef-notify] ipc ...
  • [cef-notify] dispatch ...
  • [notify-cef][...] ...

This proved that:

  • renderer helper -> browser IPC works
  • runtime dispatch works
  • openhuman-cursor receives the notification payload
  • OS notification delivery can work

5. Tokio runtime panic found and fixed in app notification delivery

Once the manual helper path reached the app, native notification delivery initially crashed with:

  • there is no reactor running, must be called from the context of a Tokio 1.x runtime

The panic came from using tokio::task::spawn_blocking(...) from a CEF callback thread in:

  • app/src-tauri/src/webview_accounts/mod.rs

Fix applied:

  • replaced tokio::task::spawn_blocking(...) with std::thread::spawn(...)

This was done for the macOS notification path and the Linux path for consistency.

After this fix, the manual helper trigger produced an actual OS notification successfully.

6. Invalid UTF-16 string messages were observed but were not the blocker

During manual notification tests, CEF emitted:

  • Invalid UTF-16 string

These warnings appeared before successful notification IPC and dispatch logs, so they were treated as noisy string-conversion warnings rather than the root failure.

7. Slack still does not automatically use the browser notification APIs for real messages

After helper-side execution logging was added, a real incoming Slack message did not produce:

  • [cef-helper-notify] execute source=0 ...
  • [cef-helper-notify] execute source=1 ...

This means the real incoming-message path in this embedded Slack session is not currently hitting:

  • new Notification(...)
  • ServiceWorkerRegistration.prototype.showNotification(...)

So the remaining problem is no longer CEF notification transport. The remaining problem is Slack-specific runtime behavior in this embedded environment.

8. An attempted hard lock on window.Notification broke Slack rendering

One experiment tried to prevent Slack from overwriting the helper-installed notification hooks by making them effectively non-overridable.

That caused Slack to render a blank screen, so that hardening change was reverted.

Current safe state:

  • Slack renders normally
  • helper shim installs
  • manual helper trigger works
  • automatic incoming-message notifications still do not use the browser notification APIs

9. Fallback strategy: synthesize notifications from the Slack scanner

Because Slacks own incoming-message path was not invoking browser notification APIs, a fallback path was added to the Slack scanner:

  • app/src-tauri/src/slack_scanner/mod.rs
  • app/src-tauri/src/webview_accounts/mod.rs

The scanner logic was updated to:

  • keep a per-channel unread baseline
  • skip notifications on the first scan
  • emit a native notification when a channel unread count increases later

However, live verification showed the scanner path was not actually active because the app was missing the scanner registry in managed state.

Observed log:

  • [webview-accounts] slack ScannerRegistry not in app state

So the scanner-based fallback is patched in code, but it still needs the app builder to manage slack_scanner::ScannerRegistry::new() in:

  • app/src-tauri/src/lib.rs

Current Outcome

Notification permission behavior

The underlying notification permission problem in tauri-cef was fixed by moving the renderer-visible granted-state shim into the actual runtime path.

This means Slack-style checks should now see notification permission as granted inside the Tauri CEF webview.

App-side notification behavior

openhuman-cursor was updated so:

  • shell-side notifications are enabled by default
  • the vendored tauri-cef includes the runtime permission fix
  • browser notification handlers are cleaned up on webview close
  • manual helper-triggered notifications reach the OS successfully
  • app-side native notification delivery no longer depends on a Tokio runtime in the callback thread

Remaining distinction

There are two separate concerns:

  1. Permission/granted-state correctness
  2. What the app does after a notification is received

The changes above fix the permission side and the runtime notification bridge plumbing. The app still needs its normal notification handling path to decide how to present or forward those notifications.

Current verified status

Verified working:

  • notification permission appears granted in the Slack webview
  • helper shim installs in the live Slack page
  • manual helper notification trigger reaches CEF browser IPC
  • runtime dispatch reaches openhuman-cursor
  • native OS notification display works for the manual helper-triggered path

Still not working automatically:

  • real Slack incoming messages do not currently surface through browser notification APIs in this embedded session
  • scanner-driven fallback notifications will not run until the scanner registry is re-enabled in app state

Files Changed

In tauri-cef

  • (external tauri-cef repo)/crates/tauri-runtime-cef/src/notification.rs
  • (external tauri-cef repo)/crates/tauri-runtime-cef/src/cef_impl.rs
  • (external tauri-cef repo)/crates/tauri-runtime-cef/src/lib.rs
  • (external tauri-cef repo)/CEF_NOTIFICATION_PERMISSION_CHANGES.md

In openhuman-cursor

  • app/src-tauri/src/notification_settings/mod.rs
  • app/src-tauri/src/webview_accounts/mod.rs
  • app/src-tauri/src/slack_scanner/mod.rs
  • app/src-tauri/src/lib.rs
  • app/src-tauri/Cargo.toml
  • app/src-tauri/vendor/tauri-plugin-notification/Cargo.toml
  • app/src-tauri/vendor/tauri-plugin-notification/src/lib.rs
  • app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs
  • app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs
  • app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/lib.rs
  • app/src-tauri/vendor/tauri-cef/cef-helper/src/notification.rs
  • app/src-tauri/vendor/tauri-cef/cef-helper/src/main.rs
  • scripts/ensure-tauri-cli.sh

Suggested Follow-Up

Recommended next functional fix:

  1. Re-enable slack_scanner::ScannerRegistry::new() in:
    • app/src-tauri/src/lib.rs
  2. Rebuild and verify unread-delta notifications from the scanner fallback path.

Secondary cleanup work:

  1. Remove temporary helper/debug instrumentation once notification behavior is finalized.
  2. If cleaner inspection is still needed, move remote debugging off 9222 to a dedicated port such as 9333.