mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(ui,webview): compact tab bar, hide inactive tabs, harden CEF link handling (#868)
This commit is contained in:
@@ -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/`)
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
<p align="center">
|
||||
<a href="https://discord.tinyhumans.ai/">Discord</a> •
|
||||
<a href="https://www.reddit.com/r/tinyhumansai/">Reddit</a> •
|
||||
<a href="https://x.com/tinyhumansai">X/Twitter</a> •
|
||||
<a href="https://x.com/intent/follow?screen_name=tinyhumansai">X/Twitter</a> •
|
||||
<a href="https://tinyhumans.gitbook.io/openhuman/">Docs</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://x.com/intent/follow?screen_name=senamakel">Follow @senamakel (Creator)</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/status-early%20beta-orange" alt="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 |
|
||||
|
||||
<!-- # Star us on GitHub
|
||||
# Star us on GitHub
|
||||
|
||||
_Building toward AGI and artificial consciousness? Star the repo and help others find the path._
|
||||
|
||||
@@ -94,7 +97,7 @@ _Building toward AGI and artificial consciousness? Star the repo and help others
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=tinyhumansai/openhuman&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
</p> -->
|
||||
</p>
|
||||
|
||||
# Contributors Hall of Fame
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<Url> {
|
||||
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<R: Runtime>(
|
||||
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<R: Runtime>(
|
||||
);
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
{
|
||||
"label": "main",
|
||||
"title": "OpenHuman",
|
||||
"width": 800,
|
||||
"height": 720,
|
||||
"width": 1000,
|
||||
"height": 800,
|
||||
"visible": true,
|
||||
"decorations": true,
|
||||
"resizable": true,
|
||||
|
||||
@@ -54,7 +54,7 @@ const tabs = [
|
||||
},
|
||||
{
|
||||
id: 'intelligence',
|
||||
label: 'Intelligence',
|
||||
label: 'Memory',
|
||||
path: '/intelligence',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -181,7 +181,7 @@ const BottomTabBar = () => {
|
||||
onBlur={e => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) setRevealed(false);
|
||||
}}>
|
||||
<nav className="pointer-events-auto inline-flex items-center gap-2 rounded-sm border border-stone-300 bg-stone-200 shadow-soft px-1 py-1">
|
||||
<nav className="pointer-events-auto inline-flex items-center gap-1 rounded-sm border border-stone-300 bg-stone-200 shadow-soft px-1 py-1">
|
||||
{tabs.map(tab => {
|
||||
const active = isActive(tab.path);
|
||||
const showBadge = tab.id === 'notifications' && unreadCount > 0;
|
||||
@@ -189,7 +189,7 @@ const BottomTabBar = () => {
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => 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
|
||||
}>
|
||||
<span className="relative inline-flex">
|
||||
<span className="relative inline-flex flex-shrink-0">
|
||||
{tab.icon}
|
||||
{showBadge && (
|
||||
<span className="absolute -top-1 -right-1 min-w-[14px] h-[14px] px-1 rounded-full bg-coral-500 text-[9px] font-bold text-white flex items-center justify-center leading-none">
|
||||
@@ -207,7 +207,14 @@ const BottomTabBar = () => {
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span>{tab.label}</span>
|
||||
<span
|
||||
className={`overflow-hidden whitespace-nowrap transition-[max-width,margin-left,opacity] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] ${
|
||||
active
|
||||
? 'max-w-[160px] ml-2 opacity-100'
|
||||
: 'max-w-0 ml-0 opacity-0 group-hover:max-w-[160px] group-hover:ml-2 group-hover:opacity-100 group-focus-visible:max-w-[160px] group-focus-visible:ml-2 group-focus-visible:opacity-100'
|
||||
}`}>
|
||||
{tab.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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).
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user