From 992ce741100d13ace58933254ae8e0fe1d2e3ac3 Mon Sep 17 00:00:00 2001 From: Amin Chirazi <32016576+AminChirazi@users.noreply.github.com> Date: Mon, 4 May 2026 16:00:18 +0400 Subject: [PATCH] =?UTF-8?q?v0.1.8=20=E2=80=94=20route=20external=20target?= =?UTF-8?q?=3D=5Fblank=20to=20OS=20browser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.1.6 forced every target=_blank click to navigate in-window so the shell would feel like an app. That worked for cross- internal links but trapped users on external destinations: clicking Capacities in the sidebar hijacked the entire shell window off to capacities.app, with no way back except ⌘←. Split the rule by destination host. INTERNAL_HOSTS (mirrors the capability's remote.urls allowlist: internal.automators.com, auth.automators.com) navigates in-window. Everything else routes through plugin:shell|open IPC → OS default browser. The shell:open permission is already in the default capability, so no security-side change. Adds a defensive try/catch + a safety-net fallback so a missing IPC bridge degrades to in- window navigation rather than a dead click. Bumps versions in lockstep across package.json, Cargo.toml, and tauri.conf.json. Tag v0.1.8 to trigger the release workflow. --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 88 +++++++++++++++++++++++++++++---------- src-tauri/tauri.conf.json | 2 +- 4 files changed, 70 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 0ed0368..79ffed3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "internal-desktop", "private": true, - "version": "0.1.7", + "version": "0.1.8", "description": "Tauri shell wrapping internal.automators.com — desktop app for the Automators internal hub.", "scripts": { "tauri": "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ab07501..f2232fc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "internal-desktop" -version = "0.1.7" +version = "0.1.8" description = "Automators Internal — Tauri shell" authors = ["Automators GmbH"] license = "UNLICENSED" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7d865a0..a892eae 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -116,27 +116,62 @@ const SHELL_CSS_INJECT: &str = r#" } }, true); // capture phase, before React handlers - // ─── Keep target="_blank" links inside the Tauri window ──── + // ─── Route target="_blank" by host ──────────────────────── // - // Default Tauri behavior for and - // window.open() is to route to the system browser — fine for - // a generic browser, wrong for a "feels like a native app" - // shell. Override both: - // 1. Capture-phase click handler intercepts target="_blank" - // anchors before the WebView's default new-window - // handling. Navigates in the current window instead. - // 2. window.open is replaced to do the same — covers SDK - // code (e.g., a third-party widget that calls - // window.open() rather than rendering an anchor). + // v0.1.6 forced every target="_blank" click to navigate + // in-window so the shell would feel like an app. That worked + // for cross-internal links but trapped users when they + // clicked an *external* link (e.g. Capacities in the + // sidebar): the entire shell window was hijacked off to + // capacities.app, the user lost their place inside internal, + // and the only way back was ⌘←. Wrong UX for "this is a + // different app" links — those should pop a real browser + // tab. // - // Same-window navigation means the user loses the current - // page when clicking out (⌘← to go back, like in a browser). - // If the dashboard ever wants to keep the current page open - // when opening a reference (a SharePoint URL, etc.), the - // upgrade path is to swap window.location.href for an IPC - // call that creates a new Tauri WebviewWindow. Skipping that - // for v0.1.6 because same-window behavior covers the most - // common complaint. + // v0.1.8 splits the rule by destination host: + // - Host ∈ INTERNAL_HOSTS → in-window navigation. Same as + // v0.1.6 — keeps the auth-redirect dance and any + // internal cross-link inside the shell. + // - Anything else → routed to the OS default browser via + // the shell:open IPC. The internal window stays put; + // the external destination opens in a real browser tab. + // + // INTERNAL_HOSTS deliberately mirrors the capability's + // remote.urls allowlist (capabilities/default.json) — those + // are the origins we trust to make IPC calls and they're + // exactly the surfaces that should keep the in-window + // experience. Adding a new internal host means updating both + // here AND the capability JSON. + // + // Modifier-clicks (⌘-click) and middle-click are left alone + // — those go through the WebView's default handling, which + // on macOS opens in the OS browser anyway, so the user's + // "force-new-tab" intent is preserved. + // + // The shell:open IPC requires `shell:allow-open`, already in + // the default capability. If for some reason the IPC bridge + // isn't reachable (origin not in remote.urls, plugin + // mis-configured) we fall through to in-window navigation as + // a safety net so the user isn't left with a dead click. + var INTERNAL_HOSTS = ['internal.automators.com', 'auth.automators.com']; + function isInternalHost(href) { + try { + return INTERNAL_HOSTS.indexOf(new URL(href, window.location.href).hostname) !== -1; + } catch (e) { + return true; // unparseable URL → safest is in-window + } + } + function openExternal(href) { + if (window.__TAURI_INTERNALS__ && window.__TAURI_INTERNALS__.invoke) { + try { + window.__TAURI_INTERNALS__.invoke('plugin:shell|open', { path: href }); + return true; + } catch (e) { + return false; + } + } + return false; + } window.addEventListener('click', function(e) { var a = e.target && e.target.closest && e.target.closest('a[target="_blank"]'); if (!a || !a.href) return; @@ -145,13 +180,24 @@ const SHELL_CSS_INJECT: &str = r#" if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // modifier-clicks e.preventDefault(); e.stopPropagation(); - window.location.href = a.href; + if (isInternalHost(a.href)) { + window.location.href = a.href; + } else if (!openExternal(a.href)) { + window.location.href = a.href; // safety-net fallback + } }, true); var origOpen = window.open; window.open = function(url) { if (typeof url === 'string' && url) { - window.location.href = url; + if (isInternalHost(url)) { + window.location.href = url; + return null; + } + if (openExternal(url)) { + return null; + } + window.location.href = url; // safety-net fallback return null; } return origOpen.apply(window, arguments); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9b38489..3b8b316 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Automators Internal", - "version": "0.1.7", + "version": "0.1.8", "identifier": "com.automators.internal", "build": { "frontendDist": "../dist"