Skip to content

feat(tauri): webview_accounts ingest for provider surfaces#801

Merged
senamakel merged 4 commits intotinyhumansai:mainfrom
jwalin-shah:pr/tauri-webview-retrieval-v1
Apr 26, 2026
Merged

feat(tauri): webview_accounts ingest for provider surfaces#801
senamakel merged 4 commits intotinyhumansai:mainfrom
jwalin-shah:pr/tauri-webview-retrieval-v1

Conversation

@jwalin-shah
Copy link
Copy Markdown
Contributor

@jwalin-shah jwalin-shah commented Apr 22, 2026

Depends on: #803

...

Summary by CodeRabbit

  • New Features

    • Added event ingestion support to send application events to external systems for enhanced tracking and analytics capabilities.
  • Bug Fixes

    • Implemented robust error handling for event delivery with automatic logging of failures.

@jwalin-shah jwalin-shah requested a review from a team April 22, 2026 18:31
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Adds an async side-channel that forwards webview_recipe_event notifications to an external RPC endpoint. The implementation normalizes event data (entity/thread IDs, title, sender, deep link, requires_attention flag, timestamp), constructs a JSON-RPC POST request, and handles potential failures gracefully with logging.

Changes

Cohort / File(s) Summary
Webview Event RPC Forwarding
app/src-tauri/src/webview_accounts/mod.rs
Introduces async side-channel logic that intercepts webview events, extracts and normalizes payload fields with fallback key resolution, constructs JSON-RPC requests, and POSTs to external RPC endpoint with 10-second timeout; includes error handling for HTTP failures, decode errors, and RPC error responses.

Sequence Diagram(s)

sequenceDiagram
    participant WVE as Webview Event<br/>(Recipe Event)
    participant EXT as Event Extraction &<br/>Normalization
    participant RPC as JSON-RPC<br/>Formatter
    participant HTTP as HTTP Client<br/>(reqwest)
    participant EP as External RPC<br/>Endpoint
    participant LOG as Logger

    WVE->>EXT: webview_recipe_event triggered<br/>(args.payload)
    EXT->>EXT: Extract fields (entity_id, thread_id,<br/>title, sender, deep_link,<br/>requires_attention, timestamp)
    EXT->>RPC: Normalized event data
    RPC->>RPC: Wrap in JSON-RPC request<br/>(method, params)
    RPC->>HTTP: POST with 10s timeout
    HTTP->>EP: Send JSON-RPC request
    
    alt Success Path
        EP-->>HTTP: HTTP success response
        HTTP-->>RPC: Decode response
        RPC->>RPC: Check for RPC error field
        alt No RPC Error
            RPC->>LOG: Log successful forward (info/debug)
        else RPC Error Present
            RPC->>LOG: Log RPC error warning
        end
    else Failure Path
        EP-->>HTTP: HTTP error or timeout
        HTTP->>LOG: Log HTTP failure warning
    else Decode Error
        RPC->>LOG: Log decode error warning
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • #874 — Implements complementary TS-side forwarding of webview notifications to ingest pipeline with Rust-side deduplication handling, part of the same notification routing infrastructure.

Suggested reviewers

  • graycyrus

Poem

🐰✨ A little hop through async lanes,
Event payloads cross the chains,
Normalized, wrapped in JSON's embrace,
Hopping data to its rightful place!
Timeouts caught with warning's care, 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding an ingest bridge for provider surfaces to the webview_accounts module in Tauri.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Pairs with core provider_surfaces + app queue polling; merge after app PR if IPC shape depends on UI rollout.

Made-with: Cursor
Fixes E0382 partial move when building WebviewEvent after post_provider_surfaces_event.

Made-with: Cursor
@jwalin-shah jwalin-shah force-pushed the pr/tauri-webview-retrieval-v1 branch from fa0aae0 to 6fe24e8 Compare April 23, 2026 19:45
Both std::time::Duration (from this PR) and the cfg-gated
std::sync::{mpsc::sync_channel, OnceLock} (from main) are needed;
keep both import lines.
Copy link
Copy Markdown
Member

@senamakel senamakel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #801 — feat(tauri): webview_accounts ingest bridge for provider surfaces

Walkthrough

This PR wires up a new fire-and-forget bridge in the Tauri shell's webview_recipe_event handler: whenever a provider webview fires an ingest event, the Tauri process normalises the raw payload into a ProviderEvent-shaped JSON struct and forwards it to the openhuman.provider_surfaces_ingest_event RPC endpoint on the core sidecar. The normalisation layer handles the naming divergence between provider schemas (e.g. threadId / chatId / conversationId) and the canonical ProviderEvent struct. A fresh reqwest client is built per call with a 10-second timeout. Errors are logged at warn level and swallowed — the existing webview:event emission path is unchanged.

Changes

File Summary
app/src-tauri/src/webview_accounts/mod.rs Adds helper functions for payload extraction, timestamp normalisation, event normalisation, and an async HTTP bridge to openhuman.provider_surfaces_ingest_event; hooks the bridge into webview_recipe_event. Also had an unresolved merge-conflict marker in the import block (fixed in this review).

Actionable comments (3)

1. app/src-tauri/src/webview_accounts/mod.rs:24-29 — Unresolved merge conflict marker (BLOCKER, fixed)

The file contained a raw <<<<<<< HEAD / ======= / >>>>>>> main conflict block in the use section. cargo check would have failed on CI and the binary would not build. Both import lines were needed: use std::time::Duration (from this PR) and the #[cfg(all(feature = "cef", target_os = "linux"))]-gated mpsc::sync_channel, OnceLock block (from main).

Fixed in this review — commit 70c00ecf; import ordering corrected by cargo fmt in 99e21fbc.


2. app/src-tauri/src/webview_accounts/mod.rs:479-498 — New reqwest::Client constructed on every ingest event (major)

post_provider_surfaces_event calls reqwest::Client::builder().build() on every invocation. On an active account with frequent ingest events this creates a new TCP connection pool each call, adds latency, and wastes file descriptors. The Tauri Axum/reqwest ecosystem expects a shared client.

Suggested change: store a reqwest::Client in WebviewAccountsState (initialised once in the Default impl) and pass it through, or use once_cell::sync::Lazy at module level:

// Before (inside post_provider_surfaces_event)
let client = reqwest::Client::builder()
    .timeout(Duration::from_secs(10))
    .build()
    .map_err(|e| format!("http client: {e}"))?;

// After — module-level singleton
static CORE_RPC_CLIENT: once_cell::sync::Lazy<reqwest::Client> =
    once_cell::sync::Lazy::new(|| {
        reqwest::Client::builder()
            .timeout(Duration::from_secs(10))
            .build()
            .expect("core RPC client init")
    });
// then inside post_provider_surfaces_event:
let resp = CORE_RPC_CLIENT.post(&url).json(&body).send().await...

3. app/src-tauri/src/webview_accounts/mod.rs:445-446requires_attention default is too aggressive (minor)

When the provider sends an event with no explicit requires_attention field, the bridge sets it to true whenever sender_name or snippet is non-None. Almost every ingest event for active conversations will have a sender and snippet, so the queue will fill with items marked actionable regardless of whether the user actually needs to respond. The original ProviderEvent struct defaults requires_attention to false.

// Before
let requires_attention = payload_bool(&args.payload, "requires_attention")
    .unwrap_or(unread > 0 || sender_name.is_some() || snippet.is_some());

// After — conservative default matching ProviderEvent::default
let requires_attention = payload_bool(&args.payload, "requires_attention")
    .unwrap_or(unread > 0);

This still promotes events with explicit unread counts without falsely flagging every message that merely has a sender name.


Nitpicks

  • app/src-tauri/src/webview_accounts/mod.rs:477std::env::var("OPENHUMAN_CORE_RPC_URL") is evaluated on every call. Cache the URL alongside the client (e.g. in the Lazy initializer or WebviewAccountsState) to avoid repeated env lookups under high event volume.
  • app/src-tauri/src/webview_accounts/mod.rs:472"id": 1 is a hardcoded JSON-RPC request ID. Any JSON-RPC server that correlates on id will always see id=1. Consider "id": null (notifications) or a monotonic counter if correlation matters.
  • No unit tests for normalize_provider_surfaces_event or event_timestamp_rfc3339. These are pure functions and easily unit-tested; the existing test block at the bottom of the file is the right place.

Verified / looks good

  • RPC method name openhuman.provider_surfaces_ingest_event correctly matches the rpc_method_name convention (openhuman.{namespace}_{function} = openhuman.provider_surfaces_ingest_event).
  • raw_payload field is present in ProviderEvent and ProviderEvent uses #[serde(deny_unknown_fields)] — no silent drops.
  • Errors from post_provider_surfaces_event are logged and swallowed; the webview:event emit path is unaffected (no regression for existing consumers).
  • webview_recipe_event already validates that the invoking webview label matches the account, so this bridge cannot be triggered by a malicious sibling webview.
  • cargo check passes after the merge-conflict fix (only pre-existing unrelated warnings).

senamakel
senamakel previously approved these changes Apr 26, 2026
Copy link
Copy Markdown
Member

@senamakel senamakel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving after resolving the unresolved merge-conflict marker in the import block (commits 70c00ec + 99e21fb on the PR branch). The bridge logic, field normalisation, error handling, and RPC method name are all correct. Two non-blocking follow-ups worth addressing before the queue sees heavy load: (1) share a single reqwest::Client instance instead of constructing one per event, and (2) narrow the requires_attention default to unread > 0 only. Full findings in the review comment above.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
app/src-tauri/src/webview_accounts/mod.rs (1)

474-485: Reuse a single reqwest::Client across events.

A new reqwest::Client (and a fresh connection pool / TLS state) is built on every recipe event. For an ingest stream that fires on the 2s recipe poll across N accounts, this throws away keep-alive and re-allocates TLS each time. Hoist the client into a OnceLock or store it on WebviewAccountsState and reuse it.

Also worth reading the URL once at startup; per-call std::env::var is fine but fixes an env snapshot per request rather than at process init.

♻️ Sketch
use std::sync::OnceLock;

static INGEST_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();

fn ingest_client() -> &'static reqwest::Client {
    INGEST_CLIENT.get_or_init(|| {
        reqwest::Client::builder()
            .timeout(Duration::from_secs(10))
            .build()
            .expect("build reqwest client")
    })
}

Then in post_provider_surfaces_event:

-    let client = reqwest::Client::builder()
-        .timeout(Duration::from_secs(10))
-        .build()
-        .map_err(|e| format!("http client: {e}"))?;
-    let resp = client
+    let resp = ingest_client()
         .post(&url)
         .json(&body)
         .send()
         .await
         .map_err(|e| format!("POST {url}: {e}"))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/webview_accounts/mod.rs` around lines 474 - 485, The code
currently builds a new reqwest::Client and reads OPENHUMAN_CORE_RPC_URL on every
call (see the client builder and std::env::var usage in the
post_provider_surfaces_event flow), which discards keep-alive/TLS state; fix
this by creating a single shared client and cached URL at startup (e.g. store
reqwest::Client in a static OnceLock or as a field on WebviewAccountsState and
read OPENHUMAN_CORE_RPC_URL once during initialization), then use that shared
client and cached URL in post_provider_surfaces_event (replace the per-call
Client::builder() and env::var calls with references to the initialized client
and URL).
🤖 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 441-443: The current default for requires_attention (computed via
payload_bool(&args.payload, "requires_attention").unwrap_or(...)) fires for
almost every ingest because it treats any message-shaped payload (sender_name or
snippet present) as requiring attention; change that fallback to a stricter
heuristic such as unwrap_or(unread > 0) or unwrap_or(unread > 0 &&
sender_name.is_some()) so requires_attention only becomes true for actual
new/unread activity; update the expression around requires_attention,
referencing the variables unread, sender_name, snippet and the payload_bool call
to implement the chosen tighter rule.
- Around line 375-460: Add unit tests for normalize_provider_surfaces_event and
the helper functions to cover the fallback and heuristic paths: write tests in
the existing tests module that assert entity_id precedence (payload keys
"entity_id" -> "threadId" -> "chatId" -> "snapshotKey" -> synthesized fallback
when none present), verify first_message_field returns the first message field
when messages is an array and returns None when messages is missing or not an
array, test event_timestamp_rfc3339 with None (produces now), a valid millis
value (produces expected RFC3339 string) and an invalid millis (ensure fallback
behavior), and exercise requires_attention resolution for explicit
requires_attention true/false as well as derived true when unread>0 and when
sender_name or snippet exist; call normalize_provider_surfaces_event with
crafted RecipeEventArgs payloads and assert fields ("entity_id", "thread_id",
"timestamp", "requires_attention", "snippet", "sender_name") in the returned
JSON.
- Around line 2346-2354: The current code awaits
post_provider_surfaces_event(&args).await before calling
app.emit("webview:event", …), which blocks the IPC reply on slow/unavailable
core RPC; instead, spawn the POST call onto the async runtime so the emit
happens immediately. Replace the direct await with a spawned task (e.g., using
tauri::async_runtime::spawn or tokio::spawn) that moves/clones args and inside
the task calls post_provider_surfaces_event(args).await and logs any Err as
before; keep the existing app.emit("webview:event", …) path
synchronous/unblocked so the webview event is emitted immediately. Ensure
spawned task captures necessary variables (args, process logger) by value and
preserves the original warning log behavior for failures.

---

Nitpick comments:
In `@app/src-tauri/src/webview_accounts/mod.rs`:
- Around line 474-485: The code currently builds a new reqwest::Client and reads
OPENHUMAN_CORE_RPC_URL on every call (see the client builder and std::env::var
usage in the post_provider_surfaces_event flow), which discards keep-alive/TLS
state; fix this by creating a single shared client and cached URL at startup
(e.g. store reqwest::Client in a static OnceLock or as a field on
WebviewAccountsState and read OPENHUMAN_CORE_RPC_URL once during
initialization), then use that shared client and cached URL in
post_provider_surfaces_event (replace the per-call Client::builder() and
env::var calls with references to the initialized client and URL).
🪄 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: 0fc18a49-1565-4ee4-b0da-01c5bfd2b76e

📥 Commits

Reviewing files that changed from the base of the PR and between f94fe99 and 99e21fb.

⛔ Files ignored due to path filters (1)
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • app/src-tauri/src/webview_accounts/mod.rs

Comment on lines +375 to +460
fn payload_string(payload: &serde_json::Value, key: &str) -> Option<String> {
payload
.get(key)
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
}

fn payload_bool(payload: &serde_json::Value, key: &str) -> Option<bool> {
payload.get(key).and_then(|v| v.as_bool())
}

fn payload_i64(payload: &serde_json::Value, key: &str) -> Option<i64> {
payload.get(key).and_then(|v| v.as_i64())
}

fn first_message_field(payload: &serde_json::Value, key: &str) -> Option<String> {
payload
.get("messages")
.and_then(|v| v.as_array())
.and_then(|messages| messages.first())
.and_then(|message| message.get(key))
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
}

fn event_timestamp_rfc3339(ts_ms: Option<i64>) -> String {
ts_ms
.and_then(|ts| Utc.timestamp_millis_opt(ts).single())
.unwrap_or_else(Utc::now)
.to_rfc3339()
}

fn normalize_provider_surfaces_event(args: &RecipeEventArgs) -> Option<serde_json::Value> {
if args.kind != "ingest" {
return None;
}

let entity_id = payload_string(&args.payload, "entity_id")
.or_else(|| payload_string(&args.payload, "threadId"))
.or_else(|| payload_string(&args.payload, "chatId"))
.or_else(|| payload_string(&args.payload, "snapshotKey"))
.unwrap_or_else(|| {
format!(
"{}:{}:{}",
args.provider,
args.account_id,
args.ts.unwrap_or_else(|| Utc::now().timestamp_millis())
)
});

let thread_id = payload_string(&args.payload, "threadId")
.or_else(|| payload_string(&args.payload, "chatId"))
.or_else(|| payload_string(&args.payload, "conversationId"));
let title = payload_string(&args.payload, "title")
.or_else(|| payload_string(&args.payload, "chatName"))
.or_else(|| payload_string(&args.payload, "channelName"));
let snippet = payload_string(&args.payload, "snippet")
.or_else(|| first_message_field(&args.payload, "body"));
let sender_name = payload_string(&args.payload, "senderName")
.or_else(|| first_message_field(&args.payload, "from"));
let sender_handle = payload_string(&args.payload, "senderHandle");
let deep_link = payload_string(&args.payload, "deepLink");
let unread = payload_i64(&args.payload, "unread").unwrap_or(0);
let requires_attention = payload_bool(&args.payload, "requires_attention")
.unwrap_or(unread > 0 || sender_name.is_some() || snippet.is_some());

Some(json!({
"provider": args.provider,
"account_id": args.account_id,
"event_kind": args.kind,
"entity_id": entity_id,
"thread_id": thread_id,
"title": title,
"snippet": snippet,
"sender_name": sender_name,
"sender_handle": sender_handle,
"timestamp": event_timestamp_rfc3339(args.ts),
"deep_link": deep_link,
"requires_attention": requires_attention,
"raw_payload": args.payload,
}))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing unit tests for the new normalization helpers.

normalize_provider_surfaces_event has non-trivial fallback logic (entity_id key precedence, messages[0] field traversal, timestamp formatting, requires_attention heuristic) and is the contract the core RPC consumes — but no tests were added in the tests module below (which already covers comparable surface area for the zoom rewrite helpers).

Please add coverage for at least: entity_id key precedence (entity_idthreadIdchatIdsnapshotKey → synthesized fallback), first_message_field happy path + missing/non-array messages, event_timestamp_rfc3339 with None/valid/invalid millis, and the requires_attention resolution paths.

As per coding guidelines: "Ship unit tests and coverage for behavior you are adding or changing before building additional features on top".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/webview_accounts/mod.rs` around lines 375 - 460, Add unit
tests for normalize_provider_surfaces_event and the helper functions to cover
the fallback and heuristic paths: write tests in the existing tests module that
assert entity_id precedence (payload keys "entity_id" -> "threadId" -> "chatId"
-> "snapshotKey" -> synthesized fallback when none present), verify
first_message_field returns the first message field when messages is an array
and returns None when messages is missing or not an array, test
event_timestamp_rfc3339 with None (produces now), a valid millis value (produces
expected RFC3339 string) and an invalid millis (ensure fallback behavior), and
exercise requires_attention resolution for explicit requires_attention
true/false as well as derived true when unread>0 and when sender_name or snippet
exist; call normalize_provider_surfaces_event with crafted RecipeEventArgs
payloads and assert fields ("entity_id", "thread_id", "timestamp",
"requires_attention", "snippet", "sender_name") in the returned JSON.

Comment on lines +441 to +443
let unread = payload_i64(&args.payload, "unread").unwrap_or(0);
let requires_attention = payload_bool(&args.payload, "requires_attention")
.unwrap_or(unread > 0 || sender_name.is_some() || snippet.is_some());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

requires_attention default fires for nearly every ingest event.

unread > 0 || sender_name.is_some() || snippet.is_some() evaluates true whenever an ingest payload carries any message-shaped data — which is the common case for the providers wired through messages[0].body/messages[0].from fallbacks (gmail, linkedin recipe). In practice this default reduces to "always true unless the caller passes requires_attention: false or sends an empty payload", which seems unlikely to be the intended signal.

Consider tightening the heuristic (e.g., unread > 0 only, or "unread > 0 AND we have a sender") so this flag still differentiates noisy polls from actual new activity downstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/webview_accounts/mod.rs` around lines 441 - 443, The
current default for requires_attention (computed via payload_bool(&args.payload,
"requires_attention").unwrap_or(...)) fires for almost every ingest because it
treats any message-shaped payload (sender_name or snippet present) as requiring
attention; change that fallback to a stricter heuristic such as unwrap_or(unread
> 0) or unwrap_or(unread > 0 && sender_name.is_some()) so requires_attention
only becomes true for actual new/unread activity; update the expression around
requires_attention, referencing the variables unread, sender_name, snippet and
the payload_bool call to implement the chosen tighter rule.

Comment on lines +2346 to +2354
if let Err(err) = post_provider_surfaces_event(&args).await {
log::warn!(
"[webview-accounts] provider_surfaces ingest failed account={} provider={} kind={}: {}",
args.account_id,
args.provider,
args.kind,
err
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sequential .await delays webview:event emit on slow/unavailable core RPC.

post_provider_surfaces_event(&args).await runs inline before the existing app.emit("webview:event", …) at line 2363. With the 10s reqwest timeout (line 477), a stalled or down core RPC will hold the IPC reply for up to 10s on every recipe event — the JS poll loop in runtime.js fires every 2s, so backpressure stacks up quickly and the React UI sees its webview:event stream visibly lag (or stop, since recipe sends are awaited from JS) until each POST resolves.

Since this is a fire-and-forget side-channel ("Failures … are caught and logged as warnings, while the existing webview:event emission path remains unchanged" per the summary), spawn it onto the runtime instead so the IPC return and the Tauri emit are not gated on the external HTTP call.

🛠️ Proposed fix: spawn the RPC post and emit immediately
-    if let Err(err) = post_provider_surfaces_event(&args).await {
-        log::warn!(
-            "[webview-accounts] provider_surfaces ingest failed account={} provider={} kind={}: {}",
-            args.account_id,
-            args.provider,
-            args.kind,
-            err
-        );
-    }
-
     let event = WebviewEvent {
         account_id: args.account_id,
         provider: args.provider,
         kind: args.kind,
         payload: args.payload,
         ts: args.ts,
     };
+    // Fire-and-forget the side-channel so a slow/unavailable core RPC
+    // can't stall the `webview:event` reply or the recipe poll loop.
+    {
+        let args_for_post = RecipeEventArgs {
+            account_id: event.account_id.clone(),
+            provider: event.provider.clone(),
+            kind: event.kind.clone(),
+            payload: event.payload.clone(),
+            ts: event.ts,
+        };
+        tauri::async_runtime::spawn(async move {
+            if let Err(err) = post_provider_surfaces_event(&args_for_post).await {
+                log::warn!(
+                    "[webview-accounts] provider_surfaces ingest failed account={} provider={} kind={}: {}",
+                    args_for_post.account_id,
+                    args_for_post.provider,
+                    args_for_post.kind,
+                    err
+                );
+            }
+        });
+    }
     app.emit("webview:event", &event)
         .map_err(|e| format!("emit failed: {e}"))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/webview_accounts/mod.rs` around lines 2346 - 2354, The
current code awaits post_provider_surfaces_event(&args).await before calling
app.emit("webview:event", …), which blocks the IPC reply on slow/unavailable
core RPC; instead, spawn the POST call onto the async runtime so the emit
happens immediately. Replace the direct await with a spawned task (e.g., using
tauri::async_runtime::spawn or tokio::spawn) that moves/clones args and inside
the task calls post_provider_surfaces_event(args).await and logs any Err as
before; keep the existing app.emit("webview:event", …) path
synchronous/unblocked so the webview event is emitted immediately. Ensure
spawned task captures necessary variables (args, process logger) by value and
preserves the original warning log behavior for failures.

@senamakel senamakel merged commit 7e90521 into tinyhumansai:main Apr 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants