From 17499f16aebcd3194813badb43068ff1fd857c90 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 5 May 2026 17:38:25 +0530 Subject: [PATCH 1/9] feat(webview/slack): add provider_supports_google_sso helper (#1036) --- app/src-tauri/src/webview_accounts/mod.rs | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/src-tauri/src/webview_accounts/mod.rs b/app/src-tauri/src/webview_accounts/mod.rs index 872406048..bf9de3ffb 100644 --- a/app/src-tauri/src/webview_accounts/mod.rs +++ b/app/src-tauri/src/webview_accounts/mod.rs @@ -240,6 +240,22 @@ fn is_provider_native_deep_link_scheme(scheme: &str) -> bool { ) } +/// `true` if this provider lets users sign in with their Google +/// account from inside the embedded webview. +/// +/// Slack workspaces commonly enable "Sign in with Google" SSO, so the +/// Google OAuth popup flow (`window.open("https://accounts.google.com/...")`) +/// must stay in the per-account CEF session — exactly the same way it +/// has to for Google Meet. Routing it to the system browser leaks the +/// auth cookie into the wrong jar and breaks sign-in (#1036). +/// +/// Keep this list narrow: only providers that actually need to issue +/// `accounts.google.com` popups should be listed. Other providers +/// continue to fall through to the default popup-handling path. +fn provider_supports_google_sso(provider: &str) -> bool { + matches!(provider, "google-meet" | "slack") +} + /// `true` if a popup request should be denied AND the parent webview /// should be navigated to the popup URL instead. /// @@ -3044,6 +3060,22 @@ mod tests { ); } + // ── provider_supports_google_sso ─────────────────────────────────── + + #[test] + fn provider_supports_google_sso_matrix() { + assert!(provider_supports_google_sso("google-meet")); + assert!(provider_supports_google_sso("slack")); + assert!(!provider_supports_google_sso("whatsapp")); + assert!(!provider_supports_google_sso("telegram")); + assert!(!provider_supports_google_sso("linkedin")); + assert!(!provider_supports_google_sso("discord")); + assert!(!provider_supports_google_sso("zoom")); + assert!(!provider_supports_google_sso("browserscan")); + assert!(!provider_supports_google_sso("")); + assert!(!provider_supports_google_sso("unknown-provider")); + } + #[test] fn google_meet_service_login_popup_navigates_parent() { assert_eq!( From ef5da2c1a7eb6c0c150cbbc01cf976b4a3731a5d Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 5 May 2026 18:43:25 +0530 Subject: [PATCH 2/9] feat(webview/slack): generalize popup_should_navigate_parent for SSO providers (#1036) --- app/src-tauri/src/webview_accounts/mod.rs | 60 +++++++++++++++++++---- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/app/src-tauri/src/webview_accounts/mod.rs b/app/src-tauri/src/webview_accounts/mod.rs index bf9de3ffb..2c7cdffd8 100644 --- a/app/src-tauri/src/webview_accounts/mod.rs +++ b/app/src-tauri/src/webview_accounts/mod.rs @@ -259,8 +259,8 @@ fn provider_supports_google_sso(provider: &str) -> bool { /// `true` if a popup request should be denied AND the parent webview /// should be navigated to the popup URL instead. /// -/// Used for Google's "Sign in" / "Use another account" flow on the -/// embedded Google Meet webview: clicking the link issues +/// Used for Google's "Sign in" / "Use another account" flow on embedded +/// providers that support Google SSO: clicking the link issues /// `window.open("https://accounts.google.com/...")`. We can't route /// that to the system browser (the auth cookie would land in the /// wrong jar) and we don't want to let CEF spawn an unmanaged child @@ -268,7 +268,7 @@ fn provider_supports_google_sso(provider: &str) -> bool { /// option is to deny the popup and replace the parent's URL so the /// in-app webview finishes the auth flow inside the embedded session. fn popup_should_navigate_parent(provider: &str, url: &Url) -> Option { - if provider != "google-meet" { + if !provider_supports_google_sso(provider) { return None; } if url.scheme() == "about" { @@ -284,9 +284,11 @@ fn popup_should_navigate_parent(provider: &str, url: &Url) -> Option { // out of OpenHuman entirely. Deny the popup and navigate the // embedded parent into the room URL instead — matches the // user's expectation that the meeting stays in-app. - if let Some(host) = url.host_str() { - if host == "meet.google.com" { - return Some(url.clone()); + if provider == "google-meet" { + if let Some(host) = url.host_str() { + if host == "meet.google.com" { + return Some(url.clone()); + } } } None @@ -2991,9 +2993,9 @@ mod tests { #[test] fn unsupported_provider_popup_does_not_navigate_parent() { - // Only the embedded google-meet webview opts into the - // popup-takeover path. Every other provider (and any unknown - // string) must fall through to the default popup-handling. + // Only providers that explicitly support Google SSO opt into + // the popup-takeover path. Every other provider (and any unknown + // string) must fall through to the default popup handling. assert!(popup_should_navigate_parent( "linkedin", &url("https://accounts.google.com/signin/v2/identifier"), @@ -3010,6 +3012,46 @@ mod tests { .is_some()); } + #[test] + fn slack_google_signin_popup_navigates_parent() { + assert_eq!( + popup_should_navigate_parent( + "slack", + &url("https://accounts.google.com/v3/signin/identifier"), + ) + .map(|u| u.to_string()), + Some("https://accounts.google.com/v3/signin/identifier".to_string()) + ); + } + + #[test] + fn slack_about_blank_popup_does_not_navigate_parent() { + assert!(popup_should_navigate_parent("slack", &url("about:blank")).is_none()); + } + + #[test] + fn slack_same_origin_popup_does_not_navigate_parent() { + assert!(popup_should_navigate_parent( + "slack", + &url("https://app.slack.com/client/T123/C456"), + ) + .is_none()); + } + + #[test] + fn slack_unrelated_popup_does_not_navigate_parent() { + assert!(popup_should_navigate_parent("slack", &url("https://example.com/blog"),).is_none()); + } + + #[test] + fn slack_meet_google_com_popup_does_not_navigate_parent() { + assert!(popup_should_navigate_parent( + "slack", + &url("https://meet.google.com/abc-defg-hij"), + ) + .is_none()); + } + #[test] fn gmeet_room_popup_navigates_parent() { // "Start an instant meeting" / "New meeting" calls From 818f1935c3a1d5b10cf3f61ccd44b180c879f7a5 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 5 May 2026 18:46:01 +0530 Subject: [PATCH 3/9] feat(webview/slack): extend provider_allowed_hosts for Google OAuth (#1036) --- app/src-tauri/src/webview_accounts/mod.rs | 65 ++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/app/src-tauri/src/webview_accounts/mod.rs b/app/src-tauri/src/webview_accounts/mod.rs index 2c7cdffd8..1e5065604 100644 --- a/app/src-tauri/src/webview_accounts/mod.rs +++ b/app/src-tauri/src/webview_accounts/mod.rs @@ -89,7 +89,18 @@ fn provider_allowed_hosts(provider: &str) -> &'static [&'static str] { "whatsapp" => &["whatsapp.com", "whatsapp.net", "wa.me"], "telegram" => &["telegram.org", "t.me"], "linkedin" => &["linkedin.com", "licdn.com"], - "slack" => &["slack.com", "slack-edge.com", "slackb.com"], + "slack" => &[ + "slack.com", + "slack-edge.com", + "slackb.com", + "accounts.google.com", + "accounts.googleusercontent.com", + "ssl.gstatic.com", + "fonts.gstatic.com", + "lh3.googleusercontent.com", + "oauth2.googleapis.com", + "www.googleapis.com", + ], "discord" => &[ "discord.com", "discord.gg", @@ -2701,6 +2712,58 @@ mod tests { assert!(hosts.contains(&"zdassets.com"), "zdassets.com in allowlist"); } + #[test] + fn slack_allowed_hosts_include_google_oauth() { + let hosts = provider_allowed_hosts("slack"); + for host in [ + "accounts.google.com", + "accounts.googleusercontent.com", + "ssl.gstatic.com", + "fonts.gstatic.com", + "lh3.googleusercontent.com", + "oauth2.googleapis.com", + "www.googleapis.com", + ] { + assert!(hosts.contains(&host), "{host} in Slack allowlist"); + } + } + + #[test] + fn slack_allowed_hosts_still_internal_for_slack_origins() { + assert!(url_is_internal( + "slack", + &url("https://app.slack.com/client/T123/C456"), + )); + assert!(url_is_internal( + "slack", + &url("https://a.slack-edge.com/bv1/app.js"), + )); + assert!(url_is_internal( + "slack", + &url("https://wss-primary.slack.com/?ticket=redacted"), + )); + } + + #[test] + fn slack_allowed_hosts_do_not_bare_allow_google() { + let hosts = provider_allowed_hosts("slack"); + assert!( + !hosts.contains(&"google.com"), + "bare google.com not allowed" + ); + assert!(!hosts.contains(&"googleusercontent.com")); + assert!(!hosts.contains(&"gstatic.com")); + assert!(!hosts.contains(&"googleapis.com")); + + assert!(url_is_internal( + "slack", + &url("https://accounts.google.com/v3/signin/identifier"), + )); + assert!(!url_is_internal("slack", &url("https://google.com/"))); + assert!(!url_is_internal("slack", &url("https://mail.google.com/"))); + assert!(!url_is_internal("slack", &url("https://apis.google.com/"))); + } + #[test] fn zoom_is_supported() { assert!(provider_is_supported("zoom")); From 48c3476550816e94a0621c9b9c66bee57797f04c Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 5 May 2026 18:52:13 +0530 Subject: [PATCH 4/9] feat(webview/slack): diagnostic log for reveal trigger + elapsed_ms (#1036) --- app/src-tauri/src/cdp/session.rs | 18 ++- app/src-tauri/src/webview_accounts/mod.rs | 138 +++++++++++++++++- ...webviewAccountService.loadListener.test.ts | 14 +- app/src/services/webviewAccountService.ts | 14 +- 4 files changed, 167 insertions(+), 17 deletions(-) diff --git a/app/src-tauri/src/cdp/session.rs b/app/src-tauri/src/cdp/session.rs index 75e52cf9c..8e255e5ec 100644 --- a/app/src-tauri/src/cdp/session.rs +++ b/app/src-tauri/src/cdp/session.rs @@ -22,7 +22,7 @@ use tokio::task::JoinHandle; use tokio::time::sleep; use super::{browser_ws_url, find_page_target_where, CdpConn}; -use crate::webview_accounts::emit_load_finished; +use crate::webview_accounts::{emit_load_finished, RevealTrigger}; /// Backoff between failed attach attempts / reconnects. Intentionally /// short — once the webview is open, the target usually shows up within @@ -147,7 +147,13 @@ pub fn spawn_session( let real_url = real_url.clone(); tokio::spawn(async move { sleep(LOAD_TIMEOUT).await; - emit_load_finished(&app, &account_id, "timeout", &real_url); + emit_load_finished( + &app, + &account_id, + "timeout", + &real_url, + RevealTrigger::Watchdog, + ); }) }; let session = tokio::spawn(async move { run_session_forever(app, account_id, real_url).await }); @@ -402,7 +408,13 @@ async fn run_session_cycle( let cb_real_url = real_url.to_string(); cdp.pump_events(&session_id, move |method, _params| { if method == "Page.loadEventFired" { - emit_load_finished(&cb_app, &cb_account_id, "finished", &cb_real_url); + emit_load_finished( + &cb_app, + &cb_account_id, + "finished", + &cb_real_url, + RevealTrigger::Load, + ); } }) .await diff --git a/app/src-tauri/src/webview_accounts/mod.rs b/app/src-tauri/src/webview_accounts/mod.rs index 1e5065604..56131ea80 100644 --- a/app/src-tauri/src/webview_accounts/mod.rs +++ b/app/src-tauri/src/webview_accounts/mod.rs @@ -23,7 +23,7 @@ use std::path::PathBuf; use std::sync::Mutex; #[cfg(target_os = "linux")] use std::sync::{mpsc::sync_channel, OnceLock}; -use std::time::Duration; +use std::time::{Duration, Instant}; use chrono::{TimeZone, Utc}; use serde::{Deserialize, Serialize}; @@ -541,6 +541,9 @@ pub fn provider_display_name(provider: &str) -> &'static str { pub struct WebviewAccountsState { /// account_id -> webview label (we use `acct_` as the label). inner: Mutex>, + /// account_id -> provider id. Kept so late reveal/close paths can log + /// provider-scoped diagnostics without trusting frontend echo fields. + account_providers: Mutex>, /// account_id -> CEF `Browser::identifier()`. Populated asynchronously /// inside the `with_webview` callback once the renderer hands us the /// browser handle, and consumed at close/purge time so we can call @@ -565,6 +568,13 @@ pub struct WebviewAccountsState { /// revealed at the right rect without the frontend having to round-trip /// them again. requested_bounds: Mutex>, + /// account_id -> `Instant` captured at the moment the cold spawn returns + /// from `add_child`. Consumed by `webview_account_reveal` to compute + /// `elapsed_ms` (spawn -> frontend reveal call) for the diagnostic log + /// instrumented for the Slack first-load investigation (#1036). Cleared + /// alongside `loaded_accounts` on close/purge so a subsequent reopen + /// starts fresh. + spawn_started_at: Mutex>, /// Runtime notification-bypass controls used by the settings UI. notification_bypass: Mutex, } @@ -625,6 +635,12 @@ impl WebviewAccountsState { if let Ok(mut g) = self.requested_bounds.lock() { g.clear(); } + if let Ok(mut g) = self.spawn_started_at.lock() { + g.clear(); + } + if let Ok(mut g) = self.account_providers.lock() { + g.clear(); + } self.inner .lock() .ok() @@ -1133,6 +1149,36 @@ pub struct BoundsArgs { pub bounds: Bounds, } +#[derive(Debug, Deserialize)] +pub struct RevealArgs { + pub account_id: String, + pub bounds: Bounds, + #[serde(default)] + pub trigger: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum RevealTrigger { + Load, + Watchdog, +} + +impl RevealTrigger { + fn as_str(self) -> &'static str { + match self { + Self::Load => "load", + Self::Watchdog => "watchdog", + } + } + + fn from_ipc(raw: Option<&str>) -> Self { + match raw { + Some("watchdog") => Self::Watchdog, + _ => Self::Load, + } + } +} + #[derive(Debug, Deserialize)] pub struct AccountIdArgs { pub account_id: String, @@ -1197,6 +1243,7 @@ pub(crate) fn emit_load_finished( account_id: &str, state: &str, url: &str, + trigger: RevealTrigger, ) { let Some(app_state) = app.try_state::() else { // No state => emit anyway so the frontend doesn't hang; best-effort. @@ -1206,7 +1253,12 @@ pub(crate) fn emit_load_finished( ); let _ = app.emit( "webview-account:load", - serde_json::json!({"account_id": account_id, "state": state, "url": url}), + serde_json::json!({ + "account_id": account_id, + "state": state, + "trigger": trigger.as_str(), + "url": url, + }), ); return; }; @@ -1228,8 +1280,9 @@ pub(crate) fn emit_load_finished( } log::info!( - "[webview-accounts][{}] load timeout event url={}", + "[webview-accounts][{}] load timeout event trigger={} url={}", account_id, + trigger.as_str(), redact_url_for_log(url) ); if let Err(err) = app.emit( @@ -1237,6 +1290,7 @@ pub(crate) fn emit_load_finished( serde_json::json!({ "account_id": account_id, "state": state, + "trigger": trigger.as_str(), "url": url, }), ) { @@ -1322,9 +1376,10 @@ pub(crate) fn emit_load_finished( // consumer that needs it has access; we just don't persist it to the // shell's log file. log::info!( - "[webview-accounts][{}] load event state={} url={}", + "[webview-accounts][{}] load event state={} trigger={} url={}", account_id, state, + trigger.as_str(), redact_url_for_log(url) ); if let Err(err) = app.emit( @@ -1332,6 +1387,7 @@ pub(crate) fn emit_load_finished( serde_json::json!({ "account_id": account_id, "state": state, + "trigger": trigger.as_str(), "url": url, }), ) { @@ -1503,6 +1559,7 @@ pub async fn webview_account_open( serde_json::json!({ "account_id": args.account_id, "state": "reused", + "trigger": RevealTrigger::Load.as_str(), "url": reuse_url, }), ) { @@ -1814,6 +1871,7 @@ pub async fn webview_account_open( &page_load_account_id, "timeout", &page_load_real_url, + RevealTrigger::Load, ); return; } @@ -1822,6 +1880,7 @@ pub async fn webview_account_open( &page_load_account_id, "finished", url.as_str(), + RevealTrigger::Load, ); }); @@ -1885,6 +1944,19 @@ pub async fn webview_account_open( .add_child(builder, initial_position, initial_size) .map_err(|e| format!("add_child failed: {e}"))?; + // Capture the cold-spawn timestamp so the reveal-time log can compute + // spawn -> frontend reveal latency for the Slack first-load investigation. + state + .spawn_started_at + .lock() + .unwrap() + .insert(args.account_id.clone(), Instant::now()); + state + .account_providers + .lock() + .unwrap() + .insert(args.account_id.clone(), args.provider.clone()); + log::info!( "[webview-accounts] spawned label={} requested_bounds={:?} initial_size={:?}", webview.label(), @@ -2122,6 +2194,16 @@ pub async fn webview_account_close( .lock() .unwrap() .remove(&args.account_id); + state + .spawn_started_at + .lock() + .unwrap() + .remove(&args.account_id); + state + .account_providers + .lock() + .unwrap() + .remove(&args.account_id); log::info!("[webview-accounts] closed label={}", label); Ok(()) } @@ -2185,6 +2267,16 @@ pub async fn webview_account_purge( .lock() .unwrap() .remove(&args.account_id); + state + .spawn_started_at + .lock() + .unwrap() + .remove(&args.account_id); + state + .account_providers + .lock() + .unwrap() + .remove(&args.account_id); let data_dir = data_directory_for(&app, &args.account_id)?; purge_data_dir_with_retry(&data_dir) @@ -2305,7 +2397,7 @@ pub async fn webview_account_bounds( pub async fn webview_account_reveal( app: AppHandle, state: tauri::State<'_, WebviewAccountsState>, - args: BoundsArgs, + args: RevealArgs, ) -> Result<(), String> { let label_opt = state.inner.lock().unwrap().get(&args.account_id).cloned(); let Some(label) = label_opt else { @@ -2330,9 +2422,28 @@ pub async fn webview_account_reveal( .lock() .unwrap() .insert(args.account_id.clone(), args.bounds); + let provider = state + .account_providers + .lock() + .unwrap() + .get(&args.account_id) + .cloned() + .unwrap_or_else(|| "unknown".to_string()); + let elapsed_ms = state + .spawn_started_at + .lock() + .ok() + .and_then(|mut g| g.remove(&args.account_id)) + .map(|started| started.elapsed().as_millis()) + .map(|ms| ms.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + let trigger = RevealTrigger::from_ipc(args.trigger.as_deref()).as_str(); log::info!( - "[webview-accounts] revealed label={} -> {:?}", - label, + "[webview-accounts][{}][{}] reveal trigger={} elapsed_ms={} bounds={:?}", + provider, + args.account_id, + trigger, + elapsed_ms, args.bounds ); Ok(()) @@ -2646,6 +2757,11 @@ mod tests { .lock() .unwrap() .insert("acct-1".into(), "acct_1".into()); + state + .account_providers + .lock() + .unwrap() + .insert("acct-1".into(), "slack".into()); state .loaded_accounts .lock() @@ -2660,6 +2776,11 @@ mod tests { height: 600.0, }, ); + state + .spawn_started_at + .lock() + .unwrap() + .insert("acct-1".into(), Instant::now()); let labels = state.drain_for_shutdown(); tokio::task::yield_now().await; @@ -2678,14 +2799,17 @@ mod tests { assert!(state.load_watchdogs.lock().unwrap().is_empty()); assert!(state.browser_ids.lock().unwrap().is_empty()); assert!(state.inner.lock().unwrap().is_empty()); + assert!(state.account_providers.lock().unwrap().is_empty()); assert!(state.loaded_accounts.lock().unwrap().is_empty()); assert!(state.requested_bounds.lock().unwrap().is_empty()); + assert!(state.spawn_started_at.lock().unwrap().is_empty()); // Second call must be a safe no-op: nothing left to drain. let labels2 = state.drain_for_shutdown(); assert!(labels2.is_empty()); assert!(state.cdp_sessions.lock().unwrap().is_empty()); assert!(state.inner.lock().unwrap().is_empty()); + assert!(state.account_providers.lock().unwrap().is_empty()); } // ── provider registry match arms ────────────────────────────────── diff --git a/app/src/services/__tests__/webviewAccountService.loadListener.test.ts b/app/src/services/__tests__/webviewAccountService.loadListener.test.ts index bf2d0967e..73583caa2 100644 --- a/app/src/services/__tests__/webviewAccountService.loadListener.test.ts +++ b/app/src/services/__tests__/webviewAccountService.loadListener.test.ts @@ -54,7 +54,11 @@ function seedAccount(): void { ); } -async function fireLoadEvent(payload: { state: string; url?: string }): Promise { +async function fireLoadEvent(payload: { + state: string; + trigger?: string; + url?: string; +}): Promise { const handler = listeners.get('webview-account:load'); if (!handler) throw new Error('webview-account:load listener not attached'); handler({ payload: { account_id: ACCOUNT_ID, url: '', ...payload } }); @@ -101,7 +105,7 @@ describe('webviewAccountService load listener', () => { await fireLoadEvent({ state: 'finished', url: 'https://web.telegram.org/' }); expect(vi.mocked(invoke)).toHaveBeenCalledWith('webview_account_reveal', { - args: { account_id: ACCOUNT_ID, bounds }, + args: { account_id: ACCOUNT_ID, bounds, trigger: 'load' }, }); expect(store.getState().accounts.accounts[ACCOUNT_ID]?.status).toBe('open'); }); @@ -119,7 +123,7 @@ describe('webviewAccountService load listener', () => { await fireLoadEvent({ state: 'finished', url: 'x' }); expect(vi.mocked(invoke)).toHaveBeenCalledWith('webview_account_reveal', { - args: { account_id: ACCOUNT_ID, bounds: resized }, + args: { account_id: ACCOUNT_ID, bounds: resized, trigger: 'load' }, }); }); @@ -159,7 +163,7 @@ describe('webviewAccountService load listener', () => { await fireLoadEvent({ state: 'finished', url: 'https://web.telegram.org/' }); expect(vi.mocked(invoke)).toHaveBeenCalledWith('webview_account_reveal', { - args: { account_id: ACCOUNT_ID, bounds }, + args: { account_id: ACCOUNT_ID, bounds, trigger: 'load' }, }); expect(store.getState().accounts.accounts[ACCOUNT_ID]?.status).toBe('open'); }); @@ -185,7 +189,7 @@ describe('webviewAccountService load listener', () => { await fireLoadEvent({ state: 'reused', url: 'https://web.telegram.org/' }); expect(vi.mocked(invoke)).toHaveBeenCalledWith('webview_account_reveal', { - args: { account_id: ACCOUNT_ID, bounds }, + args: { account_id: ACCOUNT_ID, bounds, trigger: 'load' }, }); expect(store.getState().accounts.accounts[ACCOUNT_ID]?.status).toBe('open'); }); diff --git a/app/src/services/webviewAccountService.ts b/app/src/services/webviewAccountService.ts index 5f8e2d20a..1a4f97c84 100644 --- a/app/src/services/webviewAccountService.ts +++ b/app/src/services/webviewAccountService.ts @@ -67,6 +67,9 @@ interface WebviewAccountLoadPayload { // `'timeout'` — 15 s watchdog elapsed; keep hidden and show retry UI // `'reused'` — warm re-open of already-loaded account; reveal synchronously state: 'finished' | 'timeout' | 'reused' | string; + // `'load'` — native/CDP load signal caused this event + // `'watchdog'` — fallback watchdog caused this event + trigger?: 'load' | 'watchdog' | string; url: string; } @@ -181,7 +184,13 @@ function handleWebviewAccountLoad(payload: WebviewAccountLoadPayload) { errLog('webview-account:load missing account_id — ignoring: %o', payload); return; } - log('load event account=%s state=%s url=%s', accountId, payload.state, payload.url); + log( + 'load event account=%s state=%s trigger=%s url=%s', + accountId, + payload.state, + payload.trigger, + payload.url + ); loadingAccounts.delete(accountId); const timeoutLike = @@ -213,8 +222,9 @@ function handleWebviewAccountLoad(payload: WebviewAccountLoadPayload) { // the webview will have been positioned server-side by `emit_load_finished`. const bounds = lastBoundsByAccount.get(accountId); log('load finished account=%s state=%s reveal=%s', accountId, payload.state, Boolean(bounds)); + const trigger = payload.trigger === 'watchdog' ? 'watchdog' : 'load'; if (bounds) { - invoke('webview_account_reveal', { args: { account_id: accountId, bounds } }) + invoke('webview_account_reveal', { args: { account_id: accountId, bounds, trigger } }) .catch(err => { errLog('webview_account_reveal failed account=%s: %o', accountId, err); }) From df18b80ec52487ce90f1b8fc0f9f0502d5b7cadf Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 01:41:41 +0530 Subject: [PATCH 5/9] test(webview/slack): cover watchdog reveal trigger (#1036) --- .../webviewAccountService.loadListener.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/services/__tests__/webviewAccountService.loadListener.test.ts b/app/src/services/__tests__/webviewAccountService.loadListener.test.ts index 73583caa2..840121604 100644 --- a/app/src/services/__tests__/webviewAccountService.loadListener.test.ts +++ b/app/src/services/__tests__/webviewAccountService.loadListener.test.ts @@ -110,6 +110,22 @@ describe('webviewAccountService load listener', () => { expect(store.getState().accounts.accounts[ACCOUNT_ID]?.status).toBe('open'); }); + it('propagates watchdog trigger when a watchdog-originated finished signal arrives', async () => { + const bounds = { x: 0, y: 0, width: 800, height: 600 }; + await openWebviewAccount({ accountId: ACCOUNT_ID, provider: 'telegram', bounds }); + vi.mocked(invoke).mockClear(); + + await fireLoadEvent({ + state: 'finished', + trigger: 'watchdog', + url: 'https://web.telegram.org/', + }); + + expect(vi.mocked(invoke)).toHaveBeenCalledWith('webview_account_reveal', { + args: { account_id: ACCOUNT_ID, bounds, trigger: 'watchdog' }, + }); + }); + it('reveals with latest bounds when resize landed during loading', async () => { const initial = { x: 0, y: 0, width: 800, height: 600 }; await openWebviewAccount({ accountId: ACCOUNT_ID, provider: 'telegram', bounds: initial }); From 941abc982cf5f41d3f03fdf19c70632ff0f3d4bb Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 01:47:49 +0530 Subject: [PATCH 6/9] test(agent): complete PromptContext fixture (#1036) --- src/openhuman/agent/prompts/mod_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/openhuman/agent/prompts/mod_tests.rs b/src/openhuman/agent/prompts/mod_tests.rs index 404e817b4..a9ae66fdf 100644 --- a/src/openhuman/agent/prompts/mod_tests.rs +++ b/src/openhuman/agent/prompts/mod_tests.rs @@ -1215,6 +1215,7 @@ fn ctx_with_learned(learned: LearnedContextData) -> PromptContext<'static> { connected_identities_md: String::new(), include_profile: false, include_memory_md: false, + curated_snapshot: None, user_identity: None, } } From 8f3fc561f5a0bd7f151b20302cd018f17fd1d391 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 09:53:43 +0530 Subject: [PATCH 7/9] chore: align merge cleanup with upstream (#1036) --- src/openhuman/agent/prompts/mod_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openhuman/agent/prompts/mod_tests.rs b/src/openhuman/agent/prompts/mod_tests.rs index a9ae66fdf..62d7760ed 100644 --- a/src/openhuman/agent/prompts/mod_tests.rs +++ b/src/openhuman/agent/prompts/mod_tests.rs @@ -1215,8 +1215,8 @@ fn ctx_with_learned(learned: LearnedContextData) -> PromptContext<'static> { connected_identities_md: String::new(), include_profile: false, include_memory_md: false, - curated_snapshot: None, user_identity: None, + curated_snapshot: None, } } From d452231933abe6e0b04e60898c92043b10556481 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 10:11:23 +0530 Subject: [PATCH 8/9] style(webview/slack): align reveal diagnostic mutex handling (#1036) --- app/src-tauri/src/webview_accounts/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src-tauri/src/webview_accounts/mod.rs b/app/src-tauri/src/webview_accounts/mod.rs index a545d43fc..5aa64955b 100644 --- a/app/src-tauri/src/webview_accounts/mod.rs +++ b/app/src-tauri/src/webview_accounts/mod.rs @@ -2728,8 +2728,8 @@ pub async fn webview_account_reveal( let elapsed_ms = state .spawn_started_at .lock() - .ok() - .and_then(|mut g| g.remove(&args.account_id)) + .unwrap() + .remove(&args.account_id) .map(|started| started.elapsed().as_millis()) .map(|ms| ms.to_string()) .unwrap_or_else(|| "unknown".to_string()); From 1772d683ffcf7ac5199910b17db69a2ecaed4ad9 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 6 May 2026 10:20:49 +0530 Subject: [PATCH 9/9] fix(webview/slack): warn on unknown reveal trigger (#1036) --- app/src-tauri/src/webview_accounts/mod.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src-tauri/src/webview_accounts/mod.rs b/app/src-tauri/src/webview_accounts/mod.rs index 5aa64955b..b7c4be50a 100644 --- a/app/src-tauri/src/webview_accounts/mod.rs +++ b/app/src-tauri/src/webview_accounts/mod.rs @@ -1358,8 +1358,15 @@ impl RevealTrigger { fn from_ipc(raw: Option<&str>) -> Self { match raw { + Some("load") | None => Self::Load, Some("watchdog") => Self::Watchdog, - _ => Self::Load, + Some(other) => { + log::warn!( + "[webview-accounts] unknown reveal trigger {:?}; defaulting to load", + other + ); + Self::Load + } } } } @@ -3005,6 +3012,20 @@ mod tests { Url::parse(s).expect("valid url") } + #[test] + fn reveal_trigger_from_ipc_warns_and_defaults_unknown_to_load() { + assert_eq!(RevealTrigger::from_ipc(None), RevealTrigger::Load); + assert_eq!(RevealTrigger::from_ipc(Some("load")), RevealTrigger::Load); + assert_eq!( + RevealTrigger::from_ipc(Some("watchdog")), + RevealTrigger::Watchdog + ); + assert_eq!( + RevealTrigger::from_ipc(Some("watchdog-typo")), + RevealTrigger::Load + ); + } + // ── shutdown teardown ────────────────────────────────── /// Smoke-test [`WebviewAccountsState::drain_for_shutdown`] in isolation