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 @@
Discord •
Reddit •
- X/Twitter •
+ X/Twitter •
Docs
+
+ Follow @senamakel (Creator)
+
@@ -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);
}}>
-
+
{tabs.map(tab => {
const active = isActive(tab.path);
const showBadge = tab.id === 'notifications' && unreadCount > 0;
@@ -189,7 +189,7 @@ const BottomTabBar = () => {
navigate(tab.path)}
- className={`relative flex items-center gap-2 px-4 py-2 rounded-sm text-sm transition-colors duration-150 cursor-pointer ${
+ className={`group relative flex items-center px-2 py-2 rounded-sm text-sm transition-colors duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] cursor-pointer ${
active
? 'bg-white text-stone-900 font-semibold shadow-sm'
: 'bg-transparent text-stone-500 hover:bg-stone-300/50 hover:text-stone-700'
@@ -199,7 +199,7 @@ const BottomTabBar = () => {
? `${tab.label} (${unreadCount} unread)`
: tab.label
}>
-
+
{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).
];