Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
shimmyshimmer
cbb1508513 Address review: Linux data-dir caches, bash invocation, pre-webview clear
Three review findings, all verified:

1. wry keys the WebKitGTK base-cache dir to the app DATA dir (same as
base-data), so on Linux the stale frontend cache also lives under
~/.local/share/ai.unsloth.studio. Clear the cache-typed subdirs there
(WebKitCache, CacheStorage, serviceworkers) while keeping localstorage,
indexeddb, and cookies. Tests extended to 23 assertions.

2. run_all.sh invoked the new test with sh, but the extracted setup.sh
function uses bash arrays; dash aborts with a syntax error before any
assertion. Invoke with bash.

3. The in-app desktop update runs start_backend_update before
downloadAndInstall/relaunch, so setup.ps1/setup.sh clear caches while
the live WebView still holds them and silently fail. Clear the caches
from the Rust side in main() before the Builder runs: the config window
(and the WebView lock) exists by the time setup hooks fire, so this is
the one point where the profile is guaranteed unlocked. Same cache-only
path lists per OS; cargo check passes.
2026-07-23 02:35:51 -07:00
pre-commit-ci[bot]
1eb3ac26e7 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-23 08:56:13 +00:00
shimmyshimmer
bf702033a6 Setup: clear stale WebView caches on install/update, keep user data
The desktop app's WebView caches (keyed by the Tauri bundle id
ai.unsloth.studio) hold copies of the previous frontend and keep serving
them after an update, so old styles linger even though the code on disk
is new. Clear cache-only paths on every install/update via setup.sh and
setup.ps1 (both fresh installs and 'unsloth studio update' route through
them).

Cleared: the HTTP/network caches, CacheStorage, service workers, and GPU
caches for the bundle id. Kept: LocalStorage, IndexedDB, cookies,
settings, models, and the studio database.

macOS: ~/Library/Caches/<bid> and WebsiteData/{CacheStorage,
ServiceWorkers,DiskCache}. Linux: XDG cache dir. Windows:
EBWebView\Default\{Cache,Code Cache,GPUCache,Service Worker}.

Adds tests/sh/test_setup_webview_cache_clear.sh covering both OS
branches, XDG overrides, and that user-facing storage survives.
2026-07-23 01:54:45 -07:00
7 changed files with 245 additions and 18 deletions

View file

@ -1129,6 +1129,25 @@ if ($script:StudioVtOk -and -not $env:NO_COLOR) {
Write-Host " $Rule" -ForegroundColor DarkGray
}
# WebView2 caches keyed by the app bundle id hold copies of the previous
# frontend and can keep serving it after an update (old styles linger).
# Cache-only paths: Local Storage, IndexedDB, cookies, settings, models,
# and the studio database are untouched.
if ($env:LOCALAPPDATA) {
$wvDefault = Join-Path $env:LOCALAPPDATA "ai.unsloth.studio\EBWebView\Default"
$wvCleared = $false
foreach ($wvSub in @("Cache", "Code Cache", "GPUCache", "Service Worker")) {
$wvPath = Join-Path $wvDefault $wvSub
if (Test-Path -LiteralPath $wvPath) {
try {
Remove-Item -LiteralPath $wvPath -Recurse -Force -ErrorAction Stop
$wvCleared = $true
} catch { }
}
}
if ($wvCleared) { substep "cleared stale WebView caches (ai.unsloth.studio); settings and data kept" }
}
# Back up User PATH under HKCU\Software\Unsloth before any modifications.
try {
$envKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $false)

View file

@ -475,6 +475,50 @@ rm -rf "$REPO_ROOT/unsloth_compiled_cache"
rm -rf "$SCRIPT_DIR/backend/unsloth_compiled_cache"
rm -rf "$SCRIPT_DIR/tmp/unsloth_compiled_cache"
# WebView caches keyed by the app bundle id hold copies of the previous
# frontend and can keep serving it after an update (old styles linger).
# Cache-only paths: LocalStorage, IndexedDB, cookies, settings, models,
# and the studio database are untouched.
_clear_webview_caches() {
_wvc_bid="ai.unsloth.studio"
_wvc_paths=()
case "$(uname -s 2>/dev/null)" in
Darwin)
_wvc_paths=(
"$HOME/Library/Caches/$_wvc_bid"
"$HOME/Library/WebKit/$_wvc_bid/WebsiteData/CacheStorage"
"$HOME/Library/WebKit/$_wvc_bid/WebsiteData/ServiceWorkers"
"$HOME/Library/WebKit/$_wvc_bid/WebsiteData/DiskCache"
)
;;
Linux)
# wry keys the WebKitGTK base-cache dir to the app DATA dir (same
# as base-data), so cache subdirs also live under
# ~/.local/share/<bid>. Clear only the cache-typed subdirs there;
# sibling localstorage/, indexeddb/, and cookies stay.
_wvc_data="${XDG_DATA_HOME:-$HOME/.local/share}/$_wvc_bid"
_wvc_paths=(
"${XDG_CACHE_HOME:-$HOME/.cache}/$_wvc_bid"
"$_wvc_data/WebKitCache"
"$_wvc_data/CacheStorage"
"$_wvc_data/serviceworkers"
"$_wvc_data/ServiceWorkers"
)
;;
*) return 0 ;;
esac
_wvc_cleared=false
for _wvc_p in "${_wvc_paths[@]}"; do
[ -e "$_wvc_p" ] || continue
rm -rf "$_wvc_p" 2>/dev/null && _wvc_cleared=true || true
done
if [ "$_wvc_cleared" = true ]; then
substep "cleared stale WebView caches ($_wvc_bid); settings and data kept"
fi
return 0
}
_clear_webview_caches
# ── Detect Colab ──
IS_COLAB=false
keynames=$'\n'$(printenv | cut -d= -f1)

View file

@ -161,6 +161,64 @@ fn setup_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
// Clear WebView caches before the webview initializes. An in-app update runs
// setup.sh/setup.ps1 while the old WebView still holds these files (its clear
// silently fails), so without this a relaunch can serve the previous frontend
// from cache. Cache-only paths; LocalStorage, IndexedDB, cookies, and app
// data are kept. Mirrors setup.sh _clear_webview_caches / setup.ps1.
fn clear_webview_caches(bundle_id: &str) {
use std::path::PathBuf;
let mut paths: Vec<PathBuf> = Vec::new();
#[cfg(target_os = "windows")]
if let Ok(local) = std::env::var("LOCALAPPDATA") {
if !local.is_empty() {
let profile = PathBuf::from(local)
.join(bundle_id)
.join("EBWebView")
.join("Default");
for sub in ["Cache", "Code Cache", "GPUCache", "Service Worker"] {
paths.push(profile.join(sub));
}
}
}
#[cfg(target_os = "macos")]
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
let home = PathBuf::from(home);
paths.push(home.join("Library/Caches").join(bundle_id));
let data = home.join("Library/WebKit").join(bundle_id).join("WebsiteData");
for sub in ["CacheStorage", "ServiceWorkers", "DiskCache"] {
paths.push(data.join(sub));
}
}
}
#[cfg(target_os = "linux")]
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
let home = PathBuf::from(home);
let cache = std::env::var("XDG_CACHE_HOME")
.ok()
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".cache"));
paths.push(cache.join(bundle_id));
// wry keys the WebKitGTK base-cache dir to the app data dir.
let data = std::env::var("XDG_DATA_HOME")
.ok()
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".local/share"))
.join(bundle_id);
for sub in ["WebKitCache", "CacheStorage", "serviceworkers", "ServiceWorkers"] {
paths.push(data.join(sub));
}
}
}
for p in paths {
let _ = fs::remove_dir_all(&p);
}
}
fn main() {
// Fix PATH for GUI apps (macOS .app bundles, Linux AppImage, Windows)
// GUI apps don't inherit shell dotfile PATH — this spawns the user's
@ -171,6 +229,11 @@ fn main() {
info!("Unsloth Studio desktop app starting");
windows_job::initialize();
// Must run before the Builder: the config-defined window (and its
// WebView, which locks these files) exists by the time setup hooks run.
let context = tauri::generate_context!();
clear_webview_caches(&context.config().identifier);
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
@ -247,7 +310,7 @@ fn main() {
api.prevent_close();
}
})
.build(tauri::generate_context!())
.build(context)
.expect("error while building tauri application")
.run(|app, event| {
if let tauri::RunEvent::Exit = event {

View file

@ -14,6 +14,8 @@ sh "$TESTS_DIR/sh/test_nvcc_meets_llama_minimum.sh"
sh "$TESTS_DIR/sh/test_resolve_cuda_archs.sh"
sh "$TESTS_DIR/sh/test_strixhalo_wsl_reroute.sh"
sh "$TESTS_DIR/sh/test_uninstall_shared_icon.sh"
# bash, not sh: the extracted setup.sh function uses bash arrays (dash chokes).
bash "$TESTS_DIR/sh/test_setup_webview_cache_clear.sh"
sh "$TESTS_DIR/sh/test_torch_flavor.sh"
sh "$TESTS_DIR/sh/test_redact_install_output.sh"
sh "$TESTS_DIR/sh/test_install_uv_override_space.sh"

View file

@ -0,0 +1,103 @@
#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
# Unit tests for _clear_webview_caches() from studio/setup.sh.
#
# The WebView caches keyed by the app bundle id (ai.unsloth.studio) hold copies
# of the previous frontend, so an install/update must clear them or the app can
# keep rendering old styles. Clearing must be cache-only: LocalStorage,
# IndexedDB, app data, and unrelated apps' caches stay intact.
#
# Follows the extract-via-sed pattern of test_uninstall_shared_icon.sh; uname
# is overridden per test with a shell function to select the OS branch.
# shellcheck disable=SC2329 # uname stubs are invoked inside the extracted function
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SETUP_SH="$SCRIPT_DIR/../../studio/setup.sh"
BID="ai.unsloth.studio"
PASS=0
FAIL=0
_TMP_ROOT=$(mktemp -d)
trap 'rm -rf "$_TMP_ROOT"' EXIT
assert_gone() { _l="$1"; if [ -e "$2" ]; then echo " FAIL: $_l (still present: $2)"; FAIL=$((FAIL+1)); else echo " PASS: $_l"; PASS=$((PASS+1)); fi; }
assert_present() { _l="$1"; if [ -e "$2" ]; then echo " PASS: $_l"; PASS=$((PASS+1)); else echo " FAIL: $_l (missing: $2)"; FAIL=$((FAIL+1)); fi; }
# Extract just the function definition (top-level, closes at column 0).
FUNC_FILE=$(mktemp -p "$_TMP_ROOT")
sed -n '/^_clear_webview_caches() {/,/^}/p' "$SETUP_SH" > "$FUNC_FILE"
# shellcheck disable=SC1090
. "$FUNC_FILE"
substep() { :; } # stub the setup.sh logger
# ── 1. macOS: cache paths removed, user-facing storage kept ──
H=$(mktemp -d -p "$_TMP_ROOT")
mkdir -p "$H/Library/Caches/$BID/WebKit/NetworkCache" \
"$H/Library/WebKit/$BID/WebsiteData/CacheStorage" \
"$H/Library/WebKit/$BID/WebsiteData/ServiceWorkers" \
"$H/Library/WebKit/$BID/WebsiteData/DiskCache" \
"$H/Library/WebKit/$BID/WebsiteData/LocalStorage" \
"$H/Library/WebKit/$BID/WebsiteData/IndexedDB" \
"$H/Library/Application Support/$BID" \
"$H/Library/Caches/com.other.app"
uname() { echo Darwin; }
HOME="$H" _clear_webview_caches
assert_gone "macOS: Caches/$BID removed" "$H/Library/Caches/$BID"
assert_gone "macOS: WebsiteData/CacheStorage removed" "$H/Library/WebKit/$BID/WebsiteData/CacheStorage"
assert_gone "macOS: WebsiteData/ServiceWorkers removed" "$H/Library/WebKit/$BID/WebsiteData/ServiceWorkers"
assert_gone "macOS: WebsiteData/DiskCache removed" "$H/Library/WebKit/$BID/WebsiteData/DiskCache"
assert_present "macOS: LocalStorage kept" "$H/Library/WebKit/$BID/WebsiteData/LocalStorage"
assert_present "macOS: IndexedDB kept" "$H/Library/WebKit/$BID/WebsiteData/IndexedDB"
assert_present "macOS: Application Support kept" "$H/Library/Application Support/$BID"
assert_present "macOS: unrelated app cache kept" "$H/Library/Caches/com.other.app"
# ── 2. Linux: cache paths removed (XDG cache dir AND the cache subdirs wry
# keys to the app data dir), user-facing storage kept ──
H=$(mktemp -d -p "$_TMP_ROOT")
D="$H/.local/share/$BID"
mkdir -p "$H/.cache/$BID" "$D/WebKitCache" "$D/CacheStorage" "$D/serviceworkers" \
"$D/localstorage" "$D/indexeddb" "$H/.config/$BID" "$H/.cache/other.app"
: > "$D/cookies.sqlite"
uname() { echo Linux; }
HOME="$H" XDG_CACHE_HOME="" XDG_DATA_HOME="" _clear_webview_caches
assert_gone "linux: ~/.cache/$BID removed" "$H/.cache/$BID"
assert_gone "linux: data-dir WebKitCache removed" "$D/WebKitCache"
assert_gone "linux: data-dir CacheStorage removed" "$D/CacheStorage"
assert_gone "linux: data-dir serviceworkers removed" "$D/serviceworkers"
assert_present "linux: localstorage kept" "$D/localstorage"
assert_present "linux: indexeddb kept" "$D/indexeddb"
assert_present "linux: cookies kept" "$D/cookies.sqlite"
assert_present "linux: ~/.config/$BID kept" "$H/.config/$BID"
assert_present "linux: unrelated app cache kept" "$H/.cache/other.app"
# ── 3. Linux: XDG_CACHE_HOME / XDG_DATA_HOME overrides honored ──
H=$(mktemp -d -p "$_TMP_ROOT")
XDG=$(mktemp -d -p "$_TMP_ROOT")
mkdir -p "$XDG/cache/$BID" "$XDG/data/$BID/WebKitCache" "$XDG/data/$BID/localstorage" "$H/.cache/$BID"
HOME="$H" XDG_CACHE_HOME="$XDG/cache" XDG_DATA_HOME="$XDG/data" _clear_webview_caches
assert_gone "linux: XDG_CACHE_HOME/$BID removed" "$XDG/cache/$BID"
assert_gone "linux: XDG_DATA_HOME WebKitCache removed" "$XDG/data/$BID/WebKitCache"
assert_present "linux: XDG_DATA_HOME localstorage kept" "$XDG/data/$BID/localstorage"
assert_present "linux: ~/.cache/$BID kept under override" "$H/.cache/$BID"
# ── 4. Nothing to clear is a clean no-op ──
H=$(mktemp -d -p "$_TMP_ROOT")
uname() { echo Darwin; }
if HOME="$H" _clear_webview_caches; then
echo " PASS: empty HOME -> no-op exit 0"; PASS=$((PASS+1))
else
echo " FAIL: empty HOME -> nonzero exit"; FAIL=$((FAIL+1))
fi
# ── 5. Unknown OS is a no-op ──
H=$(mktemp -d -p "$_TMP_ROOT")
mkdir -p "$H/Library/Caches/$BID"
uname() { echo SunOS; }
HOME="$H" _clear_webview_caches
assert_present "unknown OS: nothing removed" "$H/Library/Caches/$BID"
echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" = 0 ]

View file

@ -38,7 +38,11 @@ def fail(m):
raise AssertionError(f"[font-scale] FAIL: {m}")
def near(a, b, tol = 0.35):
def near(
a,
b,
tol = 0.35,
):
return a is not None and b is not None and abs(a - b) <= tol
@ -86,9 +90,7 @@ def open_appearance(page):
page.wait_for_timeout(700)
if page.get_by_role("dialog").count() == 0:
fail("settings dialog did not open")
page.get_by_role("dialog").get_by_role("button").filter(
has_text = "Appearance"
).first.click()
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click()
page.wait_for_timeout(600)
@ -153,9 +155,7 @@ def main():
page.wait_for_timeout(400)
step("overflowing select scrolls its Radix viewport")
page.get_by_role("dialog").get_by_role("button").filter(
has_text = "Voice"
).first.click()
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Voice").first.click()
page.wait_for_timeout(600)
page.set_viewport_size({"width": 1440, "height": 480})
page.locator("[aria-label='Dictation language']").click()
@ -194,9 +194,7 @@ def main():
page.wait_for_timeout(400)
step("default restores exactly")
page.get_by_role("dialog").get_by_role("button").filter(
has_text = "Appearance"
).first.click()
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click()
page.wait_for_timeout(500)
set_input(page, "UI font size", DEFAULT)
final = measure(page)

View file

@ -15,9 +15,7 @@ from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
SRC = REPO / "studio/frontend/src"
INDEX_CSS = (SRC / "index.css").read_text(encoding = "utf-8")
STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text(
encoding = "utf-8"
)
STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text(encoding = "utf-8")
SELECT = (SRC / "components/ui/select.tsx").read_text(encoding = "utf-8")
# Raw numeric fontSize props are only allowed where a scaled stylesheet rule
@ -70,9 +68,7 @@ def test_ui_token_families_exist():
def test_explicit_code_font_size_is_never_multiplied():
match = re.search(
r"html\[data-code-font-size\][^{]*\{([^}]*)\}", INDEX_CSS
)
match = re.search(r"html\[data-code-font-size\][^{]*\{([^}]*)\}", INDEX_CSS)
assert match is not None
body = match.group(1)
assert "var(--custom-code-font-size)" in body
@ -83,7 +79,9 @@ def test_radix_select_viewport_owns_the_scroll_state():
viewport = SELECT[SELECT.index("SelectPrimitive.Viewport") :]
assert "overflow-y-auto" in viewport.split("</SelectPrimitive.Viewport>")[0]
# The rounded surface itself must not scroll (WebKit squares its corners).
content_cls = re.search(r"SelectPrimitive\.Content[\s\S]*?className=\{cn\(\s*\"([^\"]+)\"", SELECT)
content_cls = re.search(
r"SelectPrimitive\.Content[\s\S]*?className=\{cn\(\s*\"([^\"]+)\"", SELECT
)
assert content_cls is not None
assert "overflow-hidden" in content_cls.group(1)
assert "overflow-y-auto" not in content_cls.group(1)