From 643ec33dec381a6f4b3b73763a2dab33e9ab0eca Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:31:34 -0700 Subject: [PATCH] feat(ui,webview): compact tab bar, hide inactive tabs, harden CEF link handling (#868) --- CLAUDE.md | 14 ++++ README.md | 27 ++++--- app/src-tauri/src/lib.rs | 17 ++++- app/src-tauri/src/webview_accounts/mod.rs | 93 +++++++++++++++++++---- app/src-tauri/tauri.conf.json | 4 +- app/src/components/BottomTabBar.tsx | 17 +++-- app/src/pages/Skills.tsx | 25 +++--- 7 files changed, 152 insertions(+), 45 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7eadca767..d57c89df4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,6 +135,20 @@ Thin desktop host: window management, daemon health, **core process lifecycle** Registered IPC (see [`docs/src-tauri/02-commands.md`](docs/src-tauri/02-commands.md)): `greet`, `write_ai_config_file`, `ai_get_config`, `ai_refresh_config`, `core_rpc_relay`, window commands, `openhuman_*` daemon helpers. +### CEF child webviews — no new JS injection + +Embedded provider webviews (`acct_*`, loading third-party origins like `web.telegram.org`, `linkedin.com`, `slack.com`, …) **must not** grow any new JavaScript injection. Do not add new `.js` files under `app/src-tauri/src/webview_accounts/`, do not append new blocks to `build_init_script` / `RUNTIME_JS`, and do not dispatch scripts via CDP `Page.addScriptToEvaluateOnNewDocument` / `Runtime.evaluate` for these webviews. The migrated providers (whatsapp, telegram, slack, discord, browserscan) load with **zero** injected JS under CEF by design — all scraping and observability runs natively via CDP in the per-provider scanner modules, and anything host-controlled that runs inside a third-party origin is a scraping/attack-surface liability. + +New behavior for these webviews lives in: + +- **CEF handlers** — `on_navigation`, `on_new_window`, `LoadHandler::OnLoadStart`, `CefRequestHandler::*` (wired in `webview_accounts/mod.rs`). +- **CDP from the scanner side** — `Network.*`, `Emulation.*`, `Input.*`, `Page.*` driven by the per-provider `*_scanner/` modules. +- **Rust-side notification/IPC hooks** — never cross into the renderer. + +If a feature truly cannot be built this way (e.g. intercepting a click the page's JS preventDefaults), the correct answer is to **surface the limitation**, not to ship an init script. Legacy injection that already exists for non-migrated providers (`gmail`, `linkedin`, `google-meet` recipe files, `ua_spoof.js`, `runtime.js` bridge) is grandfathered but should shrink, not grow. + +Watch out for Tauri plugins that inject JS by default. `tauri-plugin-opener` ships `init-iife.js` (a global click listener that calls `plugin:opener|open_url` via HTTP-IPC) unless you build it with `.open_js_links_on_click(false)`. Any new plugin added to `app/src-tauri/src/lib.rs` must be audited for a `js_init_script` call — if found, opt out or configure around it. + --- ## Rust core (`src/`) diff --git a/README.md b/README.md index 5c14af804..e99fb9be0 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,12 @@

DiscordReddit • - X/Twitter • + X/TwitterDocs

+

+ Follow @senamakel (Creator) +

Early Beta @@ -72,17 +75,17 @@ Architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). Contributor orient High-level comparison (products evolve—verify against each vendor). OpenHuman is built to **minimize vendor sprawl**, keep **workflow knowledge on-device**, and ship **deep desktop** features—not only chat. -| | Claude Code/Cowork | OpenClaw | Hermes Agent | OpenHuman | -| ----------------------- | ------------------- | ------------------ | ------------------ | ------------------------ | -| **Open-source** | 🚫 Proprietary | ✅ MIT | ✅ MIT | ✅ GNU | -| **Simple to start** | ✅ Desktop + CLI | ⚠️ Terminal-first | ⚠️ Terminal-first | ✅ Clean UI, minutes | -| **Cost** | ⚠️ Sub + add-ons | ⚠️ BYO models | ⚠️ BYO models | ✅ Local-friendly | -| **Memory & KB** | ✅ Chat-scoped | ⚠️ Plugin-reliant | ✅ Self-learning | 🚀 Local KB + learning | -| **API sprawl** | 🚫 Extra keys | 🚫 BYOK | 🚫 Multi-vendor | ✅ One account | -| **Extensibility** | ✅ MCP | ✅ SKILL.md | ✅ SKILL.md | 🚀 Rich Skills | -| **Desktop integration** | ⚠️ Basic | ⚠️ Light | ⚠️ Light | ✅ STT/TTS/screen/more | +| | Claude Code/Cowork | OpenClaw | Hermes Agent | OpenHuman | +| ----------------------- | ------------------ | ----------------- | ----------------- | ---------------------- | +| **Open-source** | 🚫 Proprietary | ✅ MIT | ✅ MIT | ✅ GNU | +| **Simple to start** | ✅ Desktop + CLI | ⚠️ Terminal-first | ⚠️ Terminal-first | ✅ Clean UI, minutes | +| **Cost** | ⚠️ Sub + add-ons | ⚠️ BYO models | ⚠️ BYO models | ✅ Local-friendly | +| **Memory & KB** | ✅ Chat-scoped | ⚠️ Plugin-reliant | ✅ Self-learning | 🚀 Local KB + learning | +| **API sprawl** | 🚫 Extra keys | 🚫 BYOK | 🚫 Multi-vendor | ✅ One account | +| **Extensibility** | ✅ MCP | ✅ SKILL.md | ✅ SKILL.md | 🚀 Rich Skills | +| **Desktop integration** | ⚠️ Basic | ⚠️ Light | ⚠️ Light | ✅ STT/TTS/screen/more | - +

# Contributors Hall of Fame diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 7b41bd0bb..246a1f071 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -598,7 +598,22 @@ pub fn run() { }; let builder = builder - .plugin(tauri_plugin_opener::init()) + // Explicitly disable `open_js_links_on_click`: tauri-plugin-opener + // defaults to injecting `init-iife.js` into *every* webview — a + // global click listener that invokes `plugin:opener|open_url` via + // HTTP-IPC. That violates our "no JS injection into CEF child + // webviews" rule (see CLAUDE.md) and also fails in practice + // because third-party origins (web.telegram.org, linkedin, …) + // trip Tauri's Origin header check and return 500. External link + // handling for `acct_*` webviews runs natively via + // `on_navigation` / `on_new_window` in webview_accounts/mod.rs; + // the main window uses `openUrl()` from `utils/openUrl.ts` when + // it needs to hand off a URL. + .plugin( + tauri_plugin_opener::Builder::default() + .open_js_links_on_click(false) + .build(), + ) .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) diff --git a/app/src-tauri/src/webview_accounts/mod.rs b/app/src-tauri/src/webview_accounts/mod.rs index 5fbc75d37..64e9f9c3e 100644 --- a/app/src-tauri/src/webview_accounts/mod.rs +++ b/app/src-tauri/src/webview_accounts/mod.rs @@ -191,13 +191,55 @@ fn popup_should_stay_in_app(provider: &str, url: &Url) -> bool { _ => false, } } +/// Unwrap provider-side "link safety" redirects so the system browser +/// lands on the real destination. +/// +/// These wrappers (LinkedIn's `/safety/go/?url=…`, etc.) require the +/// user to be logged into the provider in the destination browser. In +/// our setup the session lives inside the embedded CEF webview's cookie +/// jar, not the user's default browser — opening the wrapper URL there +/// shows a broken safety page instead of completing the redirect. +/// Extract the `url` query param and return the resolved destination. +fn unwrap_provider_redirect(url: &Url) -> Option { + let host = url.host_str()?; + let path = url.path(); + let matches_linkedin = (host == "www.linkedin.com" || host == "linkedin.com") + && (path == "/safety/go/" || path == "/safety/go" || path == "/redir/redirect"); + if !matches_linkedin { + return None; + } + let (_, raw) = url.query_pairs().find(|(k, _)| k == "url")?; + Url::parse(&raw).ok() +} + /// Fire-and-forget handoff to the OS default URL handler. Any error is /// logged but not propagated — we've already cancelled the in-app /// navigation so there's nowhere to surface a failure to. +/// +/// On macOS we shell out to `/usr/bin/open` directly rather than via +/// `tauri_plugin_opener::open_url`: the plugin returned Ok but no browser +/// actually launched in the CEF runtime (suspected sandbox/launch-service +/// interaction with the `open` crate's detached spawn). The direct +/// Command call is equivalent to what a user would type in Terminal and +/// works reliably. fn open_in_system_browser(url: &str) { - match tauri_plugin_opener::open_url(url, None::<&str>) { - Ok(()) => log::info!("[webview-accounts] opened externally: {}", url), - Err(e) => log::warn!("[webview-accounts] open_url({}) failed: {}", url, e), + #[cfg(target_os = "macos")] + { + match std::process::Command::new("/usr/bin/open").arg(url).spawn() { + Ok(_) => log::info!("[webview-accounts] opened externally (macos open): {}", url), + Err(e) => log::warn!( + "[webview-accounts] /usr/bin/open {} failed: {} — falling back to opener plugin", + url, + e + ), + } + } + #[cfg(not(target_os = "macos"))] + { + match tauri_plugin_opener::open_url(url, None::<&str>) { + Ok(()) => log::info!("[webview-accounts] opened externally: {}", url), + Err(e) => log::warn!("[webview-accounts] open_url({}) failed: {}", url, e), + } } } @@ -552,9 +594,6 @@ fn build_init_script(account_id: &str, provider: &str) -> String { } else { "" }; - // Migrated providers have no recipe under wry either (recipe.js - // files were deleted with the cef migration), but the UA shim is - // still worth shipping so fingerprint gates pass. let Some(recipe_js) = provider_recipe_js(provider) else { return spoof.to_string(); }; @@ -686,11 +725,22 @@ pub async fn webview_account_open( if url_is_internal(&nav_provider, url) { true } else { - log::info!( - "[webview-accounts] external navigation {} → system browser", - url - ); - open_in_system_browser(url.as_str()); + let target = unwrap_provider_redirect(url) + .map(|u| u.to_string()) + .unwrap_or_else(|| url.to_string()); + if target != url.as_str() { + log::info!( + "[webview-accounts] external navigation {} → (unwrapped) {} → system browser", + url, + target + ); + } else { + log::info!( + "[webview-accounts] external navigation {} → system browser", + url + ); + } + open_in_system_browser(&target); false } }); @@ -714,11 +764,22 @@ pub async fn webview_account_open( ); NewWindowResponse::Allow } else { - log::info!( - "[webview-accounts] new-window request {} → system browser", - url - ); - open_in_system_browser(url.as_str()); + let target = unwrap_provider_redirect(&url) + .map(|u| u.to_string()) + .unwrap_or_else(|| url.to_string()); + if target != url.as_str() { + log::info!( + "[webview-accounts] new-window request {} → (unwrapped) {} → system browser", + url, + target + ); + } else { + log::info!( + "[webview-accounts] new-window request {} → system browser", + url + ); + } + open_in_system_browser(&target); NewWindowResponse::Deny } }); diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 574818b1f..2932a058c 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -14,8 +14,8 @@ { "label": "main", "title": "OpenHuman", - "width": 800, - "height": 720, + "width": 1000, + "height": 800, "visible": true, "decorations": true, "resizable": true, diff --git a/app/src/components/BottomTabBar.tsx b/app/src/components/BottomTabBar.tsx index 061a800fb..bbc79a34f 100644 --- a/app/src/components/BottomTabBar.tsx +++ b/app/src/components/BottomTabBar.tsx @@ -54,7 +54,7 @@ const tabs = [ }, { id: 'intelligence', - label: 'Intelligence', + label: 'Memory', path: '/intelligence', icon: ( @@ -181,7 +181,7 @@ const BottomTabBar = () => { onBlur={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setRevealed(false); }}> - + {tab.icon} {showBadge && ( @@ -207,7 +207,14 @@ const BottomTabBar = () => { )} - {tab.label} + + {tab.label} + ); })} diff --git a/app/src/pages/Skills.tsx b/app/src/pages/Skills.tsx index 020b705f4..f68a5cc96 100644 --- a/app/src/pages/Skills.tsx +++ b/app/src/pages/Skills.tsx @@ -122,15 +122,22 @@ function composioStatusColor(connection: ComposioConnection | undefined): string // ─── Built-in skill definitions ──────────────────────────────────────────────── -const BUILT_IN_SKILLS = [ - { - id: 'screen-intelligence', - title: 'Screen Intelligence', - description: - 'Capture windows, summarize what is on screen, and feed useful context into memory.', - route: '/settings/screen-intelligence', - icon: BUILT_IN_SKILL_ICONS.screenIntelligence, - }, +const BUILT_IN_SKILLS: Array<{ + id: string; + title: string; + description: string; + route: string; + icon: React.ReactNode; +}> = [ + // Hidden — not active yet. Uncomment to re-enable. + // { + // id: 'screen-intelligence', + // title: 'Screen Intelligence', + // description: + // 'Capture windows, summarize what is on screen, and feed useful context into memory.', + // route: '/settings/screen-intelligence', + // icon: BUILT_IN_SKILL_ICONS.screenIntelligence, + // }, // text-autocomplete + voice-stt hidden per #717 (modals/status hooks retained for re-enable). ];