perf(desktop): reduce CPU/GPU overhead in Tauri shell
- SSH monitor: adaptive polling 2s→10s after stabilization, cheap TCP probe before expensive SSH subprocess check - SSH setup: exponential backoff in wait_for_master_ready and wait_local_forward_ready (250ms→2s cap) - Health checks: exponential backoff (100ms→1s / 250ms→2s) instead of flat intervals - Startup recovery poll: cap at 15 retries instead of infinite - Remove webview log target in release builds (eliminates IPC overhead) - Set global NO_PROXY env var at startup for all loopback addresses - Remove reqwest::blocking feature; use raw TCP for sidecar shutdown and SSH health checks - Disable pinch-to-zoom on macOS via WKWebView.setAllowsMagnification - Add WebView2 browser args on Windows (proxy bypass + disable unused UI features) - Add Cargo release profile: thin LTO, codegen-units=1, strip - Extract apply_platform_window_config for consistent window setup - Add vibrancy toggle in Appearance settings (macOS desktop only) with solid background fallback when disabled
This commit is contained in:
Generated
+25
-3
@@ -1212,7 +1212,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2533,6 +2532,16 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-javascript-core"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-osa-kit"
|
name = "objc2-osa-kit"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2557,6 +2566,17 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-security"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-ui-kit"
|
name = "objc2-ui-kit"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2581,6 +2601,8 @@ dependencies = [
|
|||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
|
"objc2-javascript-core",
|
||||||
|
"objc2-security",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2608,6 +2630,8 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"log",
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-web-kit",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3436,9 +3460,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ devtools = ["tauri/devtools"]
|
|||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls", "blocking"] }
|
reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls"] }
|
||||||
serde = { version = "1.0.210", features = ["derive"] }
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
serde_json = "1.0.143"
|
serde_json = "1.0.143"
|
||||||
tauri = { version = "2.10.3", features = ["macos-private-api"] }
|
tauri = { version = "2.10.3", features = ["macos-private-api"] }
|
||||||
@@ -31,5 +31,12 @@ url = "2.5"
|
|||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.5.6", features = [] }
|
tauri-build = { version = "2.5.6", features = [] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2 = "0.6"
|
||||||
|
objc2-web-kit = "0.3"
|
||||||
window-vibrancy = "0.7.1"
|
window-vibrancy = "0.7.1"
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ use window_vibrancy::{
|
|||||||
apply_vibrancy, clear_vibrancy, NSVisualEffectMaterial,
|
apply_vibrancy, clear_vibrancy, NSVisualEffectMaterial,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Disable pinch-to-zoom / magnification gestures on macOS to avoid accidental
|
||||||
|
/// zoom and the continuous gesture event processing overhead.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn disable_pinch_zoom(window: &tauri::WebviewWindow) {
|
||||||
|
let _ = window.with_webview(|webview| unsafe {
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2_web_kit::WKWebView;
|
||||||
|
let wk_webview: Retained<WKWebView> =
|
||||||
|
Retained::retain(webview.inner().cast()).unwrap();
|
||||||
|
wk_webview.setAllowsMagnification(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
fn disable_pinch_zoom(_window: &tauri::WebviewWindow) {}
|
||||||
|
|
||||||
/// Global counter for generating unique window labels.
|
/// Global counter for generating unique window labels.
|
||||||
static WINDOW_COUNTER: AtomicU64 = AtomicU64::new(1);
|
static WINDOW_COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
@@ -1174,9 +1190,11 @@ fn is_app_bundle_installed(bundle_name: &str) -> bool {
|
|||||||
const SIDECAR_NAME: &str = "openchamber-server";
|
const SIDECAR_NAME: &str = "openchamber-server";
|
||||||
const SIDECAR_NOTIFY_PREFIX: &str = "[OpenChamberDesktopNotify] ";
|
const SIDECAR_NOTIFY_PREFIX: &str = "[OpenChamberDesktopNotify] ";
|
||||||
const HEALTH_TIMEOUT: Duration = Duration::from_secs(20);
|
const HEALTH_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
const HEALTH_POLL_INTERVAL: Duration = Duration::from_millis(250);
|
const HEALTH_POLL_INITIAL_INTERVAL: Duration = Duration::from_millis(250);
|
||||||
|
const HEALTH_POLL_MAX_INTERVAL: Duration = Duration::from_millis(2000);
|
||||||
const LOCAL_SIDECAR_HEALTH_TIMEOUT: Duration = Duration::from_secs(8);
|
const LOCAL_SIDECAR_HEALTH_TIMEOUT: Duration = Duration::from_secs(8);
|
||||||
const LOCAL_SIDECAR_HEALTH_POLL_INTERVAL: Duration = Duration::from_millis(100);
|
const LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL: Duration = Duration::from_millis(100);
|
||||||
|
const LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL: Duration = Duration::from_millis(1000);
|
||||||
const STARTUP_REMOTE_PROBE_SOFT_TIMEOUT: Duration = Duration::from_secs(2);
|
const STARTUP_REMOTE_PROBE_SOFT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||||
const STARTUP_REMOTE_PROBE_HARD_TIMEOUT: Duration = Duration::from_secs(10);
|
const STARTUP_REMOTE_PROBE_HARD_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
@@ -1741,7 +1759,12 @@ fn maybe_show_sidecar_notification(app: &tauri::AppHandle, payload: SidecarNotif
|
|||||||
let _ = builder.show();
|
let _ = builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Duration) -> bool {
|
async fn wait_for_health_with(
|
||||||
|
url: &str,
|
||||||
|
timeout: Duration,
|
||||||
|
initial_interval: Duration,
|
||||||
|
max_interval: Duration,
|
||||||
|
) -> bool {
|
||||||
let client = match reqwest::Client::builder().no_proxy().build() {
|
let client = match reqwest::Client::builder().no_proxy().build() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return false,
|
Err(_) => return false,
|
||||||
@@ -1749,6 +1772,7 @@ async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Durat
|
|||||||
|
|
||||||
let deadline = std::time::Instant::now() + timeout;
|
let deadline = std::time::Instant::now() + timeout;
|
||||||
let health_url = format!("{}/health", url.trim_end_matches('/'));
|
let health_url = format!("{}/health", url.trim_end_matches('/'));
|
||||||
|
let mut interval = initial_interval;
|
||||||
|
|
||||||
while std::time::Instant::now() < deadline {
|
while std::time::Instant::now() < deadline {
|
||||||
if let Ok(resp) = client.get(&health_url).send().await {
|
if let Ok(resp) = client.get(&health_url).send().await {
|
||||||
@@ -1756,14 +1780,15 @@ async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Durat
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tokio::time::sleep(poll_interval).await;
|
tokio::time::sleep(interval).await;
|
||||||
|
interval = (interval * 2).min(max_interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_health(url: &str) -> bool {
|
async fn wait_for_health(url: &str) -> bool {
|
||||||
wait_for_health_with(url, HEALTH_TIMEOUT, HEALTH_POLL_INTERVAL).await
|
wait_for_health_with(url, HEALTH_TIMEOUT, HEALTH_POLL_INITIAL_INTERVAL, HEALTH_POLL_MAX_INTERVAL).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kill_sidecar(app: tauri::AppHandle) {
|
fn kill_sidecar(app: tauri::AppHandle) {
|
||||||
@@ -1773,16 +1798,28 @@ fn kill_sidecar(app: tauri::AppHandle) {
|
|||||||
|
|
||||||
let sidecar_url = state.url.lock().expect("sidecar url mutex").clone();
|
let sidecar_url = state.url.lock().expect("sidecar url mutex").clone();
|
||||||
if let Some(url) = sidecar_url {
|
if let Some(url) = sidecar_url {
|
||||||
let shutdown_url = format!("{}/api/system/shutdown", url.trim_end_matches('/'));
|
// Attempt graceful shutdown via a raw HTTP POST to avoid pulling in
|
||||||
if let Ok(client) = reqwest::blocking::Client::builder()
|
// reqwest::blocking (and its extra thread pool) just for this one call.
|
||||||
.no_proxy()
|
if let Ok(parsed) = url::Url::parse(&url) {
|
||||||
.timeout(Duration::from_millis(1500))
|
let host = parsed.host_str().unwrap_or("127.0.0.1");
|
||||||
.build()
|
let port = parsed.port().unwrap_or(80);
|
||||||
{
|
let path = "/api/system/shutdown";
|
||||||
if let Ok(resp) = client.post(shutdown_url).send() {
|
if let Ok(mut stream) =
|
||||||
if resp.status().is_success() {
|
std::net::TcpStream::connect_timeout(
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
&format!("{host}:{port}").parse().unwrap(),
|
||||||
}
|
Duration::from_millis(1500),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
let _ = stream.set_write_timeout(Some(Duration::from_millis(1500)));
|
||||||
|
let _ = stream.set_read_timeout(Some(Duration::from_millis(1500)));
|
||||||
|
let request = format!(
|
||||||
|
"POST {path} HTTP/1.1\r\nHost: {host}:{port}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||||
|
);
|
||||||
|
let _ = stream.write_all(request.as_bytes());
|
||||||
|
let _ = stream.flush();
|
||||||
|
// Brief pause to let the sidecar begin its shutdown sequence.
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2067,7 +2104,8 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result<String> {
|
|||||||
if !wait_for_health_with(
|
if !wait_for_health_with(
|
||||||
&url,
|
&url,
|
||||||
LOCAL_SIDECAR_HEALTH_TIMEOUT,
|
LOCAL_SIDECAR_HEALTH_TIMEOUT,
|
||||||
LOCAL_SIDECAR_HEALTH_POLL_INTERVAL,
|
LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL,
|
||||||
|
LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -2451,9 +2489,42 @@ fn read_desktop_theme_override() -> Option<tauri::Theme> {
|
|||||||
parse_theme_override(theme_mode, theme_variant)
|
parse_theme_override(theme_mode, theme_variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_desktop_vibrancy_enabled() -> bool {
|
||||||
|
let raw = fs::read_to_string(settings_file_path()).ok();
|
||||||
|
let parsed = raw
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok());
|
||||||
|
parsed
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v.get("desktopVibrancy"))
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(true) // enabled by default
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_desktop_vibrancy_to_disk(enabled: bool) -> Result<()> {
|
||||||
|
let path = settings_file_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let mut root: serde_json::Value = if let Ok(raw) = fs::read_to_string(&path) {
|
||||||
|
serde_json::from_str(&raw).unwrap_or(serde_json::json!({}))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({})
|
||||||
|
};
|
||||||
|
if !root.is_object() {
|
||||||
|
root = serde_json::json!({});
|
||||||
|
}
|
||||||
|
root["desktopVibrancy"] = serde_json::Value::Bool(enabled);
|
||||||
|
fs::write(&path, serde_json::to_string_pretty(&root)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn apply_macos_window_vibrancy(window: &tauri::WebviewWindow) {
|
fn apply_macos_window_vibrancy(window: &tauri::WebviewWindow) {
|
||||||
let _ = clear_vibrancy(window);
|
if !read_desktop_vibrancy_enabled() {
|
||||||
|
let _ = clear_vibrancy(window);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(error) = apply_vibrancy(
|
if let Err(error) = apply_vibrancy(
|
||||||
window,
|
window,
|
||||||
@@ -2468,6 +2539,56 @@ fn apply_macos_window_vibrancy(window: &tauri::WebviewWindow) {
|
|||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn apply_macos_window_vibrancy(_window: &tauri::WebviewWindow) {}
|
fn apply_macos_window_vibrancy(_window: &tauri::WebviewWindow) {}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn desktop_set_vibrancy(app: tauri::AppHandle, enabled: bool) -> Result<(), String> {
|
||||||
|
write_desktop_vibrancy_to_disk(enabled).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
for window in app.webview_windows().values() {
|
||||||
|
if enabled {
|
||||||
|
if let Err(error) = apply_vibrancy(
|
||||||
|
window,
|
||||||
|
NSVisualEffectMaterial::Sidebar,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
log::warn!("[desktop:vibrancy] Failed to apply vibrancy: {error}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = clear_vibrancy(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let _ = app;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply platform-specific window builder configuration.
|
||||||
|
fn apply_platform_window_config<M: Manager<tauri::Wry>>(
|
||||||
|
builder: WebviewWindowBuilder<'_, tauri::Wry, M>,
|
||||||
|
) -> WebviewWindowBuilder<'_, tauri::Wry, M> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let builder = builder
|
||||||
|
.transparent(true)
|
||||||
|
.hidden_title(true)
|
||||||
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
|
.traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition {
|
||||||
|
x: 17.0,
|
||||||
|
y: 26.0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let builder = builder.additional_browser_args(
|
||||||
|
"--proxy-bypass-list=<-loopback> --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection",
|
||||||
|
);
|
||||||
|
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn desktop_set_window_theme(
|
fn desktop_set_window_theme(
|
||||||
window: tauri::WebviewWindow,
|
window: tauri::WebviewWindow,
|
||||||
@@ -2631,6 +2752,8 @@ fn create_window(
|
|||||||
.visible(false)
|
.visible(false)
|
||||||
.initialization_script(&init_script);
|
.initialization_script(&init_script);
|
||||||
|
|
||||||
|
builder = apply_platform_window_config(builder);
|
||||||
|
|
||||||
let apply_restored_state = restored_state
|
let apply_restored_state = restored_state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|state| is_window_state_visible(app, state))
|
.map(|state| is_window_state_visible(app, state))
|
||||||
@@ -2644,21 +2767,10 @@ fn create_window(
|
|||||||
.position(state.x as f64, state.y as f64);
|
.position(state.x as f64, state.y as f64);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
builder = builder
|
|
||||||
.transparent(true)
|
|
||||||
.hidden_title(true)
|
|
||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
|
||||||
.traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition {
|
|
||||||
x: 17.0,
|
|
||||||
y: 26.0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let window = builder.build()?;
|
let window = builder.build()?;
|
||||||
let _ = window.set_theme(read_desktop_theme_override());
|
let _ = window.set_theme(read_desktop_theme_override());
|
||||||
apply_macos_window_vibrancy(&window);
|
apply_macos_window_vibrancy(&window);
|
||||||
|
disable_pinch_zoom(&window);
|
||||||
|
|
||||||
if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) {
|
if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) {
|
||||||
if state.maximized || state.fullscreen {
|
if state.maximized || state.fullscreen {
|
||||||
@@ -2693,6 +2805,8 @@ fn create_startup_window(app: &tauri::AppHandle, restore_geometry: bool) -> Resu
|
|||||||
.visible(true)
|
.visible(true)
|
||||||
.initialization_script(&splash_script);
|
.initialization_script(&splash_script);
|
||||||
|
|
||||||
|
builder = apply_platform_window_config(builder);
|
||||||
|
|
||||||
let apply_restored_state = restored_state
|
let apply_restored_state = restored_state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|state| is_window_state_visible(app, state))
|
.map(|state| is_window_state_visible(app, state))
|
||||||
@@ -2706,21 +2820,10 @@ fn create_startup_window(app: &tauri::AppHandle, restore_geometry: bool) -> Resu
|
|||||||
.position(state.x as f64, state.y as f64);
|
.position(state.x as f64, state.y as f64);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
builder = builder
|
|
||||||
.transparent(true)
|
|
||||||
.hidden_title(true)
|
|
||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
|
||||||
.traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition {
|
|
||||||
x: 17.0,
|
|
||||||
y: 26.0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let window = builder.build()?;
|
let window = builder.build()?;
|
||||||
let _ = window.set_theme(read_desktop_theme_override());
|
let _ = window.set_theme(read_desktop_theme_override());
|
||||||
apply_macos_window_vibrancy(&window);
|
apply_macos_window_vibrancy(&window);
|
||||||
|
disable_pinch_zoom(&window);
|
||||||
|
|
||||||
if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) {
|
if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) {
|
||||||
if state.maximized || state.fullscreen {
|
if state.maximized || state.fullscreen {
|
||||||
@@ -2926,13 +3029,38 @@ fn open_new_window(app: &tauri::AppHandle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// Ensure localhost traffic never routes through a system/VPN proxy.
|
||||||
|
for key in ["NO_PROXY", "no_proxy"] {
|
||||||
|
let existing = env::var(key).unwrap_or_default();
|
||||||
|
let loopback = ["127.0.0.1", "localhost", "::1"];
|
||||||
|
let missing: Vec<&str> = loopback
|
||||||
|
.iter()
|
||||||
|
.filter(|addr| !existing.split(',').any(|part| part.trim() == **addr))
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
if !missing.is_empty() {
|
||||||
|
let merged = if existing.is_empty() {
|
||||||
|
missing.join(",")
|
||||||
|
} else {
|
||||||
|
format!("{},{}", existing, missing.join(","))
|
||||||
|
};
|
||||||
|
env::set_var(key, &merged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let log_builder = tauri_plugin_log::Builder::default()
|
let log_builder = tauri_plugin_log::Builder::default()
|
||||||
.level(log::LevelFilter::Info)
|
.level(log::LevelFilter::Info)
|
||||||
.clear_targets()
|
.clear_targets()
|
||||||
.targets([
|
.targets(if cfg!(debug_assertions) {
|
||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
|
vec![
|
||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
|
||||||
]);
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![tauri_plugin_log::Target::new(
|
||||||
|
tauri_plugin_log::TargetKind::Stdout,
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
|
||||||
let builder = tauri::Builder::default()
|
let builder = tauri::Builder::default()
|
||||||
.manage(SidecarState::default())
|
.manage(SidecarState::default())
|
||||||
@@ -3155,6 +3283,7 @@ fn main() {
|
|||||||
desktop_hosts_set,
|
desktop_hosts_set,
|
||||||
desktop_host_probe,
|
desktop_host_probe,
|
||||||
desktop_set_window_theme,
|
desktop_set_window_theme,
|
||||||
|
desktop_set_vibrancy,
|
||||||
remote_ssh::desktop_ssh_instances_get,
|
remote_ssh::desktop_ssh_instances_get,
|
||||||
remote_ssh::desktop_ssh_instances_set,
|
remote_ssh::desktop_ssh_instances_set,
|
||||||
remote_ssh::desktop_ssh_import_hosts,
|
remote_ssh::desktop_ssh_import_hosts,
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const DEFAULT_READY_TIMEOUT_SEC: u64 = 30;
|
|||||||
const DEFAULT_RECONNECT_MAX_ATTEMPTS: u32 = 5;
|
const DEFAULT_RECONNECT_MAX_ATTEMPTS: u32 = 5;
|
||||||
const MAX_LOG_LINES_PER_INSTANCE: usize = 1200;
|
const MAX_LOG_LINES_PER_INSTANCE: usize = 1200;
|
||||||
|
|
||||||
|
/// Monitor starts with fast polling and relaxes to steady-state after stabilization.
|
||||||
|
const MONITOR_INITIAL_POLL_SECS: u64 = 2;
|
||||||
|
const MONITOR_STEADY_POLL_SECS: u64 = 10;
|
||||||
|
/// Number of healthy ticks before switching from initial to steady-state polling.
|
||||||
|
const MONITOR_STABILIZE_TICKS: u32 = 5;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DesktopSshInstancesConfig {
|
pub struct DesktopSshInstancesConfig {
|
||||||
@@ -1027,6 +1033,7 @@ fn wait_for_master_ready(
|
|||||||
master: &mut Child,
|
master: &mut Child,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let deadline = std::time::Instant::now() + Duration::from_secs(timeout_sec as u64);
|
let deadline = std::time::Instant::now() + Duration::from_secs(timeout_sec as u64);
|
||||||
|
let mut poll_ms: u64 = 250;
|
||||||
while std::time::Instant::now() < deadline {
|
while std::time::Instant::now() < deadline {
|
||||||
let args = vec![
|
let args = vec![
|
||||||
"-o".to_string(),
|
"-o".to_string(),
|
||||||
@@ -1056,7 +1063,8 @@ fn wait_for_master_ready(
|
|||||||
return Err(anyhow!(stderr.trim().to_string()));
|
return Err(anyhow!(stderr.trim().to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::thread::sleep(Duration::from_millis(250));
|
std::thread::sleep(Duration::from_millis(poll_ms));
|
||||||
|
poll_ms = (poll_ms * 2).min(2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow!("SSH ControlMaster connection timed out"))
|
Err(anyhow!("SSH ControlMaster connection timed out"))
|
||||||
@@ -1552,19 +1560,34 @@ fn is_local_tunnel_reachable(local_port: u16) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_local_forward_ready(local_port: u16) -> Result<()> {
|
fn wait_local_forward_ready(local_port: u16) -> Result<()> {
|
||||||
let client = reqwest::blocking::Client::builder()
|
|
||||||
.timeout(Duration::from_millis(1000))
|
|
||||||
.no_proxy()
|
|
||||||
.build()?;
|
|
||||||
let deadline = std::time::Instant::now() + Duration::from_secs(DEFAULT_READY_TIMEOUT_SEC);
|
let deadline = std::time::Instant::now() + Duration::from_secs(DEFAULT_READY_TIMEOUT_SEC);
|
||||||
let target = format!("http://127.0.0.1:{local_port}/health");
|
let addr: std::net::SocketAddr = format!("127.0.0.1:{local_port}").parse()?;
|
||||||
|
let mut poll_ms: u64 = 250;
|
||||||
while std::time::Instant::now() < deadline {
|
while std::time::Instant::now() < deadline {
|
||||||
if let Ok(response) = client.get(&target).send() {
|
if let Ok(mut stream) =
|
||||||
if response.status().is_success() || response.status().as_u16() == 401 {
|
TcpStream::connect_timeout(&addr, Duration::from_millis(1000))
|
||||||
return Ok(());
|
{
|
||||||
|
use std::io::{Read as IoRead, Write};
|
||||||
|
let _ = stream.set_read_timeout(Some(Duration::from_millis(1000)));
|
||||||
|
let _ = stream.set_write_timeout(Some(Duration::from_millis(1000)));
|
||||||
|
let request = format!(
|
||||||
|
"GET /health HTTP/1.1\r\nHost: 127.0.0.1:{local_port}\r\nConnection: close\r\n\r\n"
|
||||||
|
);
|
||||||
|
if stream.write_all(request.as_bytes()).is_ok() {
|
||||||
|
let mut buf = [0u8; 32];
|
||||||
|
if let Ok(n) = stream.read(&mut buf) {
|
||||||
|
let head = std::str::from_utf8(&buf[..n]).unwrap_or("");
|
||||||
|
// Match "HTTP/1.x 2xx" or "HTTP/1.x 401"
|
||||||
|
if head.starts_with("HTTP/1.")
|
||||||
|
&& (head.contains(" 2") || head.contains(" 401"))
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(250));
|
std::thread::sleep(Duration::from_millis(poll_ms));
|
||||||
|
poll_ms = (poll_ms * 2).min(2000);
|
||||||
}
|
}
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"Timed out waiting for forwarded OpenChamber health"
|
"Timed out waiting for forwarded OpenChamber health"
|
||||||
@@ -2353,8 +2376,14 @@ impl DesktopSshManagerInner {
|
|||||||
let inner = Arc::clone(self);
|
let inner = Arc::clone(self);
|
||||||
let id_for_task = id.clone();
|
let id_for_task = id.clone();
|
||||||
let handle = tauri::async_runtime::spawn(async move {
|
let handle = tauri::async_runtime::spawn(async move {
|
||||||
|
let mut healthy_ticks: u32 = 0;
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
let poll_secs = if healthy_ticks >= MONITOR_STABILIZE_TICKS {
|
||||||
|
MONITOR_STEADY_POLL_SECS
|
||||||
|
} else {
|
||||||
|
MONITOR_INITIAL_POLL_SECS
|
||||||
|
};
|
||||||
|
tokio::time::sleep(Duration::from_secs(poll_secs)).await;
|
||||||
|
|
||||||
let mut dropped_reason: Option<String> = None;
|
let mut dropped_reason: Option<String> = None;
|
||||||
let mut detached_notice: Option<String> = None;
|
let mut detached_notice: Option<String> = None;
|
||||||
@@ -2424,18 +2453,21 @@ impl DesktopSshManagerInner {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if session.master_detached {
|
} else if session.master_detached {
|
||||||
if !is_control_master_alive(&session.parsed, &session.control_path) {
|
// Fast path: check local tunnel first (cheap TCP probe)
|
||||||
if is_local_tunnel_reachable(session.local_port) {
|
// before spawning an SSH subprocess for control master check.
|
||||||
if detached_notice.is_none() {
|
if is_local_tunnel_reachable(session.local_port) {
|
||||||
detached_notice = Some(
|
// Tunnel is alive — skip the expensive SSH check entirely.
|
||||||
"SSH ControlMaster check failed but local tunnel is still reachable"
|
} else if !is_control_master_alive(
|
||||||
.to_string(),
|
&session.parsed,
|
||||||
);
|
&session.control_path,
|
||||||
}
|
) {
|
||||||
} else {
|
dropped_reason =
|
||||||
dropped_reason =
|
Some("SSH ControlMaster is not reachable".to_string());
|
||||||
Some("SSH ControlMaster is not reachable".to_string());
|
} else {
|
||||||
}
|
detached_notice = Some(
|
||||||
|
"Local tunnel unreachable but ControlMaster is alive"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if let Some(status) = session.master.try_wait().ok().flatten() {
|
} else if let Some(status) = session.master.try_wait().ok().flatten() {
|
||||||
if status.success()
|
if status.success()
|
||||||
@@ -2471,6 +2503,7 @@ impl DesktopSshManagerInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dropped_reason.is_none() {
|
if dropped_reason.is_none() {
|
||||||
|
healthy_ticks = healthy_ticks.saturating_add(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ function App({ apis }: AppProps) {
|
|||||||
if (providersCount > 0 && agentsCount > 0) return;
|
if (providersCount > 0 && agentsCount > 0) return;
|
||||||
|
|
||||||
let active = true;
|
let active = true;
|
||||||
|
let retries = 0;
|
||||||
|
const MAX_RETRIES = 15;
|
||||||
const attempt = async () => {
|
const attempt = async () => {
|
||||||
const state = useConfigStore.getState();
|
const state = useConfigStore.getState();
|
||||||
if (state.providers.length > 0 && state.agents.length > 0) return;
|
if (state.providers.length > 0 && state.agents.length > 0) return;
|
||||||
@@ -287,7 +289,11 @@ function App({ apis }: AppProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void attempt();
|
void attempt();
|
||||||
const id = setInterval(() => { if (active) void attempt(); }, 2000);
|
const id = setInterval(() => {
|
||||||
|
if (!active) return;
|
||||||
|
if (++retries >= MAX_RETRIES) { clearInterval(id); return; }
|
||||||
|
void attempt();
|
||||||
|
}, 2000);
|
||||||
return () => { active = false; clearInterval(id); };
|
return () => { active = false; clearInterval(id); };
|
||||||
}, [isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount, agentsCount]);
|
}, [isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount, agentsCount]);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
import { isDesktopShell, isVSCodeRuntime, isWebRuntime, desktopSetVibrancy } from '@/lib/desktop';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { usePwaDetection } from '@/hooks/usePwaDetection';
|
import { usePwaDetection } from '@/hooks/usePwaDetection';
|
||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
@@ -209,6 +209,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
} = useThemeSystem();
|
} = useThemeSystem();
|
||||||
|
|
||||||
const [themesReloading, setThemesReloading] = React.useState(false);
|
const [themesReloading, setThemesReloading] = React.useState(false);
|
||||||
|
const [vibrancyEnabled, setVibrancyEnabled] = React.useState(true);
|
||||||
const [chatRenderPreviewTick, setChatRenderPreviewTick] = React.useState(0);
|
const [chatRenderPreviewTick, setChatRenderPreviewTick] = React.useState(0);
|
||||||
const reportUsage = useUIStore(state => state.reportUsage);
|
const reportUsage = useUIStore(state => state.reportUsage);
|
||||||
const setReportUsage = useUIStore(state => state.setReportUsage);
|
const setReportUsage = useUIStore(state => state.setReportUsage);
|
||||||
@@ -219,6 +220,28 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
void updateDesktopSettings({ reportUsage: enabled });
|
void updateDesktopSettings({ reportUsage: enabled });
|
||||||
}, [setReportUsage]);
|
}, [setReportUsage]);
|
||||||
|
|
||||||
|
const isMacDesktop = React.useMemo(() => {
|
||||||
|
if (!isDesktopShell()) return false;
|
||||||
|
if (typeof navigator === 'undefined') return false;
|
||||||
|
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isMacDesktop) return;
|
||||||
|
const stored = localStorage.getItem('desktopVibrancy');
|
||||||
|
if (stored !== null) {
|
||||||
|
setVibrancyEnabled(stored !== 'false');
|
||||||
|
}
|
||||||
|
}, [isMacDesktop]);
|
||||||
|
|
||||||
|
const handleVibrancyChange = React.useCallback((enabled: boolean) => {
|
||||||
|
setVibrancyEnabled(enabled);
|
||||||
|
localStorage.setItem('desktopVibrancy', String(enabled));
|
||||||
|
document.documentElement.classList.toggle('no-vibrancy', !enabled);
|
||||||
|
void desktopSetVibrancy(enabled);
|
||||||
|
void updateDesktopSettings({ desktopVibrancy: enabled });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const shouldAnimateChatPreview = isSettingsDialogOpen
|
const shouldAnimateChatPreview = isSettingsDialogOpen
|
||||||
&& (visibleSettings ? visibleSettings.includes('chatRenderMode') : true);
|
&& (visibleSettings ? visibleSettings.includes('chatRenderMode') : true);
|
||||||
|
|
||||||
@@ -503,6 +526,24 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isMacDesktop && (
|
||||||
|
<div className="flex items-center gap-2 py-1.5">
|
||||||
|
<Checkbox
|
||||||
|
checked={vibrancyEnabled}
|
||||||
|
onChange={handleVibrancyChange}
|
||||||
|
ariaLabel="Toggle window vibrancy"
|
||||||
|
/>
|
||||||
|
<div className="flex min-w-0 flex-col">
|
||||||
|
<span className="typography-ui-label text-foreground">
|
||||||
|
Window vibrancy
|
||||||
|
</span>
|
||||||
|
<span className="typography-meta text-muted-foreground">
|
||||||
|
Translucent window background. Disabling may reduce energy usage.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showPwaInstallNameSetting && (
|
{showPwaInstallNameSetting && (
|
||||||
<div className="py-1.5 space-y-1.5">
|
<div className="py-1.5 space-y-1.5">
|
||||||
<div className="flex min-w-0 flex-col">
|
<div className="flex min-w-0 flex-col">
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ export type DesktopSettings = {
|
|||||||
skillCatalogs?: SkillCatalogConfig[];
|
skillCatalogs?: SkillCatalogConfig[];
|
||||||
// Opt-in to send anonymous usage reports for update checks (default: true)
|
// Opt-in to send anonymous usage reports for update checks (default: true)
|
||||||
reportUsage?: boolean;
|
reportUsage?: boolean;
|
||||||
|
// macOS window vibrancy effect (default: true)
|
||||||
|
desktopVibrancy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TauriGlobal = {
|
type TauriGlobal = {
|
||||||
@@ -637,3 +639,18 @@ export const clearDesktopCache = async (): Promise<boolean> => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const desktopSetVibrancy = async (enabled: boolean): Promise<boolean> => {
|
||||||
|
if (!isTauriShell()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||||
|
await tauri?.core?.invoke?.('desktop_set_vibrancy', { enabled });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to set vibrancy', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ const setRootDeviceAttributes = (
|
|||||||
|
|
||||||
if (isTauriShellRuntime) {
|
if (isTauriShellRuntime) {
|
||||||
root.classList.add('desktop-runtime');
|
root.classList.add('desktop-runtime');
|
||||||
|
// Apply no-vibrancy class early so the first paint uses a solid background
|
||||||
|
// when vibrancy was previously disabled by the user.
|
||||||
|
const vibrancyOff = localStorage.getItem('desktopVibrancy') === 'false';
|
||||||
|
root.classList.toggle('no-vibrancy', vibrancyOff);
|
||||||
root.style.setProperty('--is-mobile', '0');
|
root.style.setProperty('--is-mobile', '0');
|
||||||
root.style.setProperty('--device-type', 'desktop');
|
root.style.setProperty('--device-type', 'desktop');
|
||||||
root.style.setProperty('--font-scale', '1');
|
root.style.setProperty('--font-scale', '1');
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
|
|||||||
localStorage.removeItem('openchamber.pwaName');
|
localStorage.removeItem('openchamber.pwaName');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (typeof settings.desktopVibrancy === 'boolean') {
|
||||||
|
localStorage.setItem('desktopVibrancy', String(settings.desktopVibrancy));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type PersistApi = {
|
type PersistApi = {
|
||||||
|
|||||||
@@ -135,12 +135,29 @@
|
|||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.desktop-runtime body,
|
:root.desktop-runtime:not(.no-vibrancy) body,
|
||||||
:root.desktop-runtime #root {
|
:root.desktop-runtime:not(.no-vibrancy) #root {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root.desktop-runtime.no-vibrancy body,
|
||||||
|
:root.desktop-runtime.no-vibrancy #root {
|
||||||
|
background: var(--background) !important;
|
||||||
|
background-color: var(--background) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When vibrancy is off, force sidebar overlays to solid and disable blur. */
|
||||||
|
:root.desktop-runtime.no-vibrancy {
|
||||||
|
--sidebar-overlay-strong: var(--sidebar) !important;
|
||||||
|
--sidebar-overlay-soft: var(--sidebar) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.desktop-runtime.no-vibrancy .backdrop-blur {
|
||||||
|
-webkit-backdrop-filter: none !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.font-sans {
|
.font-sans {
|
||||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important;
|
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user