From f9aaf485b746dc928aa26c2cc1d479d3641abf32 Mon Sep 17 00:00:00 2001 From: Omer Shatzberg Date: Wed, 5 Aug 2026 21:20:56 +0300 Subject: [PATCH] fix(desktop): bypass loopback preview cache --- Cargo.lock | 28 +++---- Cargo.toml | 2 +- README.md | 2 +- VERSION | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.lock | 30 ++++---- apps/desktop/src-tauri/Cargo.toml | 6 +- apps/desktop/src-tauri/src/preview_webview.rs | 73 ++++++++++++++++++- apps/desktop/src-tauri/tauri.conf.json | 2 +- apps/desktop/src/update/releases.json | 5 ++ apps/desktop/tests/tauri-webview-e2e.mjs | 35 ++++++++- docs/wiki/desktop.md | 2 +- 12 files changed, 150 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b0464b..d5b8c6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1136,7 +1136,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "milim-agents" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-stream", "async-trait", @@ -1154,7 +1154,7 @@ dependencies = [ [[package]] name = "milim-automation" -version = "0.2.25" +version = "0.2.26" dependencies = [ "base64", "chrono", @@ -1171,7 +1171,7 @@ dependencies = [ [[package]] name = "milim-cli" -version = "0.2.25" +version = "0.2.26" dependencies = [ "anyhow", "clap", @@ -1195,7 +1195,7 @@ dependencies = [ [[package]] name = "milim-core" -version = "0.2.25" +version = "0.2.26" dependencies = [ "directories", "serde", @@ -1206,7 +1206,7 @@ dependencies = [ [[package]] name = "milim-identity" -version = "0.2.25" +version = "0.2.26" dependencies = [ "base64", "hex", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "milim-inference" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-stream", "async-trait", @@ -1238,7 +1238,7 @@ dependencies = [ [[package]] name = "milim-mcp-client" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-trait", "milim-core", @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "milim-memory" -version = "0.2.25" +version = "0.2.26" dependencies = [ "milim-core", "milim-inference", @@ -1267,7 +1267,7 @@ dependencies = [ [[package]] name = "milim-privacy" -version = "0.2.25" +version = "0.2.26" dependencies = [ "regex", "serde", @@ -1275,7 +1275,7 @@ dependencies = [ [[package]] name = "milim-sandbox" -version = "0.2.25" +version = "0.2.26" dependencies = [ "milim-core", "serde", @@ -1284,7 +1284,7 @@ dependencies = [ [[package]] name = "milim-server" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-stream", "async-trait", @@ -1318,7 +1318,7 @@ dependencies = [ [[package]] name = "milim-skills" -version = "0.2.25" +version = "0.2.26" dependencies = [ "milim-core", "milim-storage", @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "milim-storage" -version = "0.2.25" +version = "0.2.26" dependencies = [ "aes-gcm", "milim-core", @@ -1343,7 +1343,7 @@ dependencies = [ [[package]] name = "milim-tools" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-trait", "milim-core", diff --git a/Cargo.toml b/Cargo.toml index a4033ba..88dca1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.2.25" +version = "0.2.26" edition = "2021" license = "MIT" authors = ["Omer Shatzberg "] diff --git a/README.md b/README.md index 042bf4b..60e190f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Release artifacts target Windows and macOS. Linux packaging is intentionally dis ### Power tools -Agents, Workers, skills, schedules, MCP servers and Apps, media generation, Google Workspace, previews, and the mobile companion remain available from the app's collapsed **Tools** section. They extend the core thread without competing with the default workflow. +Agents, Workers, skills, schedules, MCP servers and Apps, media generation, Google Workspace, previews, and the mobile companion remain available from the app's collapsed **Tools** section. Loopback preview reloads bypass the browser cache so local CSS and JavaScript changes stay current while an agent works. These tools extend the core thread without competing with the default workflow. Milim keeps provider-backed chat and installed account runtimes distinct. Provider models use Milim's tool-agent loop, while account runtimes retain their native sessions and tools behind the same visible approval policy. Open gives host tools and supported account runtimes unrestricted filesystem and command access, keeps the selected folder only as the working directory, starts eligible worker plans immediately, and clears ordinary pending tool approvals; connector input and authorization remain interactive. Managed read-only Workers inherit unrestricted host reads in Open, while write-review Workers still use isolated Git worktrees. Changing the selected model affects the next turn without turning each model into a separate project history. diff --git a/VERSION b/VERSION index 8cf7c24..770cb09 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.25 +0.2.26 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index aacf320..2080194 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "milim-desktop", "private": true, - "version": "0.2.25", + "version": "0.2.26", "type": "module", "scripts": { "dev": "vite", diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 849c988..774492f 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -3061,7 +3061,7 @@ dependencies = [ [[package]] name = "milim-agents" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-stream", "futures", @@ -3078,7 +3078,7 @@ dependencies = [ [[package]] name = "milim-automation" -version = "0.2.25" +version = "0.2.26" dependencies = [ "base64 0.22.1", "chrono", @@ -3094,7 +3094,7 @@ dependencies = [ [[package]] name = "milim-core" -version = "0.2.25" +version = "0.2.26" dependencies = [ "directories", "serde", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "milim-desktop" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-trait", "base64 0.22.1", @@ -3124,6 +3124,7 @@ dependencies = [ "milim-skills", "milim-storage", "milim-tools", + "objc2-web-kit", "reqwest 0.12.28", "serde", "serde_json", @@ -3138,12 +3139,13 @@ dependencies = [ "toml 0.8.2", "tracing", "tracing-subscriber", + "webview2-com", "xcap", ] [[package]] name = "milim-identity" -version = "0.2.25" +version = "0.2.26" dependencies = [ "base64 0.22.1", "hex", @@ -3158,7 +3160,7 @@ dependencies = [ [[package]] name = "milim-inference" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-stream", "async-trait", @@ -3175,7 +3177,7 @@ dependencies = [ [[package]] name = "milim-mcp-client" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-trait", "milim-core", @@ -3190,7 +3192,7 @@ dependencies = [ [[package]] name = "milim-memory" -version = "0.2.25" +version = "0.2.26" dependencies = [ "milim-core", "milim-inference", @@ -3204,7 +3206,7 @@ dependencies = [ [[package]] name = "milim-privacy" -version = "0.2.25" +version = "0.2.26" dependencies = [ "regex", "serde", @@ -3212,7 +3214,7 @@ dependencies = [ [[package]] name = "milim-sandbox" -version = "0.2.25" +version = "0.2.26" dependencies = [ "milim-core", "serde", @@ -3221,7 +3223,7 @@ dependencies = [ [[package]] name = "milim-server" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-stream", "async-trait", @@ -3255,7 +3257,7 @@ dependencies = [ [[package]] name = "milim-skills" -version = "0.2.25" +version = "0.2.26" dependencies = [ "milim-core", "milim-storage", @@ -3268,7 +3270,7 @@ dependencies = [ [[package]] name = "milim-storage" -version = "0.2.25" +version = "0.2.26" dependencies = [ "aes-gcm", "milim-core", @@ -3280,7 +3282,7 @@ dependencies = [ [[package]] name = "milim-tools" -version = "0.2.25" +version = "0.2.26" dependencies = [ "async-trait", "milim-core", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index e53666a..c34c913 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "milim-desktop" -version = "0.2.25" +version = "0.2.26" edition = "2021" description = "milim desktop app" authors = ["Omer Shatzberg "] @@ -60,6 +60,10 @@ base64 = "0.22" [target.'cfg(target_os = "macos")'.dependencies] core-graphics = { version = "0.25", optional = true } +objc2-web-kit = { version = "0.3", default-features = false, features = ["WKWebView"] } + +[target.'cfg(windows)'.dependencies] +webview2-com = "0.38" [target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies] tauri-plugin-single-instance = "2" diff --git a/apps/desktop/src-tauri/src/preview_webview.rs b/apps/desktop/src-tauri/src/preview_webview.rs index c0271a5..726cc16 100644 --- a/apps/desktop/src-tauri/src/preview_webview.rs +++ b/apps/desktop/src-tauri/src/preview_webview.rs @@ -263,11 +263,70 @@ pub fn preview_webview_navigate( #[tauri::command] pub fn preview_webview_reload(app: tauri::AppHandle, label: String) -> Result<(), String> { - preview_webview(&app, &label)? - .reload() + let webview = preview_webview(&app, &label)?; + if webview + .url() + .map(|url| preview_reload_bypasses_cache(&url)) + .unwrap_or(false) + { + reload_without_cache(&webview) + } else { + webview.reload().map_err(|error| error.to_string()) + } +} + +#[cfg(windows)] +fn reload_without_cache(webview: &Webview) -> Result<(), String> { + use webview2_com::{CallDevToolsProtocolMethodCompletedHandler, CoTaskMemPWSTR}; + + webview + .with_webview(|platform_webview| { + let controller = platform_webview.controller(); + let result = unsafe { + (|| { + let core = controller.CoreWebView2()?; + let fallback_core = core.clone(); + let handler = CallDevToolsProtocolMethodCompletedHandler::create(Box::new( + move |status, _| status.or_else(|_| fallback_core.Reload()), + )); + let method = CoTaskMemPWSTR::from("Page.reload"); + let parameters = CoTaskMemPWSTR::from(r#"{"ignoreCache":true}"#); + let method = method.as_ref(); + let parameters = parameters.as_ref(); + core.CallDevToolsProtocolMethod( + *method.as_pcwstr(), + *parameters.as_pcwstr(), + &handler, + ) + })() + }; + if let Err(error) = result { + tracing::warn!( + "cache-bypassing preview reload failed, using normal reload: {error}" + ); + if let Ok(core) = unsafe { controller.CoreWebView2() } { + let _ = unsafe { core.Reload() }; + } + } + }) .map_err(|error| error.to_string()) } +#[cfg(target_os = "macos")] +fn reload_without_cache(webview: &Webview) -> Result<(), String> { + webview + .with_webview(|platform_webview| unsafe { + let webview: &objc2_web_kit::WKWebView = &*platform_webview.inner().cast(); + let _ = webview.reloadFromOrigin(); + }) + .map_err(|error| error.to_string()) +} + +#[cfg(not(any(windows, target_os = "macos")))] +fn reload_without_cache(webview: &Webview) -> Result<(), String> { + webview.reload().map_err(|error| error.to_string()) +} + #[tauri::command] pub fn preview_webview_history( app: tauri::AppHandle, @@ -378,6 +437,10 @@ fn preview_url_allowed(url: &Url) -> bool { || (url.scheme() == "http" && is_loopback_host(url.host_str().unwrap_or_default())) } +fn preview_reload_bypasses_cache(url: &Url) -> bool { + is_loopback_host(url.host_str().unwrap_or_default()) +} + fn is_preview_label(label: &str) -> bool { label.starts_with(PREVIEW_WEBVIEW_LABEL_PREFIX) && label[PREVIEW_WEBVIEW_LABEL_PREFIX.len()..] @@ -443,6 +506,12 @@ mod tests { assert!(allowed_preview_url("http://[::1]:4173/").is_ok()); assert!(allowed_preview_url("http://example.com/").is_err()); assert!(allowed_preview_url("javascript:alert(1)").is_err()); + assert!(preview_reload_bypasses_cache( + &Url::parse("http://localhost:4173/").unwrap() + )); + assert!(!preview_reload_bypasses_cache( + &Url::parse("https://example.com/").unwrap() + )); assert!(preview_new_tab_url_allowed( &Url::parse("about:blank").unwrap() )); diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index dbeca7c..3a716b4 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "milim", - "version": "0.2.25", + "version": "0.2.26", "identifier": "com.omershatz.milim", "build": { "frontendDist": "../dist", diff --git a/apps/desktop/src/update/releases.json b/apps/desktop/src/update/releases.json index c18e2bb..fde4115 100644 --- a/apps/desktop/src/update/releases.json +++ b/apps/desktop/src/update/releases.json @@ -1,4 +1,9 @@ { + "0.2.26": { + "version": "0.2.26", + "summary": "Loopback preview reloads now fetch current local assets without clearing browser state.", + "items": [] + }, "0.2.25": { "version": "0.2.25", "summary": "Milim 0.2.25 improves desktop text selection while carrying forward inline charts.", diff --git a/apps/desktop/tests/tauri-webview-e2e.mjs b/apps/desktop/tests/tauri-webview-e2e.mjs index de68e6d..e19bc54 100644 --- a/apps/desktop/tests/tauri-webview-e2e.mjs +++ b/apps/desktop/tests/tauri-webview-e2e.mjs @@ -2135,6 +2135,22 @@ async function runBrowserProfileCheck(session) { "Persistent browser restart", ); + await navigateE2ePreviewWebview( + session.page, + "artifact-browser-e2e-persistent-3", + `${testServer.origin}/?phase=cache-reload`, + ); + await testServer.waitForReport("cache-reload", "main", 10_000, (report) => report.version === "rgb(12, 34, 56)"); + testServer.setCacheColor("rgb(65, 43, 21)"); + await session.page.evaluate(async (label) => { + await window.__TAURI_INTERNALS__.invoke("preview_webview_reload", { label }); + }, "artifact-browser-e2e-persistent-3"); + assertBrowserReport( + await testServer.waitForReport("cache-reload", "main", 10_000, (report) => report.version === "rgb(65, 43, 21)"), + "persistent", + "Cache-bypassing loopback reload", + ); + await navigateE2ePreviewWebview( session.page, "artifact-browser-e2e-persistent-3", @@ -2240,14 +2256,24 @@ function assertBrowserCapabilities(report) { async function startBrowserProfileServer() { const reports = []; + let cacheColor = "rgb(12, 34, 56)"; const server = http.createServer((request, response) => { const url = new URL(request.url || "/", "http://127.0.0.1"); + if (url.pathname === "/cache.css") { + response.writeHead(200, { + "content-type": "text/css; charset=utf-8", + "cache-control": "public, max-age=3600", + }); + response.end(`#popup { color: ${cacheColor}; }`); + return; + } if (url.pathname === "/report") { reports.push({ phase: url.searchParams.get("phase"), kind: url.searchParams.get("kind") || "main", cookie: url.searchParams.get("cookie") || "", storage: url.searchParams.get("storage") || "", + version: url.searchParams.get("version") || "", capabilities: JSON.parse(url.searchParams.get("capabilities") || "null"), }); response.writeHead(204); @@ -2275,6 +2301,7 @@ async function startBrowserProfileServer() { response.end(` Milim browser profile test +